front.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. from streamlit_ace import st_ace
  2. from streamlit_echarts import st_echarts, JsCode
  3. import math
  4. import streamlit as st
  5. import matplotlib.pyplot as plt
  6. import numpy as np
  7. def circle(ax, x, y, radius, color='#1946BA'):
  8. from matplotlib.patches import Ellipse
  9. drawn_circle = Ellipse((x, y), radius * 2, radius * 2, clip_on=True,
  10. zorder=2, linewidth=2, edgecolor=color, facecolor=(0, 0, 0, .0125))
  11. try:
  12. ax.add_artist(drawn_circle)
  13. except:
  14. raise
  15. def plot_smith(r, i, g, r_cut, i_cut):
  16. fig = plt.figure(figsize=(10, 10))
  17. ax = fig.add_subplot()
  18. major_ticks = np.arange(-1.0, 1.1, 0.25)
  19. minor_ticks = np.arange(-1.1, 1.1, 0.05)
  20. # ax.set_xticks(major_ticks)
  21. ax.set_xticks(minor_ticks, minor=True)
  22. # ax.set_yticks(major_ticks)
  23. ax.set_yticks(minor_ticks, minor=True)
  24. ax.grid(which='major', color='grey', linewidth=1.5)
  25. ax.grid(which='minor', color='grey', linewidth=0.5, linestyle=':')
  26. plt.xlabel(r'$Re(\Gamma)$', color='gray', fontsize=16, fontname="Cambria")
  27. plt.ylabel('$Im(\Gamma)$', color='gray', fontsize=16, fontname="Cambria")
  28. plt.title('Smith chart', fontsize=24, fontname="Cambria")
  29. # unit circle
  30. circle(ax, 0, 0, 1)
  31. # input data points
  32. ax.plot(r, i, '+', ms=8, mew=2, color='#b6c7f4')
  33. # choosen data points
  34. ax.plot(r_cut, i_cut, '+', ms=8, mew=2, color='#1946BA')
  35. # circle approximation by calc
  36. radius = abs(g[1] - g[0] / g[2]) / 2
  37. x = ((g[1] + g[0] / g[2]) / 2).real
  38. y = ((g[1] + g[0] / g[2]) / 2).imag
  39. circle(ax, x, y, radius, color='#FF8400')
  40. XLIM = [-1.1, 1.1]
  41. YLIM = [-1.1, 1.1]
  42. ax.set_xlim(XLIM)
  43. ax.set_ylim(YLIM)
  44. st.pyplot(fig)
  45. interval_range = (0, 100) # in percents
  46. interval_start, interval_end = 0, 0
  47. def plot_interact_abs_from_f(f, r, i):
  48. abs_S = list((r[n] ** 2 + i[n] ** 2)**0.5 for n in range(len(r)))
  49. global interval_range, interval_start, interval_end
  50. # echarts for datazoom https://discuss.streamlit.io/t/streamlit-echarts/3655
  51. # datazoom https://echarts.apache.org/examples/en/editor.html?c=line-draggable&lang=ts
  52. # axis pointer values https://echarts.apache.org/en/option.html#axisPointer
  53. options = {
  54. "xAxis": {
  55. "type": "category",
  56. "data": f,
  57. "name": "Hz",
  58. "nameTextStyle": {"fontSize": 16},
  59. "axisLabel": {"fontSize": 16},
  60. },
  61. "yAxis": {
  62. "type": "value",
  63. "name": "abs(S)",
  64. "nameTextStyle": {"fontSize": 16},
  65. "axisLabel": {"fontSize": 16},
  66. # "axisPointer": {
  67. # "type": 'cross',
  68. # "label": {
  69. # "show":"true",
  70. # "formatter": JsCode(
  71. # "function(info){console.log(info);return 'line ' ;};"
  72. # ).js_code
  73. # }
  74. # }
  75. },
  76. "series": [{"data": abs_S, "type": "line", "name": "abs(S)"}],
  77. "height": 300,
  78. "dataZoom": [{"type": "slider", "start": 0, "end": 100, "height": 100, "bottom": 10}],
  79. "tooltip": {
  80. "trigger": "axis",
  81. "axisPointer": {
  82. "type": 'cross',
  83. # "label": {
  84. # "show":"true",
  85. # "formatter": JsCode(
  86. # "function(info){console.log(info);return 'line ' ;};"
  87. # ).js_code
  88. # }
  89. }
  90. },
  91. "toolbox": {
  92. "feature": {
  93. # "dataView": { "show": "true", "readOnly": "true" },
  94. "restore": {"show": "true"},
  95. }
  96. },
  97. }
  98. events = {
  99. "dataZoom": "function(params) { return [params.start, params.end] }",
  100. }
  101. # show echart with dataZoom and update intervals based on output
  102. interval_range = st_echarts(
  103. options=options, events=events, height="500px", key="render_basic_bar_events"
  104. )
  105. if interval_range is None:
  106. interval_range = (0, 100)
  107. n = len(f)
  108. interval_start, interval_end = (
  109. int(n*interval_range[id]*0.01) for id in (0, 1))
  110. # plot (abs(S))(f) chart with pyplot
  111. def plot_ref_from_f(f, r, i):
  112. fig = plt.figure(figsize=(10, 10))
  113. abs_S = list((r[n] ** 2 + i[n] ** 2)**0.5 for n in range(len(r)))
  114. xlim = [min(f) - abs(max(f) - min(f)) * 0.1,
  115. max(f) + abs(max(f) - min(f)) * 0.1]
  116. ylim = [min(abs_S) - abs(max(abs_S) - min(abs_S)) * 0.5,
  117. max(abs_S) + abs(max(abs_S) - min(abs_S)) * 0.5]
  118. ax = fig.add_subplot()
  119. ax.set_xlim(xlim)
  120. ax.set_ylim(ylim)
  121. ax.grid(which='major', color='k', linewidth=1)
  122. ax.grid(which='minor', color='grey', linestyle=':', linewidth=0.5)
  123. plt.xlabel(r'$f,\; 1/c$', color='gray', fontsize=16, fontname="Cambria")
  124. plt.ylabel('$|S|$', color='gray', fontsize=16, fontname="Cambria")
  125. plt.title('Absolute value of reflection coefficient from frequency',
  126. fontsize=24, fontname="Cambria")
  127. ax.plot(f, abs_S, '+', ms=8, mew=2, color='#1946BA')
  128. st.pyplot(fig)
  129. def run(calc_function):
  130. def is_float(element) -> bool:
  131. try:
  132. float(element)
  133. val = float(element)
  134. if math.isnan(val) or math.isinf(val):
  135. raise ValueError
  136. return True
  137. except ValueError:
  138. return False
  139. # to utf-8
  140. def read_data(data):
  141. for x in range(len(data)):
  142. if type(data[x]) == bytes:
  143. try:
  144. data[x] = data[x].decode('utf-8-sig', 'ignore')
  145. except:
  146. return 'Not an utf-8-sig line №: ' + str(x)
  147. return 'data read: success'
  148. # for Touchstone .snp format
  149. def parse_heading(data):
  150. nonlocal data_format_snp
  151. if data_format_snp:
  152. for x in range(len(data)):
  153. if data[x][0]=='#':
  154. line = data[x].split()
  155. if len(line)== 6:
  156. repr_map = {"RI":0,"MA":1, "DB":2}
  157. para_map = {"S":0,"Z":1}
  158. hz_map = {"GHz":10**9,"MHz":10**6,"KHz":10**3,"Hz":1}
  159. hz,measurement_parameter,data_representation,_r,ref_resistance=line[1:]
  160. try:
  161. return hz_map[hz], para_map[measurement_parameter], repr_map[data_representation], ref_resistance
  162. except:
  163. break
  164. break
  165. return 1, 0, 0, 50
  166. def unpack_data(data, input_start_line, input_end_line):
  167. nonlocal select_measurement_parameter
  168. nonlocal select_data_representation
  169. f, r, i = [], [], []
  170. return_status='data parsed'
  171. for x in range(input_start_line-1, input_end_line):
  172. if len(data[x])<2 or data[x][0]== '!' or data[x][0]=='#' or data[x][0]=='%' or data[x][0]=='/':
  173. # first is a comment line according to .snp documentation,
  174. # others detects comments in various languages
  175. continue
  176. data[x] = data[x].replace(';', ' ').replace(',', ' ') # generally we expect these chars as seperators
  177. line = data[x].split()
  178. # always at least 3 values for single data point
  179. if len(line) < 3:
  180. return_status = 'Can\'t parse line №: ' + str(x) + ',\n not enough arguments (less than 3)'
  181. break
  182. # 1: process according to data_placement
  183. a,b,c=[],[],[]
  184. try:
  185. a, b, c = (line[y] for y in range(min(len(line),3)))
  186. # for x in input_data_columns.keys():
  187. # if x=='f':
  188. # elif x=='r':
  189. # elif x=='i':
  190. except:
  191. return_status = 'Can\'t parse line №: ' + str(x) + ',\n not enough arguments'
  192. break
  193. if not ((is_float(a)) or (is_float(b)) or (is_float(c))):
  194. return_status = 'Wrong data type, expected number. Error on line: ' + str(x)
  195. break
  196. a,b,c=(float(x) for x in (a,b,c))
  197. f.append(a) # frequency
  198. # 2: process according to data_representation
  199. if select_data_representation == 'Frequency, real, imaginary':
  200. r.append(b) # Re
  201. i.append(c) # Im
  202. elif select_data_representation == 'Frequency, magnitude, angle':
  203. r.append(b*np.cos(np.deg2rad(c))) # Re
  204. i.append(b*np.sin(np.deg2rad(c))) # Im
  205. elif select_data_representation == 'Frequency, db, angle':
  206. b=10**(b/20)
  207. r.append(b*np.sin(np.deg2rad(c))) # Re
  208. i.append(b*np.cos(np.deg2rad(c))) # Im
  209. else:
  210. return_status = 'Wrong data format'
  211. break
  212. # 3: process according to measurement_parameter
  213. return f, r, i, return_status
  214. # make accessible specific range of numerical data choosen with interactive plot
  215. global interval_range, interval_start, interval_end
  216. # file upload button
  217. uploaded_file = st.file_uploader('Upload a file from your vector analizer. ' +
  218. 'Make sure the file format is .snp or it has a similar inner structure.')
  219. # check .snp
  220. data_format_snp = False
  221. if uploaded_file is None:
  222. st.write("Demonstration: ")
  223. # display DEMO
  224. data_format_snp = True
  225. try:
  226. with open('./resource/data/8_default_demo.s1p') as f:
  227. data = f.readlines()
  228. except:
  229. # 'streamlit run' call in the wrong directory. Display smaller demo:
  230. data =[line.strip()+'\n' for line in '''# Hz S MA R 50
  231. 11415403125 0.37010744 92.47802
  232. 11416090625 0.33831283 92.906929
  233. 11416778125 0.3069371 94.03318
  234. '''.split('\n')]
  235. else:
  236. data = uploaded_file.readlines()
  237. if uploaded_file.name[-4:-2]=='.s' and uploaded_file.name[-1]== 'p':
  238. data_format_snp = True
  239. validator_status = '...'
  240. ace_preview_markers = []
  241. # data loaded
  242. circle_params = []
  243. if len(data) > 0:
  244. validator_status = read_data(data)
  245. if validator_status == 'data read: success':
  246. hz, select_measurement_parameter, select_data_representation, input_ref_resistance=parse_heading(data)
  247. col1, col2 = st.columns(2)
  248. select_measurement_parameter = col1.selectbox('Measurement parameter',
  249. ['S', 'Z'],
  250. select_measurement_parameter)
  251. select_data_representation = col1.selectbox('Data representation',
  252. ['Frequency, real, imaginary',
  253. 'Frequency, magnitude, angle',
  254. 'Frequency, db, angle'],
  255. select_data_representation)
  256. if select_measurement_parameter=='Z':
  257. input_ref_resistance = col1.number_input(
  258. "Reference resistance:", min_value=0, value=input_ref_resistance)
  259. input_start_line = col1.number_input(
  260. "First line of data:", min_value=1, max_value=len(data))
  261. input_end_line = col1.number_input(
  262. "Last line of data:", min_value=1, max_value=len(data), value=len(data))
  263. f, r, i, validator_status = unpack_data(data, input_start_line, input_end_line)
  264. f = f * hz # to hz
  265. # Ace editor to show choosen data columns and rows
  266. with col2.expander("File preview"):
  267. # So little 'official' functionality in libs and lack of documentation
  268. # therefore beware: css hacks
  269. # yellow ~ ace_step
  270. # light yellow ~ ace_highlight-marker
  271. # green ~ ace_stack
  272. # red ~ ace_error-marker
  273. # no more good colors included in streamlit_ace for marking
  274. # st.markdown('''<style>
  275. # .choosen_option_1
  276. # {
  277. # color: rgb(49, 51, 63);
  278. # }</style>''', unsafe_allow_html=True)
  279. # markdown injection does not seems to work, since ace is in a different .html accessible via iframe
  280. # markers format:
  281. #[{"startRow": 2,"startCol": 0,"endRow": 2,"endCol": 3,"className": "ace_error-marker","type": "text"}]
  282. # add general marking for choosen data lines? better just mark the problematic lines?
  283. ace_preview_markers.append(
  284. {"startRow": input_start_line,"startCol": 0,
  285. "endRow": input_end_line+1,"endCol": 0,"className": "ace_highlight-marker","type": "text"})
  286. text_value = "Frequency,Hz | Re(S11) | Im(S11)\n" + \
  287. ''.join(data).strip()
  288. st_ace(value=text_value,
  289. readonly=True,
  290. auto_update=True,
  291. placeholder="Your data is empty",
  292. markers=ace_preview_markers,
  293. height="300px")
  294. st.write("Use range slider to choose best suitable data interval")
  295. plot_interact_abs_from_f(f, r, i)
  296. select_coupling_losses = st.checkbox(
  297. 'Apply corrections for coupling losses')
  298. if select_coupling_losses:
  299. st.write("Option: Lossy coupling")
  300. else:
  301. st.write("Option: Cable attenuation")
  302. f_cut, r_cut, i_cut = (x[interval_start:interval_end]
  303. for x in (f, r, i))
  304. if validator_status == 'data parsed':
  305. Q0, sigmaQ0, QL, sigmaQl, circle_params = calc_function(
  306. f_cut, r_cut, i_cut, select_coupling_losses)
  307. out_precision = '0.7f'
  308. if Q0 <= 0 or QL <= 0:
  309. st.write("Negative Q detected, fitting may be inaccurate!")
  310. st.latex(r'Q_0 =' + f'{format(Q0, out_precision)} \pm {format(sigmaQ0, out_precision)}, ' +
  311. r'\;\;\varepsilon_{Q_0} =' + f'{format(sigmaQ0 / Q0, out_precision)}')
  312. st.latex(r'Q_L =' + f'{format(QL, out_precision)} \pm {format(sigmaQl, out_precision)}, ' +
  313. r'\;\;\varepsilon_{Q_L} =' + f'{format(sigmaQl / QL, out_precision)}')
  314. st.write("Status: " + validator_status)
  315. if len(data) > 0 and validator_status == 'data parsed':
  316. with st.expander("Show static abs(S) plot"):
  317. plot_ref_from_f(f_cut, r_cut, i_cut)
  318. plot_smith(r, i, circle_params, r_cut, i_cut)