front.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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, 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. data_format_snp = False
  156. return 1, 0, 0, 50
  157. def unpack_data(data, input_start_line, input_end_line, hz):
  158. nonlocal select_measurement_parameter
  159. nonlocal select_data_representation
  160. f, r, i = [], [], []
  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 f, r, i, 'Can\'t parse line №: ' + str(x) + ',\n not enough arguments (less than 3)'
  171. if select_measurement_parameter == 'S':
  172. a, b, c = (y for y in line)
  173. if not ((is_float(a)) or (is_float(b)) or (is_float(c))):
  174. return f, r, i, 'Wrong data type, expected number. Error on line: ' + str(x)
  175. else:
  176. return f, r, i, 'Wrong data format'
  177. f.append(float(a)*hz) # frequency
  178. r.append(float(b)) # Re of S
  179. i.append(float(c)) # Im of S
  180. return f, r, i, 'data parsed'
  181. # make accessible specific range of numerical data choosen with interactive plot
  182. global interval_range, interval_start, interval_end
  183. data = []
  184. data_format_snp = False
  185. uploaded_file = st.file_uploader('Upload a csv')
  186. if uploaded_file is not None:
  187. data = uploaded_file.readlines()
  188. if uploaded_file.name[-4:-2]=='.s' and uploaded_file.name[-1]== 'p':
  189. data_format_snp = True
  190. validator_status = '...'
  191. ace_preview_markers = []
  192. # data loaded
  193. circle_params = []
  194. if len(data) > 0:
  195. validator_status = read_data(data)
  196. if validator_status == 'data read: success':
  197. hz, select_measurement_parameter, select_data_representation, input_ref_resistance=parse_heading(data)
  198. col1, col2 = st.columns(2)
  199. select_measurement_parameter = col1.selectbox('Measurement parameter',
  200. ['S', 'Z'],
  201. select_measurement_parameter)
  202. select_data_representation = col1.selectbox('Data representation',
  203. ['Frequency, real, imaginary',
  204. 'Frequency, magnitude, angle'],
  205. select_data_representation)
  206. if select_measurement_parameter=='Z':
  207. input_ref_resistance = col1.number_input(
  208. "Reference resistance:", min_value=0, value=input_ref_resistance)
  209. input_start_line = col1.number_input(
  210. "First line of data:", min_value=1, max_value=len(data))
  211. input_end_line = col1.number_input(
  212. "Last line of data:", min_value=1, max_value=len(data), value=len(data))
  213. f, r, i, validator_status = unpack_data(data, input_start_line, input_end_line, hz)
  214. # Ace editor to show choosen data columns and rows
  215. with col2.expander("File preview"):
  216. # web development is fundamentally imposible without such hacks
  217. # if we have so little 'official' functionality in libs and this lack of documentation
  218. # yellow ~ ace_step
  219. # light yellow ~ ace_highlight-marker
  220. # green ~ ace_stack
  221. # red ~ ace_error-marker
  222. # st.markdown('''<style>
  223. # .choosen_option_1
  224. # {
  225. # color: rgb(49, 51, 63);
  226. # }</style>''', unsafe_allow_html=True)
  227. # markdown injection does not work, since ace is in a different .html accessible via iframe
  228. # markers format:
  229. #[{"startRow": 2,"startCol": 0,"endRow": 2,"endCol": 3,"className": "ace_error-marker","type": "text"}]
  230. ace_preview_markers.append(
  231. {"startRow": input_start_line,"startCol": 0,
  232. "endRow": input_end_line+1,"endCol": 0,"className": "ace_highlight-marker","type": "text"})
  233. text_value = "Frequency,Hz | Re(S11) | Im(S11)\n" + \
  234. ''.join(data).strip()
  235. st_ace(value=text_value,
  236. readonly=True,
  237. auto_update=True,
  238. placeholder="Your data is empty",
  239. markers=ace_preview_markers,
  240. height="300px")
  241. st.write("Use range slider to choose best suitable data interval")
  242. plot_interact_abs_from_f(f, r, i)
  243. select_coupling_losses = st.checkbox(
  244. 'Apply corrections for coupling losses (lossy coupling)')
  245. f_cut, r_cut, i_cut = (x[interval_start:interval_end]
  246. for x in (f, r, i))
  247. if validator_status == 'data parsed':
  248. Q0, sigmaQ0, QL, sigmaQl, circle_params = calc_function(
  249. f_cut, r_cut, i_cut, select_coupling_losses)
  250. # Q0 = round_up(Q0)
  251. # sigmaQ0 = round_up(sigmaQ0)
  252. # QL = round_up(QL)
  253. # sigmaQl = round_up(sigmaQl)
  254. if select_coupling_losses:
  255. st.write("Lossy coupling")
  256. else:
  257. st.write("Cable attenuation")
  258. out_precision = '0.7f'
  259. st.latex(r'Q_0 =' + f'{format(Q0, out_precision)} \pm {format(sigmaQ0, out_precision)}, ' +
  260. r'\;\;\varepsilon_{Q_0} =' + f'{format(sigmaQ0 / Q0, out_precision)}')
  261. st.latex(r'Q_L =' + f'{format(QL, out_precision)} \pm {format(sigmaQl, out_precision)}, ' +
  262. r'\;\;\varepsilon_{Q_L} =' + f'{format(sigmaQl / QL, out_precision)}')
  263. st.write("Status: " + validator_status)
  264. if len(data) > 0 and validator_status == 'data parsed':
  265. with st.expander("Show static abs(S) plot"):
  266. plot_ref_from_f(f_cut, r_cut, i_cut)
  267. plot_data(r_cut, i_cut, circle_params)