├── 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 | --------------------------------------------------------------------------------