front.py 14 KB

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