front.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. import streamlit as st
  2. import matplotlib.pyplot as plt
  3. import numpy as np
  4. import sigfig
  5. from streamlit_ace import st_ace
  6. from .draw_smith_utils import draw_smith_circle, plot_abs_s_gridlines, plot_im_z_gridlines, plot_re_z_gridlines
  7. from .show_amplitude_echart import plot_interact_abs_from_f
  8. from .data_parsing_utils import parse_snp_header, read_data, count_columns, prepare_snp, unpack_data
  9. # see https://docs.streamlit.io/streamlit-cloud/troubleshooting#limitations-and-known-issues
  10. # if you're using Matplotlib you should wrap your code with locks
  11. from matplotlib.backends.backend_agg import RendererAgg
  12. def plot_smith(r, i, g, r_cut, i_cut):
  13. # maintaining state again (remember options for this session)
  14. if 'smith_options' not in st.session_state:
  15. st.session_state.smith_options = (True, True, False, False, False)
  16. with st.expander("Smith chart options"):
  17. smith_options_input = (st.checkbox(
  18. "Show excluded points",
  19. value=st.session_state.smith_options[0]),
  20. st.checkbox("Show grid",
  21. st.session_state.smith_options[1]),
  22. st.checkbox(
  23. "Show |S| gridlines",
  24. value=st.session_state.smith_options[2],
  25. ),
  26. st.checkbox(
  27. "Show Re(Z) gridlines",
  28. value=st.session_state.smith_options[3],
  29. ),
  30. st.checkbox(
  31. "Show Im(Z) gridlines",
  32. value=st.session_state.smith_options[4],
  33. ))
  34. if st.session_state.smith_options != smith_options_input:
  35. st.session_state.smith_options = smith_options_input
  36. st.experimental_rerun()
  37. (show_excluded_points, show_grid, show_Abs_S_gridlines,
  38. show_Re_Z_gridlines, show_Im_Z_gridlines) = st.session_state.smith_options
  39. # _lock to fix matplotlib crashing maybe?
  40. _lock = RendererAgg.lock
  41. with _lock:
  42. # normal matplotlib usage
  43. fig = plt.figure(figsize=(10, 10))
  44. ax = fig.add_subplot()
  45. ax.axis('equal')
  46. minor_ticks = np.arange(-1.1, 1.1, 0.05)
  47. ax.set_xticks(minor_ticks, minor=True)
  48. ax.set_yticks(minor_ticks, minor=True)
  49. ax.grid(which='major', color='grey', linewidth=1.5)
  50. ax.grid(which='minor', color='grey', linewidth=0.5, linestyle=':')
  51. plt.xlabel('$Re(S)$', color='gray', fontsize=16, fontname="Cambria")
  52. plt.ylabel('$Im(S)$', color='gray', fontsize=16, fontname="Cambria")
  53. plt.title('Smith chart', fontsize=24, fontname="Cambria")
  54. # unit circle
  55. draw_smith_circle(ax, 0, 0, 1, '#1946BA')
  56. if not show_grid:
  57. ax.axis('off')
  58. if show_Abs_S_gridlines:
  59. # imshow is extremely slow, so draw it in place
  60. plot_abs_s_gridlines(ax)
  61. if show_Re_Z_gridlines:
  62. plot_re_z_gridlines(ax)
  63. if show_Im_Z_gridlines:
  64. plot_im_z_gridlines(ax)
  65. # input data points
  66. if show_excluded_points:
  67. ax.plot(r, i, '+', ms=8, mew=2, color='#b6c7f4')
  68. # choosen data points
  69. ax.plot(r_cut, i_cut, '+', ms=8, mew=2, color='#1946BA')
  70. # S-circle approximation by calc
  71. radius = abs(g[1] - g[0] / g[2]) / 2
  72. x = ((g[1] + g[0] / g[2]) / 2).real
  73. y = ((g[1] + g[0] / g[2]) / 2).imag
  74. draw_smith_circle(ax, x, y, radius, color='#FF8400')
  75. XLIM = [-1.3, 1.3]
  76. YLIM = [-1.3, 1.3]
  77. ax.set_xlim(XLIM)
  78. ax.set_ylim(YLIM)
  79. try:
  80. st.pyplot(fig)
  81. except:
  82. st.write('Unexpected plot error')
  83. reload_plot=st.button('Reload')
  84. if reload_plot:
  85. st.experimental_rerun()
  86. # plot abs(S) vs f chart with pyplot
  87. def plot_abs_vs_f(f, r, i, fitted_mag_s):
  88. # _lock to fix matplotlib crashing maybe?
  89. _lock = RendererAgg.lock
  90. with _lock:
  91. fig = plt.figure(figsize=(10, 10))
  92. s = np.abs(np.array(r) + 1j * np.array(i))
  93. if st.session_state.legendselection == '|S| (dB)':
  94. m = np.max(np.where(s == 0, 10**-50, s)) # where 10**-50 ~ 0
  95. s = list(20 * np.where(s == 0, np.log10(m), np.log10(s)))
  96. m = np.max(np.where(s == 0, 10**-50, fitted_mag_s)) # where 10**-50 ~ 0
  97. fitted_mag_s = list(
  98. 20 * np.where(s == 0, np.log10(m), np.log10(fitted_mag_s)))
  99. s = list(s)
  100. min_f = min(f)
  101. max_f = max(f)
  102. xlim = [min_f - abs(max_f - min_f) * 0.1, max_f + abs(max_f - min_f) * 0.1]
  103. min_s = min(s)
  104. max_s = max(s)
  105. ylim = [min_s - abs(max_s - min_s) * 0.5, max_s + abs(max_s - min_s) * 0.5]
  106. ax = fig.add_subplot()
  107. ax.set_xlim(xlim)
  108. ax.set_ylim(ylim)
  109. ax.grid(which='major', color='k', linewidth=1)
  110. ax.grid(which='minor', color='grey', linestyle=':', linewidth=0.5)
  111. plt.xlabel(r'$f,\; 1/c$', color='gray', fontsize=16, fontname="Cambria")
  112. if st.session_state.legendselection == '|S| (dB)':
  113. plt.ylabel('$|S|$ (dB)', color='gray', fontsize=16, fontname="Cambria")
  114. plt.title('|S| (dB) vs frequency', fontsize=24, fontname="Cambria")
  115. else:
  116. plt.ylabel('$|S|$', color='gray', fontsize=16, fontname="Cambria")
  117. plt.title('|S| vs frequency', fontsize=24, fontname="Cambria")
  118. ax.plot(f, s, '+', ms=8, mew=2, color='#1946BA')
  119. ax.plot(f, fitted_mag_s, '-', linewidth=3, color='#FF8400')
  120. try:
  121. st.pyplot(fig)
  122. except:
  123. st.write('Unexpected plot error')
  124. reload_plot=st.button('Reload')
  125. if reload_plot:
  126. st.experimental_rerun()
  127. def run(calc_function):
  128. # info
  129. with st.expander("Info"):
  130. # streamlit.markdown does not support footnotes
  131. try:
  132. with open('./source/frontend/info.md') as f:
  133. st.markdown(f.read())
  134. except:
  135. st.write('Wrong start directory, see readme')
  136. # file upload button
  137. uploaded_file = st.file_uploader(
  138. 'Upload a file from your vector analizer. \
  139. Make sure the file format is .snp or it has a similar inner structure.'
  140. )
  141. # check .snp
  142. is_data_format_snp = False
  143. data_format_snp_number = 0
  144. if uploaded_file is None:
  145. st.write("DEMO: ")
  146. # display DEMO
  147. is_data_format_snp = True
  148. try:
  149. with open('./resource/data/8_default_demo.s1p') as f:
  150. data = f.readlines()
  151. except:
  152. # 'streamlit run' call in the wrong directory. Display smaller demo:
  153. data = [
  154. '# Hz S MA R 50\n\
  155. 11415403125 0.37010744 92.47802\n\
  156. 11416090625 0.33831283 92.906929\n\
  157. 11416778125 0.3069371 94.03318'
  158. ]
  159. else:
  160. data = uploaded_file.readlines()
  161. if uploaded_file.name[-4:-2] == '.s' and uploaded_file.name[-1] == 'p':
  162. is_data_format_snp = True
  163. data_format_snp_number = int(uploaded_file.name[-2])
  164. validator_status = '...'
  165. column_count = 0
  166. # data loaded
  167. circle_params = []
  168. if len(data) > 0:
  169. validator_status = read_data(data)
  170. if validator_status == 'data read, but not parsed':
  171. hz, select_measurement_parameter, select_data_representation, input_ref_resistance = parse_snp_header(
  172. data, is_data_format_snp)
  173. col1, col2 = st.columns([1, 2])
  174. ace_text_value = ''.join(data).strip()
  175. with col1.expander("Processing options"):
  176. select_measurement_parameter = st.selectbox(
  177. 'Measurement parameter', ['S', 'Z'],
  178. select_measurement_parameter)
  179. select_data_representation = st.selectbox(
  180. 'Data representation', [
  181. 'Frequency, real, imaginary',
  182. 'Frequency, magnitude, angle', 'Frequency, db, angle'
  183. ], select_data_representation)
  184. if select_measurement_parameter == 'Z':
  185. input_ref_resistance = st.number_input(
  186. "Reference resistance:",
  187. min_value=0,
  188. value=input_ref_resistance)
  189. if not is_data_format_snp:
  190. input_hz = st.selectbox('Unit of frequency',
  191. ['Hz', 'KHz', 'MHz', 'GHz'], 0)
  192. hz_map = {
  193. "ghz": 10**9,
  194. "mhz": 10**6,
  195. "khz": 10**3,
  196. "hz": 1
  197. }
  198. hz = hz_map[input_hz.lower()]
  199. input_start_line = int(
  200. st.number_input("First line for processing:",
  201. min_value=1,
  202. max_value=len(data)))
  203. input_end_line = int(
  204. st.number_input("Last line for processing:",
  205. min_value=1,
  206. max_value=len(data),
  207. value=len(data)))
  208. if input_end_line < input_start_line:
  209. input_end_line=input_start_line
  210. data = data[input_start_line - 1:input_end_line]
  211. # Ace editor to show choosen data columns and rows
  212. with col2.expander("File preview"):
  213. # So little 'official' functionality in libs and lack of documentation
  214. # therefore beware: css hacks
  215. # yellow ~ ace_step
  216. # light yellow ~ ace_highlight-marker
  217. # green ~ ace_stack
  218. # red ~ ace_error-marker
  219. # no more good colors included in streamlit_ace for marking
  220. # st.markdown('''<style>
  221. # .choosen_option_1
  222. # {
  223. # color: rgb(49, 51, 63);
  224. # }</style>''', unsafe_allow_html=True)
  225. # markdown injection does not seems to work,
  226. # since ace is in a different .html accessible via iframe
  227. # markers format:
  228. #[{"startRow": 2,"startCol": 0,"endRow": 2,"endCol": 3,"className": "ace_error-marker","type": "text"}]
  229. # add marking for choosen data lines?
  230. # todo or not todo?
  231. ace_preview_markers =[{
  232. "startRow": input_start_line - 1,
  233. "startCol": 0,
  234. "endRow": input_end_line,
  235. "endCol": 0,
  236. "className": "ace_highlight-marker",
  237. "type": "text"
  238. }]
  239. st_ace(value=ace_text_value,
  240. readonly=True,
  241. auto_update=True,
  242. placeholder="Your file is empty",
  243. markers=ace_preview_markers,
  244. height="300px")
  245. if is_data_format_snp and data_format_snp_number >= 3:
  246. data, validator_status = prepare_snp(data,
  247. data_format_snp_number)
  248. if validator_status == "data read, but not parsed":
  249. column_count, validator_status = count_columns(data)
  250. f, r, i = [], [], []
  251. pair_count = 1
  252. if validator_status == "data parsed":
  253. input_ports_pair = 1
  254. if column_count > 3:
  255. pair_count = (column_count - 1) // 2
  256. input_ports_pair = st.number_input(
  257. "Choose pair of ports with network parameters:",
  258. min_value=1,
  259. max_value=pair_count,
  260. value=1)
  261. input_ports_pair_id = input_ports_pair - 1
  262. ports_count = round(pair_count**0.5)
  263. st.write('Choosen ports: ' + select_measurement_parameter +
  264. str(input_ports_pair_id // ports_count + 1) +
  265. str(input_ports_pair_id % ports_count + 1))
  266. f, r, i, validator_status = unpack_data(data,
  267. (input_ports_pair - 1) * 2 + 1,
  268. column_count,
  269. input_ref_resistance,
  270. select_measurement_parameter,
  271. select_data_representation)
  272. f = [x * hz for x in f] # to hz
  273. st.write("Use range slider to choose best suitable data interval")
  274. if len(f)==0:
  275. validator_status = 'data unpacking error: empty data'
  276. else:
  277. # make accessible a specific range of numerical data choosen with interactive plot
  278. # line id, line id
  279. interval_start, interval_end = plot_interact_abs_from_f(f,r,i)
  280. f_cut, r_cut, i_cut = [], [], []
  281. if validator_status == "data parsed":
  282. f_cut, r_cut, i_cut = (x[interval_start:interval_end]
  283. for x in (f, r, i))
  284. # show cutting result
  285. with st.expander("Selected data interval as .s1p"):
  286. st_ace(value="# Hz S RI R 50\n" +
  287. ''.join(f'{f_cut[x]} {r_cut[x]} {i_cut[x]}\n'
  288. for x in range(len(f_cut))),
  289. readonly=True,
  290. auto_update=True,
  291. placeholder="Selection is empty",
  292. height="150px")
  293. # can't find a circle without 3 points
  294. if len(f_cut) < 3:
  295. validator_status = "Choosen interval is too small, add more points"
  296. st.write("Status: " + validator_status)
  297. if validator_status == "data parsed":
  298. col1, col2 = st.columns(2)
  299. check_coupling_loss = col1.checkbox(
  300. 'Apply correction for coupling losses', value = False)
  301. if check_coupling_loss:
  302. col1.write("Option: Lossy coupling")
  303. else:
  304. col1.write("Option: Cable attenuation")
  305. select_autoformat = col2.checkbox("Autoformat output", value=True)
  306. precision = '0.0f'
  307. if not select_autoformat:
  308. precision = col2.slider("Precision",
  309. min_value=0,
  310. max_value=7,
  311. value=4)
  312. precision = '0.' + str(precision) + 'f'
  313. Q0, sigmaQ0, QL, sigmaQL, k, ks, circle_params, fl, fitted_mag_s = calc_function(
  314. f_cut, r_cut, i_cut, check_coupling_loss)
  315. if Q0 <= 0 or QL <= 0:
  316. st.write("Negative Q detected, fitting may be inaccurate!")
  317. def show_result_in_latex(name, value, uncertainty=None):
  318. nonlocal select_autoformat
  319. if uncertainty is not None:
  320. if select_autoformat:
  321. st.latex(
  322. name + ' =' +
  323. f'{sigfig.round(value, uncertainty=uncertainty, style="PDG")}, '
  324. + r'\;\;\varepsilon_{' + name + '} =' +
  325. f'{sigfig.round(uncertainty / value, sigfigs=1, style="PDG")}'
  326. )
  327. else:
  328. st.latex(name + ' =' + f'{format(value, precision)} \pm ' +
  329. f'{format(uncertainty, precision)}, ' +
  330. r'\;\;\varepsilon_{' + name + '} =' +
  331. f'{format(uncertainty / value, precision)}')
  332. else:
  333. if select_autoformat:
  334. st.latex(name + ' =' +
  335. f'{sigfig.round(value, sigfigs=5, style="PDG")}')
  336. else:
  337. st.latex(name + ' =' + f'{format(value, precision)}')
  338. show_result_in_latex('Q_0', Q0, sigmaQ0)
  339. show_result_in_latex('Q_L', QL, sigmaQL)
  340. show_result_in_latex(r'\kappa', k)
  341. if check_coupling_loss:
  342. show_result_in_latex(r'\kappa_s', ks)
  343. st.latex('f_L =' + f'{format(fl, precision)}' + r'\text{ }Hz')
  344. # in case of .snp with n>=2
  345. if pair_count >= 4:
  346. if 'make_transmission_measurement' not in st.session_state:
  347. st.session_state['make_transmission_measurement'] = False
  348. st.session_state['make_transmission_measurement'] = st.checkbox(
  349. "Make transmission measurement on S11 & S22",
  350. value=st.session_state['make_transmission_measurement'])
  351. if st.session_state['make_transmission_measurement']:
  352. def find_k(port_pair:int):
  353. f, r, i, validator_status = unpack_data(data,
  354. (port_pair - 1) * 2 + 1,
  355. column_count,
  356. input_ref_resistance,
  357. select_measurement_parameter,
  358. select_data_representation)
  359. if validator_status!='data parsed':
  360. st.write(validator_status)
  361. return 0,0
  362. else:
  363. f = [x * hz for x in f]
  364. f, r, i = (x[interval_start:interval_end]
  365. for x in (f, r, i))
  366. Q0, sigmaQ0, QL, sigmaQL, k, ks, circle_params, fl, fitted_mag_s = calc_function(
  367. f, r, i, check_coupling_loss)
  368. return QL,k
  369. # S11
  370. QL1,k1=find_k(1)
  371. # S22
  372. QL2,k2=find_k(4)
  373. QL=(QL1+QL2)/2
  374. k_in = k1*(1+k2)/(1-k1*k2)
  375. k_out = k2*(1+k1)/(1-k1*k2)
  376. k=k_in+k_out
  377. Q0 = QL*(1+k)
  378. if Q0 <= 0 or QL <= 0:
  379. st.write("Negative Q detected, fitting may be inaccurate!")
  380. show_result_in_latex('Q_0', Q0)
  381. show_result_in_latex('Q_L', QL)
  382. show_result_in_latex(r'\kappa', k)
  383. with st.expander("Show static abs(S) plot"):
  384. plot_abs_vs_f(f_cut, r_cut, i_cut, fitted_mag_s)
  385. plot_smith(r, i, circle_params, r_cut, i_cut)