front.py 15 KB

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