Supersedes71221837. Samples every t* 0.5 (interval-mean forces), trailing t* 50..60 window (21 samples); range <= 10 % and |slope*window| <= 2 % of each official band width. Verdict on the window mean with the distance to the nearest band edge. Our engineering rule, frozen before any run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit869d60d78e)
80 lines
3.6 KiB
Python
80 lines
3.6 KiB
Python
"""CPU-only contract tests for run_dfg_re20. Not CUDA or DFG acceptance evidence."""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
import unittest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
|
import run_dfg_re20 as v # noqa: E402
|
|
import run_refined_dfg as rr # noqa: E402
|
|
|
|
h, _ = rr.helpers(rr.DEFAULT_SOURCE)
|
|
|
|
|
|
def rows(values, tstar_end=v.TSTAR_MAX):
|
|
n = int(round(tstar_end / v.SAMPLE_TSTAR))
|
|
return [dict(tstar=(i + 1) * v.SAMPLE_TSTAR, **values(i)) for i in range(n)]
|
|
|
|
|
|
class Re20Contract(unittest.TestCase):
|
|
def test_lattice_parameters_and_bounds(self):
|
|
root, uniform = v.lattice_parameters(40), v.lattice_parameters(80)
|
|
self.assertAlmostEqual(root["tau"], 0.56, places=12)
|
|
self.assertAlmostEqual(uniform["tau"], 0.62, places=12)
|
|
self.assertEqual((root["steps"], root["interval"]), (240000, 2000))
|
|
self.assertEqual((uniform["steps"], uniform["interval"]), (480000, 4000))
|
|
self.assertAlmostEqual(v.plan()["refined_fine_tau"], 0.62, places=12)
|
|
|
|
def test_config_only_changes_viscosity(self):
|
|
base = v._re100_config(h, 40, "SRT", "none")
|
|
cfg, body, p = v.config(h, 40, "SRT")
|
|
self.assertAlmostEqual(cfg["physics"]["viscosity"], 0.01 * 40 / 20, places=15)
|
|
self.assertEqual((p["re"], p["viscosity"]), (20.0, cfg["physics"]["viscosity"]))
|
|
base[0]["physics"]["viscosity"] = cfg["physics"]["viscosity"]
|
|
self.assertEqual(base[0], cfg)
|
|
self.assertEqual(base[1], body)
|
|
with self.assertRaisesRegex(ValueError, "no Jets"):
|
|
v.config(h, 40, "MRT", "zero")
|
|
|
|
def test_units(self):
|
|
s = v.sample([2.0, -0.5], 4, 40, 3.0)
|
|
den = rr.SPEED**2 * 40 / 2
|
|
self.assertAlmostEqual(s["cd"], 2.0 / 4 / den)
|
|
self.assertAlmostEqual(s["cl"], -0.5 / 4 / den)
|
|
self.assertAlmostEqual(s["p_diff"], 3.0 * 0.04)
|
|
self.assertEqual(s["dp_scaled"], 3.0)
|
|
|
|
def test_steady_and_verdict(self):
|
|
inside = dict(cd=5.58, cl=0.0106, p_diff=0.1174)
|
|
out = v.summarize(rows(lambda i: inside))
|
|
self.assertEqual((out["status"], out["steady"], out["window_samples"]),
|
|
("WITHIN_INTERVALS", True, 21))
|
|
self.assertTrue(out["cd_positive"])
|
|
self.assertAlmostEqual(out["edge_distance"]["cl"], 0.0002)
|
|
self.assertTrue(all(out["verdict"].values()))
|
|
low = v.summarize(rows(lambda i: dict(inside, cl=0.0100)))
|
|
self.assertEqual(low["status"], "OUTSIDE_INTERVALS")
|
|
self.assertFalse(low["verdict"]["cl"])
|
|
|
|
def test_slow_drift_gives_no_verdict(self):
|
|
# Range 2e-5 passes (limit 6e-5); slope * window 2e-5 exceeds 1.2e-5.
|
|
out = v.summarize(rows(lambda i: dict(cd=5.58, cl=0.0106 + 1e-6 * i, p_diff=0.1174)))
|
|
self.assertEqual((out["status"], out["verdict"]), ("NOT_STEADY", None))
|
|
self.assertLessEqual(out["ranges"]["cl"], out["range_limits"]["cl"])
|
|
self.assertGreater(out["drifts"]["cl"], out["slope_limits"]["cl"])
|
|
|
|
def test_noise_range(self):
|
|
quiet = v.summarize(rows(lambda i: dict(cd=5.58, cl=0.0106 + 2e-5 * (i % 2), p_diff=0.1174)))
|
|
self.assertTrue(quiet["steady"]) # within the range limit, no trend
|
|
noisy = v.summarize(rows(lambda i: dict(cd=5.58, cl=0.0106 + 1e-4 * (i % 2), p_diff=0.1174)))
|
|
self.assertEqual(noisy["status"], "NOT_STEADY")
|
|
self.assertGreater(noisy["ranges"]["cl"], noisy["range_limits"]["cl"])
|
|
|
|
def test_incomplete_run_gives_no_verdict(self):
|
|
out = v.summarize(rows(lambda i: dict(cd=5.58, cl=0.0106, p_diff=0.1174), tstar_end=30))
|
|
self.assertEqual((out["status"], out["verdict"]), ("INCOMPLETE", None))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|