Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Tests

on:
push:
branches: [ master ]

pull_request:
branches: [ master ]

workflow_dispatch:

concurrency:
group: ${{ github.workflow }}/${{ github.ref }}
cancel-in-progress: true

jobs:
pytest:
name: Pytest (${{ matrix.os }}, ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
python-version: [ '3.12', '3.13' ]

steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
# hatch-vcs derives the version from tags, so a shallow clone
# without them makes the build fail.
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install project and test dependencies
run: uv sync --group test --group examples

- name: Run test suite
# The maths and the theme are head-less; only the Qt widget tests
# would need a display, and there are none yet.
run: uv run pytest

- name: Rebuild the example workbook
run: uv run python examples/build_seal_workbook.py

- name: Run the worked example
run: uv run python examples/seal_tolerance.py
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,6 @@ __marimo__/
*.pdf
*.pptx


# Excel owner/lock files, created whenever a workbook is opened
~$*
96 changes: 96 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# vatic examples

Worked examples you can run, read, and check your own results against.

## Double-D seal gland tolerance stack-up

A PSA-backed double-D single-hole gland is specified by eight dimensions, each
with a symmetric tolerance. Following the convention used throughout the
original model, a `±t` tolerance is read as a **three sigma** bound, so each
dimension is modelled as `Normal(nominal, t / 3)`.

Two characteristics are checked against their requirements:

| Characteristic | Formula | Lower | Upper |
| --- | --- | --- | --- |
| Gland Fill % | `seal area / groove area` | 0.75 | 1.00 |
| Seal Comp. % | `1 - groove height / seal height` | 0.25 | 0.50 |

### Run it

```sh
python examples/seal_tolerance.py
```

This runs 10,000 Latin-hypercube trials through `vatic`'s own API — no
spreadsheet involved — and prints the mean, standard deviation, variance,
skewness, kurtosis, percentiles and the full process-capability family for
every characteristic.

### What the answer should look like

At nominal dimensions the model is deterministic, so the simulated means have
a closed form to check against:

| Characteristic | Nominal |
| --- | --- |
| Core hole area | 0.002969 in² |
| Seal area | 0.015400 in² |
| Groove area | 0.015210 in² |
| Gland Fill % | 1.0125 |
| Seal Comp. % | 0.27778 |

Note that **Gland Fill % overfills at nominal** — its mean sits above the 1.00
upper limit, so a negative `Cpk` and a defect rate in the hundreds of thousands
of PPM is the correct result, not a bug. Seal Comp. % comfortably passes. That
contrast is the point of the example: one characteristic that fails and one
that does not.

`tests/test_seal_example.py` asserts all of this.

## Rebuilding the workbook

`seal_tolerance.xlsx` holds the same model as a spreadsheet, with the input
dimensions in `C8:C15`, the requirements in `C19:D20`, and the characteristics
in `C24:C26` and `C30:C31`. Formulas are written against workbook names
(`sh`, `sw`, `d`, `cc`, `gh`, `gw`, `flat1`, `flat2`, `ca`, `sa`, `ga`), which
keeps them readable and is what a spreadsheet-driven run will bind to.

Regenerate it from source with:

```sh
uv pip install openpyxl
python examples/build_seal_workbook.py
```

## Provenance

The model, its dimensions and its formulas come from the worked example that
ships with the original **vatic** project by Abraham Lee
(<https://github.com/tisimst/vatic>), which drove the same calculation through
Microsoft Excel over COM.

That repository carries no licence, so its workbook file is not redistributed
here. `build_seal_workbook.py` rebuilds an equivalent workbook from the same
published dimensions and formulas instead, which also means the spreadsheet is
reproducible from source control rather than being an opaque binary.

## Differences from the original

The statistics deliberately follow the original conventions so results line up:
population variance, skewness as the standardised third moment with no
sample-size correction, and Pearson kurtosis where a normal distribution sits
at 3.0.

The capability metrics do **not** reproduce the original's arithmetic, which
had defects:

- it used the normal **density** where the **cumulative** distribution is
required, so every `p(N/C)`, `PPM`, `Zst` and `Zlt` value was wrong;
- it wrote the `Cpm`/`Ppm` exponent with `^`, which is bitwise XOR in Python,
so those two metrics could never be computed at all;
- it accepted a `zshift` argument and then ignored it, leaving `Zst` and `Zlt`
identical.

All three are fixed here, and `tests/test_analytics.py` pins each fix down
against an independent derivation.
176 changes: 176 additions & 0 deletions examples/build_seal_workbook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# -------------------------------------*- vatic -*----------------------------
# Open Source Risk Analysis
#
# Copyright (c) 2026, eggzec
# Contact: https://eggzec.github.io/
#
# License: GNU General Public License
# Version 3, 29 June 2007
#
# ----------------------------------------------------------------------------
#
# Author(s)
# Saud Zahir <m.saud.zahir@gmail.com>
#
# Date
# 7 May 2026
#
# Description
# Generate the Double-D seal gland workbook used by the spreadsheet
# examples and tests.
#
# ----------------------------------------------------------------------------
#
# The original vatic project by Abraham Lee
# (https://github.com/tisimst/vatic) ships an equivalent workbook, but that
# repository carries no licence, so its file is not redistributed here.
# This script rebuilds an equivalent model from the same published
# dimensions and formulas, which keeps the example self-contained and lets
# the workbook be regenerated or edited from source control.
#
# Requires openpyxl:
#
# uv pip install openpyxl
# python examples/build_seal_workbook.py
#
# ----------------------------------------------------------------------------

from __future__ import annotations

from pathlib import Path

from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.workbook.defined_name import DefinedName


OUTPUT = Path(__file__).resolve().parent / "seal_tolerance.xlsx"

SHEET = "Seal-Groove Design"

#: Row, label, nominal, tolerance, units and the workbook name bound to the
#: nominal cell so the formulas below stay readable.
INPUTS: tuple[tuple[int, str, float, float, str, str], ...] = (
(8, "Seal Height (mean)", 0.162, 0.005, "in", "sh"),
(9, "Seal Width", 0.118, 0.005, "in", "sw"),
(10, "Core Hole Diameter", 0.071, 0.005, "in", "d"),
(11, "Core Hole % Compression", 0.75, 0.10, "", "cc"),
(12, "Groove Height", 0.117, 0.002, "in", "gh"),
(13, "Groove Width", 0.130, 0.002, "in", "gw"),
(14, "Lid Flatness (GD&T)", 0.0, 0.005, "in", "flat1"),
(15, "Box Flatness (GD&T)", 0.0, 0.005, "in", "flat2"),
)

#: Row, label, formula, units and the name bound to the result cell.
INTERMEDIATES: tuple[tuple[int, str, str, str, str], ...] = (
(24, "Core Hole Area", "=cc*PI()*(d/2)^2", "in^2", "ca"),
(25, "Seal Area", "=PI()*(sw/2/2)^2+(sw*(sh-sw/4))-ca", "in^2", "sa"),
(26, "Groove Area", "=gw*(gh+flat1+flat2)", "in^2", "ga"),
)

#: Row, label, formula and the spec limits the characteristic is judged on.
OUTPUTS: tuple[tuple[int, str, str, float, float], ...] = (
(30, "Gland Fill %", "=sa/ga", 0.75, 1.00),
(31, "Seal Comp. %", "=1-gh/sh", 0.25, 0.50),
)

_INPUT_FILL = PatternFill("solid", fgColor="FFF2CC")
_HEADING = Font(bold=True, color="2323FF")
_BOLD = Font(bold=True)


def build() -> Path:
"""Write the workbook to :data:`OUTPUT`.

Returns:
The path the workbook was written to.
"""
book = Workbook()
sheet = book.active
sheet.title = SHEET

sheet["B2"] = "PSA Backed Double-D Single Hole Gland Calculator"
sheet["B2"].font = Font(bold=True, size=13, color="2323FF")
sheet["B3"] = "Model after Abraham Lee, IPPD (github.com/tisimst/vatic)"

sheet["B6"] = "INPUTS"
sheet["B6"].font = _HEADING
sheet["C6"] = "Nominal values in the shaded fields"
for column, title in (
("B", "Parameter"),
("C", "Nominal"),
("D", "Tolerance (+/-)"),
("E", "Units"),
):
cell = sheet[f"{column}7"]
cell.value = title
cell.font = _BOLD

for row, label, nominal, tolerance, units, name in INPUTS:
sheet[f"B{row}"] = label
sheet[f"C{row}"] = nominal
sheet[f"C{row}"].fill = _INPUT_FILL
sheet[f"D{row}"] = tolerance
sheet[f"E{row}"] = units
book.defined_names.add(
DefinedName(name, attr_text=f"'{SHEET}'!$C${row}")
)

sheet["B17"] = "REQUIREMENTS"
sheet["B17"].font = _HEADING
for column, title in (
("B", "Parameter"),
("C", "Lower Limit"),
("D", "Upper Limit"),
):
cell = sheet[f"{column}18"]
cell.value = title
cell.font = _BOLD

for offset, (row, _label, _formula, lower, upper) in enumerate(OUTPUTS):
spec_row = 19 + offset
sheet[f"B{spec_row}"] = f"={'B'}{row}"
sheet[f"C{spec_row}"] = lower
sheet[f"D{spec_row}"] = upper

sheet["B22"] = "INTERMEDIATE CALCULATIONS (REFERENCE)"
sheet["B22"].font = _HEADING
for column, title in (("B", "Parameter"), ("C", "Nominal"), ("D", "Units")):
cell = sheet[f"{column}23"]
cell.value = title
cell.font = _BOLD

for row, label, formula, units, name in INTERMEDIATES:
sheet[f"B{row}"] = label
sheet[f"C{row}"] = formula
sheet[f"D{row}"] = units
book.defined_names.add(
DefinedName(name, attr_text=f"'{SHEET}'!$C${row}")
)

sheet["B28"] = "OUTPUTS"
sheet["B28"].font = _HEADING
sheet["B29"] = "Parameter"
sheet["B29"].font = _BOLD
sheet["C29"] = "Nominal"
sheet["C29"].font = _BOLD

for row, label, formula, _lower, _upper in OUTPUTS:
sheet[f"B{row}"] = label
sheet[f"C{row}"] = formula

sheet.column_dimensions["B"].width = 30
for column in ("C", "D", "E"):
sheet.column_dimensions[column].width = 16
for row in sheet.iter_rows(min_row=1, max_row=32, max_col=5):
for cell in row:
if cell.column_letter in {"C", "D"}:
cell.alignment = Alignment(horizontal="right")

book.save(OUTPUT)
return OUTPUT


if __name__ == "__main__":
path = build()
print(f"wrote {path}")
Loading
Loading