front.py 15 KB

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