STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_model_param.py
Go to the documentation of this file.
1"""Provenance is mandatory, and survives a round trip.
2
3Phase 02's headline exit criterion is that *a parameter with no source is a
4validation error rather than a default*. That is what these tests hold. The rest
5check that the record survives serialisation intact, because provenance that is
6lost on the first save is provenance that does not exist.
7"""
8
9from __future__ import annotations
10
11import json
12
13import pytest
14
15from feedtwin.model import (
16 Param,
17 Provenance,
18 Uncertainty,
19 assumed_params,
20 provenance_summary,
21)
22from feedtwin.model.units import UnknownUnit
23
24
26 """The criterion, stated directly.
27
28 A missing source must not default to anything -- not to ESTIMATED, not to
29 DEFAULT. Defaulting it would mean every un-sourced number silently acquired
30 a plausible-looking provenance, which is worse than having none at all.
31 """
32 with pytest.raises(ValueError, match="missing required field"):
33 Param.from_dict({"value": 1.2, "unit": "Cv"}, where="valve.Cv")
34
35
37 with pytest.raises(ValueError, match="not a valid source"):
38 Param.from_dict(
39 {"value": 1.0, "unit": "m", "source": "vibes"}, where="pipe.bore"
40 )
41
42
44 """Constructed directly, a stringly-typed source is a type error."""
45 with pytest.raises(TypeError, match="must be a Provenance"):
46 Param(1.0, "m", "manufacturer") # type: ignore[arg-type]
47
48
50 with pytest.raises(UnknownUnit):
51 Param(1.0, "smoots", Provenance.ESTIMATED)
52
53
55 """Round-trip fidelity depends on keeping the authored form.
56
57 Converting on construction would mean a config written in psi came back as
58 a float with a trail of decimals -- a diff on every save, and a file that no
59 longer looks like what someone wrote.
60 """
61 p = Param(500.0, "psi", Provenance.MANUFACTURER, "MEOP")
62 assert p.value == 500.0
63 assert p.unit == "psi"
64 assert p.si == pytest.approx(3447378.6, rel=1e-6)
65 assert p.dimension == "pressure"
66
67
69 """A parameter survives the full save/load path unchanged."""
70 original = Param(
71 value=1.2,
72 unit="Cv",
73 source=Provenance.MANUFACTURER,
74 reference="SS-8BK-V51 datasheet rev C, table 2",
75 uncertainty=Uncertainty("relative", 0.10),
76 )
77 restored = Param.from_dict(json.loads(json.dumps(original.to_dict())))
78 assert restored == original
79
80
82 """A terse parameter stays terse when written back."""
83 data = Param(1.0, "m", Provenance.DEFAULT).to_dict()
84 assert set(data) == {"value", "unit", "source"}
85
86
88 relative = Uncertainty("relative", 0.10)
89 assert relative.bounds(100.0) == pytest.approx((90.0, 110.0))
90
91 absolute = Uncertainty("absolute", 5.0)
92 assert absolute.bounds(100.0) == pytest.approx((95.0, 105.0))
93
94 # Relative uncertainty on a negative value still widens the interval.
95 assert relative.bounds(-100.0) == pytest.approx((-110.0, -90.0))
96
97
98@pytest.mark.parametrize("kind", ["percent", "", "Relative"])
99def test_uncertainty_kind_is_checked(kind: str) -> None:
100 with pytest.raises(ValueError, match="relative|absolute"):
101 Uncertainty(kind, 0.1)
102
103
105 with pytest.raises(ValueError, match="non-negative"):
106 Uncertainty("relative", -0.1)
107
108
110 p = Param(100.0, "psi", Provenance.MEASURED, "PT-04", Uncertainty("relative", 0.01))
111 low, high = p.si_bounds() or (0.0, 0.0)
112 assert low == pytest.approx(p.si * 0.99)
113 assert high == pytest.approx(p.si * 1.01)
114 assert Param(1.0, "m", Provenance.DEFAULT).si_bounds() is None
115
116
118 assert Provenance.MEASURED.rank > Provenance.MANUFACTURER.rank
119 assert Provenance.MANUFACTURER.rank > Provenance.ESTIMATED.rank
120 assert Provenance.ESTIMATED.rank > Provenance.DEFAULT.rank
121
122
124 assert Provenance.ESTIMATED.is_assumed
125 assert Provenance.DEFAULT.is_assumed
126 assert not Provenance.MEASURED.is_assumed
127 assert not Provenance.MANUFACTURER.is_assumed
128
129
131 """The point of carrying provenance at all.
132
133 "Nine measured, four datasheet, two guesses" is a different claim from a
134 bare number, and the difference should never be invisible in a design
135 review.
136 """
137 params = {
138 "a": Param(1.0, "m", Provenance.MEASURED, "CF-2026-03"),
139 "b": Param(2.0, "m", Provenance.MANUFACTURER, "datasheet"),
140 "c": Param(3.0, "m", Provenance.ESTIMATED, "similar part"),
141 "d": Param(4.0, "m", Provenance.DEFAULT, "library default"),
142 }
143 assert provenance_summary(params) == {
144 "measured": 1,
145 "manufacturer": 1,
146 "estimated": 1,
147 "default": 1,
148 }
149 assert assumed_params(params) == ["c", "d"]
150
151
153 """A stable table, so a report does not gain and lose columns."""
154 assert set(provenance_summary({})) == {p.value for p in Provenance}
None test_a_parameter_with_no_source_is_rejected()
None test_value_is_stored_as_authored_and_converted_on_demand()
None test_an_unknown_unit_is_caught_at_construction()
None test_round_trip_through_json_is_exact()
None test_a_report_can_say_what_a_result_rests_on()
None test_provenance_is_ranked_so_measurements_win()
None test_summary_always_reports_every_kind()
None test_the_source_must_be_the_enum_not_a_string()
None test_a_source_must_be_one_of_the_declared_kinds()
None test_optional_fields_are_omitted_rather_than_nulled()
None test_si_bounds_are_in_canonical_units()
None test_uncertainty_cannot_be_negative()
None test_assumed_flags_the_values_nobody_established()
None test_uncertainty_kind_is_checked(str kind)