OIML / engine /test_nitp_b2.py
BEASTZZZ's picture
Upload test_nitp_b2.py
b9d0a50 verified
Raw History Blame Contribute Delete
5.84 kB
"""
Second known-answer anchor: NITP 6.1 to 6.4, Appendix B.2.
The first anchor (test_harness.py) is a published *approval* -- it fixes the
instrument's derived properties. This one is a published *worked example*: the
Australian National Measurement Institute's own National Instrument Test
Procedures print a complete substitution-load weighing test for a class 3
weighbridge, with every indication, every additional load and every computed
error shown. Reproducing that arithmetic exactly is the strongest correctness
argument available short of a real filled-in RRSL report.
NITP 6.1 to 6.4, First edition, second revision, January 2014
Appendix B.2, "Weighing Performance using Substitution Load Material"
Three things are anchored here:
1. The mpe at seven loads, including 10 t.
2. The error formula E = I + 0.5e - dL - L (NITP clause 5.4.2, step 8),
which is the same relation this project writes as P = I + 1/2 e - dL,
E = P - L. An independent regulator stating it identically is worth more
than our own derivation.
3. The substitution formula L_sub = I_sub + 0.5e - E (clause 5.4.2 A(e)).
Why the 10 t row is the important one
-------------------------------------
At e = 0.02 t, a load of 10 t is exactly 500 e. NMI prints its mpe as
+/- 0.01 t, which is 0.5 e -- so 500 e falls in the FIRST band. That is only
true if the second band opens at strictly greater than 500, exactly as
mpe-bands.json records it.
Anyone transcribing Table 6 from the Indian Kanoon rendering of the Seventh
Schedule gets overlapping bands and would print +/- 0.02 t here. This single
row is independent, third-party confirmation that the operators in
mpe-bands.json are right and that rendering is corrupt.
python3 engine/test_nitp_b2.py
"""
from nawi_engine import Instrument, mpe_for_load
T = 1_000_000.0 # grams in a tonne
E_T = 0.02 # verification scale interval, in tonnes
PASSED = []
FAILED = []
def check(label, got, want, tol=1e-9):
ok = abs(got - want) <= tol if isinstance(want, (int, float)) else got == want
(PASSED if ok else FAILED).append(label)
print(f" [{'ok ' if ok else 'FAIL'}] {label}: got {got}, want {want}")
def rule(title):
print("\n" + title)
print("-" * len(title))
weighbridge = Instrument(
manufacturer="(NITP 6.1 to 6.4, Appendix B.2)",
model="class 3 static weighbridge",
accuracy_class="III",
max_capacity_g=60 * T,
e_g=E_T * T,
d_g=E_T * T,
min_capacity_g=0.4 * T,
)
# ==========================================================================
rule("1. mpe at every load NMI prints in the Appendix B.2 report")
# ==========================================================================
# (load in tonnes, mpe in tonnes as printed by NMI)
for load_t, want_t in [
(0.40, 0.01), # 20 e -> 0.5 e
(10.00, 0.01), # 500 e -> 0.5 e <-- the band edge
(16.00, 0.02), # 800 e -> 1.0 e
(31.78, 0.02), # 1 589 e -> 1.0 e
(39.08, 0.02), # 1 954 e -> 1.0 e
(47.08, 0.03), # 2 354 e -> 1.5 e
(59.86, 0.03), # 2 993 e -> 1.5 e
]:
check(f"mpe at {load_t:g} t ({load_t / E_T:,.0f} e)",
mpe_for_load(weighbridge, load_t * T) / T, want_t)
# ==========================================================================
rule("2. The band edge, stated on its own because it is the whole argument")
# ==========================================================================
check("500 e lands in the first band, so mpe = 0.5 e",
mpe_for_load(weighbridge, 10 * T) / T, 0.5 * E_T)
check("just past it, 500.5 e, mpe steps to 1.0 e",
mpe_for_load(weighbridge, 10.01 * T) / T, 1.0 * E_T)
# ==========================================================================
rule("3. E = I + 0.5e - dL - L (NITP clause 5.4.2, step 8)")
# ==========================================================================
def error_t(indication_t, delta_load_t, load_t):
"""The project computes P = I + 1/2 e - dL, then E = P - L."""
return (indication_t + 0.5 * E_T - delta_load_t) - load_t
for L, I, dL, want in [
(16.00, 16.02, 0.012, +0.018),
(31.78, 31.80, 0.014, +0.016),
(39.08, 39.10, 0.016, +0.014),
(47.08, 47.10, 0.016, +0.014),
(59.86, 59.88, 0.018, +0.012),
]:
check(f"E at L = {L:g} t (I = {I:g}, dL = {dL:g})", error_t(I, dL, L), want)
# ==========================================================================
rule("4. L_sub = I_sub + 0.5e - E (NITP clause 5.4.2, method A(e))")
# ==========================================================================
for I_sub, E, want in [
(15.78, 0.018, 15.772), # Sub 1, test rig + forklift
(31.08, 0.016, 31.074), # Sub 2, gravel truck
(46.86, 0.014, 46.856), # Sub 3, Sub 1 + Sub 2
]:
check(f"L_sub from I_sub = {I_sub:g} t, E = {E:+g} t",
I_sub + 0.5 * E_T - E, want)
# ==========================================================================
rule("5. Every load in the report is inside its mpe, as NMI records")
# ==========================================================================
# NMI marks every weighing row "pass". The errors above must therefore all
# sit within the mpe for their load -- a end-to-end check of both tables.
for L, I, dL in [(16.00, 16.02, 0.012), (31.78, 31.80, 0.014),
(39.08, 39.10, 0.016), (47.08, 47.10, 0.016),
(59.86, 59.88, 0.018)]:
E = error_t(I, dL, L)
mpe = mpe_for_load(weighbridge, L * T) / T
check(f"|E| <= mpe at {L:g} t", abs(E) <= mpe + 1e-9, True)
# ==========================================================================
print("\n" + "=" * 62)
print(f" {len(PASSED)} passed, {len(FAILED)} failed")
for f in FAILED:
print(f" FAILED: {f}")
print("=" * 62)