├── suncal
├── common
│ ├── __init__.py
│ ├── style
│ │ ├── __init__.py
│ │ ├── suncal_dark.mplstyle
│ │ ├── suncal_light.mplstyle
│ │ ├── css.py
│ │ └── latexchars.py
│ ├── reporter.py
│ ├── multivariate_t.py
│ ├── ttable.py
│ └── matrix.py
├── meassys
│ ├── report
│ │ └── __init__.py
│ ├── __init__.py
│ └── meas_result.py
├── mqa
│ ├── report
│ │ ├── __init__.py
│ │ └── system.py
│ ├── __init__.py
│ ├── system.py
│ ├── costs.py
│ └── guardband.py
├── reverse
│ ├── report
│ │ └── __init__.py
│ └── __init__.py
├── risk
│ ├── report
│ │ └── __init__.py
│ ├── __init__.py
│ ├── deaver.py
│ ├── risk_montecarlo.py
│ └── guardband_tur.py
├── sweep
│ ├── report
│ │ └── __init__.py
│ ├── results
│ │ ├── __init__.py
│ │ └── revsweeper.py
│ └── __init__.py
├── curvefit
│ ├── report
│ │ └── __init__.py
│ ├── results
│ │ └── __init__.py
│ ├── waveform
│ │ ├── __init__.py
│ │ └── extrema.py
│ ├── __init__.py
│ └── fitparse.py
├── datasets
│ ├── report
│ │ └── __init__.py
│ └── __init__.py
├── distexplore
│ ├── report
│ │ └── __init__.py
│ ├── __init__.py
│ └── dist_explore.py
├── intervals
│ ├── report
│ │ └── __init__.py
│ ├── __init__.py
│ └── fit.py
├── uncertainty
│ ├── report
│ │ └── __init__.py
│ ├── results
│ │ ├── __init__.py
│ │ └── uncertainty.py
│ └── __init__.py
├── version.py
├── gui
│ ├── icons
│ │ ├── __init__.py
│ │ ├── PSLcal_logo.ico
│ │ ├── PSLcal_logo.png
│ │ ├── PSLcal_logo.icns
│ │ ├── SNL_Horizontal_Black_Blue.png
│ │ └── icons.py
│ ├── SUNCALmanual.pdf
│ ├── __init__.py
│ ├── help_strings
│ │ ├── __init__.py
│ │ ├── distexplore_help.py
│ │ └── main_help.py
│ ├── gui_math.py
│ ├── widgets
│ │ ├── __init__.py
│ │ ├── panel.py
│ │ └── stack.py
│ ├── __main__.py
│ ├── page_about.py
│ ├── gui_styles.py
│ ├── gui_common.py
│ └── tool_ttable.py
├── project
│ ├── __init__.py
│ ├── proj_explore.py
│ ├── proj_risk.py
│ ├── proj_revsweep.py
│ ├── proj_dataset.py
│ └── proj_sweep.py
└── __init__.py
├── .flake8
├── doc
├── SUNCALmanual.pdf
├── API Examples
│ ├── inst_amp.png
│ ├── PFA_vs_TUR.png
│ ├── PFA_vs_itp.png
│ └── IVdata.csv
├── Manual
│ ├── figs
│ │ ├── Figure_2_1.PNG
│ │ ├── Figure_2_2.PNG
│ │ ├── Figure_2_3.PNG
│ │ ├── Figure_2_4.png
│ │ ├── Figure_2_5.PNG
│ │ ├── Figure_2_6.PNG
│ │ ├── Figure_2_7.PNG
│ │ ├── Figure_2_8.PNG
│ │ ├── Figure_2_9.PNG
│ │ ├── Figure_3_1.PNG
│ │ ├── Figure_6_1.PNG
│ │ ├── Figure_6_2.PNG
│ │ ├── Figure_6_3.PNG
│ │ ├── Figure_6_4.PNG
│ │ ├── Figure_6_5.PNG
│ │ ├── Figure_6_6.PNG
│ │ ├── Figure_6_7.PNG
│ │ ├── Figure_6_8.PNG
│ │ ├── Figure_6_9.PNG
│ │ ├── Figure_2_10.PNG
│ │ ├── Figure_2_11.PNG
│ │ ├── Figure_2_12.PNG
│ │ ├── Figure_2_13.PNG
│ │ ├── Figure_2_14.PNG
│ │ ├── Figure_2_16.PNG
│ │ ├── Figure_2_17.PNG
│ │ ├── Figure_2_18.PNG
│ │ ├── Figure_2_19.PNG
│ │ ├── Figure_6_10.PNG
│ │ └── Figure_6_11.PNG
│ ├── header.tex
│ ├── buildman.sh
│ └── biblio.bib
└── Examples
│ ├── Data
│ ├── lava.dat
│ ├── contactresistance.txt
│ ├── IVdata.csv
│ ├── resistance.dat
│ ├── daily_resistance.csv
│ ├── 432A-TCR.csv
│ ├── pressure.txt
│ ├── logdata.txt
│ ├── A3Data.csv
│ ├── expdata.txt
│ ├── logistic.txt
│ ├── pulse.csv
│ └── S2data.csv
│ ├── ex_intervalS2.yaml
│ ├── ex_intervalvar.yaml
│ ├── ex_contactresistance.yaml
│ ├── ex_XRF.yaml
│ ├── ex_pipeorgan.yaml
│ ├── ex_magphase.yaml
│ ├── ex_expansion.yaml
│ ├── ex_gageball_reversesweep.yaml
│ ├── ex_voltageanova.yaml
│ ├── ex_intervalA3.yaml
│ ├── ex_reactance.yaml
│ ├── ex_viscometer.yaml
│ ├── ex_neutrons.yaml
│ ├── ex_acousticchamber.yaml
│ ├── ex_drift_risk.yaml
│ ├── ex_electricalcircuitproblem.yaml
│ ├── ex_temperature_coeff.yaml
│ ├── ex_pitot.yaml
│ ├── ex_stepatten.yaml
│ ├── ex_cannonball.yaml
│ ├── ex_leakrate.yaml
│ └── ex_endgauge.yaml
├── pyproject.toml
├── .pylintrc
├── test
├── test_GUMH2.csv
├── readme.md
├── ex_magphase.yaml
├── test1.yaml
├── ex_xrf.yaml
├── ex_expansion.yaml
├── test_config.py
├── ex_viscometer.yaml
├── test_ttable.py
├── ex_endgauge.yaml
├── ex_stepatten.yaml
├── ex_endgauge_units.yaml
├── test_cplx.py
├── test_parser.py
├── test_mqa.py
└── test_project.py
├── MANIFEST.in
├── requirements.txt
├── uncertwinonefile.spec
├── uncertwin.spec
├── winexe_version_info.txt
├── uncertmac.spec
├── .gitignore
├── setup.cfg
├── README.md
└── installer.iss
/suncal/common/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/common/style/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/meassys/report/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/mqa/report/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/reverse/report/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/risk/report/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/sweep/report/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/sweep/results/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/curvefit/report/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/curvefit/results/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/datasets/report/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/distexplore/report/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/intervals/report/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/uncertainty/report/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/suncal/uncertainty/results/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 120
--------------------------------------------------------------------------------
/suncal/mqa/__init__.py:
--------------------------------------------------------------------------------
1 | from .mqa import MqaQuantity
2 |
--------------------------------------------------------------------------------
/suncal/version.py:
--------------------------------------------------------------------------------
1 | __version__ = '1.7.0'
2 | __date__ = 'June 6, 2025'
3 |
--------------------------------------------------------------------------------
/suncal/gui/icons/__init__.py:
--------------------------------------------------------------------------------
1 | from .icons import icon, appicon, logo_snl
2 |
--------------------------------------------------------------------------------
/doc/SUNCALmanual.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/SUNCALmanual.pdf
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools"]
3 | build-backend = "setuptools.build_meta"
--------------------------------------------------------------------------------
/suncal/gui/SUNCALmanual.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/suncal/gui/SUNCALmanual.pdf
--------------------------------------------------------------------------------
/doc/API Examples/inst_amp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/API Examples/inst_amp.png
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_1.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_1.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_2.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_3.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_3.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_4.png
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_5.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_5.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_6.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_6.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_7.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_7.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_8.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_8.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_9.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_9.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_3_1.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_3_1.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_6_1.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_6_1.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_6_2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_6_2.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_6_3.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_6_3.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_6_4.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_6_4.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_6_5.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_6_5.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_6_6.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_6_6.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_6_7.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_6_7.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_6_8.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_6_8.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_6_9.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_6_9.PNG
--------------------------------------------------------------------------------
/doc/API Examples/PFA_vs_TUR.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/API Examples/PFA_vs_TUR.png
--------------------------------------------------------------------------------
/doc/API Examples/PFA_vs_itp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/API Examples/PFA_vs_itp.png
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_10.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_10.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_11.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_11.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_12.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_12.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_13.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_13.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_14.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_14.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_16.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_16.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_17.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_17.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_18.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_18.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_2_19.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_2_19.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_6_10.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_6_10.PNG
--------------------------------------------------------------------------------
/doc/Manual/figs/Figure_6_11.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/doc/Manual/figs/Figure_6_11.PNG
--------------------------------------------------------------------------------
/suncal/curvefit/waveform/__init__.py:
--------------------------------------------------------------------------------
1 | from . import extrema
2 | from . import pulse
3 | from . import threshold
4 |
--------------------------------------------------------------------------------
/suncal/gui/icons/PSLcal_logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/suncal/gui/icons/PSLcal_logo.ico
--------------------------------------------------------------------------------
/suncal/gui/icons/PSLcal_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/suncal/gui/icons/PSLcal_logo.png
--------------------------------------------------------------------------------
/suncal/gui/icons/PSLcal_logo.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/suncal/gui/icons/PSLcal_logo.icns
--------------------------------------------------------------------------------
/suncal/gui/icons/SNL_Horizontal_Black_Blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandialabs/suncal/HEAD/suncal/gui/icons/SNL_Horizontal_Black_Blue.png
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 | extension-pkg-whitelist=PyQt5
3 |
4 | [FORMAT]
5 | max-line-length=120
6 | good-names=i,j,k,w,x,y,z,ax,ok,ux,uy,n,Cx,Ux,Uy,CxT
7 |
--------------------------------------------------------------------------------
/doc/Examples/Data/lava.dat:
--------------------------------------------------------------------------------
1 | 1130.60
2 | 1210.64
3 | 1211.25
4 | 1047.01
5 | 1135.67
6 | 1189.19
7 | 1237.59
8 | 1164.33
9 | 1225.51
10 | 1165.79
11 |
--------------------------------------------------------------------------------
/doc/Examples/Data/contactresistance.txt:
--------------------------------------------------------------------------------
1 | Length Resistance
2 | 0.25 54.6
3 | 0.5 65.1
4 | 1.0 88.1
5 | 1.25 106.2
6 | 2.0 104.9
7 | 2.75 162.3
8 | 5.25 230.3
9 |
--------------------------------------------------------------------------------
/suncal/gui/__init__.py:
--------------------------------------------------------------------------------
1 | ''' User Interface for Uncertainty Calculator '''
2 |
3 | from .gui_main import MainGUI
4 | from ..version import __version__, __date__
5 |
--------------------------------------------------------------------------------
/suncal/meassys/__init__.py:
--------------------------------------------------------------------------------
1 | from .meassys import SystemQuantity, SystemIndirectQuantity, MeasureSystem, SystemQuantityResult
2 | from .curve import SystemCurve
3 |
--------------------------------------------------------------------------------
/test/test_GUMH2.csv:
--------------------------------------------------------------------------------
1 | V, I, theta
2 | 5.007, 19.663, 1.0456
3 | 4.994, 19.639, 1.0438
4 | 5.005, 19.640, 1.0468
5 | 4.990, 19.685, 1.0428
6 | 4.999, 19.678, 1.0433
7 |
--------------------------------------------------------------------------------
/doc/API Examples/IVdata.csv:
--------------------------------------------------------------------------------
1 | V, I, theta
2 | 5.007, 19.663, 1.0456
3 | 4.994, 19.639, 1.0438
4 | 5.005, 19.640, 1.0468
5 | 4.990, 19.685, 1.0428
6 | 4.999, 19.678, 1.0433
7 |
--------------------------------------------------------------------------------
/doc/Examples/Data/IVdata.csv:
--------------------------------------------------------------------------------
1 | V, I, theta
2 | 5.007, 19.663, 1.0456
3 | 4.994, 19.639, 1.0438
4 | 5.005, 19.640, 1.0468
5 | 4.990, 19.685, 1.0428
6 | 4.999, 19.678, 1.0433
7 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE changes.md
2 | recursive-include suncal *.mplstyle
3 | recursive-include suncal *.pdf
4 | recursive-include test *.csv *.yaml *.txt
5 | global-exclude *.pyc *.pyo
6 |
--------------------------------------------------------------------------------
/doc/Examples/Data/resistance.dat:
--------------------------------------------------------------------------------
1 | Measurement Resistance
2 | 1 32.2050
3 | 2 32.1878
4 | 3 32.2081
5 | 4 32.2201
6 | 5 32.1807
7 | 6 32.1990
8 | 7 32.1965
9 | 8 32.2120
10 | 9 32.1940
11 | 10 32.2108
12 |
--------------------------------------------------------------------------------
/suncal/sweep/__init__.py:
--------------------------------------------------------------------------------
1 | ''' Calculate Uncertainty Propagation over a range of values '''
2 |
3 | from .sweeper import UncertSweep
4 | from .revsweeper import UncertSweepReverse
5 |
6 | __all__ = ['UncertSweep', 'UncertSweepReverse']
7 |
--------------------------------------------------------------------------------
/test/readme.md:
--------------------------------------------------------------------------------
1 | # Automated Tests for PSL Uncertainty Calculator
2 |
3 | From the root source folder, run:
4 |
5 | `py.test`
6 |
7 | to execute the test cases. For code-coverage report, run:
8 |
9 | `py.test --cov=suncal --cov-report html`
10 |
--------------------------------------------------------------------------------
/suncal/reverse/__init__.py:
--------------------------------------------------------------------------------
1 | ''' Reverse Uncertainty Calculations solve for the uncertainty required in one variable to achieve the
2 | desired total uncertainty in the model function.
3 | '''
4 |
5 | from .reverse import ModelReverse
6 |
7 | __all__ = ['ModelReverse']
8 |
--------------------------------------------------------------------------------
/suncal/distexplore/__init__.py:
--------------------------------------------------------------------------------
1 | ''' Distribution Explorer provides a means for performing Monte Carlo calculations
2 | on manually-defined distributions and functions. Mostly for educational/training
3 | purposes.
4 | '''
5 |
6 | from .dist_explore import DistExplore
7 |
--------------------------------------------------------------------------------
/suncal/curvefit/__init__.py:
--------------------------------------------------------------------------------
1 | ''' Curve Fitting with Uncertainties on the inputs and outputs '''
2 |
3 | from .curvefit import linefit, linefit_lsq, linefitYork, odrfit
4 | from .curvefit_model import CurveFit, WaveCalc
5 | from .uncertarray import Array
6 |
7 | __all__ = ['CurveFit', 'Array']
8 |
--------------------------------------------------------------------------------
/doc/Manual/header.tex:
--------------------------------------------------------------------------------
1 | \usepackage{float}
2 | \let\origfigure\figure
3 | \let\endorigfigure\endfigure
4 | \renewenvironment{figure}[1][2] {
5 | \expandafter\origfigure\expandafter[H]
6 | } {
7 | \endorigfigure
8 | }
9 |
10 | \usepackage{hyperref}
11 | \usepackage{cleveref}
12 | \usepackage{titlesec}
13 | %\newcommand{\sectionbreak}{\clearpage}
14 |
--------------------------------------------------------------------------------
/suncal/datasets/__init__.py:
--------------------------------------------------------------------------------
1 | ''' DataSets are 1D or 2D sets of measured data, for computing repeatability and reproducibility information '''
2 |
3 | from .dataset import autocorrelation, uncert_autocorrelated, anova, group_stats, pooled_stats, standarderror
4 |
5 | __all__ = ['autocorrelation', 'uncert_autocorrelated', 'anova', 'group_stats', 'pooled_stats', 'standarderror']
6 |
--------------------------------------------------------------------------------
/doc/Examples/Data/daily_resistance.csv:
--------------------------------------------------------------------------------
1 | Day 1, Day 2, Day 3, Day 4, Day 5
2 | 0.189598873,0.190867894,0.200779780,0.210638108,0.184202286
3 | 0.199882413,0.175427156,0.198553538,0.175251782,0.206156336
4 | 0.193078900,0.213947485,0.179369437,0.205199833,0.195215413
5 | 0.186095503,0.206404833,0.175049838,0.208589307,0.191869021
6 | 0.198434666,0.216626088,0.209869511,0.195636910,0.194088966
7 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # Requirements for EXE build
2 | # python==3.12.9
3 | sympy==1.12
4 | numpy==1.26.1
5 | matplotlib==3.8
6 | pyyaml==6.0.1
7 | markdown==3.5
8 | pint==0.22
9 | pyqt6==6.6.1
10 | pyqt6-qt6==6.6.1
11 | scipy==1.15.0
12 | PyInstaller==5.13.2 # For building exe
13 | pip-licenses # Needed to generate license info for exe about page
14 | pytest # To run test cases
15 |
--------------------------------------------------------------------------------
/doc/Examples/Data/432A-TCR.csv:
--------------------------------------------------------------------------------
1 | Temperature,Resistance
2 | 16.18989327,200.04165
3 | 17.26065253,200.04332
4 | 18.33239628,200.04536
5 | 19.33912629,200.0478
6 | 20.42663097,200.04909
7 | 21.41168906,200.05132
8 | 22.41349359,200.053573
9 | 23.49311831,200.05482
10 | 24.52742921,200.05645
11 | 25.53612921,200.05802
12 | 26.55468017,200.0594
13 | 27.59194655,200.0605
14 | 28.63118394,200.06156
15 | 29.69800221,200.06257
--------------------------------------------------------------------------------
/suncal/gui/help_strings/__init__.py:
--------------------------------------------------------------------------------
1 | from .uncert_help import UncertHelp
2 | from .wizard_help import WizardHelp
3 | from .risk_help import RiskHelp
4 | from .interval_help import IntervalHelp
5 | from .distexplore_help import DistExploreHelp
6 | from .curvefit_help import CurveHelp
7 | from .anova_help import AnovaHelp
8 | from .main_help import MainHelp
9 | from .system_help import SystemHelp
10 | from .mqa_help import MqaHelp
11 |
--------------------------------------------------------------------------------
/doc/Examples/Data/pressure.txt:
--------------------------------------------------------------------------------
1 | 06-Nov-2011, 180.02945641, 180.26180522, 179.75678302, 180.15129924, 179.73220268
2 | 07-Oct-2013, 181.30996233, 181.20475997, 181.81435542, 181.51056448, 181.40100349
3 | 01-Aug-2015, 182.14519271, 182.317946, 182.12156617, 182.19061199, 182.44320556
4 | 23-Jul-2016, 183.04668247, 183.40622699, 183.08574395, 182.8406013, 183.07926423
5 | 11-May-2018, 184.37580285, 183.90570234, 184.28613981, 184.0489668, 184.09539855
6 |
--------------------------------------------------------------------------------
/doc/Examples/ex_intervalS2.yaml:
--------------------------------------------------------------------------------
1 | - Rt: 0.8
2 | conf: 0.95
3 | desc: Data from Table 5-1 in NASA-HDBK-8739.19-5.
4 | mode: intervalbinom
5 | name: interval
6 | ni:
7 | - 4.0
8 | - 6.0
9 | - 14.0
10 | - 13.0
11 | - 22.0
12 | - 49.0
13 | - 18.0
14 | - 6.0
15 | ri:
16 | - 0.999
17 | - 0.8333
18 | - 0.6429
19 | - 0.6154
20 | - 0.5455
21 | - 0.4082
22 | - 0.5
23 | - 0.3333
24 | ti:
25 | - 21.0
26 | - 42.0
27 | - 63.0
28 | - 84.0
29 | - 140.0
30 | - 189.0
31 | - 269.5
32 | - 346.5
33 |
--------------------------------------------------------------------------------
/suncal/intervals/__init__.py:
--------------------------------------------------------------------------------
1 | ''' Calculate optimal calibration intervals based on historical data
2 |
3 | Implements methods "A3" and "S2" defined in NCSLI Recommended Practice 1,
4 | and the "Variables" method.
5 | '''
6 |
7 | from .testa3 import A3Params, a3_testinterval, datearray
8 | from .binoms2 import S2Params, s2_binom_interval
9 | from .variables import (VariablesData,
10 | variables_reliability_target,
11 | variables_uncertainty_target,
12 | ResultsVariablesInterval)
13 |
--------------------------------------------------------------------------------
/doc/Examples/ex_intervalvar.yaml:
--------------------------------------------------------------------------------
1 | - deltas:
2 | - 0.1
3 | - 0.11
4 | - 0.251
5 | - 0.299
6 | - 0.403
7 | - 0.615
8 | desc: 'Calculating interval using variables data, of interval length and deviation
9 | from prior calibration. Both uncertainty target and reliability target methods
10 | are calculated. '
11 | dt:
12 | - 70.0
13 | - 86.0
14 | - 104.0
15 | - 135.0
16 | - 167.0
17 | - 173.0
18 | m: 2
19 | maxm: 1
20 | mode: intervalvariables
21 | name: interval
22 | rconf: 0.9
23 | rlimits:
24 | - 9.0
25 | - 11.0
26 | u0: 0.28
27 | utarget: 0.5
28 | y0: 10.03
29 |
--------------------------------------------------------------------------------
/doc/Examples/Data/logdata.txt:
--------------------------------------------------------------------------------
1 | 1.000000000000000000e+02 9.628296079622316483e+00
2 | 1.300000000000000000e+02 2.770879904468964128e+01
3 | 1.600000000000000000e+02 3.693488804038246798e+01
4 | 1.900000000000000000e+02 4.387594337244672715e+01
5 | 2.200000000000000000e+02 4.994369329165491678e+01
6 | 2.500000000000000000e+02 5.195633738717743455e+01
7 | 2.800000000000000000e+02 5.517730581714283034e+01
8 | 3.100000000000000000e+02 5.752419485393344445e+01
9 | 3.400000000000000000e+02 6.254736115199349200e+01
10 | 3.700000000000000000e+02 6.177412142582770826e+01
11 | 4.000000000000000000e+02 6.487186028505203694e+01
12 |
--------------------------------------------------------------------------------
/doc/Examples/Data/A3Data.csv:
--------------------------------------------------------------------------------
1 | Asset,Interval End,Pass/Fail
2 | SNL1000,7/1/2016,pass
3 | SNL1000,7/1/2017,pass
4 | SNL1000,7/1/2018,pass
5 | SNL1000,7/1/2019,pass
6 | SNL1000,7/1/2020,pass
7 | SNL1000,7/1/2021,fail
8 | SNL1000,7/1/2022,pass
9 | SNL1000,7/1/2023,pass
10 | SNL1001,5/1/2016,pass
11 | SNL1001,5/1/2017,fail
12 | SNL1001,5/1/2018,pass
13 | SNL1001,5/1/2019,fail
14 | SNL1001,5/1/2020,pass
15 | SNL1001,5/1/2021,fail
16 | SNL1001,5/1/2022,pass
17 | SNL1001,5/1/2023,pass
18 | SNL1002,9/1/2016,pass
19 | SNL1002,9/1/2017,pass
20 | SNL1002,9/1/2018,pass
21 | SNL1002,9/1/2019,pass
22 | SNL1002,9/1/2020,pass
23 | SNL1002,9/1/2021,pass
24 | SNL1002,9/1/2022,pass
25 | SNL1002,9/1/2023,pass
26 |
--------------------------------------------------------------------------------
/test/ex_magphase.yaml:
--------------------------------------------------------------------------------
1 | # PSL Uncertainty Calculator Config File
2 | # Magnitude and Phase from Real and Imaginary. Example from GUM supplement 2
3 |
4 | functions:
5 | - desc: Magnitude
6 | expr: sqrt(im**2 + re**2)
7 | name: mag
8 | - desc: Phase
9 | expr: atan2(im, re)
10 | name: ph
11 | inputs:
12 | - desc: Real Component
13 | mean: 0.001
14 | name: re
15 | uncerts:
16 | - degf: .inf
17 | desc: ''
18 | dist: normal
19 | name: u(re)
20 | std: '0.01'
21 | - desc: Imaginary Component
22 | mean: 0.0
23 | name: im
24 | uncerts:
25 | - degf: .inf
26 | desc: ''
27 | dist: normal
28 | name: u(im)
29 | std: '0.01'
30 | samples: 1000000
31 | seed: 1
32 |
--------------------------------------------------------------------------------
/suncal/common/reporter.py:
--------------------------------------------------------------------------------
1 | ''' Decorator for adding Report object and Markdown representer to dataclass
2 |
3 | Usage:
4 |
5 | @reporter.reporter(ReportXYZ)
6 | @dataclass
7 | class ResultsXYZ:
8 | ...
9 | '''
10 |
11 |
12 | def reporter(reportclass):
13 | def decorator(resultclass):
14 |
15 | @property
16 | def report(self):
17 | return reportclass(self)
18 |
19 | def _repr_markdown_(self):
20 | return self.report.summary().get_md()
21 |
22 | setattr(resultclass, 'report', report)
23 | setattr(resultclass, '_repr_markdown_', _repr_markdown_)
24 | return resultclass
25 | return decorator
26 |
--------------------------------------------------------------------------------
/doc/Examples/ex_contactresistance.yaml:
--------------------------------------------------------------------------------
1 | - abssigma: true
2 | arrx:
3 | - 0.25
4 | - 0.5
5 | - 1.0
6 | - 1.25
7 | - 2.0
8 | - 2.75
9 | - 5.25
10 | arry:
11 | - 54.6
12 | - 65.1
13 | - 88.1
14 | - 106.2
15 | - 104.9
16 | - 162.3
17 | - 230.3
18 | curve: line
19 | desc: Contact resistance is found by measuring resistance at various lengths and
20 | extrapolating back to zero. The y-intercept of this line is twice the contact
21 | resistance.
22 | mode: curvefit
23 | name: curvefit
24 | odr: false
25 | predictions:
26 | 2Rc:
27 | tolerance: null
28 | value: 0.0
29 | tolerances: {}
30 | waveform: {}
31 | xdates: false
32 | xname: Length
33 | yname: Resistance
34 |
--------------------------------------------------------------------------------
/doc/Manual/buildman.sh:
--------------------------------------------------------------------------------
1 |
2 | # Generate PDF and copy to gui folder for inclusion in embedded executable
3 | # Note: convert Mac screenshots to better dpi using:
4 | # convert -density 150 -units pixelsperinch oldfile.png newfile.png
5 |
6 | pandoc manual.md -N --toc --include-in-header header.tex --filter pandoc-fignos --variable subparagraph --variable geometry="margin=1in" --variable fontfamily=sans --citeproc --bibliography=biblio.bib --csl=biblio.csl -o ../SUNCALmanual.pdf
7 |
8 | # For an html version:
9 | #pandoc manual.md -N --toc --css manual.css --standalone --self-contained --webtex https://latex.codecogs.com/svg.latex? --citeproc --bibliography=biblio.bib -o ../index.html
10 |
11 | cp ../SUNCALmanual.pdf ../../suncal/gui/SUNCALmanual.pdf
12 |
13 |
14 |
--------------------------------------------------------------------------------
/suncal/uncertainty/__init__.py:
--------------------------------------------------------------------------------
1 | '''
2 | Suncal Uncertainty Module - Compute combined uncertainty of a system
3 |
4 | y = f(x1, x2... xn)
5 |
6 | given N inputs each with a given probability distribution. Inputs can have any
7 | distribution defined in scipy.stats, or a custom distribution by subclassing
8 | scipy.stats.rv_continuous or scipy.stats.rv_discrete.
9 |
10 | Example usage:
11 | >>> model = Model('f = a + b')
12 | >>> model.var('a').measure(10).typeb(dist='normal', std=0.5)
13 | >>> model.var('b').measure(5).typeb(dist='uniform', a=1)
14 | >>> model.calculate()
15 | '''
16 |
17 | from .model import Model, ModelCallable
18 | from .model_cplx import ModelComplex, ModelComplexCallable
19 |
20 | __all__ = ['Model', 'ModelCallable', 'ModelComplex', 'ModelComplexCallable']
21 |
--------------------------------------------------------------------------------
/suncal/project/__init__.py:
--------------------------------------------------------------------------------
1 | ''' Project Components are used by the GUI to manage the calculations, including save/load of configuration '''
2 |
3 | from .project import Project
4 | from .proj_uncert import ProjectUncert
5 | from .proj_risk import ProjectRisk
6 | from .proj_dataset import ProjectDataSet
7 | from .proj_explore import ProjectDistExplore
8 | from .proj_reverse import ProjectReverse
9 | from .proj_sweep import ProjectSweep
10 | from .proj_revsweep import ProjectReverseSweep
11 | from .proj_curvefit import ProjectCurveFit
12 | from .proj_interval import ProjectIntervalTest, ProjectIntervalTestAssets, ProjectIntervalBinom, \
13 | ProjectIntervalBinomAssets, ProjectIntervalVariables, ProjectIntervalVariablesAssets
14 | from .proj_meassys import ProjectMeasSys
15 | from .proj_mqa import ProjectMqa
16 |
--------------------------------------------------------------------------------
/suncal/__init__.py:
--------------------------------------------------------------------------------
1 | '''
2 | Suncal - Sandia UNcertainty CALculator
3 | Primary Standards Lab - Sandia National Laboratories
4 |
5 | Copyright 2019-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS).
6 | Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government
7 | retains certain rights in this software.
8 | '''
9 |
10 | from .version import __version__, __date__
11 |
12 | from .common import ttable, unitmgr
13 | from .common.unitmgr import ureg
14 | from .common.limit import Limit
15 | from .uncertainty import Model, ModelCallable, ModelComplex
16 | from . import reverse
17 | from . import risk
18 | from . import curvefit
19 | from . import datasets
20 |
21 | __all__ = ['__version__', '__date__', 'ttable', 'unitmgr', 'Model', 'ModelCallable', 'ModelComplex',
22 | 'reverse', 'risk', 'curvefit', 'datasets', 'ureg']
23 |
--------------------------------------------------------------------------------
/doc/Examples/ex_XRF.yaml:
--------------------------------------------------------------------------------
1 | # PSL Uncertainty Calculator Config File
2 | #
3 | description: X-Ray Fluorescence Example from SNL ENGR224 Course.
4 | functions:
5 | - desc: Corrected measurement of standard
6 | expr: X_1*Y_u/X_2
7 | name: Y_c
8 | inputs:
9 | - desc: VLSI measurement of SHS
10 | mean: 0.182
11 | name: X_1
12 | uncerts:
13 | - degf: 9.0
14 | desc: ''
15 | dist: normal
16 | name: u(X_1)
17 | std: '.00093'
18 | - desc: Uncorrected Dektak measurement of check standard
19 | mean: 0.6978
20 | name: Y_u
21 | uncerts:
22 | - degf: 19.0
23 | desc: ''
24 | dist: normal
25 | name: u(Y_u)
26 | std: '.0026'
27 | - desc: Dektak measurement of SHS
28 | mean: 0.1823
29 | name: X_2
30 | uncerts:
31 | - degf: 19.0
32 | desc: ''
33 | dist: normal
34 | name: u(X_2)
35 | std: '.00058'
36 | samples: 1000000
37 |
--------------------------------------------------------------------------------
/suncal/risk/__init__.py:
--------------------------------------------------------------------------------
1 | ''' Calucations on false accept/reject risk and guardbanding '''
2 |
3 | from .risk import (specific_risk,
4 | PFA_norm,
5 | PFR_norm,
6 | PFA,
7 | PFR,
8 | PFA_conditional,
9 | PFA_norm_conditional,
10 | get_sigmaproc_from_itp,
11 | get_sigmaproc_from_itp_arb
12 | )
13 |
14 | from . import guardband
15 | from . import guardband_tur
16 |
17 | from .risk_montecarlo import PFAR_MC
18 | from . import deaver
19 | from . import risk_quad, risk_simpson
20 |
21 | __all__ = ['specific_risk', 'guardband', 'guardband_tur', 'PFA_norm',
22 | 'PFR_norm', 'PFA', 'PFR', 'PFAR_MC', 'PFA_conditional', 'PFA_norm_conditional',
23 | 'deaver', 'get_sigmaproc_from_itp', 'get_sigmaproc_from_itp_arb']
--------------------------------------------------------------------------------
/test/test1.yaml:
--------------------------------------------------------------------------------
1 | # PSL Uncertainty Calculator Config File
2 | #
3 | correlations:
4 | - cor: '-0.3600'
5 | var1: a
6 | var2: b
7 | - cor: '-0.4000'
8 | var1: a
9 | var2: c
10 | - cor: '0.8600'
11 | var1: b
12 | var2: c
13 | functions:
14 | - desc: ''
15 | expr: (a + b) / c
16 | name: f
17 | - desc: ''
18 | expr: a - b
19 | name: g
20 | - desc: ''
21 | expr: b * c
22 | name: h
23 | inputs:
24 | - desc: ''
25 | mean: 10.0
26 | name: a
27 | uncerts:
28 | - degf: 10.0
29 | desc: ''
30 | dist: normal
31 | name: u(a)
32 | std: '0.2'
33 | - desc: ''
34 | mean: 25.0
35 | name: b
36 | uncerts:
37 | - a: '5.0'
38 | degf: .inf
39 | desc: ''
40 | dist: gamma
41 | name: u(b)
42 | scale: '2.0'
43 | - desc: ''
44 | mean: 2.0
45 | name: c
46 | uncerts:
47 | - degf: 88.0
48 | desc: ''
49 | dist: normal
50 | name: u(c)
51 | std: '0.1'
52 | samples: 1000000
53 |
--------------------------------------------------------------------------------
/doc/Examples/ex_pipeorgan.yaml:
--------------------------------------------------------------------------------
1 | - functions:
2 | - desc: ''
3 | expr: v/2/L
4 | name: f
5 | units: Hz
6 | inputs:
7 | - autocorrelate: true
8 | desc: ''
9 | mean: 1.0
10 | name: L
11 | numnewmeas: null
12 | uncerts:
13 | - degf: .inf
14 | desc: null
15 | dist: normal
16 | k: 2
17 | name: u(L)
18 | unc: 1
19 | units: meter
20 | units: meter
21 | - autocorrelate: true
22 | desc: ''
23 | mean: 345.24
24 | name: v
25 | numnewmeas: null
26 | uncerts:
27 | - degf: .inf
28 | desc: null
29 | dist: normal
30 | k: 2
31 | name: u(v)
32 | unc: '.5'
33 | units: meter / second
34 | units: meter / second
35 | mode: reverse
36 | name: uncertainty
37 | reverse:
38 | funcname: f
39 | solvefor: L
40 | targetnom: 440.0
41 | targetunc: 1.3
42 | targetunits: Hz
43 | samples: 1000000
44 | unitdefs: stadia = 185*meter
45 |
--------------------------------------------------------------------------------
/suncal/common/multivariate_t.py:
--------------------------------------------------------------------------------
1 | ''' Multivariate Student-T Distribution
2 |
3 | Scipy's multivariate_t does not account for covariance/correlation.
4 | '''
5 | import numpy as np
6 |
7 |
8 | def multivariate_t_rvs(mean, corr, df=np.inf, size=1):
9 | ''' Generate random variables from multivariate Student t distribution.
10 | Not implemented in Scipy. Code taken from scikits package.
11 |
12 | Args:
13 | mean (array): Mean of random variables, shape (M,)
14 | corr (array): Correlation matrix, shape (M,M)
15 | df (float): degrees of freedom
16 | size (int): Number of samples for output array
17 |
18 | Returns:
19 | rvs (array): Correlated random variables, shape (size, M)
20 | '''
21 | mean = np.asarray(mean)
22 | d = len(mean)
23 | if df == np.inf:
24 | x = 1.
25 | else:
26 | x = np.random.chisquare(df, size)/df
27 | z = np.random.multivariate_normal(np.zeros(d), corr, (size,))
28 | return mean + z / np.sqrt(x)[:, None]
29 |
--------------------------------------------------------------------------------
/doc/Examples/ex_magphase.yaml:
--------------------------------------------------------------------------------
1 | # PSL Uncertainty Calculator Config File
2 | #
3 | description: 'Example of converting real and imagniary into magnitude and phase from
4 | GUM Supplement 2, section 9.3.
5 |
6 |
7 | The GUM supplement considers six cases, re = 0.001, 0.01, and 0.1, with and without
8 | correlation coefficient of 0.9 between re and im.
9 |
10 |
11 | Try calculating with various values of re and adding correlation. Switch the output
12 | view to "Joint PDF" to view probability plots similar to those in the GUM.'
13 | functions:
14 | - desc: Magnitude
15 | expr: sqrt(im**2 + re**2)
16 | name: mag
17 | - desc: Phase
18 | expr: atan2(im, re)
19 | name: ph
20 | inputs:
21 | - desc: Real Component
22 | mean: 0.001
23 | name: re
24 | uncerts:
25 | - degf: .inf
26 | desc: ''
27 | dist: normal
28 | name: u(re)
29 | std: '0.01'
30 | - desc: Imaginary Component
31 | mean: 0.0
32 | name: im
33 | uncerts:
34 | - degf: .inf
35 | desc: ''
36 | dist: normal
37 | name: u(im)
38 | std: '0.01'
39 | samples: 1000000
40 |
--------------------------------------------------------------------------------
/test/ex_xrf.yaml:
--------------------------------------------------------------------------------
1 | - description: X-Ray Fluorescence Example from SNL ENGR224 Course.
2 | functions:
3 | - desc: Corrected measurement of standard
4 | expr: X_1*Y_u/X_2
5 | name: Y_c
6 | units: micrometer
7 | inputs:
8 | - desc: VLSI measurement of SHS
9 | mean: 0.182
10 | name: X_1
11 | uncerts:
12 | - degf: 9.0
13 | desc: ''
14 | dist: normal
15 | name: u(X_1)
16 | std: '.00093'
17 | units: micrometer
18 | units: micrometer
19 | - desc: Uncorrected Dektak measurement of check standard
20 | mean: 697.8
21 | name: Y_u
22 | uncerts:
23 | - degf: 19.0
24 | desc: ''
25 | dist: normal
26 | k: '1.00'
27 | name: u(Y_u)
28 | unc: '2.6'
29 | units: nanometer
30 | units: nanometer
31 | - desc: Dektak measurement of SHS
32 | mean: 0.1823
33 | name: X_2
34 | uncerts:
35 | - degf: 19.0
36 | desc: ''
37 | dist: normal
38 | name: u(X_2)
39 | std: '.00058'
40 | units: micrometer
41 | units: micrometer
42 | mode: uncertainty
43 | name: uncertainty
44 | samples: 1000000
45 |
--------------------------------------------------------------------------------
/doc/Examples/Data/expdata.txt:
--------------------------------------------------------------------------------
1 | 1.000000000000000000e+02 5.169808152473608942e+01
2 | 1.050000000000000000e+02 5.180781611856781410e+01
3 | 1.100000000000000000e+02 5.094888374724005331e+01
4 | 1.150000000000000000e+02 4.708503594700847827e+01
5 | 1.200000000000000000e+02 4.768859383715621902e+01
6 | 1.250000000000000000e+02 4.992800094178692660e+01
7 | 1.300000000000000000e+02 5.397026004241439523e+01
8 | 1.350000000000000000e+02 5.296769351659458636e+01
9 | 1.400000000000000000e+02 4.879219398910110783e+01
10 | 1.450000000000000000e+02 5.583016575616789368e+01
11 | 1.500000000000000000e+02 5.559747666790158149e+01
12 | 1.550000000000000000e+02 5.696174101241076215e+01
13 | 1.600000000000000000e+02 6.584246984343901943e+01
14 | 1.650000000000000000e+02 6.836687759559040956e+01
15 | 1.700000000000000000e+02 6.664236660396821321e+01
16 | 1.750000000000000000e+02 7.044431451372656738e+01
17 | 1.800000000000000000e+02 7.955717948741764189e+01
18 | 1.850000000000000000e+02 8.221611739404772834e+01
19 | 1.900000000000000000e+02 9.432329953680562085e+01
20 | 1.950000000000000000e+02 1.078067351745929869e+02
21 | 2.000000000000000000e+02 1.239333629513413655e+02
22 |
--------------------------------------------------------------------------------
/doc/Examples/Data/logistic.txt:
--------------------------------------------------------------------------------
1 | 0.000000000000000000e+00 -1.979552060128814794e+00
2 | 2.000000000000000000e+00 -2.009400699924129619e+00
3 | 4.000000000000000000e+00 -2.274739735488817693e+00
4 | 6.000000000000000000e+00 -1.860319467493679380e+00
5 | 8.000000000000000000e+00 -2.016032658167356217e+00
6 | 1.000000000000000000e+01 -1.883255614033866543e+00
7 | 1.200000000000000000e+01 -1.874352777610102994e+00
8 | 1.400000000000000000e+01 -1.583184455772294275e+00
9 | 1.600000000000000000e+01 -1.067305925393664534e+00
10 | 1.800000000000000000e+01 -7.583029961309668554e-01
11 | 2.000000000000000000e+01 -7.471893809822292842e-02
12 | 2.200000000000000000e+01 5.539187417616771025e-01
13 | 2.400000000000000000e+01 1.208882329518928156e+00
14 | 2.600000000000000000e+01 1.459050437323889282e+00
15 | 2.800000000000000000e+01 1.727275701470566416e+00
16 | 3.000000000000000000e+01 1.697410939874367308e+00
17 | 3.200000000000000000e+01 1.974846894161319755e+00
18 | 3.400000000000000000e+01 2.015892027703139799e+00
19 | 3.600000000000000000e+01 1.848128442244654002e+00
20 | 3.800000000000000000e+01 2.060533175689868379e+00
21 | 4.000000000000000000e+01 2.074865250804605488e+00
22 |
--------------------------------------------------------------------------------
/test/ex_expansion.yaml:
--------------------------------------------------------------------------------
1 | # PSL Uncertainty Calculator Config File
2 | # Configures the calculator for Example E13 in NIST Technical Note 1900
3 | # Thermal Expansion Coefficient (using t distribution).
4 |
5 | functions:
6 | - desc: Thermal Expansion Coefficient
7 | expr: (-L0 + L1)/(L0*(-T0 + T1))
8 | name: alpha
9 | inputs:
10 | - desc: Length at temperature T0
11 | mean: 1.4999
12 | name: L0
13 | uncerts:
14 | - degf: 3.0
15 | desc: ''
16 | df: '3'
17 | dist: t
18 | name: u(L0)
19 | unc: '.0001'
20 | - desc: Length at temperature T1
21 | mean: 1.5021
22 | name: L1
23 | uncerts:
24 | - degf: 3.0
25 | desc: ''
26 | df: '3'
27 | dist: t
28 | name: u(L1)
29 | unc: '.0002'
30 | - desc: First Measurement Temperature
31 | mean: 288.15
32 | name: T0
33 | uncerts:
34 | - degf: 3.0
35 | desc: ''
36 | df: '3'
37 | dist: t
38 | name: u(T0)
39 | unc: '.02'
40 | - desc: Second Measurement Temperature
41 | mean: 373.1
42 | name: T1
43 | uncerts:
44 | - degf: 3.0
45 | desc: ''
46 | df: '3'
47 | dist: t
48 | name: u(T1)
49 | unc: '.05'
50 | samples: 1000
51 | seed: 1
52 |
--------------------------------------------------------------------------------
/suncal/common/style/suncal_dark.mplstyle:
--------------------------------------------------------------------------------
1 | lines.linewidth : 2.0
2 | lines.color: dddddd
3 | text.color: dddddd
4 |
5 | patch.linewidth: 0.5
6 | patch.facecolor: none
7 | patch.edgecolor: eeeeee
8 | patch.antialiased: True
9 |
10 | axes.facecolor: 333333
11 | axes.edgecolor: dddddd
12 | axes.labelcolor: dddddd
13 | axes.titlecolor: dddddd
14 | axes.grid : True
15 | axes.titlesize: x-large
16 | axes.labelsize: large
17 |
18 | axes.prop_cycle: cycler('color', ['007a86', 'ba0c2f', 'ffc600', '8a387c', 'ed8b00', 'a8aa19', '63666a', 'c05131', 'd6a461', 'a7a8aa'])
19 | xtick.color: dddddd
20 | ytick.color: dddddd
21 | xtick.direction: in
22 | ytick.direction: in
23 |
24 | grid.color: 444444
25 | grid.linestyle: --
26 | grid.linewidth: 0.5
27 |
28 | legend.fancybox: True
29 | legend.labelcolor: dddddd
30 |
31 | figure.facecolor: 00000000
32 | figure.subplot.left: .12
33 | figure.subplot.right: .95
34 | figure.subplot.top: .95
35 | figure.subplot.bottom: .12
36 | figure.subplot.hspace: .4
37 | figure.subplot.wspace: .4
38 |
39 | axes.formatter.use_mathtext: True
40 | axes.formatter.limits: -4, 4
41 | axes.formatter.useoffset: False
42 | mathtext.fontset: stixsans
43 | mathtext.default: regular
44 |
45 |
--------------------------------------------------------------------------------
/suncal/common/style/suncal_light.mplstyle:
--------------------------------------------------------------------------------
1 | lines.linewidth : 2.0
2 | lines.color: 222222
3 | text.color: 222222
4 |
5 | patch.linewidth: 0.5
6 | patch.facecolor: eeeeee
7 | patch.edgecolor: 222222
8 | patch.antialiased: True
9 |
10 | axes.facecolor: eeeeee
11 | axes.edgecolor: 222222
12 | axes.labelcolor: 222222
13 | axes.titlecolor: 222222
14 | axes.grid : True
15 | axes.titlesize: x-large
16 | axes.labelsize: large
17 |
18 | axes.prop_cycle: cycler('color', ['007a86', 'ba0c2f', 'ffc600', '8a387c', 'ed8b00', 'a8aa19', '63666a', 'c05131', 'd6a461', 'a7a8aa'])
19 | xtick.color: 222222
20 | ytick.color: 222222
21 | xtick.direction: in
22 | ytick.direction: in
23 |
24 | grid.color: dddddd
25 | grid.linestyle: --
26 | grid.linewidth: 0.5
27 |
28 | legend.fancybox: True
29 | legend.labelcolor: 222222
30 |
31 | figure.facecolor: 00000000
32 | figure.subplot.left: .12
33 | figure.subplot.right: .95
34 | figure.subplot.top: .95
35 | figure.subplot.bottom: .12
36 | figure.subplot.hspace: .4
37 | figure.subplot.wspace: .4
38 |
39 | axes.formatter.use_mathtext: True
40 | axes.formatter.limits: -4, 4
41 | axes.formatter.useoffset: False
42 | mathtext.fontset: stixsans
43 | mathtext.default: regular
44 |
45 |
--------------------------------------------------------------------------------
/suncal/gui/gui_math.py:
--------------------------------------------------------------------------------
1 | ''' Render Latex Math '''
2 | from PyQt6 import QtWidgets, QtGui
3 |
4 | from ..common import uparser, report
5 | from . import gui_styles
6 |
7 |
8 | def pixmap_from_latex(expr: str, fontsize: float = 16) -> QtGui.QPixmap:
9 | ''' Render the LaTeX math expression to a QPixmap '''
10 | tex = uparser.parse_math_with_quantities_to_tex(expr)
11 | math = report.Math.from_latex(tex)
12 | return pixmap_from_reportmath(math, fontsize=fontsize)
13 |
14 |
15 | def pixmap_from_sympy(expr, fontsize: float = 16) -> QtGui.QPixmap:
16 | ''' Render the Sympy expression to a QPixmap '''
17 | math = report.Math.from_sympy(expr)
18 | return pixmap_from_reportmath(math, fontsize=fontsize)
19 |
20 |
21 | def pixmap_from_reportmath(math: report.Math, fontsize: float = 16) -> QtGui.QPixmap:
22 | ''' Render the report.Math object to a QPixmap '''
23 | color = gui_styles.color.math
24 | ratio = QtWidgets.QApplication.instance().primaryScreen().devicePixelRatio()
25 | svgbuf = math.svg_buf(fontsize=fontsize*ratio, color=color)
26 | px = QtGui.QPixmap()
27 | px.loadFromData(svgbuf.read())
28 | px.setDevicePixelRatio(ratio)
29 | return px
30 |
--------------------------------------------------------------------------------
/test/test_config.py:
--------------------------------------------------------------------------------
1 | ''' Test config load/save
2 | NOTE: Will reset settings to default.
3 | '''
4 | import pytest
5 |
6 | from suncal.gui.gui_settings import gui_settings
7 |
8 | gui_settings.set_defaults()
9 |
10 |
11 | def test_color():
12 | gui_settings.colormap_contour = 'red'
13 | gui_settings.colormap_scatter = 'yellow'
14 | assert gui_settings.colormap_contour == 'red'
15 | assert gui_settings.colormap_scatter == 'yellow'
16 |
17 |
18 | def test_dist():
19 | dlist = ['normal', 'uniform', 'alpha']
20 | gui_settings.distributions = dlist
21 | assert gui_settings.distributions == dlist
22 |
23 |
24 | def test_samples():
25 | gui_settings.samples = 1000
26 | assert gui_settings.samples == 1000
27 |
28 | gui_settings.samples = 'abc' # Invalid values go to default
29 | assert gui_settings.samples == 1000000
30 |
31 | gui_settings.samples = -10 # Negative values become 1
32 | assert gui_settings.samples == 1
33 |
34 |
35 | def test_default():
36 | gui_settings.set_defaults()
37 | assert gui_settings.colormap_scatter == 'viridis'
38 | assert gui_settings.colormap_contour == 'viridis'
39 | assert gui_settings.samples == 1000000
40 |
--------------------------------------------------------------------------------
/suncal/gui/icons/icons.py:
--------------------------------------------------------------------------------
1 | ''' GUI Icon Lookup '''
2 | from PyQt6 import QtCore, QtGui
3 |
4 | from . import pngs
5 | from . import snllogo
6 | from .. import gui_styles
7 |
8 |
9 | def icon(name):
10 | ''' Load an icon from the icons file by name '''
11 | if gui_styles.isdark():
12 | name += '_dark'
13 | img = QtGui.QPixmap()
14 | img.loadFromData(QtCore.QByteArray.fromBase64(getattr(pngs, name)), format='SVG')
15 | return QtGui.QIcon(img)
16 |
17 |
18 | def appicon(pixmap=False):
19 | ''' Load the app icon/logo
20 |
21 | Args:
22 | pixmap: Return a QPixmap if True, a QIcon if False.
23 | '''
24 | img = QtGui.QPixmap()
25 | img.loadFromData(QtCore.QByteArray.fromBase64(snllogo.logo), format='PNG')
26 | if pixmap:
27 | return img
28 | return QtGui.QIcon(img)
29 |
30 |
31 | def logo_snl(pixmap=False):
32 | ''' Load the SNL thunderbird icon/logo
33 |
34 | Args:
35 | pixmap: Return a QPixmap if True, a QIcon if False.
36 | '''
37 | img = QtGui.QPixmap()
38 | img.loadFromData(QtCore.QByteArray.fromBase64(snllogo.logosnl), format='PNG')
39 | if pixmap:
40 | return img
41 | return QtGui.QIcon(img)
42 |
--------------------------------------------------------------------------------
/suncal/mqa/system.py:
--------------------------------------------------------------------------------
1 | ''' System of multiple MQA Quantities '''
2 | from .equipment import EquipmentList
3 | from .mqa import MqaQuantity
4 | from .result import MqaSystemResult
5 | from .guardband import MqaGuardbandRuleset
6 |
7 |
8 | class MqaSystem:
9 | ''' Measurement system '''
10 | def __init__(self):
11 | self.quantities: list[MqaQuantity] = []
12 | self.equipment: EquipmentList = EquipmentList()
13 | self.gbrules: MqaGuardbandRuleset = MqaGuardbandRuleset()
14 |
15 | def add(self):
16 | ''' Add a quantity to the system '''
17 | qty = MqaQuantity()
18 | qty.equipmentlist = self.equipment
19 | qty.measurand.name = 'Quantity'
20 | self.quantities.append(qty)
21 | return qty
22 |
23 | def mqa_mode(self) -> int:
24 | ''' Determine what is enabled (relaibility decay and/or costs) '''
25 | mode = MqaQuantity.Mode.BASIC
26 | for qty in self.quantities:
27 | mode = max(mode, qty.mqa_mode)
28 | return mode
29 |
30 | def calculate(self) -> MqaSystemResult:
31 | ''' Calculate all quantities '''
32 | results = []
33 | for qty in self.quantities:
34 | results.append(qty.calculate())
35 | return MqaSystemResult(results)
36 |
--------------------------------------------------------------------------------
/suncal/gui/widgets/__init__.py:
--------------------------------------------------------------------------------
1 | from .mkdown import MarkdownTextEdit, savereport
2 | from .stack import SlidingStackedWidget
3 | from .panel import WidgetPanel
4 | from .buttons import ToolButton, RoundButton, PlusButton, MinusButton, PlusMinusButton, LeftButton, RightButton, SmallToolButton
5 | from .combo import (QHLine,
6 | ComboNoWheel,
7 | ComboLabel,
8 | SpinWidget,
9 | FloatLineEdit,
10 | PercentLineEdit,
11 | IntLineEdit,
12 | DoubleLineEdit,
13 | LineEditLabelWidget,
14 | SpinBoxLabelWidget,
15 | ListSelectWidget)
16 | from .table import (ReadOnlyTableItem,
17 | EditableTableItem,
18 | FloatTableItem,
19 | FloatTableWidget,
20 | TableItemTex)
21 | from .stats import (ExpandedConfidenceWidget,
22 | DistributionEditTable,
23 | PopupHelp)
24 | from .colormap import ColorMapDialog
25 | from .intervalbins import BinData
26 | from .assign import AssignColumnWidget
27 | from .pdf import PdfPopupButton, PdfPopupDialog
28 | from .equipment import EquipmentEdit
29 | from .mqa import ToleranceCheck, ToleranceWidget
30 |
--------------------------------------------------------------------------------
/doc/Examples/ex_expansion.yaml:
--------------------------------------------------------------------------------
1 | - description: 'Thermal Expansion Coefficient
2 |
3 | Example E13 in NIST Technical Note 1900 (using t distribution inputs).'
4 | functions:
5 | - desc: Thermal Expansion Coefficient
6 | expr: (-L0 + L1)/(L0*(-T0 + T1))
7 | name: alpha
8 | units: 1 / kelvin
9 | inputs:
10 | - desc: Length at temperature T1
11 | mean: 1.5021
12 | name: L1
13 | uncerts:
14 | - degf: 3.0
15 | desc: ''
16 | dist: t
17 | name: u(L1)
18 | unc: '.0002'
19 | units: meter
20 | units: meter
21 | - desc: Second Measurement Temperature
22 | mean: 373.1
23 | name: T1
24 | uncerts:
25 | - degf: 3.0
26 | desc: ''
27 | dist: t
28 | name: u(T1)
29 | unc: '.05'
30 | units: kelvin
31 | units: kelvin
32 | - desc: Length at temperature T0
33 | mean: 1.4999
34 | name: L0
35 | uncerts:
36 | - degf: 3.0
37 | desc: ''
38 | dist: t
39 | name: u(L0)
40 | unc: '.0001'
41 | units: meter
42 | units: meter
43 | - desc: First Measurement Temperature
44 | mean: 288.15
45 | name: T0
46 | uncerts:
47 | - degf: 3.0
48 | desc: ''
49 | dist: t
50 | name: u(T0)
51 | unc: '.02'
52 | units: kelvin
53 | units: kelvin
54 | mode: uncertainty
55 | name: uncertainty
56 | samples: 1000000
57 | seed: 44251
58 |
--------------------------------------------------------------------------------
/doc/Examples/ex_gageball_reversesweep.yaml:
--------------------------------------------------------------------------------
1 | - description: 'Customer requires density uncertainty of 0.04 g/cm^3 (k=2) for a gage
2 | ball of mass 86.03 g and diameter 22.225 mm.
3 |
4 |
5 | Find a calibration provider which can meet the density uncertainty requirement.
6 | Use a reverse-sweep. Anything provider that falls under the curve is acceptable.'
7 | functions:
8 | - desc: Gage Ball Density
9 | expr: 6*m/(pi*d**3)
10 | name: rho
11 | units: gram / centimeter ** 3
12 | inputs:
13 | - desc: Diameter measurement
14 | mean: 22.225
15 | name: d
16 | uncerts:
17 | - degf: .inf
18 | desc: Diameter Uncertainty
19 | dist: normal
20 | name: u(d)
21 | std: '5'
22 | units: micrometer
23 | units: millimeter
24 | - desc: Mass measurement
25 | mean: 86030.0
26 | name: m
27 | uncerts:
28 | - degf: .inf
29 | desc: Mass Uncertainty
30 | dist: normal
31 | name: u(m)
32 | std: '10'
33 | units: milligram
34 | units: milligram
35 | mode: reversesweep
36 | name: reverse
37 | reverse:
38 | func: 0
39 | solvefor: m
40 | targetnom: 14.967
41 | targetunc: 0.02
42 | samples: 1000000
43 | sweeps:
44 | - comp: u(d)
45 | param: std
46 | values:
47 | - 1.0
48 | - 2.0
49 | - 3.0
50 | - 4.0
51 | - 5.0
52 | - 6.0
53 | - 7.0
54 | - 8.0
55 | - 9.0
56 | var: d
57 |
--------------------------------------------------------------------------------
/uncertwinonefile.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python -*-
2 |
3 | block_cipher = None
4 |
5 |
6 | a = Analysis(['suncal\\gui\\__main__.py'],
7 | pathex=[],
8 | binaries=[],
9 | datas=[('suncal/gui/SUNCALmanual.pdf', '.'),
10 | ('suncal/common/style/suncal_light.mplstyle', '.'),
11 | ('suncal/common/style/suncal_dark.mplstyle', '.')],
12 | hiddenimports=['scipy._lib.array_api_compat.numpy.fft'],
13 | hookspath=[],
14 | hooksconfig={
15 | 'matplotlib': {'backends': ['Qt5Agg', 'SVG', 'AGG', 'PDF']},
16 | },
17 | excludes=[
18 | '_tkinter', 'tk85.dll', 'tcl85.dll',
19 | '_sqlite3', 'zmq', 'tornado', 'IPython'
20 | ],
21 | runtime_hooks=[],
22 | win_no_prefer_redirects=False,
23 | win_private_assemblies=False,
24 | cipher=block_cipher)
25 |
26 | pyz = PYZ(a.pure, a.zipped_data,
27 | cipher=block_cipher)
28 | exe = EXE(pyz,
29 | a.scripts,
30 | a.binaries,
31 | a.zipfiles,
32 | a.datas,
33 | name='Suncal',
34 | debug=False,
35 | strip=False,
36 | icon='suncal/gui/icons/PSLcal_logo.ico',
37 | upx=True,
38 | console=False,
39 | exclude_binaries=False,
40 | version='winexe_version_info.txt')
41 |
42 |
--------------------------------------------------------------------------------
/test/ex_viscometer.yaml:
--------------------------------------------------------------------------------
1 | # PSL Uncertainty Calculator Config File
2 | # Falling Ball Viscometer - Example E3 from NIST Technical Note 1900
3 |
4 | functions:
5 | - desc: Viscosity
6 | expr: mu_c*t_m*(rho_b - rho_m)/(t_c*(rho_b - rho_c))
7 | name: mu_m
8 | inputs:
9 | - desc: Viscosity of calibration liquid
10 | mean: 4.63
11 | name: mu_c
12 | uncerts:
13 | - degf: .inf
14 | desc: ''
15 | dist: normal
16 | name: u(mu_c)
17 | std: '0.0463'
18 | - desc: Travel time (s) in sodium hydroxide solution
19 | mean: 61.0
20 | name: t_m
21 | uncerts:
22 | - degf: .inf
23 | desc: ''
24 | dist: normal
25 | name: u(t_m)
26 | std: '6.1'
27 | - desc: Density (kg/m3) of sodium hydroxide solution
28 | mean: 1180.0
29 | name: rho_m
30 | uncerts:
31 | - degf: .inf
32 | desc: ''
33 | dist: normal
34 | name: u(rho_m)
35 | std: '0.5'
36 | - desc: Density (kg/m3) of calibration liquid
37 | mean: 810.0
38 | name: rho_c
39 | uncerts:
40 | - degf: .inf
41 | desc: ''
42 | dist: normal
43 | name: u(rho_c)
44 | std: '0.5'
45 | - desc: Travel time (s) in calibration liquid
46 | mean: 36.6
47 | name: t_c
48 | uncerts:
49 | - degf: .inf
50 | desc: ''
51 | dist: normal
52 | name: u(t_c)
53 | std: '5.49'
54 | - desc: Density (kg/m3) of ball
55 | mean: 2217.0
56 | name: rho_b
57 | uncerts:
58 | - degf: .inf
59 | desc: ''
60 | dist: normal
61 | name: u(rho_b)
62 | std: '0.5'
63 | samples: 1000000
64 | seed: 1
65 |
--------------------------------------------------------------------------------
/doc/Examples/ex_voltageanova.yaml:
--------------------------------------------------------------------------------
1 | - colnames:
2 | - 1.0
3 | - 2.0
4 | - 3.0
5 | - 4.0
6 | - 5.0
7 | - 6.0
8 | - 7.0
9 | - 8.0
10 | - 9.0
11 | - 10.0
12 | data:
13 | - - 10.000172
14 | - 6.0e-05
15 | - 5.0
16 | - - 10.000116
17 | - 7.7e-05
18 | - 5.0
19 | - - 10.000013
20 | - 0.000111
21 | - 5.0
22 | - - 10.000144
23 | - 0.000101
24 | - 5.0
25 | - - 10.000106
26 | - 6.7e-05
27 | - 5.0
28 | - - 10.000031
29 | - 9.3e-05
30 | - 5.0
31 | - - 10.00006
32 | - 8.0e-05
33 | - 5.0
34 | - - 10.000125
35 | - 7.3e-05
36 | - 5.0
37 | - - 10.000163
38 | - 8.8e-05
39 | - 5.0
40 | - - 10.000041
41 | - 8.6e-05
42 | - 5.0
43 | desc: This is the Analysis of Variance example described in appendix H.5 of the
44 | GUM. Data is from Table H.9. Only the mean, standard deviation, and number of
45 | measurements for each group are provided.
46 | means:
47 | - 10.000172
48 | - 10.000116
49 | - 10.000013
50 | - 10.000144
51 | - 10.000106
52 | - 10.000031
53 | - 10.00006
54 | - 10.000125
55 | - 10.000163
56 | - 10.000041
57 | mode: data
58 | name: data
59 | nmeas:
60 | - 5.0
61 | - 5.0
62 | - 5.0
63 | - 5.0
64 | - 5.0
65 | - 5.0
66 | - 5.0
67 | - 5.0
68 | - 5.0
69 | - 5.0
70 | stds:
71 | - 6.0e-05
72 | - 7.7e-05
73 | - 0.000111
74 | - 0.000101
75 | - 6.7e-05
76 | - 9.3e-05
77 | - 8.0e-05
78 | - 7.3e-05
79 | - 8.8e-05
80 | - 8.6e-05
81 | summary: true
82 |
--------------------------------------------------------------------------------
/uncertwin.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python -*-
2 |
3 | block_cipher = None
4 |
5 |
6 | a = Analysis(['suncal\\gui\\__main__.py'],
7 | pathex=[],
8 | binaries=[],
9 | datas=[('suncal/gui/SUNCALmanual.pdf', '.'),
10 | ('suncal/common/style/suncal_light.mplstyle', '.'),
11 | ('suncal/common/style/suncal_dark.mplstyle', '.')],
12 | hiddenimports=['scipy._lib.array_api_compat.numpy.fft'],
13 | hookspath=[],
14 | hooksconfig={
15 | 'matplotlib': {'backends': ['Qt5Agg', 'SVG', 'AGG', 'PDF']},
16 | },
17 | excludes=[
18 | '_tkinter', 'tk85.dll', 'tcl85.dll',
19 | '_sqlite3', 'zmq', 'tornado', 'IPython'
20 | ],
21 | runtime_hooks=[],
22 | win_no_prefer_redirects=False,
23 | win_private_assemblies=False,
24 | cipher=block_cipher)
25 |
26 | pyz = PYZ(a.pure, a.zipped_data,
27 | cipher=block_cipher)
28 | exe = EXE(pyz,
29 | a.scripts,
30 | exclude_binaries=True,
31 | name='Suncal',
32 | debug=False,
33 | strip=False,
34 | icon='suncal/gui/icons/PSLcal_logo.ico',
35 | upx=True,
36 | console=False,
37 | version='winexe_version_info.txt')
38 | coll = COLLECT(exe,
39 | a.binaries,
40 | a.zipfiles,
41 | a.datas,
42 | strip=False,
43 | upx=True,
44 | name='Suncal')
45 |
--------------------------------------------------------------------------------
/suncal/gui/help_strings/distexplore_help.py:
--------------------------------------------------------------------------------
1 | ''' Inline help reports for Distribution Explorer tools '''
2 | from ...common import report
3 |
4 |
5 | class DistExploreHelp:
6 | @staticmethod
7 | def disthelp():
8 | rpt = report.Report()
9 | rpt.hdr('Distribution Explorer', level=2)
10 | rpt.txt('Mostly for training and educational purposes, the Distribution '
11 | 'Explorer allows entry of probability distributions, '
12 | 'generating random samples from distributions, and combining '
13 | 'multiple distributions through the Monte Carlo method.\n\n')
14 | rpt.txt('Use the + and - buttons to add or remove new probability distributions. '
15 | 'Give each distribution a name (but avoid Python keywords or an `ERROR` '
16 | 'will be shown). The `normal...` button is used to change the '
17 | 'distribution parameters. Press the `Sample` button to generate '
18 | 'and display random samples from that distribution.\n\n'
19 | 'To perform a Monte Carlo analysis, enter one or more distributions, '
20 | 'then add a new distribution where the name is an expression, '
21 | 'such as `a+b`. Common arithmetic functions, trigonomotric functions, '
22 | 'exponential and log functions are recognized. When a Monte Carlo '
23 | 'expression is entered, the `Sample` button changes to `Calculate`. '
24 | 'Use this button to see the results of the combined distributions.')
25 | return rpt
26 |
--------------------------------------------------------------------------------
/doc/Examples/ex_intervalA3.yaml:
--------------------------------------------------------------------------------
1 | - I0: 180
2 | assets:
3 | A:
4 | enddates:
5 | - 735399.0
6 | - 735601.0
7 | - 735847.0
8 | - 736029.0
9 | - 736151.0
10 | passfail:
11 | - 1.0
12 | - 1.0
13 | - 1.0
14 | - 1.0
15 | - 1.0
16 | startdates:
17 | - 735127.0
18 | - 735399.0
19 | - 735601.0
20 | - 735847.0
21 | - 736029.0
22 | B:
23 | enddates:
24 | - 735488.0
25 | - 735759.0
26 | - 736156.0
27 | passfail:
28 | - 0.0
29 | - 0.0
30 | - 0.0
31 | startdates:
32 | - 735211.0
33 | - 735488.0
34 | - 735968.0
35 | C:
36 | enddates:
37 | - 735590.0
38 | - 735869.0
39 | - 736056.0
40 | - 736186.0
41 | passfail:
42 | - 1.0
43 | - 1.0
44 | - .nan
45 | - 1.0
46 | startdates:
47 | - 735251.0
48 | - 735590.0
49 | - 735869.0
50 | - 736056.0
51 | D:
52 | enddates:
53 | - 735793.0
54 | - 735977.0
55 | - 736159.0
56 | passfail:
57 | - 1.0
58 | - 1.0
59 | - 1.0
60 | startdates:
61 | - 735601.0
62 | - 735793.0
63 | - 735977.0
64 | conf: 0.5
65 | desc: 'Four assets with pass/fail data and nominal interval of 180 days are entered.
66 | Reliability target of 95% used to calculate a new interval.
67 |
68 | '
69 | maxchange: 2
70 | maxint: 1865
71 | mindelta: 5
72 | minint: 14
73 | mode: intervaltestasset
74 | name: interval
75 | target: 0.95
76 | thresh: 999
77 | tol: 56
78 |
--------------------------------------------------------------------------------
/doc/Examples/ex_reactance.yaml:
--------------------------------------------------------------------------------
1 | - confidence: 0.95
2 | correlate: true
3 | desc: ''
4 | mode: system
5 | name: system
6 | quantities:
7 | - desc: ''
8 | description: ''
9 | equation: V*cos(theta)/I
10 | name: ''
11 | symbol: R
12 | tolerance: null
13 | type: indirect
14 | units: ohm
15 | - desc: ''
16 | description: ''
17 | name: ''
18 | symbol: I
19 | testpoint: 19.660999999999998
20 | tolerance: null
21 | type: direct
22 | typea:
23 | - 19.663
24 | - 19.639
25 | - 19.64
26 | - 19.685
27 | - 19.678
28 | typeb: []
29 | units: milliampere
30 | - desc: ''
31 | description: ''
32 | name: ''
33 | symbol: V
34 | testpoint: 4.9990000000000006
35 | tolerance: null
36 | type: direct
37 | typea:
38 | - 5.007
39 | - 4.994
40 | - 5.005
41 | - 4.99
42 | - 4.999
43 | typeb: []
44 | units: volt
45 | - desc: ''
46 | description: ''
47 | name: ''
48 | symbol: theta
49 | testpoint: 1.0444600000000002
50 | tolerance: null
51 | type: direct
52 | typea:
53 | - 1.0456
54 | - 1.0438
55 | - 1.0468
56 | - 1.0428
57 | - 1.0433
58 | typeb: []
59 | units: radian
60 | - desc: ''
61 | description: ''
62 | equation: V*sin(theta)/I
63 | name: ''
64 | symbol: X
65 | tolerance: null
66 | type: indirect
67 | units: ohm
68 | - desc: ''
69 | description: ''
70 | equation: V/I
71 | name: ''
72 | symbol: Z
73 | tolerance: null
74 | type: indirect
75 | units: ohm
76 | samples: 1000000
77 | seed: null
78 |
--------------------------------------------------------------------------------
/winexe_version_info.txt:
--------------------------------------------------------------------------------
1 | # UTF-8
2 | #
3 | # For more details about fixed file info 'ffi' see:
4 | # http://msdn.microsoft.com/en-us/library/ms646997.aspx
5 | VSVersionInfo(
6 | ffi=FixedFileInfo(
7 | # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
8 | # Set not needed items to zero 0.
9 | filevers=(1, 7, 0, 1),
10 | prodvers=(1, 7, 0, 1),
11 | # Contains a bitmask that specifies the valid bits 'flags'r
12 | mask=0x0,
13 | # Contains a bitmask that specifies the Boolean attributes of the file.
14 | flags=0x0,
15 | # The operating system for which this file was designed.
16 | # 0x4 - NT and there is no need to change it.
17 | OS=0x4,
18 | # The general type of file.
19 | # 0x1 - the file is an application.
20 | fileType=0x1,
21 | # The function of the file.
22 | # 0x0 - the function is not defined for this fileType
23 | subtype=0x0,
24 | # Creation date and time stamp.
25 | date=(0, 0)
26 | ),
27 | kids=[
28 | StringFileInfo(
29 | [
30 | StringTable(
31 | u'040904b0',
32 | [StringStruct(u'CompanyName', u'Sandia National Laboratories'),
33 | StringStruct(u'FileDescription', u'Suncal - Sandia Uncertainty Calculator'),
34 | StringStruct(u'FileVersion', u'1.7.0'),
35 | StringStruct(u'LegalCopyright', u'(c) Sandia National Laboratories'),
36 | StringStruct(u'ProductName', u'Suncal - Sandia Uncertainty Calculator 1.7.0'),
37 | StringStruct(u'ProductVersion', u'1.7.0')])
38 | ]),
39 | VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
40 | ]
41 | )
42 |
--------------------------------------------------------------------------------
/doc/Examples/ex_viscometer.yaml:
--------------------------------------------------------------------------------
1 | # PSL Uncertainty Calculator Config File
2 | #
3 | description: 'Falling Ball Viscometer
4 |
5 |
6 | Example E3 from NIST Technical Note 1900.'
7 | functions:
8 | - desc: Viscosity
9 | expr: mu_c*t_m*(rho_b - rho_m)/(t_c*(rho_b - rho_c))
10 | name: mu_m
11 | units: mPa s
12 | inputs:
13 | - desc: Viscosity of calibration liquid
14 | mean: 4.63
15 | name: mu_c
16 | units: mPa s
17 | uncerts:
18 | - degf: .inf
19 | desc: ''
20 | dist: normal
21 | name: u_{mu_c}
22 | std: '0.0463'
23 | - desc: Travel time in sodium hydroxide solution
24 | mean: 61.0
25 | name: t_m
26 | units: s
27 | uncerts:
28 | - degf: .inf
29 | desc: ''
30 | dist: normal
31 | name: u_{t_m}
32 | std: '6.1'
33 | - desc: Density of sodium hydroxide solution
34 | mean: 1180.0
35 | name: rho_m
36 | units: kg/m^3
37 | uncerts:
38 | - degf: .inf
39 | desc: ''
40 | dist: normal
41 | name: u_{rho_m}
42 | std: '0.5'
43 | - desc: Density of calibration liquid
44 | mean: 810.0
45 | name: rho_c
46 | units: kg/m^3
47 | uncerts:
48 | - degf: .inf
49 | desc: ''
50 | dist: normal
51 | name: u_{rho_c}
52 | std: '0.5'
53 | - desc: Travel time in calibration liquid
54 | mean: 36.6
55 | name: t_c
56 | units: s
57 | uncerts:
58 | - degf: .inf
59 | desc: ''
60 | dist: normal
61 | name: u_{t_c}
62 | std: '5.49'
63 | - desc: Density of ball
64 | mean: 2217.0
65 | name: rho_b
66 | units: kg/m^3
67 | uncerts:
68 | - degf: .inf
69 | desc: ''
70 | dist: normal
71 | name: u_{rho_b}
72 | std: '0.5'
73 | samples: 1000000
74 |
--------------------------------------------------------------------------------
/doc/Examples/ex_neutrons.yaml:
--------------------------------------------------------------------------------
1 | - functions:
2 | - desc: ''
3 | expr: A*f*F*(S-B)
4 | name: eta
5 | units: null
6 | inputs:
7 | - autocorrelate: true
8 | desc: ''
9 | mean: 1.0
10 | name: A
11 | numnewmeas: null
12 | uncerts:
13 | - a: '.035'
14 | degf: 12.5
15 | desc: null
16 | dist: uniform
17 | k: 2
18 | name: u(A)
19 | unc: 1
20 | units: dimensionless
21 | units: null
22 | - autocorrelate: true
23 | desc: ''
24 | mean: 80.0
25 | name: B
26 | numnewmeas: null
27 | uncerts:
28 | - degf: 9.0
29 | desc: null
30 | dist: normal
31 | k: 1.0
32 | name: u(B)
33 | unc: '2.83'
34 | units: dimensionless
35 | units: null
36 | - autocorrelate: true
37 | desc: ''
38 | mean: 1.27
39 | name: F
40 | numnewmeas: null
41 | uncerts:
42 | - a: '.055'
43 | degf: 12.5
44 | desc: null
45 | dist: uniform
46 | k: 2
47 | name: u(F)
48 | unc: 1
49 | units: dimensionless
50 | units: null
51 | - autocorrelate: true
52 | desc: ''
53 | mean: 9700.0
54 | name: S
55 | numnewmeas: null
56 | uncerts:
57 | - degf: 29.0
58 | desc: null
59 | dist: poisson
60 | k: 2
61 | name: u(S)
62 | unc: 1
63 | units: dimensionless
64 | v: '9700'
65 | units: null
66 | - autocorrelate: true
67 | desc: ''
68 | mean: 4353.0
69 | name: f
70 | numnewmeas: null
71 | uncerts: []
72 | units: null
73 | mode: uncertainty
74 | name: uncertainty
75 | samples: 1000000
76 | unitdefs: stadia = 185*meter
77 |
--------------------------------------------------------------------------------
/suncal/project/proj_explore.py:
--------------------------------------------------------------------------------
1 | ''' Distribution Explorer project component '''
2 |
3 | from .component import ProjectComponent
4 | from ..common import distributions
5 | from ..distexplore import DistExplore
6 |
7 |
8 | class ProjectDistExplore(ProjectComponent):
9 | ''' Distribution Explorer project component '''
10 | def __init__(self, model=None, name='distributions'):
11 | super().__init__(name=name)
12 | self.nsamples = 10000
13 | self.seed = None
14 | if model is None:
15 | self.model = DistExplore()
16 | else:
17 | self.model = model
18 | self._result = self.model
19 |
20 | def calculate(self):
21 | ''' Run calculation '''
22 | return self._result
23 |
24 | def get_config(self):
25 | ''' Get configuration '''
26 | d = {}
27 | d['mode'] = 'distributions'
28 | d['name'] = self.name
29 | d['desc'] = self.description
30 | d['seed'] = self.model.seed
31 | d['distnames'] = [str(x) for x in self.model.dists]
32 | d['distributions'] = [x.get_config() if x is not None else None for x in self.model.dists.values()]
33 | return d
34 |
35 | def load_config(self, config):
36 | ''' Load config into this project '''
37 | self.name = config.get('name', 'distributions')
38 | self.description = config.get('desc', '')
39 | self.seed = config.get('seed', None)
40 | exprs = config.get('distnames', [])
41 | dists = [distributions.from_config(x) if x is not None else None for x in config.get('distributions', [])]
42 | self.model.dists = dict(zip(exprs, dists))
43 |
--------------------------------------------------------------------------------
/suncal/sweep/results/revsweeper.py:
--------------------------------------------------------------------------------
1 | ''' Sweep a Reverse Uncertainty calcualtion '''
2 |
3 | from dataclasses import dataclass
4 |
5 | from ...common import reporter
6 | from ...reverse.reverse import ResultsReverse
7 | from ..report.revsweep import ReportReverseSweep, ReportReverseSweepGum, ReportReverseSweepMc
8 |
9 |
10 | @reporter.reporter(ReportReverseSweepGum)
11 | @dataclass
12 | class ResultReverseSweepGum:
13 | ''' Results of reverse sweep using GUM method '''
14 | resultlist: list
15 | sweeplist: list
16 |
17 | def __len__(self):
18 | return len(self.sweeplist)
19 |
20 | def __getitem__(self, index):
21 | ''' Get results of single point at index '''
22 | return self.resultlist[index]
23 |
24 |
25 | @reporter.reporter(ReportReverseSweepMc)
26 | @dataclass
27 | class ResultReverseSweepMc:
28 | ''' Results of reverse sweep using Monte Carlo method '''
29 | resultlist: list
30 | sweeplist: list
31 |
32 | def __len__(self):
33 | return len(self.sweeplist)
34 |
35 | def __getitem__(self, index):
36 | ''' Get results of single point at index '''
37 | return self.resultlist[index]
38 |
39 |
40 | @reporter.reporter(ReportReverseSweep)
41 | @dataclass
42 | class ResultReverseSweep:
43 | ''' Results of Reverse Sweep using both GUM and Monte Carlo methods '''
44 | gum: ResultReverseSweepGum
45 | montecarlo: ResultReverseSweepMc
46 | sweeplist: list
47 |
48 | def __len__(self):
49 | return len(self.sweeplist)
50 |
51 | def __getitem__(self, index):
52 | ''' Get results of single point at index '''
53 | return ResultsReverse(self.gum[index], self.montecarlo[index])
54 |
--------------------------------------------------------------------------------
/uncertmac.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python -*-
2 |
3 | import sysconfig
4 |
5 | block_cipher = None
6 |
7 | a = Analysis(['suncal/startui.py'],
8 | pathex=['suncal'],
9 | binaries=None,
10 | datas=[('suncal/gui/SUNCALmanual.pdf', '.'),
11 | ('suncal/common/style/suncal_light.mplstyle', '.'),
12 | ('suncal/common/style/suncal_dark.mplstyle', '.')],
13 | hiddenimports=[sysconfig._get_sysconfigdata_name(True), '_sysconfigdata_m_darwin_darwin'], # Needed to patch yet another pyinstaller bug (https://github.com/pyinstaller/pyinstaller/issues/3105)
14 | hookspath=[],
15 | hooksconfig={
16 | 'matplotlib': {'backends': ['Qt5Agg', 'SVG', 'AGG', 'PDF']},
17 | },
18 | excludes=[
19 | '_tkinter', 'tk85.dll', 'tcl85.dll',
20 | '_sqlite3', 'zmq', 'tornado', 'IPython'
21 | ],
22 | runtime_hooks=[],
23 | win_no_prefer_redirects=False,
24 | win_private_assemblies=False,
25 | cipher=block_cipher)
26 | pyz = PYZ(a.pure, a.zipped_data,
27 | cipher=block_cipher)
28 | exe = EXE(pyz,
29 | a.scripts,
30 | a.binaries,
31 | a.zipfiles,
32 | a.datas,
33 | name='Suncal',
34 | debug=False,
35 | strip=False,
36 | upx=True,
37 | exclude_binaries=False,
38 | console=False )
39 | app = BUNDLE(exe,
40 | name='Suncal.app',
41 | icon='suncal/gui/icons/PSLcal_logo.icns',
42 | info_plist={'NSHighResolutionCapable': 'True'},
43 | bundle_identifier=None)
44 |
--------------------------------------------------------------------------------
/test/test_ttable.py:
--------------------------------------------------------------------------------
1 | ''' Test t-table calculations. Test values based on values in GUM Table G.2. '''
2 |
3 | import numpy as np
4 | from suncal.common import ttable
5 |
6 |
7 | def test_t():
8 | # Test calculation of k given conf and degf
9 | assert np.isclose(ttable.k_factor(conf=.6827, degf=1), 1.84, atol=.005)
10 | assert np.isclose(ttable.k_factor(conf=.95, degf=1), 12.71, atol=.005)
11 | assert np.isclose(ttable.k_factor(conf=.9973, degf=5), 5.51, atol=.005)
12 | assert np.isclose(ttable.k_factor(conf=.90, degf=20), 1.72, atol=.005)
13 | assert np.isclose(ttable.k_factor(conf=.9545, degf=1E9), 2.00, atol=.005)
14 |
15 |
16 | def test_conf():
17 | # Test calculation of confidence given k and degf. Tolerance is higher since tp values in GUM
18 | # Table were rounded
19 | assert np.isclose(ttable.confidence(tp=1.84, degf=1), .6827, atol=.005)
20 | assert np.isclose(ttable.confidence(tp=235.8, degf=1), .9973, atol=.005)
21 | assert np.isclose(ttable.confidence(tp=1.81, degf=10), .90, atol=.005)
22 | assert np.isclose(ttable.confidence(tp=2.09, degf=20), .95, atol=.005)
23 | assert np.isclose(ttable.confidence(tp=3.00, degf=1E9), .9973, atol=.005)
24 |
25 |
26 | def test_degf():
27 | # Test calculation of degf given k and confidence.
28 | assert np.isclose(ttable.degf(tp=1.84, conf=.6827), 1, atol=.1)
29 | assert np.isclose(ttable.degf(tp=19.21, conf=.9973), 2, atol=.1)
30 |
31 | # This 2.09 shows up in table on two rows! Fractional part is between 19 and 20
32 | assert np.isclose(ttable.degf(tp=2.09, conf=.95), 19.5, atol=.5)
33 |
34 | assert np.isclose(ttable.degf(tp=3.11, conf=.99), 11, atol=.1)
35 | assert ttable.degf(tp=2.00, conf=.9545) > 1E12 # Infinity
36 |
--------------------------------------------------------------------------------
/suncal/common/style/css.py:
--------------------------------------------------------------------------------
1 | ''' Nice Style Sheet for generating HTML reports '''
2 |
3 | css = '''
4 | body {
5 | font-family: sans-serif;
6 | font-size: 16px;
7 | line-height: 1.7;
8 | padding: 1em;
9 | margin: auto;
10 | max-width: 56em;
11 | }
12 |
13 | a {
14 | color: #0645ad;
15 | text-decoration: none;
16 | }
17 |
18 | a:visited {
19 | color: #0b0080;
20 | }
21 |
22 | a:hover {
23 | color: #06e;
24 | }
25 |
26 | a:active {
27 | color: #7c4ca8;
28 | }
29 |
30 | a:focus {
31 | outline: thin dotted;
32 | }
33 |
34 | *::selection {
35 | background: rgba(0, 89, 255, 0.3);
36 | color: #000;
37 | }
38 |
39 | p {
40 | font-family: sans-serif;
41 | margin: 1em 0;
42 | }
43 |
44 | img {
45 | max-width: 100%;
46 | }
47 |
48 | h1, h2, h3, h4, h5, h6 {
49 | font-family: sans-serif;
50 | line-height: 125%;
51 | margin-top: 2em;
52 | font-weight: normal;
53 | }
54 |
55 | h4, h5, h6 {
56 | font-weight: bold;
57 | }
58 |
59 | h1 {
60 | font-size: 2.5em;
61 | }
62 |
63 | h2 {
64 | font-size: 2em;
65 | }
66 |
67 | h3 {
68 | font-size: 1.5em;
69 | }
70 |
71 | h4 {
72 | font-size: 1.2em;
73 | }
74 |
75 | h5 {
76 | font-size: 1em;
77 | }
78 |
79 | h6 {
80 | font-size: 0.9em;
81 | }
82 |
83 | table {
84 | margin: 10px 5px;
85 | border-collapse: collapse;
86 | }
87 |
88 | th {
89 | background-color: #eee;
90 | font-weight: bold;
91 | }
92 |
93 | th, td {
94 | border: 1px solid lightgray;
95 | padding: .2em 1em;
96 | }'''
97 |
98 |
99 | css_dark = '''
100 | th {
101 | background-color: #444;
102 | }
103 |
104 | th, td {
105 | border: 1px solid darkgray;
106 | }
107 | '''
108 |
--------------------------------------------------------------------------------
/doc/Examples/ex_acousticchamber.yaml:
--------------------------------------------------------------------------------
1 | - functions:
2 | - desc: ''
3 | expr: g*P_0*A*S/r/V_0
4 | name: c
5 | units: Pa/V
6 | inputs:
7 | - autocorrelate: true
8 | desc: ''
9 | mean: 386.0
10 | name: A
11 | numnewmeas: null
12 | uncerts:
13 | - degf: .inf
14 | desc: null
15 | dist: normal
16 | k: 2
17 | name: u(A)
18 | unc: '.1'
19 | units: centimeter ** 2
20 | units: centimeter ** 2
21 | - autocorrelate: true
22 | desc: ''
23 | mean: 83.5
24 | name: P_0
25 | numnewmeas: null
26 | uncerts:
27 | - degf: .inf
28 | desc: null
29 | dist: normal
30 | k: 2
31 | name: u(P_0)
32 | unc: '.085'
33 | units: kilopascal
34 | units: kilopascal
35 | - autocorrelate: true
36 | desc: ''
37 | mean: 2.3
38 | name: S
39 | numnewmeas: null
40 | uncerts:
41 | - degf: .inf
42 | desc: null
43 | dist: normal
44 | k: 2
45 | name: u(S)
46 | unc: '25'
47 | units: micrometer
48 | units: centimeter
49 | - autocorrelate: true
50 | desc: ''
51 | mean: 1.419
52 | name: V_0
53 | numnewmeas: null
54 | uncerts:
55 | - degf: .inf
56 | desc: null
57 | dist: normal
58 | k: 2
59 | name: u(V_0)
60 | unc: '.016'
61 | units: meter ** 3
62 | units: meter ** 3
63 | - autocorrelate: true
64 | desc: ''
65 | mean: 1.4
66 | name: g
67 | numnewmeas: null
68 | uncerts: []
69 | units: null
70 | - autocorrelate: true
71 | desc: ''
72 | mean: 40.0
73 | name: r
74 | numnewmeas: null
75 | uncerts: []
76 | units: volt
77 | mode: uncertainty
78 | name: uncertainty
79 | samples: 1000000
80 | unitdefs: stadia = 185*meter
81 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | ###*.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 | .DS_Store
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = suncal
3 | version = attr: suncal.version.__version__
4 | author = Collin J. Delker
5 | author_email = uncertainty@sandia.gov
6 | url = https://sandialabs.github.io/suncal/
7 | description = Sandia PSL Uncertainty Calculator
8 | long_description = file: README.md
9 | long_description_content_type = text/markdown
10 | keywords = uncertainty, metrology, measurement, GUM
11 | license = GNU General Public License v3 (GPLv3)
12 | project_urls =
13 | Documentation = https://sandialabs.github.io/suncal/
14 | Source Code = https://github.com/sandialabs/suncal
15 | classifiers =
16 | Development Status :: 4 - Beta
17 | Programming Language :: Python :: 3
18 | Programming Language :: Python :: 3.9
19 | Programming Language :: Python :: 3.10
20 | Programming Language :: Python :: 3.11
21 | Programming Language :: Python :: 3.12
22 | Programming Language :: Python :: 3.13
23 | License :: OSI Approved :: GNU General Public License v3 (GPLv3)
24 | Operating System :: OS Independent
25 | Intended Audience :: Education
26 | Intended Audience :: Science/Research
27 | Intended Audience :: End Users/Desktop
28 |
29 | [options]
30 | packages = find:
31 | zip_safe = True
32 | python_requires = >= 3.9
33 | include_package_data = True
34 | install_requires =
35 | numpy >=1.26
36 | matplotlib >=3.8
37 | scipy >=1.15
38 | sympy >=1.12
39 | pint >=0.22
40 | markdown >=3.5
41 | pyyaml >=6.0
42 |
43 | [options.extras_require]
44 | gui = pyqt6
45 |
46 | [options.entry_points]
47 | console_scripts =
48 | suncal = suncal.__main__:main_unc
49 | suncalf = suncal.__main__:main_setup
50 | suncalrev = suncal.__main__:main_reverse
51 | suncalrisk = suncal.__main__:main_risk
52 | suncalfit = suncal.__main__:main_curvefit
53 | gui_scripts =
54 | suncalui = suncal.gui.gui_main:main
55 |
--------------------------------------------------------------------------------
/doc/Examples/ex_drift_risk.yaml:
--------------------------------------------------------------------------------
1 | - colnames:
2 | - '2011-11-06'
3 | - '2013-10-07'
4 | - '2015-08-01'
5 | - '2016-07-23'
6 | - '2018-05-11'
7 | data:
8 | - - 180.02945641
9 | - 180.26180522
10 | - 179.75678302
11 | - 180.15129924
12 | - 179.73220268
13 | - - 181.30996233
14 | - 181.20475997
15 | - 181.81435542
16 | - 181.51056448
17 | - 181.40100349
18 | - - 182.14519271
19 | - 182.317946
20 | - 182.12156617
21 | - 182.19061199
22 | - 182.44320556
23 | - - 183.04668247
24 | - 183.40622699
25 | - 183.08574395
26 | - 182.8406013
27 | - 183.07926423
28 | - - 184.37580285
29 | - 183.90570234
30 | - 184.28613981
31 | - 184.0489668
32 | - 184.09539855
33 | desc: Five calibrations, each consisting of five measurements, were taken between
34 | 2011 and 2018. This calculation reduces the sets of five measurements into a mean
35 | and standard deviation for each year's calibration.
36 | mode: data
37 | name: data
38 | tolerance: null
39 | - abssigma: true
40 | arruy:
41 | - 0.23570842309815002
42 | - 0.23375163476623237
43 | - 0.13486440626205345
44 | - 0.20260459361944091
45 | - 0.1884825620311871
46 | arrx:
47 | - 734447.0
48 | - 735148.0
49 | - 735811.0
50 | - 736168.0
51 | - 736825.0
52 | arry:
53 | - 179.986309314
54 | - 181.44812913799998
55 | - 182.243704486
56 | - 183.09170378800002
57 | - 184.14240207000003
58 | curve: line
59 | desc: The second step is fitting a line to the historical data in order to predict
60 | the value at the 2019 calibration due date.
61 | mode: curvefit
62 | name: drift fit
63 | odr: false
64 | predictions:
65 | Next Cal:
66 | tolerance:
67 | high: '185'
68 | low: '175'
69 | nominal: '180'
70 | units: ''
71 | value: 737241.0
72 | tolerances: {}
73 | waveform: {}
74 | xdates: true
75 | xname: x
76 | yname: y
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Uncertainty Calculator
2 |
3 | Sandia UNcertainty CALculator (SUNCAL)
4 |
5 | Copyright 2019-2025 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software.
6 | This software is distributed under the GNU General Public License.
7 |
8 | ---
9 |
10 | This tool was developed by the Primary Standards Lab at Sandia National Laboratories to calculate the combined uncertainty of a
11 | multi-variable system. Contact uncertainty@sandia.gov.
12 |
13 |
14 | ## Installation
15 |
16 | Installation of the Python package and command line interface requires Python 3.9+ with the following packages:
17 |
18 | - numpy
19 | - scipy
20 | - sympy
21 | - matplotlib
22 | - pyqt5
23 | - pyyaml
24 | - markdown
25 | - pint
26 |
27 | To install (on Windows, Mac, or Linux), from a command prompt, run:
28 |
29 | ```
30 | pip install suncal
31 | ```
32 |
33 | ## Example Usage
34 |
35 | From a python terminal, script, or notebook:
36 |
37 | ```
38 | import suncal
39 | u = suncal.Model('A*B')
40 | u.var('A').measure(100).typeb(unc=0.1)
41 | u.var('B').measure(2).typeb(unc=0.01)
42 | u.calculate()
43 | ```
44 |
45 | See the PDF user's manual and the example notebook files in the docs folder for a complete reference guide.
46 |
47 |
48 | ## Command-line script
49 |
50 | A script named suncal will be installed to your system path. From a command line, run:
51 |
52 | `suncal file`
53 |
54 | where file is the filename of a setup file. See doc/examples folder for
55 | example setup files. Refer to the PDF user's manual for other commands.
56 |
57 |
58 | ## User interface
59 | A graphical user interface is installed with the Python package. Pre-built executables are available from https://sandiapsl.github.io.
60 |
61 | To launch the user interface from a command line, run:
62 |
63 | `suncalui`
64 |
65 | or
66 |
67 | `python -m suncal.gui`
68 |
69 |
--------------------------------------------------------------------------------
/doc/Examples/ex_electricalcircuitproblem.yaml:
--------------------------------------------------------------------------------
1 | - description: "Electrical Circuit Practice Problem from SNL ENGR224 course\n\nResistor\
2 | \ R1 in series with two parallel capacitors C2 and C3.\n\nResistance type A was\
3 | \ loaded from 10 measurements in resistance.txt file using data importer.\n\n\
4 | Capacitors are given as C2 = 0.1\xB5F +/- .005uf and C3 = .22\xB5F +/- .01\xB5\
5 | F. Multimeter given with uncertainty +/- (.01% of reading + .001% of range) at\
6 | \ 95% confidence, with range of 100 k\u03A9."
7 | functions:
8 | - desc: Time Constant
9 | expr: R1*(C2 + C3)
10 | name: tau
11 | units: ms
12 | inputs:
13 | - autocorrelate: true
14 | desc: Capacitor 2
15 | mean: 0.1
16 | name: C2
17 | numnewmeas: 1
18 | uncerts:
19 | - a: '.005'
20 | degf: .inf
21 | desc: Manufacturer spec of capacitor 2
22 | dist: uniform
23 | name: u(C2)
24 | units: microfarad
25 | units: microfarad
26 | - autocorrelate: true
27 | desc: Capacitor 3
28 | mean: 0.22
29 | name: C3
30 | numnewmeas: 1
31 | uncerts:
32 | - a: '.01'
33 | degf: .inf
34 | desc: Manufacturer spec of capacitor 3
35 | dist: uniform
36 | name: u(C3)
37 | units: microfarad
38 | units: microfarad
39 | - autocorrelate: false
40 | desc: Resistor
41 | mean: 32.2014
42 | name: R1
43 | numnewmeas: 10
44 | typea:
45 | - 32.205
46 | - 32.1878
47 | - 32.2081
48 | - 32.2201
49 | - 32.1807
50 | - 32.199
51 | - 32.1965
52 | - 32.212
53 | - 32.194
54 | - 32.2108
55 | typea_uncert: 0.0038096952342382188
56 | uncerts:
57 | - conf: '.95'
58 | degf: .inf
59 | desc: Type B uncertainty of meter
60 | dist: normal
61 | name: meter
62 | unc: .01% + 0.001%range(100)
63 | units: kiloohm
64 | units: kiloohm
65 | mode: uncertainty
66 | name: uncertainty
67 | samples: 1000000
68 |
69 |
--------------------------------------------------------------------------------
/doc/Examples/ex_temperature_coeff.yaml:
--------------------------------------------------------------------------------
1 | - description: 'A resistor was calibrated at one temperature but used at another.
2 | This calculation determines the resistance and its uncertainty at the temperature
3 | of use.
4 |
5 |
6 | The units for T and T0 are absolute temperature, entered as "degC" in the units
7 | column, but their uncertainties are temperature differences, entered as "delta_degC"
8 | in the units column. Subtraction of the two absolute temperatures results in a
9 | delta temperature.'
10 | functions:
11 | - desc: Resistance corrected for temperature
12 | expr: R0*(alpha*(T - T0) + 1)
13 | name: R
14 | units: ohm
15 | inputs:
16 | - desc: Temperature Coefficient of Resistance (copper)
17 | mean: 0.0039
18 | name: alpha
19 | uncerts:
20 | - degf: .inf
21 | desc: Uncertainty in TCR value
22 | dist: normal
23 | k: '2'
24 | name: u(alpha)
25 | unc: 1%
26 | units: 1 / delta_degC
27 | units: 1 / delta_degC
28 | - desc: Reference Temperature
29 | mean: 20.0
30 | name: T0
31 | uncerts:
32 | - degf: .inf
33 | desc: Uncertainty in temperature when resistor was calibrated
34 | dist: normal
35 | k: '2'
36 | name: u1(T0)
37 | unc: '.05'
38 | units: delta_degC
39 | units: degC
40 | - desc: Measurement Temperature
41 | mean: 28.0
42 | name: T
43 | uncerts:
44 | - degf: .inf
45 | desc: Uncertainty in temperature at time of use
46 | dist: normal
47 | k: '2'
48 | name: u(T)
49 | unc: '.05'
50 | units: delta_degC
51 | units: degC
52 | - desc: Resistance measured at T0
53 | mean: 1.0
54 | name: R0
55 | uncerts:
56 | - degf: .inf
57 | desc: Uncertainty in resistor calibration
58 | dist: normal
59 | k: '2'
60 | name: u(R0)
61 | unc: '.0005'
62 | units: ohm
63 | units: ohm
64 | mode: uncertainty
65 | name: uncertainty
66 | samples: 1000000
67 |
--------------------------------------------------------------------------------
/doc/Examples/ex_pitot.yaml:
--------------------------------------------------------------------------------
1 | - confidence: 0.95
2 | correlate: true
3 | desc: ''
4 | mode: system
5 | name: system
6 | quantities:
7 | - desc: Airspeed
8 | description: Airspeed
9 | equation: sqrt(2)*sqrt(Delta_p*R_s*T_a/p_a)
10 | name: ''
11 | symbol: v
12 | tolerance: null
13 | type: indirect
14 | units: meter / second
15 | - desc: Difference in static vs total pressure
16 | description: Difference in static vs total pressure
17 | name: ''
18 | symbol: Delta_p
19 | testpoint: 8.0
20 | tolerance: null
21 | type: direct
22 | typea: null
23 | typeb:
24 | - degf: .inf
25 | desc: ''
26 | df: .inf
27 | dist: normal
28 | k: 2.0
29 | name: Type B
30 | unc: 0.1
31 | units: inch_H2O
32 | units: inch_H2O
33 | - desc: Specific gas constant for dry air
34 | description: Specific gas constant for dry air
35 | name: ''
36 | symbol: R_s
37 | testpoint: 287.06
38 | tolerance: null
39 | type: direct
40 | typea: null
41 | typeb: []
42 | units: joule / kelvin / kilogram
43 | - desc: Air temperature
44 | description: Air temperature
45 | name: ''
46 | symbol: T_a
47 | testpoint: 67.4
48 | tolerance: null
49 | type: direct
50 | typea: null
51 | typeb:
52 | - degf: .inf
53 | desc: ''
54 | df: .inf
55 | dist: normal
56 | k: 2.0
57 | name: Type B
58 | unc: 0.2
59 | units: delta_degree_Fahrenheit
60 | units: degree_Fahrenheit
61 | - desc: Air pressure
62 | description: Air pressure
63 | name: ''
64 | symbol: p_a
65 | testpoint: 14.7
66 | tolerance: null
67 | type: direct
68 | typea: null
69 | typeb:
70 | - degf: .inf
71 | desc: ''
72 | df: .inf
73 | dist: normal
74 | k: 2.0
75 | name: Type B
76 | unc: 0.3
77 | units: pound_force_per_square_inch
78 | units: pound_force_per_square_inch
79 | samples: 1000000
80 | seed: null
81 |
--------------------------------------------------------------------------------
/suncal/common/ttable.py:
--------------------------------------------------------------------------------
1 | ''' Functions for calculating Student T values '''
2 |
3 | import numpy as np
4 | from scipy.special import nctdtridf
5 | from scipy import stats
6 |
7 |
8 | def k_factor(conf, degf):
9 | ''' Return tp(v) given confidence (0-1) and degrees of freedom.
10 |
11 | Args:
12 | conf (float): Level of confidence (0-1).
13 | degf (float): Degrees of freedom
14 |
15 | Returns:
16 | tp (float: Value of tp(v)
17 | '''
18 | if not np.isfinite(degf):
19 | degf = 1E9
20 | degf = max(1, degf) # Scipy doesn't like inf, or < 1
21 | degf = min(1E9, degf)
22 | return stats.t.ppf(1-(1-conf)/2, df=degf)
23 |
24 |
25 | def t_onetail(conf, degf):
26 | ''' Return one-tailed Student T value
27 |
28 | Args:
29 | conf (float): Level of confidence (0-1).
30 | degf (float): Degrees of freedom
31 |
32 | Returns:
33 | tp (float: Value of tp(v)
34 | '''
35 | if not np.isfinite(degf):
36 | degf = 1E99
37 | return stats.t.ppf(conf, df=degf)
38 |
39 |
40 | def confidence(tp, degf):
41 | ''' Get confidence value given tp and degrees of freedom. Inverse of k_factor.
42 |
43 | Args:
44 | tp (float): Value of tp(v)
45 | degf (float): Degrees of freedom
46 |
47 | Returns:
48 | conf (float): Confidence value in the range (0-1).
49 | '''
50 | if not np.isfinite(degf):
51 | degf = 1E99
52 | return 1+2*(stats.t.cdf(tp, df=degf)-1)
53 |
54 |
55 | def degf(tp, conf):
56 | ''' Calculate degrees of freedom given tp and confidence
57 |
58 | Args:
59 | tp (float): Value of tp(v)
60 | conf (float): Level of confidence (0-1).
61 |
62 | Returns:
63 | degf (float): Degrees of freedom
64 | '''
65 | # Non-central t distribution with non-centrality parameter of 0.
66 | df = nctdtridf(1-(1-conf)/2, 0, tp)
67 | return df if df < 1E12 else np.inf
68 |
--------------------------------------------------------------------------------
/test/ex_endgauge.yaml:
--------------------------------------------------------------------------------
1 | # PSL Uncertainty Calculator Config File
2 | #
3 | functions:
4 | - desc: End Gauge Length
5 | expr: d - l_s*(alpha_s*d_theta + d_alpha*theta) + l_s
6 | name: l
7 | inputs:
8 | - desc: Length of standard at 20C from certificate
9 | mean: 50000623.6
10 | name: l_s
11 | uncerts:
12 | - degf: 18.0
13 | desc: Uncertainty of the standard
14 | dist: normal
15 | k: '3'
16 | name: u_{ls}
17 | unc: '75'
18 | - desc: Measured difference between end gauges
19 | mean: 215.0
20 | name: d
21 | uncerts:
22 | - conf: '0.95'
23 | degf: 5.0
24 | desc: Random effects of comparator
25 | dist: t
26 | name: u_{d1}
27 | unc: '10'
28 | - degf: 8.0
29 | desc: Systematic effects of comparator
30 | dist: normal
31 | k: '3'
32 | name: u_{d2}
33 | unc: '20'
34 | - degf: 24.0
35 | desc: Repeated observations
36 | dist: normal
37 | name: u_d
38 | std: '5.8'
39 | - desc: Deviation in temperature of test bed from 20C ambient
40 | mean: -0.1
41 | name: theta
42 | uncerts:
43 | - a: '0.5'
44 | degf: .inf
45 | desc: Cyclic variation of temperature in room
46 | dist: arcsine
47 | name: u_Delta
48 | - degf: .inf
49 | desc: '''Mean temperature of bed'
50 | dist: normal
51 | name: u_theta
52 | std: '0.2'
53 | - desc: Coefficient of thermal expansion
54 | mean: 1.15e-05
55 | name: alpha_s
56 | uncerts:
57 | - a: 2E-6
58 | degf: .inf
59 | desc: Thermal expansion coefficient of standard
60 | dist: uniform
61 | name: u_{alpha_s}
62 | - desc: Difference in expansion coefficients
63 | mean: 0.0
64 | name: d_alpha
65 | uncerts:
66 | - a: 1E-6
67 | degf: 50.0
68 | desc: Difference in expansion coefficients
69 | dist: uniform
70 | name: u_{da}
71 | - desc: Difference in temperatures
72 | mean: 0.0
73 | name: d_theta
74 | uncerts:
75 | - a: '0.05'
76 | degf: 2.0
77 | desc: Difference in temperatures
78 | dist: uniform
79 | name: u_{dt}
80 | samples: 1000000
81 | seed: 1
82 |
--------------------------------------------------------------------------------
/suncal/mqa/costs.py:
--------------------------------------------------------------------------------
1 | ''' MQA Cost Model '''
2 | from dataclasses import dataclass, field
3 |
4 |
5 | @dataclass
6 | class MqaItemCost:
7 | ''' Single-item false decision costs
8 |
9 | Args:
10 | cfa: Cost of a false accept (called cf in RP-19)
11 | cfr: Cost of a false reject
12 | '''
13 | cfa: float = 0
14 | cfr: float = 0
15 |
16 |
17 | @dataclass
18 | class MqaDowntime:
19 | ''' Downtime in days
20 |
21 | Args:
22 | cal: Calibration downtime (including shipping/storage)
23 | adj: Adjustment downtime
24 | rep: Repair downtime (including shipping/storage)
25 | '''
26 | cal: float = 0
27 | adj: float = 0
28 | rep: float = 0
29 |
30 |
31 | @dataclass
32 | class MqaAnnualCost:
33 | ''' Annual cost parameters
34 |
35 | Args:
36 | cal: Cost of one calibration
37 | adjust: Cost of adjustment
38 | repair: Cost to repair
39 | uut: Cost of a new UUT
40 | nuut: Number of UUTs in inventory
41 | suut: Spare readiness factor (0-1)
42 | spare_startup: Cost to start up a spare (cd in RP-19)
43 | downtime: Downtimes for calibration, adjustment, and repair
44 | pe: Probability of end-item use
45 | '''
46 | cal: float = 0
47 | adjust: float = 0
48 | repair: float = 0
49 | uut: float = 0
50 | nuut: int = 1
51 | suut: float = 1
52 | spare_startup: float = 0
53 | downtime: MqaDowntime = field(default_factory=MqaDowntime)
54 | pe: float = 1
55 |
56 |
57 | @dataclass
58 | class MqaCosts:
59 | ''' MQA cost model '''
60 | item: MqaItemCost = field(default_factory=MqaItemCost)
61 | annual: MqaAnnualCost = field(default_factory=MqaAnnualCost)
62 |
63 | @property
64 | def enabled(self) -> bool:
65 | ''' The costs are being used '''
66 | return (
67 | self.annual.cal != 0 or
68 | self.annual.adjust != 0 or
69 | self.annual.repair != 0 or
70 | self.item.cfa != 0 or
71 | self.item.cfr != 0)
72 |
--------------------------------------------------------------------------------
/suncal/gui/__main__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | ''' PSL Uncertainty Calculator - User Interface Main '''
3 | import sys
4 | from PyQt6 import QtWidgets, QtCore, QtGui
5 | import markdown
6 |
7 | from suncal import gui
8 | from suncal.gui import gui_common # Install QT breakpoint hook
9 | from suncal.gui import gui_math
10 | from suncal.gui.icons import logo_snl, appicon
11 | from suncal import version
12 |
13 |
14 | def main():
15 | app = QtWidgets.QApplication(sys.argv)
16 | app.setStyle('Fusion') # Switches light/dark modes
17 |
18 | message = f'''Suncal - Sandia Uncertainty Calculator
19 |
20 | Version: {version.__version__} - {version.__date__}
21 | Primary Standards Lab
22 | Sandia National Laboratories
23 | uncertainty@sandia.gov
24 |
25 | Copyright 2019-2025 National Technology & Engineering
26 | Solutions of Sandia, LLC (NTESS). Under the terms
27 | of Contract DE-NA0003525 with NTESS, the U.S.
28 | Government retains certain rights in this software.
29 | '''
30 | pixmap = QtGui.QPixmap(int(480), int(320))
31 | pixmap.fill(app.palette().color(QtGui.QPalette.ColorRole.Window))
32 | painter = QtGui.QPainter(pixmap)
33 | painter.drawPixmap(int(10), int(250), logo_snl(pixmap=True))
34 | painter.end()
35 | splash = QtWidgets.QSplashScreen(pixmap)
36 | font = splash.font()
37 | font.setPointSize(12)
38 | splash.setFont(font)
39 |
40 | color = app.palette().color(QtGui.QPalette.ColorRole.WindowText)
41 | splash.showMessage(message, color=color)
42 | splash.show()
43 | splash.repaint()
44 | QtCore.QTimer.singleShot(3000, splash.close)
45 |
46 | app.processEvents()
47 | app.setWindowIcon(appicon())
48 |
49 | # This line forces Matplotlib to load in its fonts (taking ~1 sec),
50 | # and Markdown to load/cache its extension (~.5 sec) now
51 | # rather than when the user opens the first project component.
52 | gui_math.pixmap_from_latex('x')
53 | markdown.markdown('x', extensions=['markdown.extensions.tables'])
54 |
55 | main = gui.gui_main.MainGUI()
56 | main.show()
57 | app.exec()
58 |
59 |
60 | if __name__ == '__main__':
61 | main()
62 |
--------------------------------------------------------------------------------
/suncal/project/proj_risk.py:
--------------------------------------------------------------------------------
1 | ''' Risk Calculation project component '''
2 |
3 | from .component import ProjectComponent
4 | from ..common import distributions
5 |
6 | from ..risk.risk_model import RiskModel, RiskResults
7 |
8 |
9 | class ProjectRisk(ProjectComponent):
10 | ''' Risk project component '''
11 | def __init__(self, model=None, name='risk'):
12 | super().__init__(name=name)
13 | if model is None:
14 | self.model = RiskModel()
15 | else:
16 | self.model = model
17 |
18 | def calculate(self) -> RiskResults:
19 | ''' Calculate values, returning the results object '''
20 | self._result = self.model.calculate()
21 | return self._result
22 |
23 | def get_config(self):
24 | ''' Get configuration dictionary '''
25 | d = {}
26 | d['mode'] = 'risk'
27 | d['name'] = self.name
28 | d['desc'] = self.description
29 | d['bias'] = self.model.testbias
30 |
31 | if self.model.process_dist is not None:
32 | d['distproc'] = self.model.process_dist.get_config()
33 |
34 | if self.model.measure_dist is not None:
35 | d['disttest'] = self.model.measure_dist.get_config()
36 |
37 | d['GBL'] = self.model.gbofsts[0]
38 | d['GBU'] = self.model.gbofsts[1]
39 | d['LL'] = self.model.speclimits[0]
40 | d['UL'] = self.model.speclimits[1]
41 | return d
42 |
43 | def load_config(self, config):
44 | ''' Load config into this project instance '''
45 | self.description = config.get('desc', '')
46 | self.model.speclimits = (config.get('LL', 0), config.get('UL', 0))
47 | self.model.gbofsts = (config.get('GBL', 0), config.get('GBU', 0))
48 | self.model.testbias = config.get('bias', 0)
49 |
50 | dproc = config.get('distproc', None)
51 | if dproc is not None:
52 | self.model.process_dist = distributions.from_config(dproc)
53 | else:
54 | self.model.process_dist = None
55 |
56 | dtest = config.get('disttest', None)
57 | if dtest is not None:
58 | self.model.measure_dist = distributions.from_config(dtest)
59 | else:
60 | self.model.measure_dist = None
61 |
--------------------------------------------------------------------------------
/suncal/mqa/guardband.py:
--------------------------------------------------------------------------------
1 | ''' Guardbanding Rules '''
2 | from typing import Literal
3 | from dataclasses import dataclass, field
4 | from uuid import uuid4
5 |
6 | from ..common.limit import Limit
7 |
8 |
9 | GbOption = Literal['auto', 'manual', 'none']
10 | GbMethod = Literal['rds', 'rp10', 'u95', 'dobbert', 'pfa', 'cpfa']
11 |
12 |
13 | @dataclass
14 | class MqaGuardbandRule:
15 | ''' A guardbanding rule, including method and thresholds for applicability '''
16 | idn: str = field(default_factory=lambda: uuid4().hex)
17 | name: str = '4:1 RDS'
18 | method: GbMethod = 'rds'
19 | threshold: float = None
20 |
21 | def __post_init__(self):
22 | if self.threshold is None:
23 | self.threshold = .02 if self.method in ['pfa', 'cpfa', 'specific'] else 4.
24 |
25 |
26 | def default_rules() -> list[MqaGuardbandRule]:
27 | ''' Create default guardbanding rules '''
28 | return [
29 | MqaGuardbandRule(),
30 | MqaGuardbandRule(name='2% PFA', method='pfa', threshold=2)
31 | ]
32 |
33 |
34 | @dataclass
35 | class MqaGuardbandRuleset:
36 | ''' Guardbanding rules for different measurements '''
37 | rules: list[MqaGuardbandRule] = field(default_factory=default_rules)
38 |
39 | def locate(self, idn: str) -> MqaGuardbandRule:
40 | ''' Find a rule by ID '''
41 | for rule in self.rules:
42 | if rule.idn == idn:
43 | return rule
44 | return None
45 |
46 | @property
47 | def rule_names(self):
48 | ''' List of guardband rule names '''
49 | return [rule.name for rule in self.rules]
50 |
51 | def by_name(self, name: str) -> MqaGuardbandRule:
52 | ''' Locate a rule using its name '''
53 | idx = self.rule_names.index(name)
54 | return self.rules[idx]
55 |
56 |
57 | @dataclass
58 | class MqaGuardband:
59 | ''' Guardbanding method and limit '''
60 | method: GbOption = 'auto'
61 | rule: MqaGuardbandRule = field(default_factory=MqaGuardbandRule)
62 | accept_limit: Limit = None
63 |
64 | def __str__(self):
65 | if self.method == 'auto':
66 | return self.rule.name
67 | elif self.method == 'none':
68 | return 'No Guardband'
69 | return str(self.accept_limit)
70 |
--------------------------------------------------------------------------------
/doc/Examples/ex_stepatten.yaml:
--------------------------------------------------------------------------------
1 | # PSL Uncertainty Calculator Config File
2 | #
3 | description: 'Example E11 - Microwave Step Attenuator
4 |
5 | From NIST Technical Note 1900
6 |
7 |
8 | TN1900, Exhibit 16, defines all inputs using an estimate and *standard* uncertainty.
9 | The calculator inputs take the distribution shape parameters directly, and calculate
10 | the standard uncertainty for you. Some parameters in the text are only given in
11 | terms of standard uncertainty and must be converted back to shape parameters for
12 | input.'
13 | functions:
14 | - desc: ''
15 | expr: Ls - dL0a + dL0b + dLd - dLia + dLib + dLk + dLm + dLs
16 | name: Lx
17 | inputs:
18 | - desc: ''
19 | mean: 30.0402
20 | name: Ls
21 | uncerts:
22 | - degf: 3.0
23 | desc: ''
24 | dist: t
25 | name: u(Ls)
26 | unc: '.0091'
27 | - desc: ''
28 | mean: 0.003
29 | name: dLs
30 | uncerts:
31 | - a: '0.00433'
32 | degf: .inf
33 | desc: ''
34 | dist: uniform
35 | name: u(dLs)
36 | - desc: ''
37 | mean: 0.0
38 | name: dLib
39 | uncerts:
40 | - a: '0.0005196'
41 | degf: .inf
42 | desc: ''
43 | dist: uniform
44 | name: u(dLib)
45 | - desc: ''
46 | mean: 0.0
47 | name: dL0a
48 | uncerts:
49 | - degf: .inf
50 | desc: ''
51 | dist: normal
52 | name: u(dL0a)
53 | std: '0.002'
54 | - desc: ''
55 | mean: 0.0
56 | name: dL0b
57 | uncerts:
58 | - degf: .inf
59 | desc: ''
60 | dist: normal
61 | name: u(dL0b)
62 | std: '0.002'
63 | - desc: ''
64 | mean: 0.0
65 | name: dLm
66 | uncerts:
67 | - a: '0.02828'
68 | degf: .inf
69 | desc: ''
70 | dist: arcsine
71 | name: u(dLm)
72 | - desc: ''
73 | mean: 0.0
74 | name: dLd
75 | uncerts:
76 | - a: '0.001980'
77 | degf: .inf
78 | desc: ''
79 | dist: arcsine
80 | name: u(dLd)
81 | - desc: ''
82 | mean: 0.0
83 | name: dLia
84 | uncerts:
85 | - a: '0.0005196'
86 | degf: .inf
87 | desc: ''
88 | dist: uniform
89 | name: u(dLia)
90 | - desc: ''
91 | mean: 0.0
92 | name: dLk
93 | uncerts:
94 | - a: '.003'
95 | degf: .inf
96 | desc: ''
97 | dist: arcsine
98 | name: u(dLk)
99 | samples: 1000000
100 |
--------------------------------------------------------------------------------
/suncal/gui/help_strings/main_help.py:
--------------------------------------------------------------------------------
1 | ''' Inline help reports for Main Project Component Selction window '''
2 | from ...common import report
3 |
4 |
5 | class MainHelp:
6 | @staticmethod
7 | def project_types():
8 | rpt = report.Report()
9 | rpt.hdr('Sandia Uncertainty Calculator')
10 | rpt.hdr('Uncertainty Calculation Types', level=2)
11 | rpt.txt('- **Uncertainty Propagation**: Calculate uncertainty of a '
12 | 'measurement model using GUM and Monte Carlo methods.\n'
13 | 'Tolerances may be entered to calculate probability of conformance.\n'
14 | '- **R&R Data**: Import 1- or 2- dimensional sets of data '
15 | 'for computing repeatability, reproducibility (R&R), and analysis of variance (ANOVA).\n'
16 | '- **Curve Fit**: Find the best-fitting curve through measured data points, '
17 | 'including uncertainty. Works for arbitrary curve models.\n')
18 | rpt.hdr('Statistical Tools', level=2)
19 | rpt.txt('- **Global Risk**: Compute average probability of false accept and false reject '
20 | 'for a measurement\n'
21 | '- **Calibration Intervals**: Find the optimal calibration interval to achieve '
22 | 'the desired end-of-period reliability\n'
23 | '- **End-to-end Measurement Quality Assurance**: Assess the capability '
24 | 'of a measurement setup, evaluating uncertainty ratios and global risks.\n'
25 | '- **All-in-one Measurement System**: Define a complete measurement system consisting '
26 | 'of one or more measurands, indirect measurement equations, and curve fit calculations.\n'
27 | '- **Distribution Explorer**: Generate random samples from probability distributions '
28 | 'and combine using Monte Carlo methods.\n')
29 |
30 | rpt.hdr('Other calculations', level=2)
31 | rpt.txt('Available in the Project > Insert menu.\n\n'
32 | '- **Reverse Propagation**: Calculate uncertainty required for '
33 | 'one input variable to achieve desired uncertainty of the model. Useful '
34 | 'for selecting equipment capabilities.\n'
35 | '- **Uncertainty Sweep**: Compute GUM uncertainty over a range of values.\n'
36 | )
37 | return rpt
38 |
--------------------------------------------------------------------------------
/test/ex_stepatten.yaml:
--------------------------------------------------------------------------------
1 | # PSL Uncertainty Calculator Config File
2 | # Example E11 - Microwave Step Attenuator
3 | # From NIST Technical Note 1900
4 | #
5 | # Notes: TN1900 defines all inputs using estimate and standard uncertainty. For the
6 | # arcsine and student-t distributions, these must be shifted/scaled to the parameters
7 | # "location" and "scale" needed by the scipy.stats.t and scipy.stats.arcsine distributions.
8 | #
9 | # For the student-t distribution, the loc is the same as the mean, but the scale parameter is
10 | # stddev / sqrt(df) / (df-2).
11 | #
12 | # For arcsine distributions, scale = stddev * 2 * sqrt(2) and loc = -scale/2 to center the
13 | # distribution around 0.
14 | #
15 | functions:
16 | - desc: ''
17 | expr: Ls - dL0a + dL0b + dLd - dLia + dLib + dLk + dLm + dLs
18 | name: Lx
19 | inputs:
20 | - desc: ''
21 | mean: 30.0402
22 | name: Ls
23 | uncerts:
24 | - degf: 3.0
25 | desc: ''
26 | df: '3'
27 | dist: t
28 | name: u(Ls)
29 | unc: '.0091'
30 | - desc: ''
31 | mean: 0.003
32 | name: dLs
33 | uncerts:
34 | - a: '0.00433'
35 | degf: .inf
36 | desc: ''
37 | dist: uniform
38 | name: u(dLs)
39 | - desc: ''
40 | mean: 0.0
41 | name: dLib
42 | uncerts:
43 | - a: '0.0005196'
44 | degf: .inf
45 | desc: ''
46 | dist: uniform
47 | name: u(dLib)
48 | - desc: ''
49 | mean: 0.0
50 | name: dL0a
51 | uncerts:
52 | - degf: .inf
53 | desc: ''
54 | dist: normal
55 | name: u(dL0a)
56 | std: '0.002'
57 | - desc: ''
58 | mean: 0.0
59 | name: dL0b
60 | uncerts:
61 | - degf: .inf
62 | desc: ''
63 | dist: normal
64 | name: u(dL0b)
65 | std: '0.002'
66 | - desc: ''
67 | mean: 0.0
68 | name: dLm
69 | uncerts:
70 | - a: '0.02828'
71 | degf: .inf
72 | desc: ''
73 | dist: arcsine
74 | name: u(dLm)
75 | - desc: ''
76 | mean: 0.0
77 | name: dLd
78 | uncerts:
79 | - a: '0.001980'
80 | degf: .inf
81 | desc: ''
82 | dist: arcsine
83 | name: u(dLd)
84 | - desc: ''
85 | mean: 0.0
86 | name: dLia
87 | uncerts:
88 | - a: '0.0005196'
89 | degf: .inf
90 | desc: ''
91 | dist: uniform
92 | name: u(dLia)
93 | - desc: ''
94 | mean: 0.0
95 | name: dLk
96 | uncerts:
97 | - a: '0.0029698'
98 | degf: .inf
99 | desc: ''
100 | dist: arcsine
101 | name: u(dLk)
102 | samples: 1000000
103 | seed: 1
104 |
--------------------------------------------------------------------------------
/suncal/gui/page_about.py:
--------------------------------------------------------------------------------
1 | ''' About box for GUI '''
2 |
3 | from PyQt6 import QtWidgets
4 |
5 | from .. import version
6 | from . import icons
7 | from .licenses import licenses
8 |
9 |
10 | class AboutUC(QtWidgets.QWidget):
11 | ''' Widget with the normal "About" information '''
12 |
13 | ABOUT = f'''Suncal - Sandia Uncertainty Calculator
14 | Version: {version.__version__} - {version.__date__}
15 | Primary Standards Lab
Sandia National Laboratories
16 | uncertainty@sandia.gov
17 |
18 | Copyright 2019-2025 National Technology & Engineering Solutions of Sandia, LLC (NTESS).
19 |
Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government
20 |
retains certain rights in this software.
21 | '''
22 |
23 | def __init__(self, parent=None):
24 | super().__init__(parent=parent)
25 | layout = QtWidgets.QVBoxLayout()
26 | layout.addWidget(QtWidgets.QLabel(self.ABOUT))
27 | snlico = icons.logo_snl(pixmap=True)
28 | ratio = QtWidgets.QApplication.instance().primaryScreen().devicePixelRatio()
29 | snlico.setDevicePixelRatio(ratio)
30 | snllbl = QtWidgets.QLabel()
31 | snllbl.setPixmap(snlico)
32 | layout.addStretch()
33 | layout.addWidget(snllbl)
34 | self.setLayout(layout)
35 |
36 |
37 | class AboutBox(QtWidgets.QDialog):
38 | ''' About dialog with copyright, credits, and license information '''
39 | def __init__(self, parent=None):
40 | super().__init__(parent=parent)
41 | self.setWindowTitle('Suncal')
42 | self.setMinimumHeight(450)
43 | font = self.font()
44 | font.setPointSize(10)
45 | self.setFont(font)
46 |
47 | self.ok = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Ok)
48 | self.ok.accepted.connect(self.accept)
49 | self.txtLicense = QtWidgets.QTextEdit()
50 | self.txtLicense.setReadOnly(True)
51 | self.txtLicense.setHtml(licenses)
52 | self.tab = QtWidgets.QTabWidget()
53 | self.tab.addTab(AboutUC(), 'About')
54 | self.tab.addTab(self.txtLicense, 'Acknowledgements')
55 | layout = QtWidgets.QVBoxLayout()
56 | layout.addWidget(self.tab)
57 | layout.addWidget(self.ok)
58 | self.setLayout(layout)
59 |
60 |
61 | def show():
62 | ''' Show the about dialog '''
63 | dlg = AboutBox()
64 | dlg.exec()
65 |
--------------------------------------------------------------------------------
/installer.iss:
--------------------------------------------------------------------------------
1 | ; Script generated by the Inno Setup Script Wizard.
2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
3 |
4 | #define MyAppName "Suncal"
5 | ;#define MyAppVersion "X.XX" ; Set via command line in buildexe.bat
6 | #define MyAppPublisher "Sandia National Laboratories"
7 | #define MyAppURL "https://sandiapsl.github.io"
8 | #define MyAppExeName "Suncal.exe"
9 |
10 | [Setup]
11 | ; NOTE: The value of AppId uniquely identifies this application.
12 | ; Do not use the same AppId value in installers for other applications.
13 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
14 | AppId={{9515EFD7-C957-4447-8901-5EF7C251CB72}
15 | AppName={#MyAppName}
16 | AppVersion={#MyAppVersion}
17 | ;AppVerName={#MyAppName} {#MyAppVersion}
18 | AppPublisher={#MyAppPublisher}
19 | AppPublisherURL={#MyAppURL}
20 | AppSupportURL={#MyAppURL}
21 | AppUpdatesURL={#MyAppURL}
22 | DefaultDirName={commonpf}\{#MyAppName}
23 | DisableProgramGroupPage=yes
24 | OutputBaseFilename=SuncalInstall
25 | OutputDir=dist
26 | Compression=lzma
27 | SolidCompression=yes
28 | UninstallDisplayIcon={app}\{#MyAppExeName}
29 | VersionInfoCopyright=Copyright 2023 NTESS, LLC
30 | VersionInfoVersion={#MyAppVersion}
31 |
32 | [Languages]
33 | Name: "english"; MessagesFile: "compiler:Default.isl"
34 |
35 | [Tasks]
36 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
37 |
38 | [Files]
39 | Source: "dist\Suncal\Suncal.exe"; DestDir: "{app}\Suncal"; Flags: ignoreversion
40 | Source: "dist\Suncal\*"; DestDir: "{app}\Suncal"; Flags: ignoreversion recursesubdirs createallsubdirs
41 | Source: "doc\Examples\*.yaml"; DestDir: "{app}\Examples"; Flags: ignoreversion recursesubdirs createallsubdirs
42 | Source: "doc\Examples\*.dat"; DestDir: "{app}\Examples"; Flags: ignoreversion recursesubdirs createallsubdirs
43 | Source: "doc\Examples\*.txt"; DestDir: "{app}\Examples"; Flags: ignoreversion recursesubdirs createallsubdirs
44 | Source: "doc\SUNCALmanual.pdf"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
45 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
46 |
47 | [Icons]
48 | Name: "{commonprograms}\{#MyAppName}"; Filename: "{app}\Suncal\{#MyAppExeName}"
49 | Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\Suncal\{#MyAppExeName}"; Tasks: desktopicon
50 |
51 | [Run]
52 | Filename: "{app}\Suncal\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
53 |
54 |
--------------------------------------------------------------------------------
/doc/Examples/ex_cannonball.yaml:
--------------------------------------------------------------------------------
1 | - desc: ''
2 | equipment: []
3 | gbrules:
4 | - idn: 3aa6123c63654177b8a587284b824e2c
5 | method: rds
6 | name: 4:1 RDS
7 | threshold: 4.0
8 | - idn: e3f8cca1f3d1409d8f3b2b8e7b9da494
9 | method: cpfa
10 | name: 1% CPFA
11 | threshold: 1.0
12 | mode: mqa
13 | name: mqa
14 | quantities:
15 | - costs:
16 | annual:
17 | adjust: 0
18 | cal: 0
19 | downtime:
20 | adj: 0
21 | cal: 0
22 | rep: 0
23 | nuut: 1
24 | pe: 1
25 | repair: 0
26 | spare_startup: 0
27 | suut: 1
28 | uut: 0
29 | item:
30 | cfa: 0
31 | cfr: 0
32 | enditem: true
33 | guardband:
34 | accept_limit:
35 | high: '1'
36 | low: '-1'
37 | nominal: '0'
38 | units: ''
39 | method: none
40 | rule: e3f8cca1f3d1409d8f3b2b8e7b9da494
41 | measurand:
42 | degrade:
43 | high: '19.808'
44 | low: '19.575'
45 | nominal: null
46 | units: ''
47 | description: ''
48 | eopr_pct: 0.95
49 | eopr_pdf: null
50 | eopr_true: true
51 | fail:
52 | high: '19.827'
53 | low: '19.48'
54 | nominal: null
55 | units: ''
56 | name: Cannon Ball
57 | psr: 1.0
58 | testpoint: 19.7
59 | tolerance:
60 | high: '19.81'
61 | low: '19.51'
62 | nominal: null
63 | units: ''
64 | units: centimeter
65 | measurement:
66 | calibration:
67 | mte_adjust: null
68 | mte_repair: null
69 | p_discard: 0
70 | policy: never
71 | repair_limit: null
72 | stress_post: null
73 | stress_pre: null
74 | equation: ''
75 | indirect: {}
76 | interval:
77 | reliability: none
78 | years: 1.0
79 | mte:
80 | accuracy_eopr: 0.95
81 | accuracy_plusminus: 0.004
82 | equipment: null
83 | name: Guang Lu DM101 Caliper
84 | quantity: null
85 | range: null
86 | testpoints: {}
87 | typea: null
88 | typeb:
89 | - degf: .inf
90 | desc: ''
91 | dist: normal
92 | name: Repeatability
93 | unc: 0.0648
94 | units: null
95 | - a: 0.001
96 | degf: .inf
97 | desc: ''
98 | dist: resolution
99 | name: Resolution
100 | units: null
101 |
--------------------------------------------------------------------------------
/suncal/risk/deaver.py:
--------------------------------------------------------------------------------
1 | ''' Deaver used different definition for TUR and Specification Limit.
2 | These functions allow recreating plots in his papers
3 |
4 | The functions PFA_deaver and PFR_deaver use the equations in Deaver's "How to
5 | "Maintain Confidence" paper, which require specification limits in terms of
6 | standard deviations of the process distribution, and use a slightly different
7 | definition for TUR. These functions are provided for convenience when working
8 | with this definition.
9 | '''
10 |
11 | import math
12 | from scipy.integrate import dblquad
13 |
14 |
15 | def PFA_deaver(SL, TUR, GB=1):
16 | ''' Calculate Probability of False Accept (Consumer Risk) for normal
17 | distributions given spec limit and TUR, using Deaver's equation.
18 |
19 | Args:
20 | SL (float): Specification Limit in terms of standard deviations,
21 | symmetric on each side of the mean
22 | TUR (float): Test Uncertainty Ratio (sigma_uut / sigma_test). Note this is
23 | definition used by Deaver's papers, NOT the typical SL/(2*sigma_test) definition.
24 | GB (float): Guardband factor (0-1) with 1 being no guardband
25 |
26 | Returns:
27 | PFA (float): Probability of False Accept
28 |
29 | Reference:
30 | Equation 6 in Deaver - How to Maintain Confidence
31 | '''
32 | c, _ = dblquad(lambda y, t: math.exp(-(y*y + t*t)/2) / math.pi, SL, math.inf,
33 | gfun=lambda t: -TUR*(t+SL*GB), hfun=lambda t: -TUR*(t-SL*GB))
34 | return c
35 |
36 |
37 | def PFR_deaver(SL, TUR, GB=1):
38 | ''' Calculate Probability of False Reject (Producer Risk) for normal
39 | distributions given spec limit and TUR, using Deaver's equation.
40 |
41 | Args:
42 | SL (float): Specification Limit in terms of standard deviations,
43 | symmetric on each side of the mean
44 | TUR (float): Test Uncertainty Ratio (sigma_uut / sigma_test). Note this is
45 | definition used by Deaver's papers, NOT the typical SL/(2*sigma_test) definition.
46 | GB (float): Guardband factor (0-1) with 1 being no guardband
47 |
48 | Returns:
49 | PFR (float): Probability of False Reject
50 |
51 | Reference:
52 | Equation 7 in Deaver - How to Maintain Confidence
53 | '''
54 | p, _ = dblquad(lambda y, t: math.exp(-(y*y + t*t)/2) / math.pi, -SL, SL,
55 | gfun=lambda t: TUR*(GB*SL-t), hfun=lambda t: math.inf)
56 | return p
57 |
--------------------------------------------------------------------------------
/test/ex_endgauge_units.yaml:
--------------------------------------------------------------------------------
1 | - functions:
2 | - desc: ''
3 | expr: d - l_s*(alpha_s*d_theta + d_alpha*theta) + l_s
4 | name: ''
5 | units: nm
6 | inputs:
7 | - desc: Length of standard at 20C from certificate
8 | mean: 50000623.6
9 | name: l_s
10 | uncerts:
11 | - degf: 18
12 | desc: Uncertainty of the standard
13 | dist: normal
14 | k: 3
15 | name: u_{ls}
16 | unc: 75
17 | units: nanometer
18 | units: nanometer
19 | - desc: Measured difference between end gauges
20 | mean: 215
21 | name: d
22 | uncerts:
23 | - conf: 0.95
24 | degf: 5
25 | desc: Random effects of comparator
26 | dist: t
27 | name: u_{d1}
28 | unc: 10
29 | units: nanometer
30 | - degf: 8
31 | desc: Systematic effects of comparator
32 | dist: normal
33 | k: 3
34 | name: u_{d2}
35 | unc: 20
36 | units: nanometer
37 | - degf: 24
38 | desc: Repeated obeservations
39 | dist: normal
40 | name: u_d
41 | std: 5.8
42 | units: nanometer
43 | units: nanometer
44 | - desc: Deviation in temperature of test bed from 20C ambient
45 | mean: -0.1
46 | name: theta
47 | uncerts:
48 | - a: 0.5
49 | degf: .inf
50 | desc: Cyclic variation of temperature in room
51 | dist: arcsine
52 | name: u_Delta
53 | units: delta_degC
54 | - degf: .inf
55 | desc: Mean temperature of the bed
56 | dist: normal
57 | name: u_theta
58 | std: 0.2
59 | units: delta_degC
60 | units: delta_degC
61 | - desc: Coefficient of thermal expansion
62 | mean: 1.15e-05
63 | name: alpha_s
64 | uncerts:
65 | - a: 2.0e-06
66 | degf: .inf
67 | desc: Thermal expansion coefficient of standard
68 | dist: uniform
69 | name: u_{alpha_s}
70 | units: 1 / delta_degC
71 | units: 1 / delta_degC
72 | - desc: Difference in expansion coefficients
73 | mean: 0
74 | name: d_alpha
75 | uncerts:
76 | - a: 1.0e-06
77 | d: 1.0e-07
78 | degf: 50
79 | desc: Difference in expansion coefficients
80 | dist: curvtrap
81 | name: u_{da}
82 | units: 1 / delta_degC
83 | units: 1 / delta_degC
84 | - desc: Difference in temperatures
85 | mean: 0
86 | name: d_theta
87 | uncerts:
88 | - a: 0.05
89 | d: 0.025
90 | degf: 2
91 | desc: Difference in temperatures
92 | dist: curvtrap
93 | name: u_{dt}
94 | units: delta_degC
95 | units: delta_degC
96 | mode: uncertainty
97 | name: uncertainty
98 | samples: 1000000
99 |
--------------------------------------------------------------------------------
/doc/Examples/ex_leakrate.yaml:
--------------------------------------------------------------------------------
1 | - functions:
2 | - desc: ''
3 | expr: Q_std*(1+alpha*(T-Tc))*(H_uut-H_bkg)/(H_std-H_bkg)
4 | name: Q_uut
5 | units: "cm\xB3/s"
6 | inputs:
7 | - autocorrelate: true
8 | desc: ''
9 | mean: -0.02
10 | name: H_bkg
11 | numnewmeas: null
12 | uncerts:
13 | - degf: .inf
14 | desc: null
15 | dist: normal
16 | k: 2
17 | name: u(H_bkg)
18 | unc: .001% + .01%range(10)
19 | units: volt
20 | units: volt
21 | - autocorrelate: true
22 | desc: ''
23 | mean: 3.0
24 | name: H_std
25 | numnewmeas: null
26 | uncerts:
27 | - degf: .inf
28 | desc: null
29 | dist: normal
30 | k: 2
31 | name: u(H_std)
32 | unc: .001% + .01%range(10)
33 | units: volt
34 | units: volt
35 | - autocorrelate: true
36 | desc: ''
37 | mean: 2.0
38 | name: H_uut
39 | numnewmeas: null
40 | uncerts:
41 | - degf: .inf
42 | desc: null
43 | dist: normal
44 | k: 2
45 | name: u(H_uut)
46 | unc: .001% + .01%range(10)
47 | units: volt
48 | units: volt
49 | - autocorrelate: true
50 | desc: ''
51 | mean: 1.539e-06
52 | name: Q_std
53 | numnewmeas: null
54 | uncerts:
55 | - degf: .inf
56 | desc: null
57 | dist: normal
58 | k: 2
59 | name: u(Q_std)
60 | unc: 2%
61 | units: centimeter ** 3 / second
62 | units: centimeter ** 3 / second
63 | - autocorrelate: true
64 | desc: ''
65 | mean: 23.8
66 | name: T
67 | numnewmeas: null
68 | uncerts:
69 | - degf: .inf
70 | desc: null
71 | dist: normal
72 | k: 2
73 | name: u(T)
74 | unc: '.1'
75 | units: delta_degree_Celsius
76 | units: degree_Celsius
77 | - autocorrelate: true
78 | desc: ''
79 | mean: 26.67
80 | name: Tc
81 | numnewmeas: null
82 | uncerts:
83 | - degf: .inf
84 | desc: null
85 | dist: normal
86 | k: 2
87 | name: u(Tc)
88 | unc: '.1'
89 | units: delta_degree_Celsius
90 | units: degree_Celsius
91 | - autocorrelate: true
92 | desc: ''
93 | mean: 0.03
94 | name: alpha
95 | numnewmeas: null
96 | uncerts:
97 | - degf: .inf
98 | desc: null
99 | dist: normal
100 | k: 2
101 | name: u(alpha)
102 | unc: '.0005'
103 | units: 1 / delta_degree_Celsius
104 | units: 1 / delta_degree_Celsius
105 | mode: uncertainty
106 | name: uncertainty
107 | samples: 1000000
108 | unitdefs: stadia = 185*meter
109 |
--------------------------------------------------------------------------------
/test/test_cplx.py:
--------------------------------------------------------------------------------
1 | ''' Test complex number module '''
2 | import numpy as np
3 | import sympy
4 |
5 | from suncal.uncertainty import ModelComplex
6 | from suncal.common import uparser
7 |
8 |
9 | def test_E15():
10 | ''' Problem E15 in NIST-1900, Voltage Reflection Coefficient '''
11 | np.random.seed(2038942)
12 | u = ModelComplex('Gamma = S22 - S12*S23/S13', magphase=False)
13 | u.var('S22').measure_magphase(.24776, 4.88683, .00337, .01392, k=1, degrees=False)
14 | u.var('S12').measure_magphase(.49935, 4.78595, .00340, .00835, k=1, degrees=False)
15 | u.var('S23').measure_magphase(.24971, 4.85989, .00170, .00842, k=1, degrees=False)
16 | u.var('S13').measure_magphase(.49952, 4.79054, .00340, .00835, k=1, degrees=False)
17 | gum = u.calculate_gum()
18 | mc = u.monte_carlo(samples=100000)
19 |
20 | # Compare with results in NIST1900
21 | assert np.isclose(gum.expected['Gamma_real'], .0074, atol=.00005) # Real part
22 | assert np.isclose(gum.uncertainty['Gamma_real'], .005, atol=.0005) # Real part
23 | assert np.isclose(gum.expected['Gamma_imag'], .0031, atol=.00005) # Imag part
24 | assert np.isclose(gum.uncertainty['Gamma_imag'], .0045, atol=.0005) # Imag part
25 |
26 | # Monte Carlo
27 | assert np.isclose(mc.expected['Gamma_real'], .0074, atol=.0005) # Real part
28 | assert np.isclose(mc.uncertainty['Gamma_real'], .005, atol=.0005) # Real part
29 | assert np.isclose(mc.expected['Gamma_imag'], .0031, atol=.0005) # Imag part
30 | assert np.isclose(mc.uncertainty['Gamma_imag'], .0045, atol=.0005) # Imag part
31 |
32 | # Check correlation coefficient
33 | assert np.isclose(gum.correlation()['Gamma_real']['Gamma_imag'], 0.0323, atol=.005)
34 | assert np.isclose(mc.correlation()['Gamma_real']['Gamma_imag'], 0.0323, atol=.005)
35 |
36 |
37 | def test_parse():
38 | ''' Test parsing expression with cplx '''
39 | expr = 'I * omega * C'
40 |
41 | # Without allowing on complex numbers, I is a symbol
42 | assert sympy.Symbol('I') in uparser.parse_math(expr).free_symbols
43 |
44 | # I interpreted as sqrt(-1)
45 | assert sympy.Symbol('I') not in uparser.parse_math(expr, allowcomplex=True).free_symbols
46 |
47 | assert sympy.I in uparser.parse_math(expr, allowcomplex=True).atoms(sympy.I)
48 |
49 | u = ModelComplex('f=a+b')
50 | u.var('a').measure(1+1j, uncertainty=0)
51 | u.var('b').measure(1+1j, uncertainty=0)
52 | u._build_model_sympy()
53 | re, im = u.model.sympys
54 |
55 | ar, br = sympy.symbols('a_real b_real')
56 | assert (re - ar - br) == 0
57 | ai, bi = sympy.symbols('a_imag b_imag')
58 | assert (im - ai - bi) == 0
59 |
--------------------------------------------------------------------------------
/suncal/curvefit/waveform/extrema.py:
--------------------------------------------------------------------------------
1 | ''' Uncertainty functions for max, min, and peak-peak '''
2 | import numpy as np
3 | from scipy import stats
4 |
5 | from ..uncertarray import Array, EventUncert
6 |
7 |
8 | def maximum(wf: Array) -> EventUncert:
9 | ''' Uncertainty in maximum value of waveform.
10 | Also returns time of maximum value as a component
11 | '''
12 | ymax = wf.y.max()
13 | if all(wf.uy) == 0.:
14 | idx = wf.y.argmax()
15 | tunc = EventUncert(wf.x[idx], 0, wf.x[idx], wf.x[idx])
16 | yunc = EventUncert(ymax, 0, ymax, ymax, components={'time': tunc})
17 | return yunc
18 |
19 | mn = (wf.y - 6*wf.uy).min()
20 | mx = (wf.y + 6*wf.uy).max()
21 | xx = np.linspace(mn, mx, 1000)
22 | yy = np.ones_like(xx)
23 |
24 | for i in range(len(wf.x)):
25 | yy *= stats.norm.cdf(xx, loc=wf.y[i], scale=wf.uy[i])
26 |
27 | try:
28 | ylow = xx[np.where(yy > .025)[0][0]]
29 | except IndexError:
30 | ylow = xx[0]
31 |
32 | try:
33 | yhigh = xx[np.where(yy > .975)[0][0]]
34 | except IndexError:
35 | yhigh = xx[-1]
36 |
37 | pt = np.zeros_like(wf.x)
38 | for i in range(len(wf.x)):
39 | pt[i] = 1 - stats.norm.cdf(ymax, loc=wf.y[i], scale=wf.uy[i])
40 | pt = np.cumsum(pt / sum(pt))
41 |
42 | try:
43 | tlow = wf.x[np.where(pt > .025)[0][0]]
44 | except IndexError:
45 | tlow = wf.x[0]
46 |
47 | try:
48 | thigh = wf.x[np.where(pt > .975)[0][0]]
49 | except IndexError:
50 | thigh = wf.x[-1]
51 |
52 | yexpected = (ylow+yhigh)/2
53 | texpected = (tlow+thigh)/2
54 |
55 | tunc = EventUncert(texpected, (thigh-texpected)/2, tlow, thigh)
56 | yunc = EventUncert(yexpected, (yhigh-yexpected)/2, ylow, yhigh,
57 | components={'time': tunc})
58 | return yunc
59 |
60 |
61 | def minimum(wf: Array) -> EventUncert:
62 | ''' Uncertainty in minimum value of waveform
63 | Also returns time of minimum value as a component
64 | '''
65 | wf = Array(wf.x, -wf.y, wf.ux, wf.uy)
66 | out = maximum(wf)
67 | out.nominal = -out.nominal
68 | out.low = -out.low
69 | out.high = -out.high
70 | return out
71 |
72 |
73 | def peak_peak(wf: Array) -> EventUncert:
74 | ''' Uncertainty in peak-to-peak '''
75 | mx = maximum(wf)
76 | mn = minimum(wf)
77 | pkpk = mx.nominal - mn.nominal
78 | u_pkpk = np.sqrt(mx.uncert**2 + mn.uncert**2)
79 | components = {
80 | 'max': mx,
81 | 'min': mn,
82 | }
83 | return EventUncert(pkpk, u_pkpk, pkpk-u_pkpk*2, pkpk+u_pkpk*2,
84 | components=components)
85 |
--------------------------------------------------------------------------------
/suncal/intervals/fit.py:
--------------------------------------------------------------------------------
1 |
2 | from collections import namedtuple
3 | import numpy as np
4 |
5 |
6 | def fitpoly(x, y, m=1):
7 | ''' Fit polynomial, order m, with zero intercept
8 |
9 | Args:
10 | x: X-values
11 | y: Y-values
12 | m: Polynomial order
13 |
14 | Returns:
15 | b (array): Polynomial coefficients where
16 | y = b[0] * x + b[1]*x**2 ... + b[i]*x**(i+1)
17 | cov (array): Covariance matrix of b parameters
18 | '''
19 | # Redefine the curve fit here rather than using curvefit.py for speed/efficiency
20 | x = np.atleast_1d(x)
21 | y = np.atleast_1d(y)
22 |
23 | T = np.vstack([x**n for n in range(1, m+1)]).T # Castrup (7)
24 | TTinv = np.linalg.inv(T.T @ T)
25 | b = TTinv @ T.T @ y # Castrup (6)
26 |
27 | rss = sum((y - y_pred(x, b))**2) # Castrup (5)
28 | s2 = rss / (len(x)-m) # Castrup (10)
29 | S = s2 * np.eye(m) # Castrup (9)
30 | cov = TTinv @ S # Castrup (8)
31 | return b, cov, np.sqrt(s2)
32 |
33 |
34 | def y_pred(x, b, y0=0):
35 | ''' Predict y at the x value given b polynomial coefficients from fitpoly() '''
36 | scalar = not np.asarray(x).shape
37 | x = np.atleast_1d(x)
38 | y = np.zeros(len(x))
39 | m = len(b)
40 | for i, xval in enumerate(x):
41 | tprime = np.array([xval**n for n in range(1, m+1)])
42 | y[i] = tprime @ b # Castrup (12)
43 | if scalar:
44 | return y0 + y[0]
45 | return y0 + y
46 |
47 |
48 | def u_pred(x, b, cov, syx):
49 | ''' Prediction band at x (based on residual scatter) '''
50 | scalar = not np.asarray(x).shape
51 | x = np.atleast_1d(x)
52 | upred = np.zeros(len(x))
53 | m = len(b)
54 | for i, xval in enumerate(x):
55 | tprime = np.array([xval**n for n in range(1, m+1)])
56 | # upred[i] = syx * np.sqrt(1 + tprime.T @ cov @ tprime) # Castrup (19) appears to be wrong???
57 | upred[i] = np.sqrt(syx**2 + (tprime.T @ cov @ tprime))
58 | if scalar:
59 | return upred[0]
60 | return upred
61 |
62 |
63 | def u_conf(x, b, cov):
64 | ''' Confidence band at x (based on residual scatter) '''
65 | scalar = not np.asarray(x).shape
66 | x = np.atleast_1d(x)
67 | uconf = np.zeros(len(x))
68 | m = len(b)
69 | for i, xval in enumerate(x):
70 | tprime = np.array([xval**n for n in range(1, m+1)])
71 | uconf[i] = np.sqrt(tprime.T @ cov @ tprime)
72 | if scalar:
73 | return uconf[0]
74 | return uconf
75 |
--------------------------------------------------------------------------------
/suncal/meassys/meas_result.py:
--------------------------------------------------------------------------------
1 | ''' Results of a Measurement System calculation '''
2 | from dataclasses import dataclass, field
3 |
4 | from ..common import reporter, ttable
5 | from ..common.limit import Limit
6 | from ..uncertainty.variables import RandomVariable
7 | from .report.meassys import SystemQuantityReport, SystemReport
8 |
9 |
10 | @reporter.reporter(SystemQuantityReport)
11 | @dataclass
12 | class SystemQuantityResult:
13 | ''' Result from one quantity in a measurement system
14 | May be from a SystemQuantity, SystemIndirect, or SystemCurve
15 |
16 | Args:
17 | symbol: Symbol/variable name for this quantity
18 | value: Average/expected value of the quantity
19 | uncertainty: Standard uncertainty
20 | units: Units of measure (string)
21 | degrees_freedom: Effective degrees of freedom
22 | tolerance: Optional tolerance for pr(conformance)
23 | p_conformance: Probability of conformance to the tolerance, if defined
24 | confidence: Level of confidence for calculating expanded uncertainty
25 | qty: The SystemQuantity defining the calculation
26 | meta: Dictionary of metadata based on the type of calculation used to determine this quantity
27 | '''
28 | symbol: str
29 | value: float
30 | uncertainty: float # standard
31 | units: str
32 | degrees_freedom: float
33 | tolerance: Limit
34 | p_conformance: float
35 | qty: 'SystemQuantity|SystemIndirectQuantity|SystemCurve'
36 | meta: dict = field(default_factory=dict)
37 |
38 | def expanded(self, conf=None) -> float:
39 | ''' Expanded uncertainty '''
40 | if conf is None:
41 | conf = self.meta.get('confidence', .95)
42 | k = ttable.k_factor(conf, self.degrees_freedom)
43 | return self.uncertainty * k
44 |
45 | def randomvariable(self) -> RandomVariable:
46 | ''' Convert the coefficient into a RandomVariable '''
47 | if hasattr(self.qty, 'randomvariable'):
48 | rv = self.qty.randomvariable()
49 | else:
50 | rv = RandomVariable(self.value)
51 | rv.typeb(std=self.uncertainty, df=self.degrees_freedom)
52 | return rv
53 |
54 |
55 | @reporter.reporter(SystemReport)
56 | @dataclass
57 | class SystemResult:
58 | ''' Result for a system of quantities '''
59 | quantities: list[SystemQuantityResult] = None
60 | confidence: float = .95
61 |
62 | def get_result(self, symbol):
63 | symbols = [q.symbol for q in self.quantities]
64 | try:
65 | idx = symbols.index(symbol)
66 | except (ValueError, IndexError):
67 | return None
68 | return self.quantities[idx]
69 |
--------------------------------------------------------------------------------
/doc/Examples/ex_endgauge.yaml:
--------------------------------------------------------------------------------
1 | - description: 'End-gauge calibration
2 |
3 |
4 | Example H.1 from GUM with distribution parameters from 9.5 in GUM Supplement 1.
5 | Compare results to Figure 17 in GUM-Supplement 1.
6 |
7 | '
8 | functions:
9 | - desc: End Gauge Length
10 | expr: d - l_s*(alpha_s*d_theta + d_alpha*theta) + l_s
11 | name: l
12 | units: nanometer
13 | inputs:
14 | - desc: Length of standard at 20C from certificate
15 | mean: 50000623.6
16 | name: l_s
17 | uncerts:
18 | - degf: 18
19 | desc: Uncertainty of the standard
20 | dist: normal
21 | k: 3
22 | name: u_{ls}
23 | unc: 75
24 | units: nanometer
25 | units: nanometer
26 | - desc: Measured difference between end gauges
27 | mean: 215.0
28 | name: d
29 | uncerts:
30 | - conf: 0.95
31 | degf: 5
32 | desc: Random effects of comparator
33 | dist: t
34 | name: u_{d1}
35 | unc: 10
36 | units: nanometer
37 | - degf: 8
38 | desc: Systematic effects of comparator
39 | dist: normal
40 | k: 3
41 | name: u_{d2}
42 | unc: 20
43 | units: nanometer
44 | - degf: 24
45 | desc: Repeated obeservations
46 | dist: normal
47 | name: u_d
48 | std: 5.8
49 | units: nanometer
50 | units: nanometer
51 | - desc: Deviation in temperature of test bed from 20C ambient
52 | mean: -0.1
53 | name: theta
54 | uncerts:
55 | - a: 0.5
56 | degf: .inf
57 | desc: Cyclic variation of temperature in room
58 | dist: arcsine
59 | name: u_Delta
60 | units: delta_degC
61 | - degf: .inf
62 | desc: Mean temperature of the bed
63 | dist: normal
64 | name: u_theta
65 | std: 0.2
66 | units: delta_degC
67 | units: delta_degC
68 | - desc: Coefficient of thermal expansion
69 | mean: 1.15e-05
70 | name: alpha_s
71 | uncerts:
72 | - a: 2.0e-06
73 | degf: .inf
74 | desc: Thermal expansion coefficient of standard
75 | dist: uniform
76 | name: u_{alpha_s}
77 | units: 1 / delta_degC
78 | units: 1 / delta_degC
79 | - desc: Difference in expansion coefficients
80 | mean: 0.0
81 | name: d_alpha
82 | uncerts:
83 | - a: 1.0e-06
84 | d: 1.0e-07
85 | degf: 50
86 | desc: Difference in expansion coefficients
87 | dist: curvtrap
88 | name: u_{da}
89 | units: 1 / delta_degC
90 | units: 1 / delta_degC
91 | - desc: Difference in temperatures
92 | mean: 0.0
93 | name: d_theta
94 | uncerts:
95 | - a: 0.05
96 | d: 0.025
97 | degf: 2
98 | desc: Difference in temperatures
99 | dist: curvtrap
100 | name: u_{dt}
101 | units: delta_degC
102 | units: delta_degC
103 | mode: uncertainty
104 | name: uncertainty
105 | samples: 1000000
106 |
--------------------------------------------------------------------------------
/doc/Examples/Data/pulse.csv:
--------------------------------------------------------------------------------
1 | -0.02438615,-0.013413709
2 | 0.837600184,-0.006736466
3 | 1.985365399,-0.004186536
4 | 2.960510313,-0.01527054
5 | 3.968379369,-0.005581955
6 | 4.911952867,0.020261729
7 | 6.040457706,0.012803556
8 | 6.92372737,0.001556642
9 | 8.026355713,0.004069561
10 | 8.995836009,0.007986862
11 | 9.925193093,0.014606394
12 | 10.88763279,0.008861731
13 | 11.92345839,0.000531143
14 | 12.90866069,-0.009901167
15 | 14.03722791,0.003120113
16 | 14.983426,0.001548118
17 | 16.08982421,-0.002411995
18 | 16.83525229,0.005555075
19 | 18.09066428,-0.002125856
20 | 18.92290255,0.007751403
21 | 19.86934846,0.027310498
22 | 21.00984534,0.019382539
23 | 21.95834655,0.046506122
24 | 23.00099132,0.08447042
25 | 23.90630987,0.107742138
26 | 25.06465157,0.15052898
27 | 26.11150976,0.222211695
28 | 26.94054355,0.258528702
29 | 28.14315388,0.338761635
30 | 29.00124454,0.409908021
31 | 30.03974468,0.503922283
32 | 31.07213715,0.581084331
33 | 32.00802677,0.651083981
34 | 32.98571408,0.7198599
35 | 34.0761936,0.813029527
36 | 34.99021521,0.859721778
37 | 35.80600663,0.893324968
38 | 36.96204264,0.911180729
39 | 38.01541685,0.952720997
40 | 38.87932917,0.958777094
41 | 39.84666861,0.989363927
42 | 40.99321862,0.980162508
43 | 41.98032524,0.977448318
44 | 43.11735924,1.014642539
45 | 43.76944227,0.996034679
46 | 44.90569733,1.009458149
47 | 45.8710282,1.010989498
48 | 47.01185253,1.002526145
49 | 47.89636229,1.001973363
50 | 48.95037107,0.993480484
51 | 50.1238675,0.965357773
52 | 50.89775051,0.95548658
53 | 51.87178954,0.970696736
54 | 52.89822312,0.935868692
55 | 53.98435098,0.896309108
56 | 54.91031375,0.841729852
57 | 55.97530661,0.777362883
58 | 56.97521168,0.712054643
59 | 58.13490148,0.654977709
60 | 58.9670728,0.586348471
61 | 59.85046711,0.502410816
62 | 60.85149085,0.446821511
63 | 61.97672232,0.340454895
64 | 62.8911286,0.273975247
65 | 63.79147032,0.225669592
66 | 64.92389604,0.172362802
67 | 66.0269287,0.119880907
68 | 66.89006388,0.073294245
69 | 68.11633611,0.064015062
70 | 69.01245071,0.037307195
71 | 69.99967379,0.03285102
72 | 71.0658061,0.013539356
73 | 71.94014719,0.011964052
74 | 72.92254744,-0.003920501
75 | 73.90411056,0.030410387
76 | 75.00573315,0.009506511
77 | 75.93317075,-0.004426745
78 | 77.01442033,0.015346672
79 | 78.19452475,-0.007830449
80 | 78.8247825,0.002115711
81 | 79.83465011,0.001779176
82 | 80.96334388,0.018396869
83 | 82.16029885,-0.003004883
84 | 83.00847007,0.008240629
85 | 84.097471,-0.010211993
86 | 84.87737129,-0.008151374
87 | 86.18176087,-0.004963958
88 | 86.9933421,-0.010562457
89 | 88.07587377,-0.011330744
90 | 88.86324092,0.007623092
91 | 90.1096315,-0.006508574
92 | 91.13764592,0.00702808
93 | 92.08971305,-0.009796423
94 | 93.04902854,-0.000164178
95 | 94.15605755,0.019614964
96 | 94.8284097,0.011308063
97 | 96.1070305,0.005014161
98 | 96.99370274,0.008463229
99 | 97.869038,-0.007496569
100 | 99.07021517,-0.000748
101 | 99.98465715,-0.000216383
102 | [ ]:,[ ]:
103 |
--------------------------------------------------------------------------------
/suncal/gui/gui_styles.py:
--------------------------------------------------------------------------------
1 | ''' Style and themes including dark and light mode themes '''
2 | from contextlib import contextmanager
3 | import logging
4 | import matplotlib as mpl
5 | from PyQt6 import QtWidgets, QtCore, QtGui
6 |
7 | from ..common import plotting
8 | from .gui_settings import gui_settings
9 |
10 |
11 | def isdark() -> bool:
12 | ''' Is dark mode enabled? '''
13 | return QtWidgets.QApplication.instance().styleHints().colorScheme() == QtCore.Qt.ColorScheme.Dark
14 |
15 |
16 | def darkmode_signal():
17 | ''' Signal emitted when dark mode changes '''
18 | return QtWidgets.QApplication.instance().styleHints().colorSchemeChanged
19 |
20 |
21 | class _Color:
22 | ''' Color options for Light and Dark modes '''
23 | _DARK = {
24 | 'transparent': QtGui.QBrush(QtCore.Qt.GlobalColor.transparent),
25 | 'invalid': QtGui.QBrush(QtCore.Qt.GlobalColor.darkRed),
26 | 'column_highlight': QtGui.QBrush(QtGui.QColor(68, 85, 68, 255)),
27 | 'mathtext': '#cccccc'}
28 | _LIGHT = {
29 | 'transparent': QtGui.QBrush(QtCore.Qt.GlobalColor.transparent),
30 | 'invalid': QtGui.QBrush(QtCore.Qt.GlobalColor.darkRed),
31 | 'column_highlight': QtGui.QBrush(QtGui.QColor(204, 255, 204, 255)),
32 | 'mathtext': 'black'}
33 |
34 | @property
35 | def _themedict(self):
36 | ''' Get color dictionary for the current dark/light theme '''
37 | return self._DARK if isdark() else self._LIGHT
38 |
39 | @property
40 | def math(self) -> str:
41 | ''' Color of math text, as string for matplotlib '''
42 | return self._themedict.get('mathtext')
43 |
44 | @property
45 | def invalid(self) -> QtGui.QBrush:
46 | ''' Color for table cells with invalid input '''
47 | return self._themedict.get('invalid')
48 |
49 | @property
50 | def column_highlight(self) -> QtGui.QBrush:
51 | ''' Highlighted/selected column in data importer '''
52 | return self._themedict.get('column_highlight')
53 |
54 | @property
55 | def transparent(self) -> QtGui.QBrush:
56 | ''' Transparent color '''
57 | return self._themedict.get('transparent')
58 |
59 | def set_plot_style(self):
60 | ''' Configure matplotlib with plot style from saved settings '''
61 | plotting.activate_plotstyle(gui_settings.plot_style, dark=isdark())
62 | if (cycle := gui_settings.color_cycle):
63 | mpl.style.use({'axes.prop_cycle': cycle})
64 | for k, v in gui_settings.plotparams.items():
65 | try:
66 | mpl.style.use({k: v}) # Override anything in base style
67 | except ValueError:
68 | logging.warn(f'Bad parameter {v} for key {k}')
69 |
70 |
71 | color = _Color()
72 |
73 |
74 | @contextmanager
75 | def LightPlotstyle():
76 | ''' Activate a light-mode plot style using context manager '''
77 | plotting.activate_plotstyle(gui_settings.plot_style, dark=False)
78 | yield
79 | plotting.activate_plotstyle(gui_settings.plot_style, dark=isdark())
80 |
--------------------------------------------------------------------------------
/suncal/gui/gui_common.py:
--------------------------------------------------------------------------------
1 | ''' System-level GUI functions '''
2 |
3 | import os
4 | import sys
5 | import traceback
6 | from PyQt6 import QtWidgets, QtCore, QtGui
7 |
8 |
9 | # Breakpoint handler for breakpoint() function to disable QT problems when breaking
10 | def _qtbreakpoint(*args, **kwargs):
11 | from pdb import set_trace
12 | QtCore.pyqtRemoveInputHook()
13 | set_trace()
14 |
15 |
16 | sys.breakpointhook = _qtbreakpoint
17 |
18 |
19 | # System exception handler. Not pretty, but better than just shutting down.
20 | def handle_exception(exc_type, exc_value, exc_traceback):
21 | ''' Show exceptions in message box and print to console. '''
22 | if isinstance(exc_value, KeyboardInterrupt):
23 | return
24 | msg = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
25 | print(msg)
26 | msgbox = QtWidgets.QMessageBox()
27 | msgbox.setWindowTitle('Suncal')
28 | msgbox.setText('The following exception occurred.')
29 | msgbox.setInformativeText(msg)
30 | msgbox.exec()
31 |
32 |
33 | sys.excepthook = handle_exception
34 |
35 |
36 | def centerWindow(window, w, h):
37 | ''' Set window geometry so it appears centered in the window
38 | If window size is too big, maximize it
39 | '''
40 | desktopsize = QtGui.QGuiApplication.primaryScreen().availableGeometry()
41 | window.setGeometry(desktopsize.width()//2 - w//2, desktopsize.height()//2 - h//2, w, h) # Center window on screen
42 | if h >= desktopsize.height() or w >= desktopsize.width():
43 | window.showMaximized()
44 |
45 |
46 | class InfValidator(QtGui.QDoubleValidator):
47 | ''' Double Validator that allows "inf" and "-inf" entry '''
48 | def validate(self, s, pos):
49 | ''' Validate the string '''
50 | if s.lower() in ['inf', '-inf']:
51 | return QtGui.QValidator.State.Acceptable, s, pos
52 | elif s.lower() in '-inf':
53 | return QtGui.QValidator.State.Intermediate, s, pos
54 | return super().validate(s, pos)
55 |
56 |
57 | # This function allows switching file path when run from pyInstaller.
58 | def resource_path(relative):
59 | ''' Get absolute file path for resource. Will switch between pyInstaller tmp dir and gui folder '''
60 | try:
61 | base = sys._MEIPASS # MEIPASS is added by pyinstaller
62 | except AttributeError:
63 | base = os.path.dirname(__file__)
64 | return os.path.join(base, relative)
65 |
66 |
67 | class BlockedSignals:
68 | ''' Context manager for blocking pyqt signals that
69 | restores the signal block to its previous state when done.
70 |
71 | with BlockedSignal(widget):
72 | ...
73 | '''
74 | def __init__(self, widget):
75 | self.widget = widget
76 | self._saved_state = self.widget.signalsBlocked()
77 |
78 | def __enter__(self):
79 | self._saved_state = self.widget.signalsBlocked()
80 | self.widget.blockSignals(True)
81 |
82 | def __exit__(self, exc_type, exc_val, exc_tb):
83 | self.widget.blockSignals(self._saved_state)
--------------------------------------------------------------------------------
/suncal/distexplore/dist_explore.py:
--------------------------------------------------------------------------------
1 | ''' Backend for distribution explorer. This is mostly an educational/training function. '''
2 | import numpy as np
3 |
4 | from ..common import uparser
5 | from .report.dist_explore import ReportDistExplore
6 |
7 |
8 | class DistExplore:
9 | ''' Distribution Explorer (same as DistExploreResults)
10 |
11 | For setting up stats distributions, sampling them, and calculating Monte-Carlos
12 | using the sampled values.
13 |
14 | Args:
15 | name (string): Name for the distribution explorer object
16 | samples (int): Number of random samples to run
17 | seed (int): Random number seed
18 | '''
19 | def __init__(self, samples=10000, seed=None):
20 | self.dists = {} # Dictionary of name (or expr): stats distribution
21 | self.nsamples = samples # Number of samples
22 | self.samplevalues = {} # Dictionary of name: sample array
23 | self.seed = seed
24 | self.report = ReportDistExplore(self)
25 |
26 | def set_numsamples(self, N):
27 | ''' Set number of samples '''
28 | self.nsamples = N
29 | self.samplevalues = {}
30 |
31 | def sample(self, name):
32 | ''' Sample input with given name
33 |
34 | Args:
35 | name (str): Name of variable to sample
36 |
37 | Returns:
38 | samples (array): Array of random samples
39 | '''
40 | dist = self.dists.get(name, None)
41 | expr = uparser.parse_math(name, raiseonerr=False)
42 | if expr is None:
43 | raise ValueError(f'Invalid expression {name}')
44 |
45 | if expr.is_symbol:
46 | # This is a base distribution, just sample it
47 | assert dist is not None
48 | self.samplevalues[name] = dist.rvs(self.nsamples)
49 |
50 | # But check for downstream Monte Carlos that use this variable and sample them too
51 | for mcexpr in [uparser.parse_math(n, raiseonerr=False) for n in self.dists]:
52 | if mcexpr is not None and str(mcexpr) != name and name in [str(x) for x in mcexpr.free_symbols]:
53 | self.sample(str(mcexpr))
54 |
55 | else:
56 | # This is an expression. Sample all the input variables if not sampled already.
57 | inputs = {}
58 | for i in [str(x) for x in expr.free_symbols]:
59 | if i not in self.samplevalues and i in self.dists:
60 | self.sample(i)
61 | elif i not in self.dists:
62 | raise ValueError(f'Variable {i} has not been defined')
63 |
64 | inputs[i] = self.samplevalues[i]
65 | self.samplevalues[name] = uparser.callf(name, inputs)
66 | return self.samplevalues[name]
67 |
68 | def calculate(self):
69 | ''' Sample all distributions and return report '''
70 | if self.seed is not None:
71 | np.random.seed(self.seed)
72 | for name in self.dists:
73 | self.sample(name)
74 | return self
75 |
--------------------------------------------------------------------------------
/suncal/mqa/report/system.py:
--------------------------------------------------------------------------------
1 | ''' Report for Measured Data Evaluation '''
2 | from ...common.report import Report, Number
3 |
4 |
5 | class MqaSystemReport:
6 | ''' Report of Mqa System '''
7 | def __init__(self, results: 'MeasSysResults'):
8 | self.result = results
9 |
10 | def _repr_markdown_(self):
11 | return self.summary().get_md()
12 |
13 | def report_details(self, **kwargs) -> Report:
14 | ''' Report table of reliability details '''
15 | rpt = Report(**kwargs)
16 | for qty in self.result.quantities:
17 | name = qty.item.measurand.name
18 | rpt.hdr(name, level=3)
19 | rpt.append(qty.report.summary(**kwargs))
20 |
21 | if hasattr(qty, 'cost_item'):
22 | rpt.hdr('Calibration Costs:', level=4)
23 | rpt.append(qty.report.report_cost(**kwargs))
24 | rpt.hdr('Total Cost:', level=4)
25 | rpt.append(qty.report.report_cost_total(**kwargs))
26 | return rpt
27 |
28 | def summary(self, **kwargs) -> Report:
29 | ''' Generate summary table '''
30 | rpt = Report(**kwargs)
31 | hdr = [
32 | 'Quantity',
33 | 'Testpoint',
34 | 'Tolerance',
35 | 'Equipment',
36 | 'Meas. Uncertainty',
37 | 'TAR',
38 | 'TUR',
39 | 'PFA %']
40 |
41 | rows = []
42 |
43 | def add_qty(name, qty):
44 | name = name if name else qty.item.measurand.name
45 | rows.append([
46 | name,
47 | Number(qty.testpoint),
48 | str(qty.tolerance),
49 | qty.item.measurement.mte.equip_name(),
50 | ('±', Number(qty.uncertainty.accuracy, fmin=0)),
51 | Number(qty.capability.tar, fmin=0),
52 | Number(qty.capability.tur, fmin=0),
53 | Number(qty.risk.pfa_true*100, fmin=0, postfix=' %')
54 | ])
55 | if qty.item.measurement.mte.quantity is not None:
56 | add_qty('→ ' + qty.item.measurement.mte.quantity.measurand.name,
57 | qty.item.measurement.mte.quantity.calculate())
58 | elif qty.item.measurement.equation is not None:
59 | for mtename, mte in qty.item.measurement.mteindirect.items():
60 | if mte.quantity is not None:
61 | add_qty(f'→ {mtename}', mte.quantity)
62 | else:
63 | rows.append([
64 | f'→ {mtename}',
65 | qty.item.measurement.testpoints[mtename],
66 | '-',
67 | str(mte.equipment) if mte.equipment else mte.name,
68 | str(mte.accuracy_plusminus),
69 | '-',
70 | '-',
71 | '-'
72 | ])
73 |
74 | for qty in self.result.quantities:
75 | add_qty(None, qty)
76 |
77 | rpt.table(rows, hdr)
78 | return rpt
79 |
--------------------------------------------------------------------------------
/suncal/gui/widgets/panel.py:
--------------------------------------------------------------------------------
1 | ''' Widget Panel '''
2 | from contextlib import suppress
3 | from PyQt6 import QtWidgets, QtCore
4 |
5 | from .buttons import PlusMinusButton
6 |
7 |
8 | class WidgetPanel(QtWidgets.QTreeWidget):
9 | ''' Tree widget for expanding/collapsing other widgets '''
10 | def __init__(self, parent=None):
11 | super().__init__(parent=parent)
12 | self.setHeaderHidden(True)
13 | self.setVerticalScrollMode(QtWidgets.QTreeView.ScrollMode.ScrollPerPixel)
14 | self.setColumnCount(1)
15 | self.itemExpanded.connect(self.wasexpanded)
16 | self.itemCollapsed.connect(self.wasexpanded)
17 |
18 | def add_widget(self, name, widget, buttons=False):
19 | ''' Add a widget to the tree at the end '''
20 | idx = self.invisibleRootItem().childCount()
21 | return self.insert_widget(name, widget, idx, buttons=buttons)
22 |
23 | def expand(self, name):
24 | ''' Expand the widget with the given name '''
25 | item = self.findItems(name, QtCore.Qt.MatchFlag.MatchExactly, 0)
26 | with suppress(IndexError):
27 | self.expandItem(item[0])
28 | self.wasexpanded(item[0])
29 |
30 | root = self.invisibleRootItem()
31 | for i in range(root.childCount()):
32 | item = root.child(i)
33 | if self.itemWidget(item, 0) and self.itemWidget(item, 0).label.text() == name:
34 | self.expandItem(item)
35 | self.wasexpanded(item)
36 | break
37 |
38 | def hide_widget(self, name, hide=True):
39 | ''' Show or hide an item '''
40 | item = self.findItems(name, QtCore.Qt.MatchFlag.MatchExactly, 0)
41 | with suppress(IndexError):
42 | item[0].setHidden(hide)
43 | self.wasexpanded(item[0])
44 |
45 | def insert_widget(self, name, widget, idx, buttons=False):
46 | ''' Insert a widget into the tree '''
47 | item = QtWidgets.QTreeWidgetItem()
48 | item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) # Enable, but not selectable/editable
49 | self.insertTopLevelItem(idx, item)
50 | if buttons:
51 | bwidget = PlusMinusButton(name)
52 | bwidget.btnplus.setVisible(False)
53 | bwidget.btnminus.setVisible(False)
54 | self.setItemWidget(item, 0, bwidget)
55 | else:
56 | bwidget = None
57 | item.setText(0, name)
58 |
59 | witem = QtWidgets.QTreeWidgetItem()
60 | witem.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled)
61 | item.addChild(witem)
62 | self.setItemWidget(witem, 0, widget)
63 | return item, bwidget
64 |
65 | def fixSize(self):
66 | ''' Adjust the size of tree rows to fit the widget sizes '''
67 | self.scheduleDelayedItemsLayout()
68 |
69 | def wasexpanded(self, item):
70 | ''' Show/hide buttons when item is expanded '''
71 | buttons = self.itemWidget(item, 0)
72 | if buttons:
73 | with suppress(AttributeError):
74 | buttons.btnplus.setVisible(item.isExpanded())
75 | buttons.btnminus.setVisible(item.isExpanded())
76 |
--------------------------------------------------------------------------------
/suncal/project/proj_revsweep.py:
--------------------------------------------------------------------------------
1 | ''' Reverse sweep project component '''
2 |
3 | import numpy as np
4 |
5 | from .proj_sweep import ProjectSweep
6 | from ..sweep import UncertSweepReverse
7 | from ..common import unitmgr
8 |
9 |
10 | class ProjectReverseSweep(ProjectSweep):
11 | ''' Reverse Sweep project component '''
12 | def __init__(self, model=None, name='revsweep'):
13 | super().__init__(model, name)
14 | if model is None:
15 | self.model = UncertSweepReverse('f=x', solvefor='x', targetnom=1)
16 | self.model.model.var('x').typeb(dist='normal', unc=1, k=2, name='u(x)')
17 |
18 | def calculate(self, mc=True):
19 | ''' Run the calculation '''
20 | if self.seed:
21 | np.random.seed(self.seed)
22 | self._result = self.model.calculate(mc=mc, samples=self.nsamples)
23 | return self._result
24 |
25 | def get_arrays(self):
26 | d = {}
27 | for name in self.model.model.functionnames:
28 | if self._result is not None and self.result.gum is not None:
29 | d[f'{name} (GUM)'] = self.to_array(gum=True, funcname=name)
30 | if self._result is not None and self.result.montecarlo is not None:
31 | d[f'{name} (MC)'] = self.to_array(gum=False, funcname=name)
32 | return d
33 |
34 | def to_array(self, gum=True, funcname=None):
35 | ''' Return dictionary {x: y: uy:} of swept data and uncertainties
36 |
37 | Args:
38 | gum (bool): Use gum (True) or monte carlo (False) values
39 | funcidx (int): Index of function in calculator as y values
40 | '''
41 | xvals = unitmgr.strip_units(self.result.report._sweepvals[0]) # Only first x value
42 | if gum:
43 | yvals = [unitmgr.strip_units(g.solvefor_value) for g in self.result.gum.resultlist]
44 | uyvals = [unitmgr.strip_units(g.u_solvefor_value) for g in self.result.gum.resultlist]
45 | else:
46 | yvals = [unitmgr.strip_units(g.solvefor_value) for g in self.result.montecarlo.resultlist]
47 | uyvals = [unitmgr.strip_units(g.u_solvefor_value) for g in self.result.montecarlo.resultlist]
48 | return {'x': xvals, 'y': yvals, 'u(y)': uyvals}
49 |
50 | def get_config(self):
51 | ''' Get configuration dictionary '''
52 | d = super().get_config()
53 | d['mode'] = 'reversesweep'
54 | d['reverse'] = self.model.reverseparams
55 | return d
56 |
57 | def load_config(self, config):
58 | ''' Load configuration into project '''
59 | super().load_config(config) # UncertProp and Sweeps
60 | self.model.reverseparams = config.get('reverse', {})
61 | if 'targetunits' not in self.model.reverseparams:
62 | if 'func' in self.model.reverseparams:
63 | self.model.reverseparams['funcname'] = self.model.model.functionnames[self.model.reverseparams.get('func')]
64 | self.model.reverseparams.pop('func')
65 | funcname = self.model.reverseparams.get('funcname')
66 | self.model.reverseparams['targetunits'] = self.outunits.get(funcname)
67 | return
68 |
--------------------------------------------------------------------------------
/test/test_parser.py:
--------------------------------------------------------------------------------
1 | ''' Test cases for uparser.py.
2 | Usage: run py.test from root folder.
3 | '''
4 | import pytest
5 | import numpy
6 | import sympy
7 |
8 | from suncal.common import uparser
9 |
10 |
11 | def test_parse_math_ok():
12 | ''' Test parse_math. These should evaluate ok, no exception raised. '''
13 | uparser.parse_math('1+1')
14 | uparser._parse_math('2*6+cos(30)', fns=['cos'])
15 | uparser.parse_math('2**2')
16 | uparser.parse_math('(10+5)/3')
17 | uparser.parse_math('a+b')
18 | uparser.parse_math('sqrt(-1)*sqrt(-1)') # Complex not supported, but this evaluates to real
19 |
20 |
21 | def test_parse_math_fail():
22 | ''' Test parse_math. These should raise ValueError. '''
23 | with pytest.raises(ValueError):
24 | uparser.parse_math('import os') # imports disabled
25 |
26 | with pytest.raises(ValueError):
27 | uparser.parse_math('print("ABC")') # builtin functions disabled
28 |
29 | with pytest.raises(ValueError):
30 | uparser.parse_math('import(os)') # Syntax error
31 |
32 | with pytest.raises(ValueError):
33 | uparser.parse_math('os.system("ls")') # non-allowed function
34 |
35 | with pytest.raises(ValueError):
36 | uparser.parse_math('().__class__') # Hack to get at base classes
37 |
38 | with pytest.raises(ValueError):
39 | uparser._parse_math('sin(pi)', fns=[]) # No fn list given, sin not allowed
40 |
41 | with pytest.raises(ValueError):
42 | uparser.parse_math('lambda x: x+1') # Lambdas disabled
43 |
44 | with pytest.raises(ValueError):
45 | uparser.parse_math('def x(): pass') # Function def disabled
46 |
47 | with pytest.raises(ValueError):
48 | uparser.parse_math('numpy.pi') # Attributes disabled
49 |
50 | with pytest.raises(ValueError):
51 | uparser.parse_math('2*f', name='f') # Name parameter, can't be recursive
52 |
53 | with pytest.raises(ValueError):
54 | uparser.parse_math('#a+b') # comments are ok, but here there's no expression before it
55 |
56 | with pytest.raises(ValueError):
57 | uparser.parse_math('sqrt(-1)') # Imaginary numbers not supported
58 |
59 |
60 | def test_call():
61 | ''' Test callf function, verify results are same as plain math. '''
62 | assert uparser.callf('2+2') == 2+2
63 | assert uparser.callf('4**2') == 4**2
64 | assert uparser.callf('4^2') == 4**2 # Replacing ^ with ** for user
65 | assert uparser.callf('cos(pi)') == numpy.cos(numpy.pi)
66 | assert uparser.callf('exp(-1)') == numpy.exp(-1)
67 | assert uparser.callf('ln(exp(1))') == numpy.log(numpy.exp(1))
68 | assert uparser.callf('x + y', {'x': 3, 'y': 4}) == 7
69 |
70 |
71 | def test_callf_sympy():
72 | ''' Test callf with a sympy expression '''
73 | a, b = sympy.symbols('a b')
74 | f = (a+b)/2
75 | assert uparser.callf(f, {'a': 10, 'b': 6}) == (10+6)/2
76 |
77 |
78 | def test_callf_callable():
79 | ''' Test callf with python function '''
80 |
81 | def myfunc(a, b):
82 | return (a+b)/2
83 | assert uparser.callf(myfunc, {'a': 10, 'b': 6}) == (10+6)/2
84 |
85 | with pytest.raises(TypeError):
86 | uparser.callf(numpy) # Object that won't translate into function
87 |
--------------------------------------------------------------------------------
/doc/Examples/Data/S2data.csv:
--------------------------------------------------------------------------------
1 | Asset,Interval Start,Interval End,Pass/Fail,,Interval Months
2 | SNL1000,1/1/2016,2/1/2016,pass,,1
3 | SNL1000,2/1/2016,3/1/2016,pass,,1
4 | SNL1000,3/1/2016,4/1/2016,pass,,1
5 | SNL1000,4/1/2016,5/1/2016,pass,,1
6 | SNL1000,5/1/2016,6/1/2016,pass,,1
7 | SNL1000,6/1/2016,7/1/2016,pass,,1
8 | SNL1000,7/1/2016,8/1/2016,pass,,1
9 | SNL1000,8/1/2016,9/1/2016,pass,,1
10 | SNL1000,9/1/2016,10/1/2016,pass,,1
11 | SNL1000,10/1/2016,11/1/2016,pass,,1
12 | SNL1000,11/1/2016,12/1/2016,pass,,1
13 | SNL1000,12/1/2016,1/1/2017,pass,,1
14 | SNL1001,1/1/2016,3/1/2016,pass,,2
15 | SNL1001,3/1/2016,5/1/2016,pass,,2
16 | SNL1001,5/1/2016,7/1/2016,pass,,2
17 | SNL1001,7/1/2016,9/1/2016,pass,,2
18 | SNL1001,9/1/2016,11/1/2016,pass,,2
19 | SNL1001,11/1/2016,1/1/2017,fail,,2
20 | SNL1001,1/1/2017,3/1/2017,pass,,2
21 | SNL1001,3/1/2017,5/1/2017,pass,,2
22 | SNL1001,5/1/2017,7/1/2017,pass,,2
23 | SNL1001,7/1/2017,9/1/2017,pass,,2
24 | SNL1001,9/1/2017,11/1/2017,pass,,2
25 | SNL1001,11/1/2017,1/1/2018,pass,,2
26 | SNL1002,1/1/2016,4/1/2016,pass,,3
27 | SNL1002,4/1/2016,7/1/2016,pass,,3
28 | SNL1002,7/1/2016,10/1/2016,pass,,3
29 | SNL1002,10/1/2016,1/1/2017,pass,,3
30 | SNL1002,1/1/2017,4/1/2017,pass,,3
31 | SNL1002,4/1/2017,7/1/2017,fail,,3
32 | SNL1002,7/1/2017,10/1/2017,pass,,3
33 | SNL1002,10/1/2017,1/1/2018,fail,,3
34 | SNL1002,1/1/2018,4/1/2018,pass,,3
35 | SNL1002,4/1/2018,7/1/2018,pass,,3
36 | SNL1002,7/1/2018,10/1/2018,pass,,3
37 | SNL1002,10/1/2018,1/1/2019,pass,,3
38 | SNL1003,1/1/2016,5/1/2016,pass,,4
39 | SNL1003,5/1/2016,9/1/2016,pass,,4
40 | SNL1003,9/1/2016,1/1/2017,pass,,4
41 | SNL1003,1/1/2017,5/1/2017,pass,,4
42 | SNL1003,5/1/2017,9/1/2017,pass,,4
43 | SNL1003,9/1/2017,1/1/2018,fail,,4
44 | SNL1003,1/1/2018,5/1/2018,pass,,4
45 | SNL1003,5/1/2018,9/1/2018,fail,,4
46 | SNL1003,9/1/2018,1/1/2019,pass,,4
47 | SNL1003,1/1/2019,5/1/2019,pass,,4
48 | SNL1003,5/1/2019,9/1/2019,pass,,4
49 | SNL1003,9/1/2019,1/1/2020,pass,,4
50 | SNL1004,1/1/2016,7/1/2016,pass,,6
51 | SNL1004,7/1/2016,1/1/2017,pass,,6
52 | SNL1004,1/1/2017,7/1/2017,pass,,6
53 | SNL1004,7/1/2017,1/1/2018,pass,,6
54 | SNL1004,1/1/2018,7/1/2018,pass,,6
55 | SNL1004,7/1/2018,1/1/2019,fail,,6
56 | SNL1004,1/1/2019,7/1/2019,pass,,6
57 | SNL1004,7/1/2019,1/1/2020,fail,,6
58 | SNL1004,1/1/2020,7/1/2020,pass,,6
59 | SNL1004,7/1/2020,1/1/2021,pass,,6
60 | SNL1004,1/1/2021,7/1/2021,pass,,6
61 | SNL1004,7/1/2021,1/1/2022,pass,,6
62 | SNL1005,1/1/2016,10/1/2016,pass,,9
63 | SNL1005,10/1/2016,7/1/2017,fail,,9
64 | SNL1005,7/1/2017,4/1/2018,pass,,9
65 | SNL1005,4/1/2018,1/1/2019,pass,,9
66 | SNL1005,1/1/2019,10/1/2019,pass,,9
67 | SNL1005,10/1/2019,7/1/2020,fail,,9
68 | SNL1005,7/1/2020,4/1/2021,pass,,9
69 | SNL1005,4/1/2021,1/1/2022,fail,,9
70 | SNL1005,1/1/2022,10/1/2022,pass,,9
71 | SNL1005,10/1/2022,7/1/2023,pass,,9
72 | SNL1005,7/1/2023,4/1/2024,pass,,9
73 | SNL1005,4/1/2024,1/1/2025,pass,,9
74 | SNL1006,1/1/2025,1/1/2026,pass,,12
75 | SNL1006,1/1/2026,1/1/2027,fail,,12
76 | SNL1006,1/1/2027,1/1/2028,pass,,12
77 | SNL1006,1/1/2028,1/1/2029,pass,,12
78 | SNL1006,1/1/2029,1/1/2030,pass,,12
79 | SNL1006,1/1/2030,1/1/2031,fail,,12
80 | SNL1006,1/1/2031,1/1/2032,pass,,12
81 | SNL1006,1/1/2032,1/1/2033,fail,,12
82 | SNL1006,1/1/2033,1/1/2034,pass,,12
83 | SNL1006,1/1/2034,1/1/2035,pass,,12
84 | SNL1006,1/1/2035,1/1/2036,fail,,12
85 | SNL1006,1/1/2036,1/1/2037,fail,,12
86 | ,,,,,
87 | ,,,,,
88 |
--------------------------------------------------------------------------------
/suncal/risk/risk_montecarlo.py:
--------------------------------------------------------------------------------
1 | ''' Calculate false accept and reject risk using Monte Carlo method '''
2 | from collections import namedtuple
3 | import numpy as np
4 |
5 | from ..common import distributions
6 |
7 |
8 | def PFAR_MC(dist_proc, dist_test, LL, UL, GBL=0, GBU=0, N=100000, testbias=0):
9 | ''' Probability of False Accept/Reject using Monte Carlo Method
10 |
11 | Args:
12 | dist_proc (stats.rv_frozen or distributions.Distribution):
13 | Distribution of possible unit under test values from process
14 | dist_test (stats.rv_frozen or distributions.Distribution):
15 | Distribution of possible test measurement values
16 | LL (float):Lower specification limit (absolute)
17 | UL (float): Upper specification limit (absolute)
18 | GBL (float): Lower guardband, as offset. Test limit is LL + GBL.
19 | GBU (float): Upper guardband, as offset. Test limit is UL - GBU.
20 | testbias (float): Bias (difference between distribution median and expected value)
21 | in test distribution
22 | approx (bool): Approximate using discrete probability distribution. This
23 | uses trapz integration so it may be faster than letting scipy integrate
24 | the actual pdf function.
25 | N (int): Number of Monte Carlo samples
26 |
27 | Returns:
28 | pfa: False accept probability, P(OOT and Accepted)
29 | pfr: False reject probability, P(IT and Rejected)
30 | proc_samples: Monte Carlo samples for uut
31 | test_samples: Monte Carlo samples for test measurement
32 | cpfa: Conditional False Accept Probability, P(OOT | Accepted)
33 | '''
34 | Result = namedtuple('MCRisk', ['pfa', 'pfr', 'process_samples', 'test_samples', 'cpfa'])
35 | proc_samples = dist_proc.rvs(size=N)
36 | expected = dist_test.median() - testbias
37 | kwds = distributions.get_distargs(dist_test)
38 | locorig = kwds.pop('loc', 0)
39 | try:
40 | # Works for normal stats distributions, but not rv_histograms
41 | test_samples = dist_test.dist.rvs(loc=proc_samples-(expected-locorig), size=N, **kwds)
42 | except TypeError:
43 | # Works for histograms, but not regular distributions...
44 | test_samples = dist_test.dist(**kwds).rvs(loc=proc_samples-(expected-locorig), size=N)
45 | except ValueError:
46 | # Invalid parameter in kwds
47 | test_samples = np.array([])
48 | return Result(np.nan, np.nan, None, None)
49 |
50 | # Unconditional False Accept
51 | FA = np.count_nonzero(((test_samples < UL-GBU) & (test_samples > LL+GBL)) &
52 | ((proc_samples > UL) | (proc_samples < LL))) / N
53 | FR = np.count_nonzero(((test_samples > UL-GBU) | (test_samples < LL+GBL)) &
54 | ((proc_samples < UL) & (proc_samples > LL))) / N
55 |
56 | # Conditional False Accept
57 | p_intol_and_accepted = np.count_nonzero(((test_samples < UL-GBU) & (test_samples > LL+GBL)) &
58 | ((proc_samples <= UL) & (proc_samples >= LL))) / N
59 | p_accepted = np.count_nonzero(((test_samples < UL-GBU) & (test_samples > LL+GBL))) / N
60 | try:
61 | cpfa = 1 - p_intol_and_accepted / p_accepted
62 | except ZeroDivisionError:
63 | cpfa = 0.
64 | return Result(FA, FR, proc_samples, test_samples, cpfa)
65 |
--------------------------------------------------------------------------------
/suncal/common/matrix.py:
--------------------------------------------------------------------------------
1 | ''' Matrix operations using Python lists so the operations may be done on Sympy
2 | expressions, not just numbers.
3 | '''
4 |
5 | # Note: sympy has Matrix object which would handle some of this, but
6 | # it can't subs() Pint quantities, so the eval functions are still needed.
7 |
8 | import sympy
9 |
10 |
11 | def matmul(a, b):
12 | ''' Matrix multiply. Manually looped to preserve units since Pint
13 | doesn't allow matrices with different units on each element.
14 |
15 | Args:
16 | a: List of list of sympy expressions
17 | b: List of list of sympy expressions
18 | '''
19 | result = []
20 | for i in range(len(a)):
21 | row = []
22 | for j in range(len(b[0])):
23 | product = 0
24 | for v in range(len(a[i])):
25 | if a[i][v] == 1: # Symbolic gets ugly when multiplying by 1
26 | product += b[v][j]
27 | elif b[v][j] == 1:
28 | product += a[i][v]
29 | else:
30 | product += a[i][v] * b[v][j]
31 | row.append(product)
32 | result.append(row)
33 | return result
34 |
35 |
36 | def diagonal(a):
37 | ''' Return diagonal of square matrix
38 |
39 | Args:
40 | a: list of list of sympy expressions
41 | '''
42 | if len(a) > 0 and a[0]:
43 | return [a[i][i] for i in range(len(a))]
44 | else:
45 | return []
46 |
47 |
48 | def transpose(a):
49 | ''' Transpose matrix (to preserve units)
50 |
51 | Args:
52 | a: list of list of sympy expressions
53 | '''
54 | return list(map(list, zip(*a)))
55 |
56 |
57 | def eval_matrix(U, values):
58 | ''' Evaluate matrix (list of lists) of sympy expressions U with values
59 |
60 | Args:
61 | U: list of list of sympy expressions
62 | values: dictionary of {name:value} to substitute
63 |
64 | Returns:
65 | list of list of floats
66 | '''
67 | U_eval = []
68 | for row in U:
69 | U_row = []
70 | for expr in row:
71 | df = sympy.lambdify(values.keys(), expr, 'numpy') # Can't subs() with pint Quantities
72 | U_row.append(df(**values))
73 | U_eval.append(U_row)
74 | return U_eval
75 |
76 |
77 | def eval_list(U, values):
78 | ''' Evaluate a list of sympy expressions U with values
79 |
80 | Args:
81 | U: list of sympy expressions
82 | values: dictionary of {name:value} to substitute
83 |
84 | Returns:
85 | list of floats
86 | '''
87 | U_eval = []
88 | for expr in U:
89 | df = sympy.lambdify(values.keys(), expr, 'numpy') # Can't subs() with pint Quantities
90 | U_eval.append(df(**values))
91 | return U_eval
92 |
93 |
94 | def eval_dict(U, values):
95 | ''' Evaluate a dictionary of sympy expressions U with values
96 |
97 | Args:
98 | U: dictionary of {name:sympy expressions}
99 | values: dictionary of {name:value} to substitute
100 |
101 | Returns:
102 | dictionary of {name:float}
103 | '''
104 | U_eval = {}
105 | for name, expr in U.items():
106 | df = sympy.lambdify(values.keys(), expr, 'numpy') # Can't subs() with pint Quantities
107 | U_eval[name] = df(**values)
108 | return U_eval
109 |
--------------------------------------------------------------------------------
/test/test_mqa.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | from suncal import Limit
4 | from suncal.mqa.mqa import MqaQuantity
5 | from suncal.mqa.measure import Typeb
6 | from suncal.project import ProjectMqa
7 |
8 |
9 | def test_cannonball():
10 | ''' Test the Cannon Ball example from Draft RP-19 '''
11 | ball = MqaQuantity()
12 |
13 | # Define the measurand and its tolerances
14 | ball.measurand.name = 'Canonball'
15 | ball.measurand.testpoint = 19.7
16 | ball.measurand.tolerance = Limit(19.51, 19.81)
17 | ball.measurand.degrade_limit = Limit(19.575, 19.808)
18 | ball.measurand.fail_limit = Limit(19.48, 19.827)
19 | ball.measurand.eopr_pct = 0.95
20 | ball.measurand.eopr_true = True # 95% is a True EOPR, not an Observed EOPR
21 |
22 | # Specify the M&TE used to measure the ball
23 | ball.measurement.mte.accuracy_plusminus = .004
24 | ball.measurement.mte.accuracy_eopr = .95
25 |
26 | # No guardbanding
27 | ball.guardband.method = 'none'
28 |
29 | # Add other uncertainties
30 | ball.measurement.typebs = [
31 | Typeb('normal', name='repeatability', nominal=19.7, std=.0648),
32 | Typeb('uniform', name='resolution', nominal=19.7, a=.0005),
33 | ]
34 |
35 | result = ball.calculate()
36 | assert np.isclose(result.risk.cpfa_true, .02, atol=.05) # Draft RP19 says 1.990%
37 | assert np.isclose(result.reliability.success, .956, atol=.001) # Draft RP19 does not complete the integral...
38 | assert np.isclose(result.uncertainty.stdev, .0648, atol=.0001)
39 |
40 |
41 | def test_solar():
42 | ''' Test Solar Experiment example from Draft RP-19 '''
43 | proj = ProjectMqa.from_configfile(r'test\ex_solarexperiment.yaml')
44 | result = proj.calculate()
45 |
46 | solar = result.quantities[0]
47 | lamp = solar.uncertainty.parent
48 | comp = lamp.uncertainty.parent
49 |
50 | # RP19 Table 5-8
51 | assert np.isclose(solar.eopr.true.pct, .99994, atol=.00001) # True EOP = 99.994%
52 | assert np.isclose(solar.reliability.aop.pct, .99996, atol=.00001) # True AOP = 99.99(7)%
53 | assert np.isclose(solar.risk.pfa_true, 0.0, atol=.00005) # PFA = 0.00%
54 | assert np.isclose(solar.risk.pfr_true, 0.0, atol=.00005) # PFR = 0.00%
55 |
56 | # RP19 Table 5-9
57 | assert np.isclose(lamp.eopr.true.pct, .9986, atol=.0001) # True EOP = 99.86%
58 | assert np.isclose(lamp.reliability.aop.pct, .9991, atol=.00005) # True AOP = 99.91%
59 | assert np.isclose(lamp.risk.pfa_true, 0.0004, atol=.00005) # PFA = 0.04%
60 | assert np.isclose(lamp.risk.pfr_true, 0.0017, atol=.00005) # PFR = 0.17%
61 |
62 | # RP19 Table 5-10
63 | assert np.isclose(comp.eopr.true.pct, .9527, atol=.0001) # True EOP = 95.27%
64 | assert np.isclose(comp.reliability.aop.pct, .9733, atol=.0004) # True AOP = 97.33%
65 | assert np.isclose(comp.reliability.bop.pct, .9943, atol=.0004) # True BOP = 99.43%
66 | assert np.isclose(comp.risk.pfa_true, 0.0057, atol=.00005) # PFA = 0.57%
67 | assert np.isclose(comp.risk.pfr_true, 0.0084, atol=.00005) # PFR = 0.84%
68 |
69 | total = solar.total_costs()
70 |
71 | # Table 5-12
72 | assert np.isclose(total.cal, 176637, atol=1) # RP19 = 176637
73 | assert np.isclose(total.adj, 10, atol=5) # RP19 = 10
74 | assert np.isclose(total.support, 178687, atol=1) # RP19 = 178687
75 | assert np.isclose(total.total, 178690, atol=15) # RP19 = 178690
76 | assert np.isclose(total.spare_cost, 29801, atol=5) # RP19 = 29801
77 |
--------------------------------------------------------------------------------
/suncal/gui/widgets/stack.py:
--------------------------------------------------------------------------------
1 | ''' Stack Widget with animation '''
2 |
3 | from PyQt6 import QtWidgets, QtCore
4 |
5 |
6 | class SlidingStackedWidget(QtWidgets.QStackedWidget):
7 | ''' Animated Stack Widget
8 | adapted from:
9 | https://github.com/ThePBone/SlidingStackedWidget
10 | '''
11 | def __init__(self, parent=None):
12 | super(SlidingStackedWidget, self).__init__(parent)
13 | self.m_direction = QtCore.Qt.Orientation.Horizontal
14 | self.m_speed = 500
15 | self.m_animationType = QtCore.QEasingCurve.Type.InCurve
16 | self.m_now = 0
17 | self.m_next = 0
18 | self.m_wrap = False
19 | self.m_pnow = QtCore.QPoint(0, 0)
20 | self.m_active = False
21 |
22 | def setDirection(self, direction):
23 | self.m_direction = direction
24 |
25 | def setSpeed(self, speed):
26 | self.m_speed = speed
27 |
28 | def setAnimation(self, animation_type):
29 | self.m_animationType = animation_type
30 |
31 | def setWrap(self, wrap):
32 | self.m_wrap = wrap
33 |
34 | def slideInLeft(self, idx):
35 | self.slideInWgt(self.widget(idx), 'left')
36 |
37 | def slideInRight(self, idx):
38 | self.slideInWgt(self.widget(idx), 'right')
39 |
40 | def slideInWgt(self, new_widget, direction='left'):
41 | if self.m_active:
42 | return
43 |
44 | self.m_active = True
45 |
46 | _now = self.currentIndex()
47 | _next = self.indexOf(new_widget)
48 |
49 | if _now == _next:
50 | self.m_active = False
51 | return
52 |
53 | offset_X, offset_Y = self.frameRect().width(), self.frameRect().height()
54 | self.widget(_next).setGeometry(self.frameRect())
55 |
56 | if not self.m_direction == QtCore.Qt.Orientation.Horizontal:
57 | if direction == 'left':
58 | offset_X, offset_Y = 0, -offset_Y
59 | else:
60 | offset_X = 0
61 | else:
62 | if direction == 'left':
63 | offset_X, offset_Y = -offset_X, 0
64 | else:
65 | offset_Y = 0
66 |
67 | page_next = self.widget(_next).pos()
68 | pnow = self.widget(_now).pos()
69 | self.m_pnow = pnow
70 |
71 | offset = QtCore.QPoint(offset_X, offset_Y)
72 | self.widget(_next).move(page_next - offset)
73 | self.widget(_next).show()
74 | self.widget(_next).raise_()
75 |
76 | anim_group = QtCore.QParallelAnimationGroup(self)
77 | anim_group.finished.connect(self.animationDoneSlot)
78 |
79 | for index, start, end in zip(
80 | (_now, _next), (pnow, page_next - offset), (pnow + offset, page_next)
81 | ):
82 | animation = QtCore.QPropertyAnimation(self.widget(index), b'pos')
83 | animation.setEasingCurve(self.m_animationType)
84 | animation.setDuration(self.m_speed)
85 | animation.setStartValue(start)
86 | animation.setEndValue(end)
87 | anim_group.addAnimation(animation)
88 |
89 | self.m_next = _next
90 | self.m_now = _now
91 | self.m_active = True
92 | anim_group.start(QtCore.QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
93 |
94 | @QtCore.pyqtSlot()
95 | def animationDoneSlot(self):
96 | self.setCurrentIndex(self.m_next)
97 | self.widget(self.m_now).hide()
98 | self.widget(self.m_now).move(self.m_pnow)
99 | self.m_active = False
100 |
--------------------------------------------------------------------------------
/suncal/common/style/latexchars.py:
--------------------------------------------------------------------------------
1 | r'''
2 | Custom error handler for str.encode() that converts special unicode characters
3 | into LaTeX codes that are pdflatex-compatible. Importing this module
4 | installs the error handler named 'latex'.
5 |
6 | Usage:
7 |
8 | '100 µΩ'.encode('ascii', 'latex')
9 | >>> '100 $\mu \Omega$'
10 |
11 | '''
12 |
13 | import codecs
14 |
15 | # Could add others, but this gets all the greek letters, sub/superscripts,
16 | # and the most common math symbols.
17 | textable = {
18 | 'Α': 'A', # Greek capital alpha - replace with A
19 | 'Β': 'B', # Greek capital beta...
20 | 'Γ': r'\Gamma',
21 | 'Δ': r'\Delta',
22 | 'Ε': 'E',
23 | 'Ζ': 'Z',
24 | 'Η': 'H',
25 | 'Ι': 'I',
26 | 'Κ': 'K',
27 | 'Θ': r'\Theta',
28 | 'Λ': r'\Lambda',
29 | 'Μ': 'M',
30 | 'Ν': 'N',
31 | 'Ξ': r'\Xi',
32 | 'Ο': 'O',
33 | 'Π': r'\Pi',
34 | 'Ρ': 'P',
35 | 'Σ': r'\Sigma',
36 | 'Τ': 'T',
37 | 'Υ': r'\Upsilon',
38 | 'Φ': r'\Phi',
39 | 'Χ': 'X',
40 | 'Ψ': r'\Psi',
41 | 'Ω': r'\Omega',
42 | 'α': r'\alpha',
43 | 'β': r'\beta',
44 | 'γ': r'\gamma',
45 | 'δ': r'\delta',
46 | 'ϵ': r'\epsilon',
47 | 'ε': r'\varepsilon',
48 | 'ζ': r'\zeta',
49 | 'η': r'\eta',
50 | 'θ': r'\theta',
51 | 'ϑ': r'\vartheta',
52 | 'ι': r'\iota',
53 | 'κ': r'\kappa',
54 | 'λ': r'\lambda',
55 | 'μ': r'\mu', # \u03BC - "Greek small letter mu"
56 | 'µ': r'\mu', # \uB5 - alt+m on mac "Micro Sign"
57 | 'ν': r'\nu',
58 | 'ξ': r'\xi',
59 | 'ο': r'\omicron',
60 | 'π': r'\pi',
61 | 'ϖ': r'\varpi',
62 | 'ρ': r'\rho',
63 | 'σ': r'\sigma',
64 | 'τ': r'\tau',
65 | 'υ': r'\upsilon',
66 | 'ϕ': r'\phi',
67 | 'φ': r'\varphi',
68 | 'χ': r'\chi',
69 | 'ψ': r'\psi',
70 | 'ω': r'\omega',
71 |
72 | '±': r'\pm',
73 | '∓': r'\mp',
74 | '×': r'\times',
75 | '÷': r'\div',
76 | '≠': r'\neq',
77 | '≤': r'\leq',
78 | '≥': r'\geq',
79 | '≪': r'\ll',
80 | '≫': r'\gg',
81 | '⊂': r'\subset',
82 | '⊆': r'\subseteq',
83 | '∑': r'\sum',
84 | '∏': r'\prod',
85 | '∐': r'\coprod',
86 | '∫': r'\int',
87 | '∬': r'\iint',
88 | '∭': r'\iiint',
89 | '∮': r'\oint',
90 | '∞': r'\infty',
91 | '∇': r'\nabla',
92 | 'ℜ': r'\Re',
93 | 'ℑ': r'\Im',
94 | '∠': r'\angle',
95 | '∡': r'\measuredangle',
96 | '℧': r'\mho',
97 | '∂': r'\partial',
98 | 'Å': r'\mathrm{\mathring{A}}',
99 | 'ℏ': r'\hbar',
100 | '°': r'^{\circ}',
101 |
102 | '⁰': r'^{0}',
103 | '¹': r'^{1}',
104 | '²': r'^{2}',
105 | '³': r'^{3}',
106 | '⁴': r'^{4}',
107 | '⁵': r'^{5}',
108 | '⁶': r'^{6}',
109 | '⁷': r'^{7}',
110 | '⁸': r'^{8}',
111 | '⁹': r'^{9}',
112 | '₀': r'_{0}',
113 | '₁': r'_{1}',
114 | '₂': r'_{2}',
115 | '₃': r'_{3}',
116 | '₄': r'_{4}',
117 | '₅': r'_{5}',
118 | '₆': r'_{6}',
119 | '₇': r'_{7}',
120 | '₈': r'_{8}',
121 | '₉': r'_{9}',
122 | }
123 |
124 |
125 | def texhandler(err):
126 | ''' Codec handler for replacing special characters with LaTeX
127 | math codes, wrapped in $..$ if necessary.
128 | '''
129 | tex = [textable.get(err.object[p], '?') for p in range(err.start, err.end)]
130 | hasdollar = err.object[:err.start].count('$') % 2 # odd number of $ signs, already in math mode
131 | if not hasdollar:
132 | return (f'${" ".join(tex)}$', err.end)
133 | return (f'{" ".join(tex)} ', err.end)
134 |
135 |
136 | codecs.register_error('latex', texhandler)
137 |
--------------------------------------------------------------------------------
/suncal/uncertainty/results/uncertainty.py:
--------------------------------------------------------------------------------
1 |
2 | from dataclasses import dataclass
3 |
4 | from ...common import reporter
5 | from ..report.uncertainty import ReportUncertainty
6 | from ..report.cplx import ReportComplex
7 | from .monte import McResults
8 | from .gum import GumResults
9 |
10 |
11 | @reporter.reporter(ReportUncertainty)
12 | @dataclass
13 | class UncertaintyResults:
14 | ''' Results of GUM and Monte Carlo uncertainty calculation
15 |
16 | Attributes:
17 | gum: Results of GUM calculation
18 | montecarlo: Results of Monte Carlo calculation
19 | report (Report): Generate formatted reports of the results
20 |
21 | Methods:
22 | units: Convert the units of uncertainty and expected
23 | '''
24 | gum: GumResults
25 | montecarlo: McResults
26 |
27 | @property
28 | def functionnames(self):
29 | ''' List of function names in model '''
30 | if self.gum is not None:
31 | return self.gum.functionnames
32 | if self.montecarlo is not None:
33 | return self.montecarlo.functionnames
34 | return None
35 |
36 | @property
37 | def variablenames(self):
38 | ''' List of input variable names in model '''
39 | if self.gum is not None:
40 | return self.gum.variablenames
41 | if self.montecarlo is not None:
42 | return self.montecarlo.variablenames
43 | return None
44 |
45 | @property
46 | def descriptions(self):
47 | ''' Dictionary of function descriptions '''
48 | if self.gum is not None and self.gum.descriptions is not None:
49 | return self.gum.descriptions
50 | if self.montecarlo is not None and self.montecarlo.descriptions is not None:
51 | return self.montecarlo.descriptions
52 |
53 | def units(self, **units):
54 | ''' Convert units of uncertainty results
55 |
56 | Args:
57 | **units: functionnames and unit string to convert each
58 | model function result to
59 | '''
60 | if self.gum is not None:
61 | self.gum.units(**units)
62 | if self.montecarlo is not None:
63 | self.montecarlo.units(**units)
64 | return self
65 |
66 | def getunits(self):
67 | ''' Get Pint units currently configured in result '''
68 | if self.gum is not None:
69 | return self.gum.getunits()
70 | if self.montecarlo is not None:
71 | return self.montecarlo.getunits()
72 | return {}
73 |
74 |
75 | @reporter.reporter(ReportComplex)
76 | class UncertaintyCplxResults:
77 | ''' Results of GUM and Monte Carlo uncertainty calculation
78 | with Complex numbers
79 |
80 | Attributes:
81 | gum: Results of GUM calculation
82 | montecarlo: Results of Monte Carlo calculation
83 | '''
84 | def __init__(self, gumresults, mcresults):
85 | self.gum = gumresults
86 | self.montecarlo = mcresults
87 | self.componentresults = UncertaintyResults(self.gum._gumresults, self.montecarlo._mcresults)
88 | self._degrees = False
89 |
90 | def units(self, **units):
91 | ''' Convert units of uncertainty results
92 |
93 | Args:
94 | **units: functionnames and unit string to convert each
95 | model function result to
96 | '''
97 | if self.gum is not None:
98 | self.gum.units(**units)
99 | if self.montecarlo is not None:
100 | self.montecarlo.units(**units)
101 | return self
102 |
103 | def degrees(self, degrees):
104 | self._degrees = degrees
105 | if self.gum is not None:
106 | self.gum.degrees(degrees)
107 | if self.montecarlo is not None:
108 | self.montecarlo.degrees(degrees)
109 | return self
110 |
--------------------------------------------------------------------------------
/suncal/project/proj_dataset.py:
--------------------------------------------------------------------------------
1 | ''' DataSet calculation project component '''
2 |
3 | import numpy as np
4 |
5 | from ..common.limit import Limit
6 | from .component import ProjectComponent
7 | from ..datasets.dataset_model import DataSet, DataSetSummary
8 |
9 |
10 | class ProjectDataSet(ProjectComponent):
11 | ''' DataSet project component '''
12 | def __init__(self, model=None, name='dataset'):
13 | super().__init__(name=name)
14 | if model is None:
15 | self.model = DataSet()
16 | else:
17 | self.model = model
18 |
19 | def issummary(self):
20 | ''' Is the data in DataSetSummary form? '''
21 | return isinstance(self.model, DataSetSummary)
22 |
23 | def setdata(self, data):
24 | self.model.data = data
25 | self._result = None
26 |
27 | def setcolnames(self, names):
28 | self.model.colnames = names
29 | self._result = None
30 |
31 | def clear(self):
32 | self._result = None
33 | self.model.clear()
34 |
35 | def calculate(self):
36 | ''' Run calculation '''
37 | self._result = self.model.calculate()
38 | return self._result
39 |
40 | def get_dists(self):
41 | ''' Get dictionary of distributions in this dataset '''
42 | d = {}
43 | colnames = self.model.colnames
44 |
45 | # Pooled stats returned as mean/std/df dictionary
46 | if len(colnames) > 1:
47 | # Get pooled statistics
48 | pstats = self.model.result.pooled
49 | stderr = self.model.result.uncertainty
50 | d['Stdev of Mean'] = {
51 | 'median': pstats.mean,
52 | 'std': stderr.stderr,
53 | 'df': stderr.stderr_degf}
54 | d['Repeatability Stdev'] = {
55 | 'median': pstats.mean,
56 | 'std': pstats.repeatability,
57 | 'df': pstats.repeat_degf}
58 | d['Reproducibility Stdev'] = {
59 | 'median': pstats.mean,
60 | 'std': pstats.reproducibility,
61 | 'df': pstats.reprod_degf}
62 |
63 | # Individual columns are returned as sampled data
64 | for col in colnames:
65 | d[f'Column {col}'] = {'samples': self.model.get_column(col)}
66 |
67 | return d
68 |
69 | def get_arrays(self):
70 | d = {'Group Means': self.model.to_array()}
71 | return d
72 |
73 | def get_config(self):
74 | ''' Get the dataset configuration dictionary '''
75 | d = {}
76 | d['mode'] = 'data'
77 | d['name'] = self.name
78 | d['colnames'] = self.model.colnames
79 | d['data'] = self.model.data.astype('float').tolist()
80 | d['desc'] = self.description
81 | d['tolerance'] = self.model.tolerance.config() if self.model.tolerance else None
82 |
83 | if isinstance(self.model, DataSetSummary):
84 | d['nmeas'] = self.model.nmeas.astype('float').tolist()
85 | d['means'] = self.model.means.astype('float').tolist()
86 | d['stds'] = self.model.stdevs.astype('float').tolist()
87 | d['summary'] = True
88 | return d
89 |
90 | def load_config(self, config):
91 | ''' Load config into this DataSet '''
92 | self.name = config.get('name', 'data')
93 | self.description = config.get('desc', '')
94 | colnames = config.get('colnames')
95 | if 'nmeas' in config:
96 | means = config.get('means')
97 | stds = config.get('stds')
98 | nmeas = config.get('nmeas')
99 | self.model = DataSetSummary(means, stds, nmeas, column_names=colnames)
100 | else:
101 | self.model = DataSet(np.array(config['data']), column_names=colnames)
102 | tolcfg = config.get('tolerance')
103 | if tolcfg:
104 | self.model.tolerance = Limit.from_config(tolcfg)
105 | self.calculate()
106 |
--------------------------------------------------------------------------------
/suncal/curvefit/fitparse.py:
--------------------------------------------------------------------------------
1 | ''' Parse a curve fit expression to ensure it has an x variable and
2 | fit coefficients
3 | '''
4 |
5 | from collections import namedtuple
6 | import numpy as np
7 | import sympy
8 |
9 | from ..common import uparser
10 |
11 |
12 | def parse_fit_expr(expr, predictorvar: str = 'x'):
13 | ''' Check expr string for a valid curvefit function including an x variable
14 | and at least one fit parameter.
15 |
16 | Returns:
17 | func (callable): Lambdified function of expr
18 | symexpr (sympy): Sympy expression of function
19 | argnames (list of strings): Names of arguments (except x) to function
20 | '''
21 | uparser.parse_math(expr) # Will raise if not valid expression
22 | symexpr = sympy.sympify(expr)
23 | argnames = sorted(str(s) for s in symexpr.free_symbols)
24 | if predictorvar not in argnames:
25 | raise ValueError(f'Expression must contain "{predictorvar}" variable.')
26 | argnames.remove(predictorvar)
27 | if len(argnames) == 0:
28 | raise ValueError('Expression must contain one or more parameters to fit.')
29 | # Make sure to specify 'numpy' so nans are returned instead of complex numbers
30 | func = sympy.lambdify([predictorvar] + argnames, symexpr, 'numpy')
31 | ParsedMath = namedtuple('ParsedMath', ['function', 'sympyexpr', 'argnames'])
32 | return ParsedMath(func, symexpr, argnames)
33 |
34 |
35 | def fit_callable(model: str, polyorder: int = 2, predictor_var='x'):
36 | ''' Get fit callable and sympy expression for the function '''
37 | if model == 'line':
38 | expr = sympy.sympify('a + b*x')
39 |
40 | def func(x, b, a):
41 | return a + b*x
42 |
43 | elif model == 'exp': # Full exponential
44 | expr = sympy.sympify('c + a * exp(x/b)')
45 |
46 | def func(x, a, b, c):
47 | return c + a * np.exp(x/b)
48 |
49 | elif model == 'decay': # Exponential decay to zero (no c parameter)
50 | expr = sympy.sympify('a * exp(-x/b)')
51 |
52 | def func(x, a, b):
53 | return a * np.exp(-x/b)
54 |
55 | elif model == 'decay2': # Exponential decay, using rate lambda rather than time constant tau
56 | expr = sympy.sympify('a * exp(-x*b)')
57 |
58 | def func(x, a, b):
59 | return a * np.exp(-x*b)
60 |
61 | elif model == 'log':
62 | expr = sympy.sympify('a + b * log(x-c)')
63 |
64 | def func(x, a, b, c):
65 | return a + b * np.log(x-c)
66 |
67 | elif model == 'logistic':
68 | expr = sympy.sympify('a / (1 + exp((x-c)/b)) + d')
69 |
70 | def func(x, a, b, c, d):
71 | return d + a / (1 + np.exp((x-c)/b))
72 |
73 | elif model == 'quad' or (model == 'poly' and polyorder == 2):
74 | expr = sympy.sympify('a + b*x + c*x**2')
75 |
76 | def func(x, a, b, c):
77 | return a + b*x + c*x*x
78 |
79 | elif model == 'cubic' or (model == 'poly' and polyorder == 3):
80 | expr = sympy.sympify('a + b*x + c*x**2 + d*x**3')
81 |
82 | def func(x, a, b, c, d):
83 | return a + b*x + c*x*x + d*x*x*x
84 |
85 | elif model == 'quartic' or (model == 'poly' and polyorder == 4):
86 | expr = sympy.sympify('a + b*x + c*x**2 + d*x**3 + f*x**4')
87 |
88 | def func(x, a, b, c, d, e):
89 | return a + b*x + c*x*x + d*x*x*x + e*x*x*x*x
90 |
91 | elif model == 'poly':
92 | def func(x, *p):
93 | return np.poly1d(p[::-1])(x) # Coeffs go in reverse order (...e, d, c, b, a)
94 |
95 | polyorder = int(polyorder)
96 | if polyorder < 1 or polyorder > 12:
97 | raise ValueError('Polynomial order out of range')
98 | varnames = [chr(ord('a')+i) for i in range(polyorder+1)]
99 | expr = sympy.sympify('+'.join(f'{v}*x**{i}' for i, v in enumerate(varnames)))
100 |
101 | else:
102 | # actual expression as string
103 | func, expr, _ = parse_fit_expr(model, predictor_var)
104 | return func, expr
105 |
--------------------------------------------------------------------------------
/suncal/risk/guardband_tur.py:
--------------------------------------------------------------------------------
1 | ''' TUR-based guardbands, returning guardband factor as multipler of
2 | the plus-minus tolerance
3 | '''
4 | import numpy as np
5 | from scipy import stats
6 | from scipy.optimize import fsolve
7 |
8 | from .risk import PFA_norm
9 |
10 |
11 | def dobbert(tur: float) -> float:
12 | ''' Calculate guardband factor using Dobbert's method, also known as
13 | "Method 6". Guarantees 2% PFA or less for any in-tolerance
14 | probability.
15 |
16 | GBF = 1 - (1.04 - exp(0.38 log(tur) - 0.54)) / TUR
17 |
18 | Args:
19 | tur: Test Uncertainty Ratio (Tolerance / Uncertainty)
20 | '''
21 | M = 1.04 - np.exp(0.38 * np.log(tur) - 0.54)
22 | return 1 - M / tur
23 |
24 |
25 | def rss(tur: float) -> float:
26 | ''' Calculate guardband factor using RSS method
27 |
28 | GBF = sqrt(1-1/tur**2)
29 |
30 | Args:
31 | tur: Test Uncertainty Ratio (Tolerance / Uncertainty)
32 | '''
33 | return np.sqrt(1-1/tur**2)
34 |
35 |
36 | def test95(tur: float) -> float:
37 | ''' Calculate guardband using 95% test uncertainty method
38 | (same as subtracting the 95% uncertainty from the tolerance)
39 |
40 | GBF = 1 - 1/TUR
41 |
42 | Args:
43 | tur: Test Uncertainty Ratio (Tolerance / Uncertainty)
44 | '''
45 | return 1-1/tur if tur <= 4 else 1
46 |
47 |
48 | def rp10(tur: float) -> float:
49 | ''' Calculate guardband using NCSLI RP-10 (similar to test95, but
50 | less conservative)
51 |
52 | GBF = 1.25 - 1/TUR
53 |
54 | Args:
55 | tur: Test Uncertainty Ratio (Tolerance / Uncertainty)
56 | '''
57 | return 1.25 - 1/tur if tur <= 4 else 1
58 |
59 |
60 | def four_to_1(tur: float, itp: float = 0.95) -> float:
61 | ''' Calculate guardband that results in the same PFA as if the TUR
62 | was 4:1.
63 |
64 | Args:
65 | tur: Test Uncertainty Ratio (Tolerance / Uncertainty)
66 | itp: In-tolerance probability (also called
67 | end-of-period-reliability)
68 | '''
69 | target = PFA_norm(itp, TUR=4)
70 | return pfa_target(tur, itp, target)
71 |
72 |
73 | def pfa_target(tur: float, itp: float = 0.95, pfa: float = 0.08) -> float:
74 | ''' Calculate guardband required to acheive the desired PFA
75 |
76 | Args:
77 | tur: Test Uncertainty Ratio (Tolerance / Uncertainty)
78 | itp: In-tolerance probability (also called
79 | end-of-period-reliability)
80 | pfa: Desired Probability of False Accept
81 | '''
82 | result, _, ier, msg = fsolve(lambda x: PFA_norm(itp, tur, GB=x)-pfa, x0=.8, full_output=True)
83 | if ier == 1:
84 | return result[0]
85 | return np.nan
86 |
87 |
88 | def mincost(tur: float, itp: float = 0.95, cc_over_cp: float = 10) -> float:
89 | ''' Calculate guardband using the Mincost method, minimizing the
90 | total expected cost due to all false decisions.
91 | Reference Easterling 1991.
92 |
93 | Args:
94 | tur: Test Uncertainty Ratio (Tolerance / Uncertainty)
95 | itp: In-tolerance probability (also called
96 | end-of-period-reliability)
97 | cc_over_cp: Ratio of cost of a false accept to cost of the part
98 | '''
99 | conf = 1 - (1 / (1 + cc_over_cp))
100 | sigtest = 1/tur/2
101 | sigprod = 1/stats.norm.ppf((1+itp)/2)
102 | k = stats.norm.ppf(conf) * np.sqrt(1 + sigtest**2/sigprod**2) - sigtest/sigprod**2
103 | return 1 - k * sigtest
104 |
105 |
106 | def minimax(tur: float, cc_over_cp: float = 10) -> float:
107 | ''' Calculate guardband by minimizing the maximum expected cost due
108 | to all false decisions
109 | Reference Easterling 1991.
110 |
111 | Args:
112 | tur: Test Uncertainty Ratio (Tolerance / Uncertainty)
113 | cc_over_cp: Ratio of cost of a false accept to cost of the part
114 | '''
115 | conf = 1 - (1 / (1 + cc_over_cp))
116 | k = stats.norm.ppf(conf)
117 | return k * (1/tur/2)
118 |
--------------------------------------------------------------------------------
/test/test_project.py:
--------------------------------------------------------------------------------
1 | ''' Test project class - saving/loading config file for multiple calculations '''
2 |
3 | from io import StringIO
4 | import numpy as np
5 |
6 | import suncal
7 | from suncal.common import distributions
8 | from suncal import project
9 | from suncal import sweep
10 | from suncal import reverse
11 | from suncal import curvefit
12 |
13 |
14 | def test_saveload_fname(tmpdir):
15 | # Set up a Project with all types of calculations. Save it to a file (both using file NAME
16 | # and file OBJECT, read back the results, and compare the output report.
17 | # Make sure to store seeds for everything that does MC.
18 |
19 | np.random.seed(588132535)
20 | # Set up several project components of the different types
21 | u = suncal.Model('f = m/(pi*r**2)')
22 | u.var('m').measure(2).typeb(std=.2)
23 | u.var('r').measure(1).typeb(std=.1)
24 | proj1 = project.ProjectUncert(u)
25 | proj1.seed = 44444
26 |
27 | u2 = suncal.Model('g = m * 5')
28 | u2.var('m').measure(5).typeb(std=.5)
29 | proj2 = project.ProjectUncert(u2)
30 | proj2.seed = 4444
31 |
32 | projrsk = project.ProjectRisk()
33 | projrsk.model.procdist = distributions.get_distribution('t', loc=.5, scale=1, df=9)
34 |
35 | swp = sweep.UncertSweep(u)
36 | swp.add_sweep_nom('m', values=[.5, 1, 1.5, 2])
37 | projswp = project.ProjectSweep(swp)
38 |
39 | x = np.linspace(-10, 10, num=21)
40 | y = x*2 + np.random.normal(loc=0, scale=.5, size=len(x))
41 | arr = curvefit.Array(x, y, uy=0.5)
42 | fit = curvefit.CurveFit(arr)
43 | projfit = project.ProjectCurveFit(fit)
44 | projfit.seed = 909090
45 |
46 | rev = reverse.ModelReverse('f = m/(pi*r**2)', targetnom=20, targetunc=.5, solvefor='m')
47 | rev.var('r').measure(5).typeb(std=.05)
48 | projrev = project.ProjectReverse(rev)
49 | projrev.seed = 5555
50 |
51 | revswp = sweep.UncertSweepReverse('f = m/(pi*r**2)', targetnom=20, targetunc=.5, solvefor='m')
52 | revswp.model.var('r').measure(5).typeb(std=.05)
53 | revswp.add_sweep_unc('r', values=[.01, .02, .03, .04], comp='Type B', param='std')
54 | projrevswp = project.ProjectReverseSweep(revswp)
55 |
56 | projexp = project.ProjectDistExplore()
57 | projexp.seed = 8888
58 | projexp.model.dists = {'a': distributions.get_distribution('normal', loc=3, scale=2),
59 | 'b': distributions.get_distribution('uniform', loc=0, scale=2),
60 | 'a+b': None}
61 | projexp.get_config()
62 |
63 | projdset = project.ProjectDataSet()
64 | projdset.setdata(np.random.normal(loc=10, scale=1, size=(5, 5)))
65 | projdset.setcolnames([1, 2, 3, 4, 5])
66 |
67 | proj = project.Project()
68 | proj.add_item(proj1)
69 | proj.add_item(proj2)
70 | proj.add_item(projrsk)
71 | proj.add_item(projswp)
72 | proj.add_item(projfit)
73 | proj.add_item(projrev)
74 | proj.add_item(projrevswp)
75 | proj.add_item(projexp)
76 | proj.add_item(projdset)
77 | reportorig = proj.calculate()
78 |
79 | # Save_config given file NAME
80 | outfile = tmpdir.join('projconfig.yaml')
81 | proj.save_config(outfile)
82 |
83 | # Save_config given file OBJECT
84 | outfileobj = StringIO()
85 | proj.save_config(outfileobj)
86 | outfileobj.seek(0)
87 |
88 | proj2 = project.Project.from_configfile(outfile)
89 | reportnew = proj2.calculate()
90 | assert str(reportorig) == str(reportnew)
91 |
92 | proj3 = project.Project.from_configfile(outfileobj)
93 | reportobj = proj3.calculate()
94 | assert str(reportorig) == str(reportobj)
95 | outfileobj.close()
96 |
97 |
98 | def test_projrem():
99 | # Test add/remove items from project
100 | u = suncal.Model('f = a*b')
101 | u2 = suncal.Model('g = c*d')
102 | p1 = project.ProjectUncert(u, name='function1')
103 | p2 = project.ProjectUncert(u2, name='function2')
104 |
105 | proj = project.Project()
106 | proj.add_item(p1)
107 | proj.add_item(p2)
108 | assert proj.get_names() == ['function1', 'function2']
109 | proj.rem_item(0)
110 | assert proj.get_names() == ['function2']
111 |
112 | proj = project.Project()
113 | proj.add_item(p1)
114 | proj.add_item(p2)
115 | proj.rem_item('function2')
116 | assert proj.get_names() == ['function1']
117 |
--------------------------------------------------------------------------------
/doc/Manual/biblio.bib:
--------------------------------------------------------------------------------
1 | %% This BibTeX bibliography file was created using BibDesk.
2 | %% http://bibdesk.sourceforge.net/
3 |
4 | %% Created for Delker, Collin J at 2020-08-19 11:51:24 -0600
5 |
6 |
7 | %% Saved with string encoding Unicode (UTF-8)
8 |
9 |
10 |
11 | @techreport{NASA8739,
12 | Date-Added = {2020-08-19 17:25:19 +0000},
13 | Date-Modified = {2020-08-19 17:51:24 +0000},
14 | Institution = {NASA},
15 | Title = {{NASA}-{HDBK}-8739.19-5. Establishment and Adjustment of Calibration Intervals},
16 | Year = {2010}}
17 |
18 | @techreport{RP1,
19 | Date-Added = {2020-08-19 17:23:24 +0000},
20 | Date-Modified = {2020-08-19 17:50:24 +0000},
21 | Institution = {NCSLI},
22 | Title = {Recommended Practice {RP-1}: Establishment and Adjustment of Calibration Intervals},
23 | Year = {2012}}
24 |
25 | @techreport{RP19,
26 | Date-Added = {2025-03-05 17:23:24 +0000},
27 | Date-Modified = {2025-03-05 17:50:24 +0000},
28 | Institution = {NCSLI},
29 | Title = {Recommended Practice {RP-19}: Measurement Quality Assurance End-to-End (Draft)},
30 | Year = {2025}}
31 |
32 | @inproceedings{deaver,
33 | Author = {Deaver, Dave and Everett, Washington},
34 | Booktitle = {Proc. 1993 {NCSL} {W}orkshop and {S}ymposium},
35 | Date-Added = {2018-09-25 22:51:33 +0000},
36 | Date-Modified = {2018-11-06 00:04:26 +0000},
37 | Title = {How to maintain your confidence},
38 | Year = {1993}}
39 |
40 | @article{mcmc,
41 | Author = {Elster, Clemens and Toman, Blaza},
42 | Date-Added = {2018-09-25 22:25:39 +0000},
43 | Date-Modified = {2018-11-06 00:01:42 +0000},
44 | Journal = {Metrologia},
45 | Number = {5},
46 | Pages = {233},
47 | Publisher = {IOP Publishing},
48 | Title = {Bayesian uncertainty analysis for a regression model versus application of {GUM} {S}upplement 1 to the least-squares estimate},
49 | Volume = {48},
50 | Year = {2011}}
51 |
52 | @article{wehr,
53 | Author = {Wehr, Richard and Saleska, Scott R},
54 | Date-Added = {2018-09-25 22:13:40 +0000},
55 | Date-Modified = {2018-09-25 23:19:42 +0000},
56 | Journal = {Biogeosciences},
57 | Title = {The long-solved problem of the best-fit straight line: application to isotopic mixing lines},
58 | Year = {2017}}
59 |
60 | @article{delker,
61 | Author = {Delker, Collin},
62 | Date-Added = {2025-03-05 22:13:40 +0000},
63 | Date-Modified = {2025-03-05 23:19:42 +0000},
64 | Journal = {NCSLI Workshop & Symposium},
65 | Title = {Evaluating uncertainty of waveform calculations},
66 | Year = {2025, Submitted}}
67 |
68 | @article{york,
69 | Author = {York, Derek and Evensen, Norman M and Mart{\i}nez, Margarita L{\'o}pez and De Basabe Delgado, Jon{\'a}s},
70 | Date-Added = {2018-09-25 22:13:16 +0000},
71 | Date-Modified = {2018-09-25 22:13:22 +0000},
72 | Journal = {American Journal of Physics},
73 | Number = {3},
74 | Pages = {367--375},
75 | Publisher = {AAPT},
76 | Title = {Unified equations for the slope, intercept, and standard errors of the best straight line},
77 | Volume = {72},
78 | Year = {2004}}
79 |
80 | @book{NUMERICALC,
81 | Author = {Press, William H and Teukolsky, Saul A and Vetterling, William T and Flannery, Brian P},
82 | Date-Added = {2018-09-25 22:12:17 +0000},
83 | Date-Modified = {2018-11-06 00:04:33 +0000},
84 | Publisher = {Cambridge University Press},
85 | Title = {Numerical recipes in {C}},
86 | Year = {1988}}
87 |
88 | @techreport{NPL10,
89 | Author = {Cox, Harris},
90 | Date-Added = {2018-09-25 19:46:37 +0000},
91 | Date-Modified = {2018-09-25 19:47:15 +0000},
92 | Institution = {National Physical Laboratory},
93 | Title = {Software specifications for uncertainty evaluation},
94 | Year = {2006}}
95 |
96 | @techreport{GUM,
97 | Author = {BIPM},
98 | Date-Added = {2018-09-25 19:38:40 +0000},
99 | Date-Modified = {2018-11-06 00:02:34 +0000},
100 | Institution = {Joint Committee for Guides in Metrology},
101 | Title = {Evaluation of {M}easurement {D}ata - {G}uide to the {E}xpression of {U}ncertainty in {M}easurement},
102 | Year = {2008}}
103 |
104 | @techreport{GUMS1,
105 | Author = {BIPM},
106 | Date-Added = {2018-09-25 19:38:13 +0000},
107 | Date-Modified = {2018-11-06 00:03:49 +0000},
108 | Institution = {Joint Committee for Guides in Metrology},
109 | Title = {Evaluation of {M}easurement {D}ata - {S}upplement 1 to the {G}uide to the {E}xpression of {U}ncertainty in {M}easurement, {P}ropagation of {D}istributions using a {M}onte {C}arlo {M}ethod},
110 | Year = {2008}}
111 |
--------------------------------------------------------------------------------
/suncal/gui/tool_ttable.py:
--------------------------------------------------------------------------------
1 | ''' Widget for calculating and comparing t distributions '''
2 |
3 | import numpy as np
4 | from scipy import stats
5 |
6 | from PyQt6 import QtWidgets, QtGui
7 | from matplotlib.figure import Figure
8 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
9 |
10 | from . import widgets
11 | from ..common import ttable
12 |
13 |
14 | class TTableDialog(QtWidgets.QDialog):
15 | ''' Dialog for calculating t-distribution. Input any two of k, confidence, and degf,
16 | and solve for the third.
17 | '''
18 | def __init__(self, parent=None):
19 | super().__init__(parent=parent)
20 | self.setWindowTitle('T-table')
21 | font = self.font()
22 | font.setPointSize(10)
23 | self.setFont(font)
24 | self.cmbSolveFor = QtWidgets.QComboBox()
25 | self.cmbSolveFor.addItems(['Coverage Factor', 'Confidence', 'Degrees of Freedom'])
26 | self.degf = widgets.LineEditLabelWidget('Degrees of Freedom', '30')
27 | self.degf.setValidator(QtGui.QIntValidator(1, 10000000))
28 | self.conf = widgets.LineEditLabelWidget('Confidence Percent', '95.45')
29 | self.conf.setValidator(QtGui.QDoubleValidator(0, 100, 6))
30 | self.k = widgets.LineEditLabelWidget('Coverage Factor', '2.00')
31 | self.k.setValidator(QtGui.QDoubleValidator(0, 1000, 6))
32 | self.k.setVisible(False)
33 | self.output = QtWidgets.QLabel()
34 | self.fig = Figure()
35 | self.canvas = FigureCanvas(self.fig)
36 | self.canvas.setStyleSheet("background-color:transparent;")
37 | self.canvas.setMaximumWidth(500)
38 | self.canvas.setMaximumHeight(500)
39 |
40 | hlayout = QtWidgets.QHBoxLayout()
41 | hlayout.addWidget(QtWidgets.QLabel('Solve for:'))
42 | hlayout.addWidget(self.cmbSolveFor)
43 | hlayout.addStretch()
44 | layout = QtWidgets.QVBoxLayout()
45 | layout.addLayout(hlayout)
46 | layout.addWidget(self.degf)
47 | layout.addWidget(self.conf)
48 | layout.addWidget(self.k)
49 | layout.addSpacing(5)
50 | layout.addStretch()
51 | layout.addWidget(self.output)
52 | layout.addWidget(self.canvas)
53 | self.setLayout(layout)
54 |
55 | self.calculate()
56 | self.degf.editingFinished.connect(self.calculate)
57 | self.conf.editingFinished.connect(self.calculate)
58 | self.k.editingFinished.connect(self.calculate)
59 | self.cmbSolveFor.currentIndexChanged.connect(self.change_solvefor)
60 |
61 | def calculate(self):
62 | ''' Run the calculation and plot '''
63 | if self.cmbSolveFor.currentText() == 'Coverage Factor':
64 | degf = float(self.degf.text())
65 | k = ttable.k_factor(float(self.conf.text())/100, float(self.degf.text()))
66 | self.output.setText(f'k: {k:.4f}')
67 | elif self.cmbSolveFor.currentText() == 'Confidence':
68 | degf = float(self.degf.text())
69 | k = float(self.k.text())
70 | conf = ttable.confidence(k, degf) * 100
71 | self.output.setText(f'Confidence: {conf:.3f}%')
72 | else:
73 | k = float(self.k.text())
74 | degf = ttable.degf(k, float(self.conf.text())/100)
75 | if degf > 1E6:
76 | self.output.setText('Degrees of Freedom: ∞')
77 | else:
78 | self.output.setText(f'Degrees of Freedom: {degf:.2f}')
79 |
80 | self.fig.clf()
81 | ax = self.fig.add_subplot(1, 1, 1)
82 | xx = np.linspace(-4, 4, num=200)
83 | ax.plot(xx, stats.norm.pdf(xx), label='Normal')
84 | ax.plot(xx, stats.t.pdf(xx, df=degf),
85 | label=f't (df={degf:.2f})' if degf < 1E6 else r't ($\infty$)')
86 | ax.axvline(k, ls=':', color='black')
87 | ax.axvline(-k, ls=':', color='black')
88 | ax.set_ylabel('Probability Density Function')
89 | ax.legend(loc='upper right')
90 | self.canvas.draw_idle()
91 |
92 | def change_solvefor(self):
93 | ''' Solve-for option changed. Show/hide inputs '''
94 | solvefor = self.cmbSolveFor.currentText()
95 | self.k.setVisible(solvefor != 'Coverage Factor')
96 | self.conf.setVisible(solvefor != 'Confidence')
97 | self.degf.setVisible(solvefor != 'Degrees of Freedom')
98 | self.calculate()
99 |
--------------------------------------------------------------------------------
/suncal/project/proj_sweep.py:
--------------------------------------------------------------------------------
1 | ''' Uncertainty sweep project component '''
2 |
3 | import numpy as np
4 |
5 | from .component import ProjectComponent
6 | from .proj_uncert import ProjectUncert
7 | from ..sweep import UncertSweep
8 | from ..uncertainty import Model
9 | from ..uncertainty.report.units import units_report
10 | from ..common import unitmgr
11 |
12 |
13 | class ProjectSweep(ProjectComponent):
14 | ''' Uncertainty project component '''
15 | def __init__(self, model=None, name='sweep'):
16 | super().__init__(name=name)
17 | if model is not None:
18 | self.model = model
19 | else:
20 | self.model = UncertSweep(Model('f=x'))
21 | self.model.model.var('x').typeb(dist='normal', unc=1, k=2, name='u(x)')
22 | self.nsamples = 1000000
23 | self.seed = None
24 | self.outunits = {}
25 |
26 | def calculate(self):
27 | ''' Run the calculation '''
28 | if self.seed:
29 | np.random.seed(self.seed)
30 | self._result = self.model.calculate(samples=self.nsamples)
31 | return self._result
32 |
33 | def units_report(self, **kwargs):
34 | ''' Create report showing units of all parameters '''
35 | return units_report(self.model, self.outunits)
36 |
37 | def get_arrays(self):
38 | d = {}
39 | for name in self.model.model.functionnames:
40 | if self._result is not None and self.result.gum is not None:
41 | d[f'{name} (GUM)'] = self.to_array(gum=True, funcname=name)
42 | if self._result is not None and self.result.montecarlo is not None:
43 | d[f'{name} (MC)'] = self.to_array(gum=False, funcname=name)
44 | return d
45 |
46 | def to_array(self, gum=True, funcname=None):
47 | ''' Return dictionary {x: y: uy:} of swept data and uncertainties
48 |
49 | Args:
50 | gum (bool): Use gum (True) or monte carlo (False) values
51 | funcidx (int): Index of function in calculator as y values
52 | '''
53 | xvals = unitmgr.strip_units(self.result.report._sweepvals[0]) # Only first x value
54 | if gum:
55 | yvals = unitmgr.strip_units(self.result.gum.expected()[funcname])
56 | uyvals = unitmgr.strip_units(self.result.gum.uncertainties()[funcname])
57 | else:
58 | yvals = unitmgr.strip_units(self.result.montecarlo.expected()[funcname])
59 | uyvals = unitmgr.strip_units(self.result.montecarlo.uncertainties()[funcname])
60 | return {'x': xvals, 'y': yvals, 'u(y)': uyvals}
61 |
62 | def load_config(self, config):
63 | ''' Load configuration into this project '''
64 | puncert = ProjectUncert()
65 | puncert.load_config(config)
66 | self.model.model = puncert.model
67 | self.name = puncert.name
68 | self.outunits = puncert.outunits
69 | self.nsamples = puncert.nsamples
70 | self.description = puncert.description
71 | self.seed = puncert.seed
72 | self.model.sweeplist = []
73 |
74 | sweeps = config.get('sweeps', [])
75 | for sweep in sweeps:
76 | var = sweep['var']
77 | comp = sweep.get('comp', None)
78 | param = sweep.get('param', None)
79 | values = sweep['values']
80 | if var == 'corr':
81 | self.model.add_sweep_corr(sweep.get('var1', None), sweep.get('var2', None), values)
82 | elif comp == 'nom':
83 | self.model.add_sweep_nom(var, values)
84 | elif param == 'df':
85 | self.model.add_sweep_df(var, values, comp)
86 | else:
87 | self.model.add_sweep_unc(var, values, comp, param)
88 |
89 | def get_config(self):
90 | ''' Get configuration dictionary '''
91 | puncert = ProjectUncert(self.model.model)
92 | puncert.outunits = self.outunits
93 | puncert.description = self.description
94 | puncert.seed = self.seed
95 | puncert.nsamples = self.nsamples
96 | d = puncert.get_config()
97 | d['name'] = self.name
98 | d['mode'] = 'sweep'
99 | sweeps = []
100 | for sweep in self.model.sweeplist:
101 | sweep['values'] = list(sweep.get('values', []))
102 | sweeps.append(sweep)
103 | d['sweeps'] = sweeps
104 | return d
105 |
--------------------------------------------------------------------------------