front.py 24 KB

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