front.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. from streamlit_ace import st_ace
  2. from streamlit_echarts import st_echarts
  3. import math
  4. import streamlit as st
  5. import matplotlib.pyplot as plt
  6. import numpy as np
  7. XLIM = [-1.1, 1.1]
  8. YLIM = [-1.1, 1.1]
  9. def circle(ax, x, y, radius, color='#1946BA'):
  10. from matplotlib.patches import Ellipse
  11. drawn_circle = Ellipse((x, y), radius * 2, radius * 2, clip_on=False,
  12. zorder=2, linewidth=2, edgecolor=color, facecolor=(0, 0, 0, .0125))
  13. ax.add_artist(drawn_circle)
  14. def plot_data(r, i, g):
  15. fig = plt.figure(figsize=(10, 10))
  16. ax = fig.add_subplot()
  17. major_ticks = np.arange(-1.0, 1.1, 0.25)
  18. minor_ticks = np.arange(-1.1, 1.1, 0.05)
  19. ax.set_xticks(major_ticks)
  20. ax.set_xticks(minor_ticks, minor=True)
  21. ax.set_yticks(major_ticks)
  22. ax.set_yticks(minor_ticks, minor=True)
  23. ax.grid(which='major', color='grey', linewidth=1.5)
  24. ax.grid(which='minor', color='grey', linewidth=0.5, linestyle=':')
  25. plt.xlabel(r'$Re(\Gamma)$', color='gray', fontsize=16, fontname="Cambria")
  26. plt.ylabel('$Im(\Gamma)$', color='gray', fontsize=16, fontname="Cambria")
  27. plt.title('Smith chart', fontsize=24, fontname="Cambria")
  28. # circle approximation
  29. radius = abs(g[1] - g[0] / g[2]) / 2
  30. x = ((g[1] + g[0] / g[2]) / 2).real
  31. y = ((g[1] + g[0] / g[2]) / 2).imag
  32. circle(ax, x, y, radius, color='#FF8400')
  33. #
  34. # unit circle
  35. circle(ax, 0, 0, 1)
  36. #
  37. # data
  38. ax.plot(r, i, '+', ms=10, mew=2, color='#1946BA')
  39. #
  40. ax.set_xlim(XLIM)
  41. ax.set_ylim(YLIM)
  42. try:
  43. st.pyplot(fig)
  44. except:
  45. st.write("Plot size is too big. There are some problems with fitting. Check your input")
  46. interval_range = (0, 100)
  47. interval_start, interval_end = 0, 0
  48. def plot_interact_abs_from_f(f, r, i):
  49. abs_S = list((r[n] ** 2 + i[n] ** 2)**0.5 for n in range(len(r)))
  50. global interval_range, interval_start, interval_end
  51. # echarts for datazoom https://discuss.streamlit.io/t/streamlit-echarts/3655
  52. # datazoom https://echarts.apache.org/examples/en/editor.html?c=line-draggable&lang=ts
  53. # axis pointer values https://echarts.apache.org/en/option.html#axisPointer
  54. options = {
  55. "xAxis": {
  56. "type": "category",
  57. "data": f,
  58. "name": "Hz",
  59. "nameTextStyle": {"fontSize": 16},
  60. "axisLabel": {"fontSize": 16}
  61. },
  62. "yAxis": {
  63. "type": "value",
  64. "name": "abs(S)",
  65. "nameTextStyle": {"fontSize": 16},
  66. "axisLabel": {"fontSize": 16}
  67. },
  68. "series": [{"data": abs_S, "type": "line", "name": "abs(S)"}],
  69. "height": 300,
  70. "dataZoom": [{"type": "slider", "start": 0, "end": 100, "height": 100, "bottom": 10}],
  71. "tooltip": {
  72. "trigger": "axis",
  73. "axisPointer": {
  74. "type": 'cross',
  75. # "label": {
  76. # "show":"true",
  77. # "formatter": JsCode(
  78. # "function(info){return info.value;};"
  79. # ).js_code
  80. # }
  81. }
  82. },
  83. "toolbox": {
  84. "feature": {
  85. # "dataView": { "show": "true", "readOnly": "true" },
  86. "restore": {"show": "true"},
  87. }
  88. },
  89. }
  90. events = {
  91. "dataZoom": "function(params) { return [params.start, params.end] }",
  92. }
  93. interval_range = st_echarts(
  94. options=options, events=events, height="500px", key="render_basic_bar_events"
  95. )
  96. if interval_range is None:
  97. interval_range = (0, 100)
  98. n = len(f)
  99. interval_start, interval_end = (
  100. int(n*interval_range[id]*0.01) for id in (0, 1))
  101. def plot_ref_from_f(f, r, i):
  102. fig = plt.figure(figsize=(10, 10))
  103. abs_S = list((r[n] ** 2 + i[n] ** 2)**0.5 for n in range(len(r)))
  104. xlim = [min(f) - abs(max(f) - min(f)) * 0.1,
  105. max(f) + abs(max(f) - min(f)) * 0.1]
  106. ylim = [min(abs_S) - abs(max(abs_S) - min(abs_S)) * 0.5,
  107. max(abs_S) + abs(max(abs_S) - min(abs_S)) * 0.5]
  108. ax = fig.add_subplot()
  109. ax.set_xlim(xlim)
  110. ax.set_ylim(ylim)
  111. ax.grid(which='major', color='k', linewidth=1)
  112. ax.grid(which='minor', color='grey', linestyle=':', linewidth=0.5)
  113. plt.xlabel(r'$f,\; 1/c$', color='gray', fontsize=16, fontname="Cambria")
  114. plt.ylabel('$|S|$', color='gray', fontsize=16, fontname="Cambria")
  115. plt.title('Absolute value of reflection coefficient from frequency',
  116. fontsize=24, fontname="Cambria")
  117. ax.plot(f, abs_S, '+', ms=10, mew=2, color='#1946BA')
  118. st.pyplot(fig)
  119. def run(calc_function):
  120. def is_float(element) -> bool:
  121. try:
  122. float(element)
  123. val = float(element)
  124. if math.isnan(val) or math.isinf(val):
  125. raise ValueError
  126. return True
  127. except ValueError:
  128. return False
  129. # to utf-8
  130. def read_data(data):
  131. for x in range(len(data)):
  132. if type(data[x]) == bytes:
  133. try:
  134. data[x] = data[x].decode('utf-8-sig', 'ignore')
  135. except:
  136. return 'Not an utf-8-sig line №: ' + str(x)
  137. return 'data read: success'
  138. # for Touchstone .snp format
  139. def parse_heading(data):
  140. nonlocal data_format_snp
  141. if data_format_snp:
  142. for x in range(len(data)):
  143. if data[x][0]=='#':
  144. line = data[x].split()
  145. if len(line)== 6:
  146. repr_map = {"RI":0,"MA":1, "DB":2}
  147. para_map = {"S":0,"Z":1}
  148. hz_map = {"GHz":10**9,"MHz":10**6,"KHz":10**3,"Hz":1}
  149. hz,measurement_parameter,data_representation,_r,ref_resistance=line[1:]
  150. try:
  151. return hz_map[hz], para_map[measurement_parameter], repr_map[data_representation], ref_resistance
  152. except:
  153. break
  154. break
  155. return 1, 0, 0, 50
  156. def unpack_data(data, input_start_line, input_end_line):
  157. nonlocal select_measurement_parameter
  158. nonlocal select_data_representation
  159. f, r, i = [], [], []
  160. return_status='data parsed'
  161. for x in range(input_start_line-1, input_end_line):
  162. if len(data[x])<2 or data[x][0]== '!' or data[x][0]=='#' or data[x][0]=='%' or data[x][0]=='/':
  163. # first is a comment line according to .snp documentation,
  164. # others detects comments in various languages
  165. continue
  166. data[x] = data[x].replace(';', ' ').replace(',', ' ')
  167. line = data[x].split()
  168. # always at least 3 values for single data point
  169. if len(line) < 3:
  170. return_status = 'Can\'t parse line №: ' + str(x) + ',\n not enough arguments (less than 3)'
  171. break
  172. a,b,c=[],[],[]
  173. try:
  174. a, b, c = (line[y] for y in range(min(len(line),3)))
  175. # for x in input_data_columns.keys():
  176. # if x=='f':
  177. # elif x=='r':
  178. # elif x=='i':
  179. except:
  180. return_status = 'Can\'t parse line №: ' + str(x) + ',\n not enough arguments'
  181. break
  182. if not ((is_float(a)) or (is_float(b)) or (is_float(c))):
  183. return_status = 'Wrong data type, expected number. Error on line: ' + str(x)
  184. break
  185. a,b,c=(float(x) for x in (a,b,c))
  186. f.append(a) # frequency
  187. if select_data_representation == 'Frequency, real, imaginary':
  188. r.append(b) # Re
  189. i.append(c) # Im
  190. elif select_data_representation == 'Frequency, magnitude, angle':
  191. r.append(b*np.cos(np.deg2rad(c))) # Re
  192. i.append(b*np.sin(np.deg2rad(c))) # Im
  193. elif select_data_representation == 'Frequency, db, angle':
  194. b=10**(b/20)
  195. r.append(b*np.sin(np.deg2rad(c))) # Re
  196. i.append(b*np.cos(np.deg2rad(c))) # Im
  197. else:
  198. return_status = 'Wrong data format'
  199. break
  200. return f, r, i, return_status
  201. # make accessible specific range of numerical data choosen with interactive plot
  202. global interval_range, interval_start, interval_end
  203. data = []
  204. uploaded_file = st.file_uploader('Upload a csv')
  205. if uploaded_file is not None:
  206. data_format_snp = False
  207. data = uploaded_file.readlines()
  208. if uploaded_file.name[-4:-2]=='.s' and uploaded_file.name[-1]== 'p':
  209. data_format_snp = True
  210. validator_status = '...'
  211. ace_preview_markers = []
  212. # data loaded
  213. circle_params = []
  214. if len(data) > 0:
  215. validator_status = read_data(data)
  216. if validator_status == 'data read: success':
  217. hz, select_measurement_parameter, select_data_representation, input_ref_resistance=parse_heading(data)
  218. col1, col2 = st.columns(2)
  219. select_measurement_parameter = col1.selectbox('Measurement parameter',
  220. ['S', 'Z'],
  221. select_measurement_parameter)
  222. select_data_representation = col1.selectbox('Data representation',
  223. ['Frequency, real, imaginary',
  224. 'Frequency, magnitude, angle',
  225. 'Frequency, db, angle'],
  226. select_data_representation)
  227. if select_measurement_parameter=='Z':
  228. input_ref_resistance = col1.number_input(
  229. "Reference resistance:", min_value=0, value=input_ref_resistance)
  230. input_start_line = col1.number_input(
  231. "First line of data:", min_value=1, max_value=len(data))
  232. input_end_line = col1.number_input(
  233. "Last line of data:", min_value=1, max_value=len(data), value=len(data))
  234. f, r, i, validator_status = unpack_data(data, input_start_line, input_end_line)
  235. f = f * hz # to hz
  236. # Ace editor to show choosen data columns and rows
  237. with col2.expander("File preview"):
  238. # web development is fundamentally imposible without such hacks
  239. # if we have so little 'official' functionality in libs and this lack of documentation
  240. # yellow ~ ace_step
  241. # light yellow ~ ace_highlight-marker
  242. # green ~ ace_stack
  243. # red ~ ace_error-marker
  244. # st.markdown('''<style>
  245. # .choosen_option_1
  246. # {
  247. # color: rgb(49, 51, 63);
  248. # }</style>''', unsafe_allow_html=True)
  249. # markdown injection does not work, since ace is in a different .html accessible via iframe
  250. # markers format:
  251. #[{"startRow": 2,"startCol": 0,"endRow": 2,"endCol": 3,"className": "ace_error-marker","type": "text"}]
  252. ace_preview_markers.append(
  253. {"startRow": input_start_line,"startCol": 0,
  254. "endRow": input_end_line+1,"endCol": 0,"className": "ace_highlight-marker","type": "text"})
  255. text_value = "Frequency,Hz | Re(S11) | Im(S11)\n" + \
  256. ''.join(data).strip()
  257. st_ace(value=text_value,
  258. readonly=True,
  259. auto_update=True,
  260. placeholder="Your data is empty",
  261. markers=ace_preview_markers,
  262. height="300px")
  263. st.write("Use range slider to choose best suitable data interval")
  264. plot_interact_abs_from_f(f, r, i)
  265. select_coupling_losses = st.checkbox(
  266. 'Apply corrections for coupling losses (lossy coupling)')
  267. f_cut, r_cut, i_cut = (x[interval_start:interval_end]
  268. for x in (f, r, i))
  269. # for x in range(len(f_cut)):
  270. # print(f_cut[x], r_cut[x], i_cut[x])
  271. if validator_status == 'data parsed':
  272. Q0, sigmaQ0, QL, sigmaQl, circle_params = calc_function(
  273. f_cut, r_cut, i_cut, select_coupling_losses)
  274. # Q0 = round_up(Q0)
  275. # sigmaQ0 = round_up(sigmaQ0)
  276. # QL = round_up(QL)
  277. # sigmaQl = round_up(sigmaQl)
  278. if select_coupling_losses:
  279. st.write("Lossy coupling")
  280. else:
  281. st.write("Cable attenuation")
  282. out_precision = '0.7f'
  283. st.latex(r'Q_0 =' + f'{format(Q0, out_precision)} \pm {format(sigmaQ0, out_precision)}, ' +
  284. r'\;\;\varepsilon_{Q_0} =' + f'{format(sigmaQ0 / Q0, out_precision)}')
  285. st.latex(r'Q_L =' + f'{format(QL, out_precision)} \pm {format(sigmaQl, out_precision)}, ' +
  286. r'\;\;\varepsilon_{Q_L} =' + f'{format(sigmaQl / QL, out_precision)}')
  287. st.write("Status: " + validator_status)
  288. if len(data) > 0 and validator_status == 'data parsed':
  289. with st.expander("Show static abs(S) plot"):
  290. plot_ref_from_f(f_cut, r_cut, i_cut)
  291. plot_data(r_cut, i_cut, circle_params)