Преглед изворни кода

Add demo;
Add support for Z network parameter;
Add info tab;
Add copy button

ricet8ur пре 2 година
родитељ
комит
e423da3e46
5 измењених фајлова са 386 додато и 176 уклоњено
  1. 9 8
      TODO.md
  2. 4 2
      requirements.txt
  3. 7 0
      resource/data/9_Z_test.s1p
  4. 322 166
      source/frontend/front.py
  5. 44 0
      source/frontend/info.md

+ 9 - 8
TODO.md

@@ -1,16 +1,17 @@
 # Todos sorted by importance
 
-1. Add a description for frontend.
+1. [x] Add a description for frontend.
     * What is calculated? The format of the result
     * What is Q circle?
     * What data formats are supported?
-2. Demo
-3. [x] Support .snp
-4. Add window with copyable fragment of choosen data to compare with other programs.
-5. Advanced output options (only frontend):
+2. [x] Demo
+3. Support .snp
+    * .s3p and more
+4. [x] Add button to copy choosen fragment to compare with other programs.
+5. [x] Advanced output options (only frontend):
     * Option to choose output values precision
-6. Add approximation for second chart (abs(S11) from f)
-7. Add impedance input option (Z)
+6. Add approximation for second chart (abs(S) from f)
+7. [x] Add impedance input option (Z)
     * additional field for omega | from .snp
     * convertion to reflection coeffitient (explanation: <https://radioprog.ru/post/195>)
 8. Codestyle fix:
@@ -25,7 +26,7 @@
 10. [x] Advanced file loading:
     * Show file preview
     * Options to skip first and last lines
-11. Advanced file preview: .snp support for .s2p (or more) 
+11. Advanced file preview: highlight choosen data fragments
 12. [x] Make charts more interactive
 13. Make an option to pass the whole program to .html site via iframe? - It works, but where to host?
 14. Add support lines for smith chart?

+ 4 - 2
requirements.txt

@@ -2,6 +2,8 @@
 streamlit
 matplotlib
 numpy
-# sympy?
+
 streamlit_echarts
-streamlit-ace
+streamlit-ace
+sigfig
+paperclip

+ 7 - 0
resource/data/9_Z_test.s1p

@@ -0,0 +1,7 @@
+# MHz Z MA R 75
+!freq magZ11 angZ11
+100 0.99 -4
+200 0.80 -22
+300 0.707 -45
+400 0.40 -62
+500 0.01 -89

+ 322 - 166
source/frontend/front.py

@@ -1,65 +1,19 @@
-from streamlit_ace import st_ace
-from streamlit_echarts import st_echarts, JsCode
 import math
 import streamlit as st
 import matplotlib.pyplot as plt
 import numpy as np
+import sigfig
+import pyperclip
+from streamlit_ace import st_ace
+from streamlit_echarts import st_echarts, JsCode
 
-def circle(ax, x, y, radius, color='#1946BA'):
-    from matplotlib.patches import Ellipse
-    drawn_circle = Ellipse((x, y), radius * 2, radius * 2, clip_on=True,
-                           zorder=2, linewidth=2, edgecolor=color, facecolor=(0, 0, 0, .0125))
-    try:
-        ax.add_artist(drawn_circle)
-    except:
-        raise
-
-def plot_smith(r, i, g, r_cut, i_cut):
-    fig = plt.figure(figsize=(10, 10))
-    ax = fig.add_subplot()
-
-    major_ticks = np.arange(-1.0, 1.1, 0.25)
-    minor_ticks = np.arange(-1.1, 1.1, 0.05)
-    # ax.set_xticks(major_ticks)
-    ax.set_xticks(minor_ticks, minor=True)
-    # ax.set_yticks(major_ticks)
-    ax.set_yticks(minor_ticks, minor=True)
-    ax.grid(which='major', color='grey', linewidth=1.5)
-    ax.grid(which='minor', color='grey', linewidth=0.5, linestyle=':')
-    plt.xlabel(r'$Re(\Gamma)$', color='gray', fontsize=16, fontname="Cambria")
-    plt.ylabel('$Im(\Gamma)$', color='gray', fontsize=16, fontname="Cambria")
-    plt.title('Smith chart', fontsize=24, fontname="Cambria")
-
-    # unit circle
-    circle(ax, 0, 0, 1)
-
-    # input data points
-    ax.plot(r, i, '+', ms=8, mew=2, color='#b6c7f4')
-    
-    # choosen data points
-    ax.plot(r_cut, i_cut, '+', ms=8, mew=2, color='#1946BA')
-
-    # circle approximation by calc
-    radius = abs(g[1] - g[0] / g[2]) / 2
-    x = ((g[1] + g[0] / g[2]) / 2).real
-    y = ((g[1] + g[0] / g[2]) / 2).imag
-    circle(ax, x, y, radius, color='#FF8400')
-
-    XLIM = [-1.1, 1.1]
-    YLIM = [-1.1, 1.1]
-    ax.set_xlim(XLIM)
-    ax.set_ylim(YLIM)
-    st.pyplot(fig)
-
-
-
-interval_range = (0, 100) # in percents
-interval_start, interval_end = 0, 0
 
+# So that you can choose an interval of points on which we apply q-calc algorithm
+def plot_interact_abs_from_f(f, r, i, interval_range):
+    if interval_range is None:
+        interval_range = (0, 100)
 
-def plot_interact_abs_from_f(f, r, i):
-    abs_S = list((r[n] ** 2 + i[n] ** 2)**0.5 for n in range(len(r)))
-    global interval_range, interval_start, interval_end
+    abs_S = list(abs(np.array(r) + 1j * np.array(i)))
     # echarts for datazoom https://discuss.streamlit.io/t/streamlit-echarts/3655
     # datazoom https://echarts.apache.org/examples/en/editor.html?c=line-draggable&lang=ts
     # axis pointer values https://echarts.apache.org/en/option.html#axisPointer
@@ -88,7 +42,7 @@ def plot_interact_abs_from_f(f, r, i):
         },
         "series": [{"data": abs_S, "type": "line", "name": "abs(S)"}],
         "height": 300,
-        "dataZoom": [{"type": "slider", "start": 0, "end": 100, "height": 100, "bottom": 10}],
+        "dataZoom": [{"type": "slider", "start": interval_range[0], "end": interval_range[1], "height": 100, "bottom": 10}],
         "tooltip": {
                 "trigger": "axis",
                 "axisPointer": {
@@ -108,24 +62,76 @@ def plot_interact_abs_from_f(f, r, i):
             }
         },
     }
+    # DataZoom event is not fired on new file upload. There are no default event to fix it.
     events = {
-        "dataZoom": "function(params) { return [params.start, params.end] }",
+        "dataZoom": "function(params) { return ['dataZoom', params.start, params.end] }",
+        "restore": "function() { return ['restore'] }",
     }
 
     # show echart with dataZoom and update intervals based on output
-    interval_range = st_echarts(
+
+    get_event = st_echarts(
         options=options, events=events, height="500px", key="render_basic_bar_events"
     )
 
-    if interval_range is None:
-        interval_range = (0, 100)
+    if not get_event is None and get_event[0] == 'dataZoom':
+        interval_range = get_event[1:]
 
     n = len(f)
     interval_start, interval_end = (
         int(n*interval_range[id]*0.01) for id in (0, 1))
+    return interval_range, interval_start, interval_end
+
+
+
+def circle(ax, x, y, radius, color='#1946BA'):
+    from matplotlib.patches import Ellipse
+    drawn_circle = Ellipse((x, y), radius * 2, radius * 2, clip_on=True,
+                           zorder=2, linewidth=2, edgecolor=color, facecolor=(0, 0, 0, .0125))
+    ax.add_artist(drawn_circle)
+
+
+def plot_smith(r, i, g, r_cut, i_cut, show_excluded):
+    fig = plt.figure(figsize=(10, 10))
+    ax = fig.add_subplot()
+
+    # major_ticks = np.arange(-1.0, 1.1, 0.25)
+    minor_ticks = np.arange(-1.1, 1.1, 0.05)
+    # ax.set_xticks(major_ticks)
+    ax.set_xticks(minor_ticks, minor=True)
+    # ax.set_yticks(major_ticks)
+    ax.set_yticks(minor_ticks, minor=True)
+    ax.grid(which='major', color='grey', linewidth=1.5)
+    ax.grid(which='minor', color='grey', linewidth=0.5, linestyle=':')
+    plt.xlabel('$Re(\Gamma)$', color='gray', fontsize=16, fontname="Cambria")
+    plt.ylabel('$Im(\Gamma)$', color='gray', fontsize=16, fontname="Cambria")
+    plt.title('Smith chart', fontsize=24, fontname="Cambria")
+
+    # unit circle
+    circle(ax, 0, 0, 1)
+
+    # input data points
+    if show_excluded:
+        ax.plot(r, i, '+', ms=8, mew=2, color='#b6c7f4')
+
+    # choosen data points
+    ax.plot(r_cut, i_cut, '+', ms=8, mew=2, color='#1946BA')
+
+    # circle approximation by calc
+    radius = abs(g[1] - g[0] / g[2]) / 2
+    x = ((g[1] + g[0] / g[2]) / 2).real
+    y = ((g[1] + g[0] / g[2]) / 2).imag
+    circle(ax, x, y, radius, color='#FF8400')
+
+    XLIM = [-1.1, 1.1]
+    YLIM = [-1.1, 1.1]
+    ax.set_xlim(XLIM)
+    ax.set_ylim(YLIM)
+    st.pyplot(fig)
+
 
 # plot (abs(S))(f) chart with pyplot
-def plot_ref_from_f(f, r, i):
+def plot_abs_vs_f(f, r, i):
     fig = plt.figure(figsize=(10, 10))
     abs_S = list((r[n] ** 2 + i[n] ** 2)**0.5 for n in range(len(r)))
     xlim = [min(f) - abs(max(f) - min(f)) * 0.1,
@@ -139,10 +145,14 @@ def plot_ref_from_f(f, r, i):
     ax.grid(which='minor', color='grey', linestyle=':', linewidth=0.5)
     plt.xlabel(r'$f,\; 1/c$', color='gray', fontsize=16, fontname="Cambria")
     plt.ylabel('$|S|$', color='gray', fontsize=16, fontname="Cambria")
-    plt.title('Absolute value of reflection coefficient from frequency',
+    plt.title('Abs(S) vs frequency',
               fontsize=24, fontname="Cambria")
 
     ax.plot(f, abs_S, '+', ms=8, mew=2, color='#1946BA')
+
+    # radius = abs(g[1] - g[0] / g[2]) / 2
+    # x = ((g[1] + g[0] / g[2]) / 2).real
+    # y = ((g[1] + g[0] / g[2]) / 2).imag
     st.pyplot(fig)
 
 
@@ -173,88 +183,176 @@ def run(calc_function):
         nonlocal data_format_snp
         if data_format_snp:
             for x in range(len(data)):
-                if data[x][0]=='#':
+                if data[x][0] == '#':
                     line = data[x].split()
-                    if len(line)== 6:
-                        repr_map = {"RI":0,"MA":1, "DB":2}
-                        para_map = {"S":0,"Z":1}
-                        hz_map = {"GHz":10**9,"MHz":10**6,"KHz":10**3,"Hz":1}
-                        hz,measurement_parameter,data_representation,_r,ref_resistance=line[1:]
+                    if len(line) == 6:
+                        repr_map = {"RI": 0, "MA": 1, "DB": 2}
+                        para_map = {"S": 0, "Z": 1}
+                        hz_map = {"GHz": 10**9, "MHz": 10 **6, "KHz": 10**3, "Hz": 1}
+                        hz, measurement_parameter, data_representation, _r, ref_resistance = line[1:]
                         try:
-                            return hz_map[hz], para_map[measurement_parameter], repr_map[data_representation], ref_resistance
+                            return hz_map[hz], para_map[measurement_parameter], repr_map[data_representation], int(ref_resistance)
                         except:
                             break
                     break
         return 1, 0, 0, 50
-    
 
-    def unpack_data(data, input_start_line, input_end_line):
+    # check if line has comments
+    # first is a comment line according to .snp documentation,
+    # others detects comments in various languages
+    def check_line_comments(line):
+        if len(line) < 2 or line[0] == '!' or line[0] == '#' or line[0] == '%' or line[0] == '/':
+            return None
+        else:
+            # generally we expect these chars as separators
+            line = line.replace(';', ' ').replace(',', ' ')
+            if '!' in line:
+                line = line[:line.find('!')]
+            return line
+
+    # unpack a few first lines of the file to get number of ports
+    def count_columns(data):
+        return_status = 'data parsed'
+        column_count = 0
+        for x in range(len(data)):
+            line = check_line_comments(data[x])
+            if line is None:
+                continue
+            line = line.split()
+            # always at least 3 values for single data point
+            if len(line) < 3:
+                return_status = 'Can\'t parse line № ' + \
+                    str(x) + ',\n not enough arguments (less than 3)'
+                break
+            column_count = len(line)
+            break
+        return column_count, return_status
+
+    def unpack_data(data, first_column, column_count, ref_resistance, ace_preview_markers):
         nonlocal select_measurement_parameter
         nonlocal select_data_representation
         f, r, i = [], [], []
-        return_status='data parsed'
-        for x in range(input_start_line-1, input_end_line):
-            if len(data[x])<2 or data[x][0]== '!' or data[x][0]=='#' or data[x][0]=='%' or data[x][0]=='/':
-                # first is a comment line according to .snp documentation,
-                # others detects comments in various languages
+        return_status = 'data parsed'
+        for x in range(len(data)):
+            line = check_line_comments(data[x])
+            if line is None:
                 continue
-            data[x] = data[x].replace(';', ' ').replace(',', ' ') # generally we expect these chars as seperators
-            line = data[x].split()
 
-            # always at least 3 values for single data point
-            if len(line) < 3:
-                return_status = 'Can\'t parse line №: ' + str(x) + ',\n not enough arguments (less than 3)'
+            line = line.split()
+
+            if column_count != len(line):
+                return_status = "Wrong number of parameters on line № " + str(x)
                 break
 
             # 1: process according to data_placement
-            a,b,c=[],[],[]
+            a, b, c = None, None, None
             try:
-                a, b, c = (line[y] for y in range(min(len(line),3)))
-                    # for x in input_data_columns.keys():
-                    #     if x=='f':
-
-                    #     elif x=='r':
-
-                    #     elif x=='i':
+                a = line[0]
+                b = line[first_column]
+                c = line[first_column+1]
             except:
-                return_status = 'Can\'t parse line №: ' + str(x) + ',\n not enough arguments'
-                break    
+                return_status = 'Can\'t parse line №: ' + \
+                    str(x) + ',\n not enough arguments'
+                break
             if not ((is_float(a)) or (is_float(b)) or (is_float(c))):
-                return_status = 'Wrong data type, expected number. Error on line: ' + str(x)
+                return_status = 'Wrong data type, expected number. Error on line: ' + \
+                    str(x)
                 break
-            a,b,c=(float(x) for x in (a,b,c))
+
+            # mark as processed
+            for y in (a,b,c):
+                ace_preview_markers.append(
+                    {"startRow": x,"startCol": 0,
+                    "endRow": x,"endCol": data[x].find(y)+len(y),
+                    "className": "ace_stack","type": "text"})
+
+            a, b, c = (float(x) for x in (a, b, c))
             f.append(a)  # frequency
 
             # 2: process according to data_representation
             if select_data_representation == 'Frequency, real, imaginary':
+                # std format
                 r.append(b)  # Re
                 i.append(c)  # Im
             elif select_data_representation == 'Frequency, magnitude, angle':
-                r.append(b*np.cos(np.deg2rad(c)))  # Re
-                i.append(b*np.sin(np.deg2rad(c)))  # Im
+                r.append(b*np.cos(np.deg2rad(c)))
+                i.append(b*np.sin(np.deg2rad(c)))
             elif select_data_representation == 'Frequency, db, angle':
-                b=10**(b/20)
-                r.append(b*np.sin(np.deg2rad(c)))  # Re
-                i.append(b*np.cos(np.deg2rad(c)))  # Im
+                b = 10**(b/20)
+                r.append(b*np.cos(np.deg2rad(c)))
+                i.append(b*np.sin(np.deg2rad(c)))
             else:
                 return_status = 'Wrong data format'
                 break
 
             # 3: process according to measurement_parameter
+            if select_measurement_parameter == 'Z':
+                # normalization
+                r[-1] = r[-1]/ref_resistance
+                i[-1] = i[-1]/ref_resistance
+
+                # translate to S
+                try:
+                    # center_x + 1j*center_y, radius
+                    p1, r1 = r[-1] / (1 + r[-1]) + 0j, 1 / (1 + r[-1])  #real
+                    p2, r2 = 1 + 1j * (1 / i[-1]), 1 / i[-1]  #imag
+
+                    d = abs(p2-p1)
+                    q = (r1**2 - r2**2 + d**2) / (2 * d)
+
+                    h = (r1**2 - q**2)**0.5
+
+                    p = p1 + q * (p2 - p1) / d
+
+                    intersect = [
+                        (p.real + h * (p2.imag - p1.imag) / d,
+                        p.imag - h * (p2.real - p1.real) / d),
+                        (p.real - h * (p2.imag - p1.imag) / d,
+                         p.imag + h * (p2.real - p1.real) / d)]
+
+                    intersect = [x+1j*y for x,y in intersect]
+                    intersect_shift = [p-(1+0j) for p in intersect]
+                    intersect_shift = abs(np.array(intersect_shift))
+                    p=intersect[0]
+                    if intersect_shift[0]<intersect_shift[1]:
+                        p=intersect[1]
+                    r[-1] = p.real
+                    i[-1] = p.imag
+                except:
+                    r.pop()
+                    i.pop()
+                    f.pop()
+
+        if len(f) < 3 or len(f) != len(r) or len(f) != len(i):
+            return_status = 'Choosen data range is too small, add more points'
+        elif max(abs(np.array(r)+ 1j* np.array(i))) > 2:
+            return_status = 'Your data points have an abnormality:\
+                        they are too far outlise the unit cirlce.\
+                        Make sure the format is correct'
 
         return f, r, i, return_status
 
-    # make accessible specific range of numerical data choosen with interactive plot 
-    global interval_range, interval_start, interval_end
+    # make accessible a specific range of numerical data choosen with interactive plot
+    # percent, line id, line id
+    interval_range, interval_start, interval_end = None, None, None
+
+    # info
+    with st.expander("Info"):
+        # streamlit.markdown does not support footnotes
+        try:
+            with open('./source/frontend/info.md') as f:
+                st.markdown(f.read())
+        except:
+            st.write('Wrong start directory, see readme')
 
     # file upload button
-    uploaded_file = st.file_uploader('Upload a file from your vector analizer. ' +
-        'Make sure the file format is .snp or it has a similar inner structure.')
-    
+    uploaded_file = st.file_uploader('Upload a file from your vector analizer. \
+        Make sure the file format is .snp or it has a similar inner structure.'                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         )
+
     # check .snp
     data_format_snp = False
     if uploaded_file is None:
-        st.write("Demonstration: ")
+        st.write("DEMO: ")
         # display DEMO
         data_format_snp = True
         try:
@@ -269,13 +367,12 @@ def run(calc_function):
                 '''.split('\n')]
     else:
         data = uploaded_file.readlines()
-
         if uploaded_file.name[-4:-2]=='.s' and uploaded_file.name[-1]== 'p':
             data_format_snp = True
-            
-    
+
     validator_status = '...'
     ace_preview_markers = []
+    column_count = 0
 
     # data loaded
     circle_params = []
@@ -283,32 +380,34 @@ def run(calc_function):
 
         validator_status = read_data(data)
         if validator_status == 'data read: success':
-            hz, select_measurement_parameter, select_data_representation, input_ref_resistance=parse_heading(data)
-
-            col1, col2 = st.columns(2)
-            select_measurement_parameter = col1.selectbox('Measurement parameter',
-                                                      ['S', 'Z'],
-                                                      select_measurement_parameter)
-            select_data_representation = col1.selectbox('Data representation',
-                                                    ['Frequency, real, imaginary',
-                                                     'Frequency, magnitude, angle',
-                                                     'Frequency, db, angle'],
-                                                     select_data_representation)
-            if select_measurement_parameter=='Z':
-                input_ref_resistance = col1.number_input(
-                    "Reference resistance:", min_value=0, value=input_ref_resistance)                                    
-            input_start_line = col1.number_input(
-                "First line of data:", min_value=1, max_value=len(data))
-            input_end_line = col1.number_input(
-                "Last line of data:", min_value=1, max_value=len(data), value=len(data))
-
-            f, r, i, validator_status = unpack_data(data, input_start_line, input_end_line)
-            f = f * hz  # to hz
+            hz, select_measurement_parameter, select_data_representation, input_ref_resistance = parse_heading(data)
+            col1, col2 = st.columns([1,2])
+
+            with col1.expander("Processing options"):
+                select_measurement_parameter = st.selectbox('Measurement parameter',
+                                                          ['S', 'Z'],
+                                                          select_measurement_parameter)
+                select_data_representation = st.selectbox('Data representation',
+                                                        ['Frequency, real, imaginary',
+                                                         'Frequency, magnitude, angle',
+                                                         'Frequency, db, angle'],
+                                                         select_data_representation)
+                if select_measurement_parameter=='Z':
+                    input_ref_resistance = st.number_input(
+                        "Reference resistance:", min_value=0, value=input_ref_resistance)
+                input_start_line = int(st.number_input(
+                    "First line for processing:", min_value=1, max_value=len(data)))
+                input_end_line = int(st.number_input(
+                    "Last line for processing:", min_value=1, max_value=len(data), value=len(data)))
+                data = data[input_start_line-1:input_end_line]
+
 
             # Ace editor to show choosen data columns and rows
             with col2.expander("File preview"):
+                # st.button(copy selection)
+
                 # So little 'official' functionality in libs and lack of documentation
-                # therefore beware: css hacks 
+                # therefore beware: css hacks
 
                 # yellow ~ ace_step
                 # light yellow ~ ace_highlight-marker
@@ -327,51 +426,108 @@ def run(calc_function):
 
                 # markers format:
                 #[{"startRow": 2,"startCol": 0,"endRow": 2,"endCol": 3,"className": "ace_error-marker","type": "text"}]
-                
-                # add general marking for choosen data lines? better just mark the problematic lines?
-                ace_preview_markers.append(
-                    {"startRow": input_start_line,"startCol": 0,
-                    "endRow": input_end_line+1,"endCol": 0,"className": "ace_highlight-marker","type": "text"})
 
-                text_value = "Frequency,Hz  | Re(S11) | Im(S11)\n" + \
-                    ''.join(data).strip()
-                st_ace(value=text_value,
+                # add marking for choosen data lines TODO
+                ace_preview_markers.append({
+                    "startRow": input_start_line - 1,
+                    "startCol": 0,
+                    "endRow": input_end_line,
+                    "endCol": 0,
+                    "className": "ace_highlight-marker",
+                    "type": "text"
+                })
+
+                ace_text_value = ''.join(data).strip()
+                st_ace(value=ace_text_value,
                        readonly=True,
                        auto_update=True,
-                       placeholder="Your data is empty",
+                       placeholder="Your file is empty",
                        markers=ace_preview_markers,
                        height="300px")
 
-            st.write("Use range slider to choose best suitable data interval")
-            plot_interact_abs_from_f(f, r, i)
+            column_count, validator_status = count_columns(data)
+
+    if validator_status == "data parsed":
+        input_ports_pair = 1
+        if column_count > 3:
+            input_ports_pair = st.number_input(
+                "Pair of data columns (pair of ports)\n with network parameters:",
+                min_value=1,
+                max_value=(column_count - 1) // 2,
+                value=1)
+        f, r, i, validator_status = unpack_data(
+            data,(input_ports_pair - 1) * 2 + 1, column_count, input_ref_resistance,
+            ace_preview_markers)
+        f = [x * hz for x in f]  # to hz
+        
+    st.write("Use range slider to choose best suitable data interval")
+    interval_range, interval_start, interval_end = plot_interact_abs_from_f(f, r, i, interval_range)
+
+    f_cut, r_cut, i_cut = [], [], []
+    if validator_status == "data parsed":
+        f_cut, r_cut, i_cut = (x[interval_start:interval_end]
+                           for x in (f, r, i))
+
+        def copy_to_clip_s_single():
+            pyperclip.copy("# Hz S RI R 50\n" +
+            ''.join(f'{f_cut[x]} {r_cut[x]} {i_cut[x]}\n' for x in range(len(f_cut))))
+
+        st.button("Copy selected data interval to clipboard as .s1p", on_click=copy_to_clip_s_single)
+
+        if len(f_cut) < 3:
+            validator_status = "Choosen interval is too small, add more points"
 
-            select_coupling_losses = st.checkbox(
-                'Apply corrections for coupling losses')
-
-            if select_coupling_losses:
-                st.write("Option: Lossy coupling")
-            else:
-                st.write("Option: Cable attenuation")
-
-            f_cut, r_cut, i_cut = (x[interval_start:interval_end]
-                                   for x in (f, r, i))
-
-            if validator_status == 'data parsed':
-                Q0, sigmaQ0, QL, sigmaQl, circle_params = calc_function(
-                    f_cut, r_cut, i_cut, select_coupling_losses)
+    st.write("Status: " + validator_status)
 
-                out_precision = '0.7f'
-                if Q0 <= 0 or QL <= 0:
-                    st.write("Negative Q detected, fitting may be inaccurate!")
+    if validator_status == "data parsed":
+        col1, col2 = st.columns(2)
+
+        check_coupling_loss = col1.checkbox(
+            'Apply correction for coupling loss')
+
+        if check_coupling_loss:
+            col1.write("Option: Lossy coupling")
+        else:
+            col1.write("Option: Cable attenuation")
+
+        select_autoformat = col2.checkbox("Autoformat output", value=True)
+        precision = None
+        if not select_autoformat:
+            precision = col2.slider("Precision", min_value=0, max_value=7, value = 4)
+            precision = '0.'+str(precision)+'f'
+
+        Q0, sigmaQ0, QL, sigmaQL, circle_params = calc_function(
+            f_cut, r_cut, i_cut, check_coupling_loss)
+
+        if Q0 <= 0 or QL <= 0:
+            st.write("Negative Q detected, fitting may be inaccurate!")
+
+        if select_autoformat:
+            st.latex(
+                r'Q_0 =' +
+                f'{sigfig.round(Q0, uncertainty=sigmaQ0, style="PDG")},  '
+                + r'\;\;\varepsilon_{Q_0} =' +
+                f'{sigfig.round(sigmaQ0 / Q0, sigfigs=1, style="PDG")}')
+            st.latex(
+                r'Q_L =' +
+                f'{sigfig.round(QL, uncertainty=sigmaQL, style="PDG")},  '
+                + r'\;\;\varepsilon_{Q_L} =' +
+                f'{sigfig.round(sigmaQL / QL, sigfigs=1, style="PDG")}')
+        else:
+            st.latex(
+                r'Q_0 =' +
+                f'{format(Q0, precision)} \pm ' + f'{format(sigmaQ0, precision)},  '
+                + r'\;\;\varepsilon_{Q_0} =' +
+                 f'{format(sigmaQ0 / Q0, precision)}')
+            st.latex(
+                r'Q_L =' +
+                f'{format(QL, precision)} \pm ' + f'{format(sigmaQL, precision)},  '
+                + r'\;\;\varepsilon_{Q_L} =' +
+                 f'{format(sigmaQL / QL, precision)}')
 
-                st.latex(r'Q_0 =' + f'{format(Q0, out_precision)} \pm {format(sigmaQ0, out_precision)},  ' +
-                         r'\;\;\varepsilon_{Q_0} =' + f'{format(sigmaQ0 / Q0, out_precision)}')
-                st.latex(r'Q_L =' + f'{format(QL, out_precision)} \pm {format(sigmaQl, out_precision)},  ' +
-                         r'\;\;\varepsilon_{Q_L} =' + f'{format(sigmaQl / QL, out_precision)}')
+        with st.expander("Show static abs(S) plot"):
+            plot_abs_vs_f(f_cut, r_cut, i_cut)
 
-    st.write("Status: " + validator_status)
+        select_show_excluded_points = st.checkbox("Show excluded points", value=True)
 
-    if len(data) > 0 and validator_status == 'data parsed':
-        with st.expander("Show static abs(S) plot"):
-            plot_ref_from_f(f_cut, r_cut, i_cut)
-        plot_smith(r, i, circle_params, r_cut, i_cut)
+        plot_smith(r, i, circle_params, r_cut, i_cut, select_show_excluded_points)

+ 44 - 0
source/frontend/info.md

@@ -0,0 +1,44 @@
+##### Usage:
+
+App calculates the quality factor and its random error in the electric circuit
+according to the measurements of the [vector analyzer](https://en.wikipedia.org/wiki/Network_analyzer_(electrical)).
+
+##### Input data
+
+The measurements made by a vector network analyzer (set of frequencies and corresponding network parameters).
+Main supported file format is .snp, but similar formats are accepted too. Noise data is not supported.
+
+* Supported network parameters: S, Z
+* Supported parameters representations:
+real and imaginary; magnitude and angle; db and angle
+
+##### Result
+
+* Loaded and unloaded quality factor (q-factor) with random errors
+* plot amplitude vs frequency
+* [Smith chart](https://en.wikipedia.org/wiki/Smith_chart)
+
+##### Calculation method
+
+Calculation of Q-factor and random errors is based on the algorithm, described in [1-2].
+Correction for coupling loss is implemented according to [3].
+
+##### References
+
+[1]: Random and Systematic Uncertainties of Reflection-Type Q-Factor Measurement with Network Analyzer (2003).
+    DOI: 10.1109/TMTT.2002.807831
+
+[2]: Data Processing For Q Factor Measurement (1994).
+    DOI: 10.1109/ARFTG.1994.327064
+
+[3]: Q Factor Measurements Using MATLAB (2011).
+    ISBN: 978-1-60807-161-6
+
+##### External links
+
+[Github](https://github.com/ricet8ur/calc-factor-of-vna)
+
+##### Alternatives
+
+* http://scikit-rf.org/
+* https://people.engineering.olemiss.edu/darko-kajfez/software/