front.py 14 KB

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