diff --git a/experiments/summarize_density_order.py b/experiments/summarize_density_order.py index 7ead56a..5172bee 100644 --- a/experiments/summarize_density_order.py +++ b/experiments/summarize_density_order.py @@ -151,6 +151,8 @@ def build_rows(audits): for key in ("normalized_unbordered_residual", "central_border", "normalized_border_action"): row["final_" + key] = finite(physical18[key], "final " + key, nonnegative=key != "central_border") line_search = case["line_search"] + row["line_search_counts"] = {key: line_search[key] for key in + ("total_trials", "counts_by_source", "counts_by_disposition", "source_disposition_counts")} for key in ("pressure_force_rejections", "residual_metric_rejections"): row[key] = finite(line_search[key], key, True) row["minimum_accepted_alpha"] = line_search["minimum_accepted_alpha"] @@ -178,6 +180,24 @@ def build_rows(audits): return frozen, coupled +def trial_stage_rows(coupled): + table = [] + for row in coupled: + counts = row["line_search_counts"] + total = 0 + for stage in counts["source_disposition_counts"]: + count = finite(stage["count"], "stage count", True) + require(count > 0 and int(count) == count, "Invalid integer stage count") + code = finite(stage["disposition_code"], "disposition code", True) + require(int(code) == code and code <= 5, "Invalid disposition code") + total += count + table.append({key: row[key] for key in ("case", "n", "J", "density_degree", "campaign")} | + {"source": stage["source"], "disposition": stage["disposition"], "disposition_code": code, + "rejected": code != 0, "count": count}) + require(total == counts["total_trials"], "Joint stage counts do not cover all recorded trials") + return table + + def comparisons(frozen, coupled): result = [] for kind, table in (("frozen", frozen), ("coupled", coupled)): @@ -315,6 +335,20 @@ def plot_figure(frozen, coupled, output): class Tests(unittest.TestCase): + def test_distinct_preparation_stage_counts(self): + row = {"case": "sample", "n": 3., "J": 0., "density_degree": 4, "campaign": "recorded", + "line_search_counts": {"total_trials": 4, "source_disposition_counts": [ + {"source": "barotropic closure", "disposition": "inadmissible_state", "disposition_code": 1, "count": 2}, + {"source": "pressure force", "disposition": "inadmissible_state", "disposition_code": 1, "count": 1}, + {"source": "", "disposition": "accepted", "disposition_code": 0, "count": 1}]}} + table = trial_stage_rows([row]) + self.assertEqual({r["source"]: r["count"] for r in table}, {"barotropic closure": 2, "pressure force": 1, "": 1}) + self.assertTrue(table[0]["rejected"]) + self.assertFalse(table[-1]["rejected"]) + row["line_search_counts"]["total_trials"] = 5 + with self.assertRaisesRegex(ValueError, "cover all recorded trials"): + trial_stage_rows([row]) + def test_cross_audit_non_density_layout(self): def case(density_size, gravity_size=23): return {"n": 1.5, "J": 0., "block_sizes": {"value/density": density_size, @@ -385,7 +419,13 @@ class Tests(unittest.TestCase): "final_residual": .01, "threshold": 1e-8, "accepted_steps": 1, "linear_iterations": 7, "iteration_seconds": 10., "iterations": [{"linear_seconds": 6.}]}, "observer_timings": {"observer_total": 200.}, "run": {"seconds": 215., "utc_minus_monotonic_seconds": .01}, - "line_search": {"pressure_force_rejections": 4, "residual_metric_rejections": 1, "minimum_accepted_alpha": 1/32}, + "line_search": {"pressure_force_rejections": 4, "residual_metric_rejections": 1, "minimum_accepted_alpha": 1/32, + "total_trials": 6, "counts_by_source": {"pressure force": 4, "residual metric": 1, "": 1}, + "counts_by_disposition": {"inadmissible_state": 4, "insufficient_decrease": 1, "accepted": 1}, + "source_disposition_counts": [ + {"source": "pressure force", "disposition": "inadmissible_state", "disposition_code": 1, "count": 4}, + {"source": "residual metric", "disposition": "insufficient_decrease", "disposition_code": 4, "count": 1}, + {"source": "", "disposition": "accepted", "disposition_code": 0, "count": 1}]}, "physical": {"14": {"metrics": metrics}, "18": {"metrics": metrics}}} audit = {"artifact_passed": True, "campaigns": {"/recorded/campaign": { "producer_commit": "test", "mesh_sha256": "test", "cases": {"sample": case}}}} @@ -405,6 +445,8 @@ class Tests(unittest.TestCase): self.assertEqual(row["pressure_force_rejections"], 4) self.assertEqual(row["residual_metric_rejections"], 1) self.assertEqual(row["minimum_accepted_alpha"], 1/32) + self.assertEqual(row["line_search_counts"], {key: case["line_search"][key] for key in + ("total_trials", "counts_by_source", "counts_by_disposition", "source_disposition_counts")}) def main(): @@ -426,6 +468,7 @@ def main(): frozen, coupled = build_rows(inputs) require(frozen or coupled, "No cases in input audits") relative = comparisons(frozen, coupled) + stages = trial_stage_rows(coupled) args.output.mkdir(parents=True, exist_ok=False) write_csv(args.output / "frozen_projection.csv", frozen, ["case", "n", "J", "density_degree", "density_dofs", "negative_mass_over_abs_mass", "negative_volume_fraction", "negative_classification", "negative_mass_over_uncertainty_scale", @@ -439,6 +482,8 @@ def main(): "h_energy_virial_q14", "h_force_virial_q14", "negative_mass_over_abs_mass", "negative_volume_fraction", "negative_classification", "eos_l2_q30", "eos_rms_q30"]) write_csv(args.output / "relative_to_Q2.csv", relative) + write_csv(args.output / "trial_stages.csv", stages, + ["case", "n", "J", "density_degree", "campaign", "source", "disposition", "disposition_code", "rejected", "count"]) if not args.tables_only: plot_figure(frozen, coupled, args.output) digest = lambda path: hashlib.sha256(path.read_bytes()).hexdigest() @@ -452,11 +497,12 @@ def main(): "Newton seconds sum reported nonlinear-iteration times, excluding observer and setup costs; total elapsed time is kept separately.", "Final unbordered residual, signed central border and normalized border action come from q18 physical diagnostics; bordered convergence does not imply that these vanish.", "Pressure-force and residual-metric rejection counts retain the recorded stage distinction; residual-metric counts include any nonfinite metric failures as well as insufficient decrease.", + "All source/disposition counts, including accepted trials, are preserved in trial_stages.csv and JSON; preparation-stage counts alone do not establish where a candidate would fail if processing continued.", "Ratios to Q2 compare recorded endpoints and do not imply equal convergence or error tolerances were attained.", "Undefined values are null/blank; zero is kept exactly. No logarithmic plot floor is inserted.", "Uncertain-only negative observations retain exact table values and appear as separate plot annotations; resolved trends do not connect through them. Colors are stable by (n,J).", "P(h) virial milestones do not establish pointwise density validity; density-based virials are retained separately."], - "frozen_projection": frozen, "coupled_outcomes": coupled, "relative_to_Q2": relative, + "frozen_projection": frozen, "coupled_outcomes": coupled, "relative_to_Q2": relative, "trial_stages": stages, "artifacts_sha256": {path.name: digest(path) for path in args.output.iterdir() if path.is_file()}} (args.output / "summary.json").write_text(json.dumps(summary, indent=2, allow_nan=False) + "\n") print("Wrote", args.output)