front.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import math
  2. from time import perf_counter
  3. import streamlit as st
  4. import matplotlib.pyplot as plt
  5. import numpy as np
  6. import sigfig
  7. from streamlit_ace import st_ace
  8. from .show_echart import plot_interact_abs_from_f
  9. from .data_parsing_utils import is_float, read_data, check_line_comments, count_columns, prepare_snp
  10. def circle(ax, x, y, radius, color='#1946BA'):
  11. from matplotlib.patches import Ellipse
  12. drawn_circle = Ellipse((x, y),
  13. radius * 2,
  14. radius * 2,
  15. clip_on=True,
  16. zorder=2,
  17. linewidth=2,
  18. edgecolor=color,
  19. facecolor=(0, 0, 0, .0))
  20. ax.add_artist(drawn_circle)
  21. def plot_smith(r, i, g, r_cut, i_cut):
  22. from matplotlib.image import AxesImage
  23. show_excluded_points = True
  24. show_Abs_S_scale = False
  25. show_Re_Z_scale = False
  26. show_Im_Z_scale = False
  27. show_grid = True
  28. with st.expander("Smith chart options"):
  29. show_excluded_points = st.checkbox("Show excluded points",
  30. value=show_excluded_points)
  31. show_grid = st.checkbox("Show grid", value=show_grid)
  32. show_Abs_S_scale = st.checkbox("Show |S| gridlines",
  33. value=show_Abs_S_scale)
  34. show_Re_Z_scale = st.checkbox("Show Re(Z) gridlines",
  35. value=show_Re_Z_scale)
  36. show_Im_Z_scale = st.checkbox("Show Im(Z) gridlines",
  37. value=show_Im_Z_scale)
  38. fig = plt.figure(figsize=(10, 10))
  39. ax = fig.add_subplot()
  40. # major_ticks = np.arange(-1.0, 1.1, 0.25)
  41. minor_ticks = np.arange(-1.1, 1.1, 0.05)
  42. # ax.set_xticks(major_ticks)
  43. ax.set_xticks(minor_ticks, minor=True)
  44. # ax.set_yticks(major_ticks)
  45. ax.set_yticks(minor_ticks, minor=True)
  46. ax.grid(which='major', color='grey', linewidth=1.5)
  47. ax.grid(which='minor', color='grey', linewidth=0.5, linestyle=':')
  48. plt.xlabel('$Re(S)$', color='gray', fontsize=16, fontname="Cambria")
  49. plt.ylabel('$Im(S)$', color='gray', fontsize=16, fontname="Cambria")
  50. plt.title('Smith chart', fontsize=24, fontname="Cambria")
  51. # unit circle
  52. circle(ax, 0, 0, 1)
  53. if not show_grid:
  54. ax.axis('off')
  55. background_img_x = -1.981
  56. background_img_y = -1.949
  57. background_img_box = [
  58. background_img_x,
  59. background_img_x + 3.87,
  60. background_img_y,
  61. background_img_y + 3.87
  62. ]
  63. if show_Abs_S_scale:
  64. # imshow is extremely slow
  65. # TODO draw primitives in place
  66. background = plt.imread("./source/frontend/images/s.png")
  67. background = ax.imshow(background, extent=background_img_box, interpolation= 'none')
  68. if show_Re_Z_scale:
  69. background = plt.imread("./source/frontend/images/re(z).png")
  70. background = ax.imshow(background, extent=background_img_box, interpolation= 'none')
  71. if show_Im_Z_scale:
  72. background = plt.imread("./source/frontend/images/im(z).png")
  73. background = ax.imshow(background, extent=background_img_box, interpolation= 'none')
  74. # input data points
  75. if show_excluded_points:
  76. ax.plot(r, i, '+', ms=8, mew=2, color='#b6c7f4')
  77. # choosen data points
  78. ax.plot(r_cut, i_cut, '+', ms=8, mew=2, color='#1946BA')
  79. # circle approximation by calc
  80. radius = abs(g[1] - g[0] / g[2]) / 2
  81. x = ((g[1] + g[0] / g[2]) / 2).real
  82. y = ((g[1] + g[0] / g[2]) / 2).imag
  83. circle(ax, x, y, radius, color='#FF8400')
  84. XLIM = [-1.3, 1.3]
  85. YLIM = [-1.3, 1.3]
  86. ax.set_xlim(XLIM)
  87. ax.set_ylim(YLIM)
  88. st.pyplot(fig)
  89. # plot abs(S) vs f chart with pyplot
  90. def plot_abs_vs_f(f, r, i, fitted_mag_s):
  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.min(np.where(s==0, np.inf, s))
  95. s = list(20*np.where(s==0, np.log10(m), np.log10(s)))
  96. m = np.min(np.where(s==0, np.inf, fitted_mag_s))
  97. fitted_mag_s = list(20*np.where(s==0, np.log10(m), np.log10(fitted_mag_s)))
  98. s = list(s)
  99. min_f = min(f)
  100. max_f = max(f)
  101. xlim = [
  102. min_f - abs(max_f - min_f) * 0.1,
  103. max_f + abs(max_f - min_f) * 0.1
  104. ]
  105. min_s = min(s)
  106. max_s = max(s)
  107. print(min_s,max_s)
  108. ylim = [
  109. min_s - abs(max_s - min_s) * 0.5,
  110. max_s + abs(max_s - min_s) * 0.5
  111. ]
  112. ax = fig.add_subplot()
  113. ax.set_xlim(xlim)
  114. ax.set_ylim(ylim)
  115. ax.grid(which='major', color='k', linewidth=1)
  116. ax.grid(which='minor', color='grey', linestyle=':', linewidth=0.5)
  117. plt.xlabel(r'$f,\; 1/c$', color='gray', fontsize=16, fontname="Cambria")
  118. if st.session_state.legendselection == '|S| (dB)':
  119. plt.ylabel('$|S|$ (dB)', color='gray', fontsize=16, fontname="Cambria")
  120. plt.title('|S| (dB) vs frequency', fontsize=24, fontname="Cambria")
  121. else:
  122. plt.ylabel('$|S|$', color='gray', fontsize=16, fontname="Cambria")
  123. plt.title('|S| vs frequency', fontsize=24, fontname="Cambria")
  124. ax.plot(f, s, '+', ms=8, mew=2, color='#1946BA')
  125. ax.plot(f, fitted_mag_s, '-', linewidth=3, color='#FF8400')
  126. st.pyplot(fig)
  127. def run(calc_function):
  128. # for Touchstone .snp format
  129. def parse_heading(data):
  130. nonlocal data_format_snp
  131. if data_format_snp:
  132. for x in range(len(data)):
  133. if data[x].lstrip()[0] == '#':
  134. line = data[x].split()
  135. if len(line) == 6:
  136. repr_map = {"ri": 0, "ma": 1, "db": 2}
  137. para_map = {"s": 0, "z": 1}
  138. hz_map = {
  139. "ghz": 10**9,
  140. "mhz": 10**6,
  141. "khz": 10**3,
  142. "hz": 1
  143. }
  144. hz, measurement_parameter, data_representation, _r, ref_resistance = (
  145. x.lower() for x in line[1:])
  146. try:
  147. return hz_map[hz], para_map[
  148. measurement_parameter], repr_map[
  149. data_representation], int(
  150. float(ref_resistance))
  151. except:
  152. break
  153. break
  154. return 1, 0, 0, 50
  155. def unpack_data(data, first_column, column_count, ref_resistance,
  156. ace_preview_markers):
  157. nonlocal select_measurement_parameter
  158. nonlocal select_data_representation
  159. f, r, i = [], [], []
  160. return_status = 'data parsed'
  161. for x in range(len(data)):
  162. line = check_line_comments(data[x])
  163. if line is None:
  164. continue
  165. line = line.split()
  166. if column_count != len(line):
  167. return_status = "Wrong number of parameters on line № " + str(
  168. x)
  169. break
  170. # 1: process according to data_placement
  171. a, b, c = None, None, None
  172. try:
  173. a = line[0]
  174. b = line[first_column]
  175. c = line[first_column + 1]
  176. except:
  177. return_status = 'Can\'t parse line №: ' + \
  178. str(x) + ',\n not enough arguments'
  179. break
  180. if not ((is_float(a)) or (is_float(b)) or (is_float(c))):
  181. return_status = 'Wrong data type, expected number. Error on line: ' + \
  182. str(x)
  183. break
  184. # mark as processed
  185. # for y in (a,b,c):
  186. # ace_preview_markers.append(
  187. # {"startRow": x,"startCol": 0,
  188. # "endRow": x,"endCol": data[x].find(y)+len(y),
  189. # "className": "ace_stack","type": "text"})
  190. a, b, c = (float(x) for x in (a, b, c))
  191. f.append(a) # frequency
  192. # 2: process according to data_representation
  193. if select_data_representation == 'Frequency, real, imaginary':
  194. # std format
  195. r.append(b) # Re
  196. i.append(c) # Im
  197. elif select_data_representation == 'Frequency, magnitude, angle':
  198. r.append(b * np.cos(np.deg2rad(c)))
  199. i.append(b * np.sin(np.deg2rad(c)))
  200. elif select_data_representation == 'Frequency, db, angle':
  201. b = 10**(b / 20)
  202. r.append(b * np.cos(np.deg2rad(c)))
  203. i.append(b * np.sin(np.deg2rad(c)))
  204. else:
  205. return_status = 'Wrong data format'
  206. break
  207. # 3: process according to measurement_parameter
  208. if select_measurement_parameter == 'Z':
  209. # normalization
  210. r[-1] = r[-1] / ref_resistance
  211. i[-1] = i[-1] / ref_resistance
  212. # translate to S
  213. try:
  214. # center_x + 1j*center_y, radius
  215. p1, r1 = r[-1] / (1 + r[-1]) + 0j, 1 / (1 + r[-1]) #real
  216. p2, r2 = 1 + 1j * (1 / i[-1]), 1 / i[-1] #imag
  217. d = abs(p2 - p1)
  218. q = (r1**2 - r2**2 + d**2) / (2 * d)
  219. h = (r1**2 - q**2)**0.5
  220. p = p1 + q * (p2 - p1) / d
  221. intersect = [(p.real + h * (p2.imag - p1.imag) / d,
  222. p.imag - h * (p2.real - p1.real) / d),
  223. (p.real - h * (p2.imag - p1.imag) / d,
  224. p.imag + h * (p2.real - p1.real) / d)]
  225. intersect = [x + 1j * y for x, y in intersect]
  226. intersect_shift = [p - (1 + 0j) for p in intersect]
  227. intersect_shift = abs(np.array(intersect_shift))
  228. p = intersect[0]
  229. if intersect_shift[0] < intersect_shift[1]:
  230. p = intersect[1]
  231. r[-1] = p.real
  232. i[-1] = p.imag
  233. except:
  234. r.pop()
  235. i.pop()
  236. f.pop()
  237. if return_status == 'data parsed':
  238. if len(f) < 3 or len(f) != len(r) or len(f) != len(i):
  239. return_status = 'Choosen data range is too small, add more points'
  240. elif max(abs(np.array(r) + 1j * np.array(i))) > 2:
  241. return_status = 'Your data points have an abnormality:\
  242. they are too far outside the unit cirlce.\
  243. Make sure the format is correct'
  244. return f, r, i, return_status
  245. # info
  246. with st.expander("Info"):
  247. # streamlit.markdown does not support footnotes
  248. try:
  249. with open('./source/frontend/info.md') as f:
  250. st.markdown(f.read())
  251. except:
  252. st.write('Wrong start directory, see readme')
  253. # file upload button
  254. uploaded_file = st.file_uploader(
  255. 'Upload a file from your vector analizer. \
  256. Make sure the file format is .snp or it has a similar inner structure.'
  257. )
  258. # check .snp
  259. data_format_snp = False
  260. data_format_snp_number = 0
  261. if uploaded_file is None:
  262. st.write("DEMO: ")
  263. # display DEMO
  264. data_format_snp = True
  265. try:
  266. with open('./resource/data/8_default_demo.s1p') as f:
  267. data = f.readlines()
  268. except:
  269. # 'streamlit run' call in the wrong directory. Display smaller demo:
  270. data = [
  271. '# Hz S MA R 50\n\
  272. 11415403125 0.37010744 92.47802\n\
  273. 11416090625 0.33831283 92.906929\n\
  274. 11416778125 0.3069371 94.03318'
  275. ]
  276. else:
  277. data = uploaded_file.readlines()
  278. if uploaded_file.name[-4:-2] == '.s' and uploaded_file.name[-1] == 'p':
  279. data_format_snp = True
  280. data_format_snp_number = int(uploaded_file.name[-2])
  281. validator_status = '...'
  282. ace_preview_markers = []
  283. column_count = 0
  284. # data loaded
  285. circle_params = []
  286. if len(data) > 0:
  287. validator_status = read_data(data)
  288. if validator_status == 'data read, but not parsed':
  289. hz, select_measurement_parameter, select_data_representation, input_ref_resistance = parse_heading(
  290. data)
  291. col1, col2 = st.columns([1, 2])
  292. ace_text_value = ''.join(data).strip()
  293. with col1.expander("Processing options"):
  294. select_measurement_parameter = st.selectbox(
  295. 'Measurement parameter', ['S', 'Z'],
  296. select_measurement_parameter)
  297. select_data_representation = st.selectbox(
  298. 'Data representation', [
  299. 'Frequency, real, imaginary',
  300. 'Frequency, magnitude, angle', 'Frequency, db, angle'
  301. ], select_data_representation)
  302. if select_measurement_parameter == 'Z':
  303. input_ref_resistance = st.number_input(
  304. "Reference resistance:",
  305. min_value=0,
  306. value=input_ref_resistance)
  307. if not data_format_snp:
  308. input_hz = st.selectbox('Unit of frequency',
  309. ['Hz', 'KHz', 'MHz', 'GHz'], 0)
  310. hz_map = {
  311. "ghz": 10**9,
  312. "mhz": 10**6,
  313. "khz": 10**3,
  314. "hz": 1
  315. }
  316. hz = hz_map[input_hz.lower()]
  317. input_start_line = int(
  318. st.number_input("First line for processing:",
  319. min_value=1,
  320. max_value=len(data)))
  321. input_end_line = int(
  322. st.number_input("Last line for processing:",
  323. min_value=1,
  324. max_value=len(data),
  325. value=len(data)))
  326. data = data[input_start_line - 1:input_end_line]
  327. # Ace editor to show choosen data columns and rows
  328. with col2.expander("File preview"):
  329. # So little 'official' functionality in libs and lack of documentation
  330. # therefore beware: css hacks
  331. # yellow ~ ace_step
  332. # light yellow ~ ace_highlight-marker
  333. # green ~ ace_stack
  334. # red ~ ace_error-marker
  335. # no more good colors included in streamlit_ace for marking
  336. # st.markdown('''<style>
  337. # .choosen_option_1
  338. # {
  339. # color: rgb(49, 51, 63);
  340. # }</style>''', unsafe_allow_html=True)
  341. # markdown injection does not seems to work,
  342. # since ace is in a different .html accessible via iframe
  343. # markers format:
  344. #[{"startRow": 2,"startCol": 0,"endRow": 2,"endCol": 3,"className": "ace_error-marker","type": "text"}]
  345. # add marking for choosen data lines?
  346. # todo or not todo?
  347. ace_preview_markers.append({
  348. "startRow": input_start_line - 1,
  349. "startCol": 0,
  350. "endRow": input_end_line,
  351. "endCol": 0,
  352. "className": "ace_highlight-marker",
  353. "type": "text"
  354. })
  355. st_ace(value=ace_text_value,
  356. readonly=True,
  357. auto_update=True,
  358. placeholder="Your file is empty",
  359. markers=ace_preview_markers,
  360. height="300px")
  361. if data_format_snp and data_format_snp_number >= 3:
  362. data, validator_status = prepare_snp(data,
  363. data_format_snp_number)
  364. if validator_status == "data read, but not parsed":
  365. column_count, validator_status = count_columns(data)
  366. f, r, i = [], [], []
  367. if validator_status == "data parsed":
  368. input_ports_pair = 1
  369. if column_count > 3:
  370. pair_count = (column_count - 1) // 2
  371. input_ports_pair = st.number_input(
  372. "Choose pair of ports with network parameters:",
  373. min_value=1,
  374. max_value=pair_count,
  375. value=1)
  376. input_ports_pair_id = input_ports_pair - 1
  377. ports_count = round(pair_count**0.5)
  378. st.write('Choosen ports: ' + select_measurement_parameter +
  379. str(input_ports_pair_id // ports_count + 1) +
  380. str(input_ports_pair_id % ports_count + 1))
  381. f, r, i, validator_status = unpack_data(data,
  382. (input_ports_pair - 1) * 2 + 1,
  383. column_count,
  384. input_ref_resistance,
  385. ace_preview_markers)
  386. f = [x * hz for x in f] # to hz
  387. st.write("Use range slider to choose best suitable data interval")
  388. # make accessible a specific range of numerical data choosen with interactive plot
  389. # line id, line id
  390. interval_start, interval_end = plot_interact_abs_from_f(f,r,i)
  391. # plot_interact_abs_from_f( f, r, i, interval_range)
  392. f_cut, r_cut, i_cut = [], [], []
  393. if validator_status == "data parsed":
  394. f_cut, r_cut, i_cut = (x[interval_start:interval_end]
  395. for x in (f, r, i))
  396. with st.expander("Selected data interval as .s1p"):
  397. st_ace(value="# Hz S RI R 50\n" +
  398. ''.join(f'{f_cut[x]} {r_cut[x]} {i_cut[x]}\n'
  399. for x in range(len(f_cut))),
  400. readonly=True,
  401. auto_update=True,
  402. placeholder="Selection is empty",
  403. height="150px")
  404. if len(f_cut) < 3:
  405. validator_status = "Choosen interval is too small, add more points"
  406. st.write("Status: " + validator_status)
  407. if validator_status == "data parsed":
  408. col1, col2 = st.columns(2)
  409. check_coupling_loss = col1.checkbox(
  410. 'Apply correction for coupling losses')
  411. if check_coupling_loss:
  412. col1.write("Option: Lossy coupling")
  413. else:
  414. col1.write("Option: Cable attenuation")
  415. select_autoformat = col2.checkbox("Autoformat output", value=True)
  416. precision = '0.0f'
  417. if not select_autoformat:
  418. precision = col2.slider("Precision",
  419. min_value=0,
  420. max_value=7,
  421. value=4)
  422. precision = '0.' + str(precision) + 'f'
  423. Q0, sigmaQ0, QL, sigmaQL, k, ks, circle_params, fl, fitted_mag_s = calc_function(
  424. f_cut, r_cut, i_cut, check_coupling_loss)
  425. if Q0 <= 0 or QL <= 0:
  426. st.write("Negative Q detected, fitting may be inaccurate!")
  427. def show_result_in_latex(name, value, uncertainty=None):
  428. nonlocal select_autoformat
  429. if uncertainty is not None:
  430. if select_autoformat:
  431. st.latex(
  432. name + ' =' +
  433. f'{sigfig.round(value, uncertainty=uncertainty, style="PDG")}, '
  434. + r'\;\;\varepsilon_{' + name + '} =' +
  435. f'{sigfig.round(uncertainty / value, sigfigs=1, style="PDG")}'
  436. )
  437. else:
  438. st.latex(name + ' =' + f'{format(value, precision)} \pm ' +
  439. f'{format(uncertainty, precision)}, ' +
  440. r'\;\;\varepsilon_{' + name + '} =' +
  441. f'{format(uncertainty / value, precision)}')
  442. else:
  443. if select_autoformat:
  444. st.latex(name + ' =' +
  445. f'{sigfig.round(value, sigfigs=5, style="PDG")}')
  446. else:
  447. st.latex(name + ' =' + f'{format(value, precision)}')
  448. show_result_in_latex('Q_0', Q0, sigmaQ0)
  449. show_result_in_latex('Q_L', QL, sigmaQL)
  450. show_result_in_latex(r'\kappa', k)
  451. if check_coupling_loss:
  452. show_result_in_latex(r'\kappa_s', ks)
  453. st.latex('f_L =' + f'{format(fl, precision)}' + r'\text{ }Hz')
  454. with st.expander("Show static abs(S) plot"):
  455. plot_abs_vs_f(f_cut, r_cut, i_cut, fitted_mag_s)
  456. t1= perf_counter()
  457. plot_smith(r, i, circle_params, r_cut, i_cut)
  458. print(perf_counter()-t1)