diff --git a/experiments/audit_density_order.py b/experiments/audit_density_order.py index f4d6ecb..dac3d22 100644 --- a/experiments/audit_density_order.py +++ b/experiments/audit_density_order.py @@ -60,6 +60,13 @@ def numeric(row, omit=("control",)): return {k: float(v) for k, v in row.items() if k not in omit} +def enthalpy_observations(records, key): + values = [row[key] for row in records] + require(all(map(math.isfinite, values)), "Nonfinite saved enthalpy observation") + return {"samples": len(values), "minimum_enthalpy": min(values) if values else None, + "negative_enthalpy_samples": sum(value < 0 for value in values)} + + def poly_value(coefficients, x): value = coefficients[-1] for coefficient in reversed(coefficients[:-1]): @@ -305,11 +312,17 @@ def audit_measures(get, expected_controls, n, K): require(traces, "Missing surface samples") unique = {(r["face"], r["u"], r["v"]) for r in traces} require(len(unique) == len(traces) == 25 * len({r["face"] for r in traces}), "Surface coverage") + witnesses = [row for element in support for row in pieces[(control, degree, element)]] result[f"Q{degree}/{control}"] = { "summary": {str(q): totals[(control, degree, q)] for q in ORDERS}, + "summary_enthalpy_scope": "Integration nodes only; EOS availability flags do not cover boundary samples or density-minimum witnesses.", "surface": {"samples": len(traces), "minimum_density": min(r["rho"] for r in traces), "negative_samples": sum(r["rho"] < 0 for r in traces), - "resolved_negative_samples": sum(r["resolved_negative"] for r in traces)}, + "resolved_negative_samples": sum(r["resolved_negative"] for r in traces), + "enthalpy": {**enthalpy_observations(traces, "h"), + "scope": "Saved one-sided 5-by-5 face samples; counts are not volume or area fractions."}}, + "negative_interval_witness_enthalpy": {**enthalpy_observations(witnesses, "witness_h"), + "scope": "One saved density-minimum witness per exported negative interval on order30 tangential lines; this is not an enthalpy-extremum search."}, "negative_mass_q30_minus_q22": totals[(control, degree, 30)]["negative_mass"] - totals[(control, degree, 22)]["negative_mass"], "negative_volume_q30_minus_q22": totals[(control, degree, 30)]["negative_volume"] - totals[(control, degree, 22)]["negative_volume"]} return result, support, totals, {"negative_lines": len(lines), "fully_resolved_negative_lines": resolved_lines} @@ -605,6 +618,14 @@ def audit_case(campaign, name): class SelfTests(unittest.TestCase): + def test_separate_enthalpy_observations(self): + self.assertEqual(enthalpy_observations([], "h"), + {"samples": 0, "minimum_enthalpy": None, "negative_enthalpy_samples": 0}) + self.assertEqual(enthalpy_observations([{"h": -1e-18}, {"h": 0.}, {"h": 1.}], "h"), + {"samples": 3, "minimum_enthalpy": -1e-18, "negative_enthalpy_samples": 1}) + with self.assertRaises(AssertionError): + enthalpy_observations([{"witness_h": math.nan}], "witness_h") + def test_independent_roots(self): cases = ((["0", ".1875", "-1", "1", "0"], [(.25, .75)]), ([".025634765625", "-.34375", "1.34375", "-2", "1"], [(.125, .375), (.625, .875)]),