moju 2.3.2__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. {moju-2.3.2 → moju-2.4.0}/PKG-INFO +27 -1
  2. {moju-2.3.2 → moju-2.4.0}/README.md +26 -0
  3. {moju-2.3.2 → moju-2.4.0}/moju/__init__.py +1 -1
  4. {moju-2.3.2 → moju-2.4.0}/moju/monitor/__init__.py +14 -0
  5. {moju-2.3.2 → moju-2.4.0}/moju/monitor/auditor.py +184 -12
  6. {moju-2.3.2 → moju-2.4.0}/moju/monitor/evaluate_batch.py +26 -1
  7. moju-2.4.0/moju/monitor/evaluation_coverage.py +338 -0
  8. {moju-2.3.2 → moju-2.4.0}/moju/monitor/monitor_log_export.py +61 -8
  9. {moju-2.3.2 → moju-2.4.0}/moju/monitor/report.py +12 -2
  10. {moju-2.3.2 → moju-2.4.0}/moju/monitor/tiers.py +30 -4
  11. {moju-2.3.2 → moju-2.4.0}/moju/monitor/visualize_components.py +16 -2
  12. {moju-2.3.2 → moju-2.4.0}/moju/monitor/visualize_plotly.py +22 -10
  13. {moju-2.3.2 → moju-2.4.0}/moju.egg-info/PKG-INFO +27 -1
  14. {moju-2.3.2 → moju-2.4.0}/moju.egg-info/SOURCES.txt +2 -0
  15. {moju-2.3.2 → moju-2.4.0}/pyproject.toml +1 -1
  16. moju-2.4.0/tests/test_evaluation_coverage.py +353 -0
  17. {moju-2.3.2 → moju-2.4.0}/tests/test_imports.py +6 -0
  18. {moju-2.3.2 → moju-2.4.0}/tests/test_monitor_log_export.py +45 -0
  19. {moju-2.3.2 → moju-2.4.0}/tests/test_report_schema.py +3 -1
  20. {moju-2.3.2 → moju-2.4.0}/LICENSE +0 -0
  21. {moju-2.3.2 → moju-2.4.0}/moju/monitor/analytic_flows.py +0 -0
  22. {moju-2.3.2 → moju-2.4.0}/moju/monitor/audit_meta.py +0 -0
  23. {moju-2.3.2 → moju-2.4.0}/moju/monitor/boundary_closures.py +0 -0
  24. {moju-2.3.2 → moju-2.4.0}/moju/monitor/closure_registry.py +0 -0
  25. {moju-2.3.2 → moju-2.4.0}/moju/monitor/config.py +0 -0
  26. {moju-2.3.2 → moju-2.4.0}/moju/monitor/constitutive_closure_summary.py +0 -0
  27. {moju-2.3.2 → moju-2.4.0}/moju/monitor/constitutive_closures.py +0 -0
  28. {moju-2.3.2 → moju-2.4.0}/moju/monitor/curvilinear.py +0 -0
  29. {moju-2.3.2 → moju-2.4.0}/moju/monitor/derivative_error.py +0 -0
  30. {moju-2.3.2 → moju-2.4.0}/moju/monitor/derivative_keys.py +0 -0
  31. {moju-2.3.2 → moju-2.4.0}/moju/monitor/derivative_metadata.py +0 -0
  32. {moju-2.3.2 → moju-2.4.0}/moju/monitor/derivative_provenance.py +0 -0
  33. {moju-2.3.2 → moju-2.4.0}/moju/monitor/derived_state_chain.py +0 -0
  34. {moju-2.3.2 → moju-2.4.0}/moju/monitor/eval_mask.py +0 -0
  35. {moju-2.3.2 → moju-2.4.0}/moju/monitor/fd_stencils.py +0 -0
  36. {moju-2.3.2 → moju-2.4.0}/moju/monitor/implied_scoring.py +0 -0
  37. {moju-2.3.2 → moju-2.4.0}/moju/monitor/key_declarations.py +0 -0
  38. {moju-2.3.2 → moju-2.4.0}/moju/monitor/law_fd_recipes.py +0 -0
  39. {moju-2.3.2 → moju-2.4.0}/moju/monitor/law_group_inference.py +0 -0
  40. {moju-2.3.2 → moju-2.4.0}/moju/monitor/law_implied_diagnostics.py +0 -0
  41. {moju-2.3.2 → moju-2.4.0}/moju/monitor/law_scale_recipes.py +0 -0
  42. {moju-2.3.2 → moju-2.4.0}/moju/monitor/model_derived_registry.py +0 -0
  43. {moju-2.3.2 → moju-2.4.0}/moju/monitor/nondim_inference.py +0 -0
  44. {moju-2.3.2 → moju-2.4.0}/moju/monitor/nondim_scales_parse.py +0 -0
  45. {moju-2.3.2 → moju-2.4.0}/moju/monitor/path_b_derivatives.py +0 -0
  46. {moju-2.3.2 → moju-2.4.0}/moju/monitor/path_b_spectral.py +0 -0
  47. {moju-2.3.2 → moju-2.4.0}/moju/monitor/regions.py +0 -0
  48. {moju-2.3.2 → moju-2.4.0}/moju/monitor/resolution_sensor.py +0 -0
  49. {moju-2.3.2 → moju-2.4.0}/moju/monitor/scaling_closures.py +0 -0
  50. {moju-2.3.2 → moju-2.4.0}/moju/monitor/spatial_rnorm_panels.py +0 -0
  51. {moju-2.3.2 → moju-2.4.0}/moju/monitor/state_builder.py +0 -0
  52. {moju-2.3.2 → moju-2.4.0}/moju/monitor/state_ref.py +0 -0
  53. {moju-2.3.2 → moju-2.4.0}/moju/monitor/term_breakdown.py +0 -0
  54. {moju-2.3.2 → moju-2.4.0}/moju/monitor/types.py +0 -0
  55. {moju-2.3.2 → moju-2.4.0}/moju/monitor/visualize_constitutive.py +0 -0
  56. {moju-2.3.2 → moju-2.4.0}/moju/monitor/visualize_export.py +0 -0
  57. {moju-2.3.2 → moju-2.4.0}/moju/monitor/visualize_labels.py +0 -0
  58. {moju-2.3.2 → moju-2.4.0}/moju/monitor/visualize_theme.py +0 -0
  59. {moju-2.3.2 → moju-2.4.0}/moju/monitor/weighted_norms.py +0 -0
  60. {moju-2.3.2 → moju-2.4.0}/moju/monitor/weighted_safeguard.py +0 -0
  61. {moju-2.3.2 → moju-2.4.0}/moju/piratio/__init__.py +0 -0
  62. {moju-2.3.2 → moju-2.4.0}/moju/piratio/groups.py +0 -0
  63. {moju-2.3.2 → moju-2.4.0}/moju/piratio/laws.py +0 -0
  64. {moju-2.3.2 → moju-2.4.0}/moju/piratio/models.py +0 -0
  65. {moju-2.3.2 → moju-2.4.0}/moju/piratio/nondim.py +0 -0
  66. {moju-2.3.2 → moju-2.4.0}/moju/piratio/operators.py +0 -0
  67. {moju-2.3.2 → moju-2.4.0}/moju/registry.py +0 -0
  68. {moju-2.3.2 → moju-2.4.0}/moju/torch/__init__.py +0 -0
  69. {moju-2.3.2 → moju-2.4.0}/moju/torch/_closure.py +0 -0
  70. {moju-2.3.2 → moju-2.4.0}/moju/torch/_derived.py +0 -0
  71. {moju-2.3.2 → moju-2.4.0}/moju/torch/_engine.py +0 -0
  72. {moju-2.3.2 → moju-2.4.0}/moju/torch/_fd_stencils.py +0 -0
  73. {moju-2.3.2 → moju-2.4.0}/moju/torch/_implied_diagnostics.py +0 -0
  74. {moju-2.3.2 → moju-2.4.0}/moju/torch/_nondim.py +0 -0
  75. {moju-2.3.2 → moju-2.4.0}/moju/torch/_path_b.py +0 -0
  76. {moju-2.3.2 → moju-2.4.0}/moju/torch/_r_eff.py +0 -0
  77. {moju-2.3.2 → moju-2.4.0}/moju/torch_interop.py +0 -0
  78. {moju-2.3.2 → moju-2.4.0}/moju.egg-info/dependency_links.txt +0 -0
  79. {moju-2.3.2 → moju-2.4.0}/moju.egg-info/requires.txt +0 -0
  80. {moju-2.3.2 → moju-2.4.0}/moju.egg-info/top_level.txt +0 -0
  81. {moju-2.3.2 → moju-2.4.0}/setup.cfg +0 -0
  82. {moju-2.3.2 → moju-2.4.0}/tests/test_analytic_ns_diagnostics.py +0 -0
  83. {moju-2.3.2 → moju-2.4.0}/tests/test_audit_meta.py +0 -0
  84. {moju-2.3.2 → moju-2.4.0}/tests/test_auditor.py +0 -0
  85. {moju-2.3.2 → moju-2.4.0}/tests/test_builtin_law_evaluate_batch.py +0 -0
  86. {moju-2.3.2 → moju-2.4.0}/tests/test_closure_debug_sidecar.py +0 -0
  87. {moju-2.3.2 → moju-2.4.0}/tests/test_compat_2_0_defaults.py +0 -0
  88. {moju-2.3.2 → moju-2.4.0}/tests/test_config_forms.py +0 -0
  89. {moju-2.3.2 → moju-2.4.0}/tests/test_constitutive_closure_summary.py +0 -0
  90. {moju-2.3.2 → moju-2.4.0}/tests/test_custom_law_hooks.py +0 -0
  91. {moju-2.3.2 → moju-2.4.0}/tests/test_derivative_provenance.py +0 -0
  92. {moju-2.3.2 → moju-2.4.0}/tests/test_derived_state_chain.py +0 -0
  93. {moju-2.3.2 → moju-2.4.0}/tests/test_evaluate.py +0 -0
  94. {moju-2.3.2 → moju-2.4.0}/tests/test_evaluate_optin_batched.py +0 -0
  95. {moju-2.3.2 → moju-2.4.0}/tests/test_evaluate_regions_batched.py +0 -0
  96. {moju-2.3.2 → moju-2.4.0}/tests/test_examples_implied_cookbooks.py +0 -0
  97. {moju-2.3.2 → moju-2.4.0}/tests/test_examples_path_b_fd_cookbooks.py +0 -0
  98. {moju-2.3.2 → moju-2.4.0}/tests/test_examples_turbulence_cookbooks.py +0 -0
  99. {moju-2.3.2 → moju-2.4.0}/tests/test_fd_stencils.py +0 -0
  100. {moju-2.3.2 → moju-2.4.0}/tests/test_fornberg_and_dual_spacing.py +0 -0
  101. {moju-2.3.2 → moju-2.4.0}/tests/test_groups.py +0 -0
  102. {moju-2.3.2 → moju-2.4.0}/tests/test_implied_robust_scoring.py +0 -0
  103. {moju-2.3.2 → moju-2.4.0}/tests/test_implied_viscosity_ls.py +0 -0
  104. {moju-2.3.2 → moju-2.4.0}/tests/test_jax_torch_fd_parity.py +0 -0
  105. {moju-2.3.2 → moju-2.4.0}/tests/test_law_fd_recipes.py +0 -0
  106. {moju-2.3.2 → moju-2.4.0}/tests/test_law_group_inference.py +0 -0
  107. {moju-2.3.2 → moju-2.4.0}/tests/test_law_implied_diagnostics.py +0 -0
  108. {moju-2.3.2 → moju-2.4.0}/tests/test_law_registry.py +0 -0
  109. {moju-2.3.2 → moju-2.4.0}/tests/test_law_scale_recipes.py +0 -0
  110. {moju-2.3.2 → moju-2.4.0}/tests/test_laws.py +0 -0
  111. {moju-2.3.2 → moju-2.4.0}/tests/test_minimal_input_flow.py +0 -0
  112. {moju-2.3.2 → moju-2.4.0}/tests/test_model_derived_registry.py +0 -0
  113. {moju-2.3.2 → moju-2.4.0}/tests/test_models.py +0 -0
  114. {moju-2.3.2 → moju-2.4.0}/tests/test_nondim.py +0 -0
  115. {moju-2.3.2 → moju-2.4.0}/tests/test_nondim_inference.py +0 -0
  116. {moju-2.3.2 → moju-2.4.0}/tests/test_not_identifiable_tier.py +0 -0
  117. {moju-2.3.2 → moju-2.4.0}/tests/test_notebooks_media.py +0 -0
  118. {moju-2.3.2 → moju-2.4.0}/tests/test_operators.py +0 -0
  119. {moju-2.3.2 → moju-2.4.0}/tests/test_paper_goldens.py +0 -0
  120. {moju-2.3.2 → moju-2.4.0}/tests/test_path_b_derivatives.py +0 -0
  121. {moju-2.3.2 → moju-2.4.0}/tests/test_path_b_spectral.py +0 -0
  122. {moju-2.3.2 → moju-2.4.0}/tests/test_phase_a2_derivative_layer.py +0 -0
  123. {moju-2.3.2 → moju-2.4.0}/tests/test_phase_a5_periodic.py +0 -0
  124. {moju-2.3.2 → moju-2.4.0}/tests/test_readme_examples.py +0 -0
  125. {moju-2.3.2 → moju-2.4.0}/tests/test_readme_pypi_links.py +0 -0
  126. {moju-2.3.2 → moju-2.4.0}/tests/test_region_aware_scoring.py +0 -0
  127. {moju-2.3.2 → moju-2.4.0}/tests/test_registry.py +0 -0
  128. {moju-2.3.2 → moju-2.4.0}/tests/test_safeguard_diagnostics.py +0 -0
  129. {moju-2.3.2 → moju-2.4.0}/tests/test_scoring_and_bounds.py +0 -0
  130. {moju-2.3.2 → moju-2.4.0}/tests/test_spatial_rnorm_panels.py +0 -0
  131. {moju-2.3.2 → moju-2.4.0}/tests/test_split_admissibility.py +0 -0
  132. {moju-2.3.2 → moju-2.4.0}/tests/test_state_ref.py +0 -0
  133. {moju-2.3.2 → moju-2.4.0}/tests/test_state_ref_hdf5.py +0 -0
  134. {moju-2.3.2 → moju-2.4.0}/tests/test_state_ref_meshio.py +0 -0
  135. {moju-2.3.2 → moju-2.4.0}/tests/test_studio_auto_config.py +0 -0
  136. {moju-2.3.2 → moju-2.4.0}/tests/test_studio_dependency_planner.py +0 -0
  137. {moju-2.3.2 → moju-2.4.0}/tests/test_studio_implied_groups.py +0 -0
  138. {moju-2.3.2 → moju-2.4.0}/tests/test_studio_io.py +0 -0
  139. {moju-2.3.2 → moju-2.4.0}/tests/test_studio_law_fd_hints.py +0 -0
  140. {moju-2.3.2 → moju-2.4.0}/tests/test_studio_model_derived_registry.py +0 -0
  141. {moju-2.3.2 → moju-2.4.0}/tests/test_studio_pi_gating.py +0 -0
  142. {moju-2.3.2 → moju-2.4.0}/tests/test_studio_plots.py +0 -0
  143. {moju-2.3.2 → moju-2.4.0}/tests/test_torch_engine.py +0 -0
  144. {moju-2.3.2 → moju-2.4.0}/tests/test_torch_interop.py +0 -0
  145. {moju-2.3.2 → moju-2.4.0}/tests/test_visualize_components.py +0 -0
  146. {moju-2.3.2 → moju-2.4.0}/tests/test_visualize_constitutive.py +0 -0
  147. {moju-2.3.2 → moju-2.4.0}/tests/test_visualize_labels.py +0 -0
  148. {moju-2.3.2 → moju-2.4.0}/tests/test_visualize_parity.py +0 -0
  149. {moju-2.3.2 → moju-2.4.0}/tests/test_visualize_theme.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moju
3
- Version: 2.3.2
3
+ Version: 2.4.0
4
4
  Summary: Physics admissibility auditing for ML surrogates and simulation data: scores how well predicted fields satisfy governing laws and constitutive relations, with implied-parameter checks and admissibility tiers, for JAX and PyTorch.
5
5
  Author-email: "Ifimo Lab (Ifimo Global Solutions LLC)" <lab@ifimoanalytics.com>
6
6
  Maintainer-email: "Ifimo Global Solutions LLC (dba Ifimo Lab)" <info@ifimoanalytics.com>
@@ -80,6 +80,32 @@ Moju is an open-source physics admissibility auditing library for scientific ML
80
80
 
81
81
  A tier is computed from an admissibility score in `[0, 1]`: High at `≥ 0.909`, Moderate at `≥ 0.667`, Low at `≥ 0.5`, and Non-Admissible below that (see `moju.monitor.auditor.admissibility_level`). A passing tier indicates consistency with the stated physics within the audited conditions: the laws, constitutive models, sample points, and scales you supplied. It is not a guarantee of correctness.
82
82
 
83
+ ### Evaluation coverage
84
+
85
+ Coverage counts residual elements, including trailing vector/tensor components.
86
+ An evaluation mask excludes all components at each excluded spatial point.
87
+ Audit-scope monitor-log exports preserve the non-finite policy recorded by
88
+ `audit`, including incomplete per-key verdicts and null scores in JSON series.
89
+
90
+ NaN-aware reductions may score on the finite subset of a residual. Every scored key and the aggregate now carry an evaluation-coverage record (`n_total`, `n_masked`, `n_selected`, `n_finite`, `n_nonfinite`, `coverage_status`). Default `nonfinite_policy="flag"` keeps scores and tiers unchanged; set `"strict"` to treat incomplete or empty coverage as tier **Incomplete** (not assessable — distinct from **Not identifiable**). Under `"flag"`, a NaN residual is dropped from the score while an infinite residual enters the RMS and drives the score to 0; both are reported as `coverage_status = "incomplete"`. `"strict"` treats NaN and inf identically (null score, Incomplete).
91
+
92
+ **Known limitation.** Coverage counts non-finite values in the residual array. A required input that is non-finite while the law's residual stays finite (for example, a law that applies `jnp.where` or `zeros_like` to that input) is currently reported as `coverage_status = "ok"`. Counting non-finite inputs per sample needs a mapping from input samples to residual samples that holds for scalars, parameters and broadcast shapes; this is planned for a follow-up release.
93
+
94
+ ```python
95
+ import jax.numpy as jnp
96
+ from moju.monitor import ResidualEngine, LawSpec, Scoring, audit
97
+
98
+ eng = ResidualEngine(
99
+ laws=[LawSpec(name="unit_law", fn=lambda r: r, state_map={"r": "r"},
100
+ scoring=Scoring("rms", dimensionless=True))],
101
+ law_implied_audits=False,
102
+ )
103
+ eng.compute_residuals({"r": jnp.array([0.0, float("nan")])}, run_mode="eval")
104
+ report = audit(eng.log) # default flag: High score, coverage_status="incomplete"
105
+ print(report["coverage_status"])
106
+ print(report["per_key"]["laws/unit_law"]["evaluation_coverage"])
107
+ ```
108
+
83
109
  ## What Moju Does
84
110
 
85
111
  - Builds residuals from composable `Laws`, `Groups`, and `Models`.
@@ -31,6 +31,32 @@ Moju is an open-source physics admissibility auditing library for scientific ML
31
31
 
32
32
  A tier is computed from an admissibility score in `[0, 1]`: High at `≥ 0.909`, Moderate at `≥ 0.667`, Low at `≥ 0.5`, and Non-Admissible below that (see `moju.monitor.auditor.admissibility_level`). A passing tier indicates consistency with the stated physics within the audited conditions: the laws, constitutive models, sample points, and scales you supplied. It is not a guarantee of correctness.
33
33
 
34
+ ### Evaluation coverage
35
+
36
+ Coverage counts residual elements, including trailing vector/tensor components.
37
+ An evaluation mask excludes all components at each excluded spatial point.
38
+ Audit-scope monitor-log exports preserve the non-finite policy recorded by
39
+ `audit`, including incomplete per-key verdicts and null scores in JSON series.
40
+
41
+ NaN-aware reductions may score on the finite subset of a residual. Every scored key and the aggregate now carry an evaluation-coverage record (`n_total`, `n_masked`, `n_selected`, `n_finite`, `n_nonfinite`, `coverage_status`). Default `nonfinite_policy="flag"` keeps scores and tiers unchanged; set `"strict"` to treat incomplete or empty coverage as tier **Incomplete** (not assessable — distinct from **Not identifiable**). Under `"flag"`, a NaN residual is dropped from the score while an infinite residual enters the RMS and drives the score to 0; both are reported as `coverage_status = "incomplete"`. `"strict"` treats NaN and inf identically (null score, Incomplete).
42
+
43
+ **Known limitation.** Coverage counts non-finite values in the residual array. A required input that is non-finite while the law's residual stays finite (for example, a law that applies `jnp.where` or `zeros_like` to that input) is currently reported as `coverage_status = "ok"`. Counting non-finite inputs per sample needs a mapping from input samples to residual samples that holds for scalars, parameters and broadcast shapes; this is planned for a follow-up release.
44
+
45
+ ```python
46
+ import jax.numpy as jnp
47
+ from moju.monitor import ResidualEngine, LawSpec, Scoring, audit
48
+
49
+ eng = ResidualEngine(
50
+ laws=[LawSpec(name="unit_law", fn=lambda r: r, state_map={"r": "r"},
51
+ scoring=Scoring("rms", dimensionless=True))],
52
+ law_implied_audits=False,
53
+ )
54
+ eng.compute_residuals({"r": jnp.array([0.0, float("nan")])}, run_mode="eval")
55
+ report = audit(eng.log) # default flag: High score, coverage_status="incomplete"
56
+ print(report["coverage_status"])
57
+ print(report["per_key"]["laws/unit_law"]["evaluation_coverage"])
58
+ ```
59
+
34
60
  ## What Moju Does
35
61
 
36
62
  - Builds residuals from composable `Laws`, `Groups`, and `Models`.
@@ -1,3 +1,3 @@
1
1
  """Moju: Physics-AI supervision for engineering-grade simulations."""
2
2
 
3
- __version__ = "2.3.2"
3
+ __version__ = "2.4.0"
@@ -45,6 +45,12 @@ from moju.monitor.implied_scoring import (
45
45
  resolve_implied_scoring_options,
46
46
  score_from_r_max,
47
47
  )
48
+ from moju.monitor.evaluation_coverage import (
49
+ DEFAULT_NONFINITE_POLICY,
50
+ TIER_INCOMPLETE,
51
+ coverage_from_residual,
52
+ validate_nonfinite_policy,
53
+ )
48
54
  from moju.monitor.regions import (
49
55
  DEFAULT_REGION_MIN_POINTS,
50
56
  normalize_regions,
@@ -101,9 +107,11 @@ from moju.monitor.visualize_labels import pretty_category_name, pretty_residual_
101
107
  from moju.monitor.tiers import (
102
108
  TIER_CUTOFFS,
103
109
  TIER_DEFINITION,
110
+ TIER_NON_SCORED,
104
111
  TIER_ORDER,
105
112
  tier_definition,
106
113
  tier_for_score,
114
+ tier_rank,
107
115
  )
108
116
  from moju.monitor.types import (
109
117
  REPORT_SCHEMA_VERSION,
@@ -154,6 +162,10 @@ __all__ = [
154
162
  "parse_implied_scoring_options",
155
163
  "resolve_implied_scoring_options",
156
164
  "score_from_r_max",
165
+ "DEFAULT_NONFINITE_POLICY",
166
+ "TIER_INCOMPLETE",
167
+ "coverage_from_residual",
168
+ "validate_nonfinite_policy",
157
169
  "DEFAULT_REGION_MIN_POINTS",
158
170
  "normalize_regions",
159
171
  "percentile_tail_adequacy",
@@ -176,9 +188,11 @@ __all__ = [
176
188
  "merge_implied_groups_first",
177
189
  "TIER_CUTOFFS",
178
190
  "TIER_DEFINITION",
191
+ "TIER_NON_SCORED",
179
192
  "TIER_ORDER",
180
193
  "tier_definition",
181
194
  "tier_for_score",
195
+ "tier_rank",
182
196
  "REPORT_SCHEMA_VERSION",
183
197
  "AuditReport",
184
198
  "BoundCheck",
@@ -381,7 +381,7 @@ def _r_eff_scalar(x: jnp.ndarray) -> jnp.ndarray:
381
381
  return rms_r * jnp.power(q, p)
382
382
 
383
383
 
384
- def admissibility_level(score: float) -> str:
384
+ def admissibility_level(score: Optional[float]) -> str:
385
385
  """
386
386
  Map admissibility score in ``[0, 1]`` to a qualitative label (``Unknown`` if non-finite).
387
387
 
@@ -389,14 +389,25 @@ def admissibility_level(score: float) -> str:
389
389
  ``scale_k`` (:data:`ADM_HIGH_THRESHOLD`, :data:`ADM_MODERATE_THRESHOLD`,
390
390
  :data:`ADM_LOW_THRESHOLD`). High requires ``score >= ADM_HIGH_THRESHOLD`` (~90.9% at ±0.1%
391
391
  fractional RMS); Moderate and Low use inclusive lower bounds at ~66.7% and 50%.
392
+
393
+ ``None`` maps to ``Incomplete`` (not assessable under ``nonfinite_policy=\"strict\"``;
394
+ distinct from ``Not identifiable``).
392
395
  """
393
- if not math.isfinite(score):
396
+ if score is None:
397
+ from moju.monitor.evaluation_coverage import TIER_INCOMPLETE
398
+
399
+ return TIER_INCOMPLETE
400
+ try:
401
+ s = float(score)
402
+ except (TypeError, ValueError):
394
403
  return "Unknown"
395
- if score >= ADM_HIGH_THRESHOLD:
404
+ if not math.isfinite(s):
405
+ return "Unknown"
406
+ if s >= ADM_HIGH_THRESHOLD:
396
407
  return "High Admissibility"
397
- if score >= ADM_MODERATE_THRESHOLD:
408
+ if s >= ADM_MODERATE_THRESHOLD:
398
409
  return "Moderate Admissibility"
399
- if score >= ADM_LOW_THRESHOLD:
410
+ if s >= ADM_LOW_THRESHOLD:
400
411
  return "Low Admissibility"
401
412
  return "Non-Admissible"
402
413
 
@@ -982,6 +993,103 @@ def _apply_implied_scoring_to_entry(
982
993
  entry["implied_scoring"] = implied_scoring
983
994
 
984
995
 
996
+ def _apply_evaluation_coverage_policy(
997
+ report: Dict[str, Any],
998
+ entry: Mapping[str, Any],
999
+ *,
1000
+ policy: str,
1001
+ last_residual_dict: Optional[Dict[str, Any]] = None,
1002
+ eval_mask_label: Optional[str] = None,
1003
+ ) -> None:
1004
+ """Attach coverage records and apply ``nonfinite_policy`` to per-key / overall."""
1005
+ from moju.monitor.evaluation_coverage import (
1006
+ TIER_INCOMPLETE,
1007
+ aggregate_coverage,
1008
+ apply_nonfinite_policy_to_key,
1009
+ coverage_map_from_flat,
1010
+ is_incomplete_key,
1011
+ validate_nonfinite_policy,
1012
+ )
1013
+
1014
+ policy = validate_nonfinite_policy(policy)
1015
+ cov_map: Dict[str, Any] = {}
1016
+ raw = entry.get("evaluation_coverage")
1017
+ if isinstance(raw, dict) and raw:
1018
+ cov_map = {str(k): dict(v) for k, v in raw.items() if isinstance(v, dict)}
1019
+ elif last_residual_dict is not None:
1020
+ flat: Dict[str, Any] = {}
1021
+ for cat, content in last_residual_dict.items():
1022
+ if cat == "closure_debug" or not isinstance(content, dict):
1023
+ continue
1024
+ for name, arr in content.items():
1025
+ flat[f"{cat}/{name}"] = arr
1026
+ cov_map = coverage_map_from_flat(
1027
+ flat,
1028
+ eval_mask_label=eval_mask_label
1029
+ or (entry.get("eval_mask") or {}).get("label"),
1030
+ mask_meta=entry.get("eval_mask") if isinstance(entry.get("eval_mask"), dict) else None,
1031
+ )
1032
+ per_key = report.get("per_key") or {}
1033
+ incomplete_keys: List[str] = []
1034
+ for k, row in per_key.items():
1035
+ if not isinstance(row, dict):
1036
+ continue
1037
+ cov = cov_map.get(k)
1038
+ apply_nonfinite_policy_to_key(row, cov, policy=policy)
1039
+ if is_incomplete_key(row):
1040
+ incomplete_keys.append(str(k))
1041
+ if cov_map:
1042
+ report["evaluation_coverage"] = {
1043
+ "per_key": cov_map,
1044
+ "aggregate": aggregate_coverage(cov_map),
1045
+ }
1046
+ report["coverage_status"] = report["evaluation_coverage"]["aggregate"]["coverage_status"]
1047
+ report["nonfinite_policy"] = policy
1048
+ if policy == "strict" and incomplete_keys:
1049
+ report["overall_admissibility_score"] = None
1050
+ report["overall_admissibility_level"] = TIER_INCOMPLETE
1051
+ report["incomplete_keys"] = incomplete_keys
1052
+ # Drop incomplete keys from category numeric rollup already reflected via null scores;
1053
+ # recompute categories excluding Incomplete / Not identifiable.
1054
+ cats: Dict[str, float] = {}
1055
+ buckets: Dict[str, List[float]] = {
1056
+ "laws": [],
1057
+ "constitutive": [],
1058
+ "scaling": [],
1059
+ "data": [],
1060
+ }
1061
+ for k, row in per_key.items():
1062
+ if not isinstance(row, dict):
1063
+ continue
1064
+ if is_incomplete_key(row):
1065
+ continue
1066
+ if (
1067
+ row.get("admissibility_level") == "Not identifiable"
1068
+ or row.get("identifiability_status") == "not_identifiable"
1069
+ ):
1070
+ continue
1071
+ if "/" not in k:
1072
+ continue
1073
+ prefix = k.split("/", 1)[0]
1074
+ if prefix not in buckets:
1075
+ continue
1076
+ try:
1077
+ vf = float(row.get("admissibility_score"))
1078
+ except (TypeError, ValueError):
1079
+ continue
1080
+ if not math.isfinite(vf):
1081
+ continue
1082
+ buckets[prefix].append(vf)
1083
+ for cat, vals in buckets.items():
1084
+ if not vals:
1085
+ continue
1086
+ if cat == "constitutive":
1087
+ cats[cat] = _min_admissibility(vals)
1088
+ else:
1089
+ cats[cat] = _geom_mean_admissibility(vals)
1090
+ report["per_category"] = cats
1091
+
1092
+
985
1093
  def audit(
986
1094
  log: List[Dict[str, Any]],
987
1095
  r_ref: Optional[Dict[str, float]] = None,
@@ -999,6 +1107,8 @@ def audit(
999
1107
  regions: Any = None,
1000
1108
  region_names: Optional[Mapping[Any, str]] = None,
1001
1109
  region_min_points: Optional[int] = None,
1110
+ nonfinite_policy: Optional[str] = None,
1111
+ eval_mask_label: Optional[str] = None,
1002
1112
  ) -> Dict[str, Any]:
1003
1113
  """
1004
1114
  Physics admissibility from logged **R_eff** (field ``rms``) and scales.
@@ -1226,6 +1336,27 @@ def audit(
1226
1336
 
1227
1337
  report["audit_meta"] = audit_meta(log, r_ref=r_ref)
1228
1338
 
1339
+ _apply_evaluation_coverage_policy(
1340
+ report,
1341
+ log[-1],
1342
+ policy=nonfinite_policy,
1343
+ last_residual_dict=last_residual_dict,
1344
+ eval_mask_label=eval_mask_label,
1345
+ )
1346
+ # Keep log entry aligned with the returned report (strict may null scores).
1347
+ if log:
1348
+ log[-1]["overall_admissibility_score"] = report.get("overall_admissibility_score")
1349
+ log[-1]["overall_admissibility_level"] = report.get("overall_admissibility_level")
1350
+ if report.get("coverage_status") is not None:
1351
+ log[-1]["coverage_status"] = report.get("coverage_status")
1352
+ if report.get("nonfinite_policy") is not None:
1353
+ log[-1]["nonfinite_policy"] = report.get("nonfinite_policy")
1354
+ adm_map = log[-1].get("admissibility_score")
1355
+ if isinstance(adm_map, dict):
1356
+ for k, row in (report.get("per_key") or {}).items():
1357
+ if isinstance(row, dict) and row.get("admissibility_level") == "Incomplete":
1358
+ adm_map[k] = None
1359
+
1229
1360
  if export_dir:
1230
1361
  import zipfile
1231
1362
  from pathlib import Path
@@ -1424,11 +1555,12 @@ def _worst_keys_table_rows(
1424
1555
  continue
1425
1556
  level = str(rep.get("admissibility_level") or "")
1426
1557
  is_ni = level == "Not identifiable" or rep.get("identifiability_status") == "not_identifiable"
1558
+ is_incomplete = level == "Incomplete" or rep.get("coverage_incomplete") is True
1427
1559
  try:
1428
1560
  rn_f = float(rep.get("r_norm"))
1429
1561
  except (TypeError, ValueError):
1430
1562
  rn_f = float("nan")
1431
- if not is_ni and not math.isfinite(rn_f):
1563
+ if not is_ni and not is_incomplete and not math.isfinite(rn_f):
1432
1564
  continue
1433
1565
  try:
1434
1566
  r_eff = float(rep.get("rms"))
@@ -1445,7 +1577,8 @@ def _worst_keys_table_rows(
1445
1577
  else:
1446
1578
  sk_f = float("nan")
1447
1579
  try:
1448
- adm_f = float(rep.get("admissibility_score"))
1580
+ adm_raw = rep.get("admissibility_score")
1581
+ adm_f = float(adm_raw) if adm_raw is not None else float("nan")
1449
1582
  except (TypeError, ValueError):
1450
1583
  adm_f = float("nan")
1451
1584
  prefix = k.split("/", 1)[0] if "/" in str(k) else "other"
@@ -1455,9 +1588,9 @@ def _worst_keys_table_rows(
1455
1588
  "r_eff": r_eff,
1456
1589
  "scale_k": sk_f,
1457
1590
  "r_norm": rn_f,
1458
- "admissibility": adm_f,
1459
- "admissibility_score": adm_f,
1460
- "admissibility_level": level or admissibility_level(adm_f),
1591
+ "admissibility": adm_f if math.isfinite(adm_f) else None,
1592
+ "admissibility_score": adm_f if math.isfinite(adm_f) else None,
1593
+ "admissibility_level": level or admissibility_level(adm_f if math.isfinite(adm_f) else None),
1461
1594
  "category": prefix,
1462
1595
  }
1463
1596
  if rep.get("implied_metric") is not None:
@@ -1477,6 +1610,11 @@ def _worst_keys_table_rows(
1477
1610
  if rep.get("identifiability_reason"):
1478
1611
  row["identifiability_reason"] = rep.get("identifiability_reason")
1479
1612
  not_ident_rows.append(row)
1613
+ elif is_incomplete:
1614
+ row["admissibility_level"] = "Incomplete"
1615
+ row["admissibility"] = None
1616
+ row["admissibility_score"] = None
1617
+ rows.append(row)
1480
1618
  else:
1481
1619
  rows.append(row)
1482
1620
  rows.sort(key=lambda r: r["r_norm"], reverse=True)
@@ -1609,7 +1747,14 @@ def _build_visualize_bundle(
1609
1747
  "bar_display": bar_display,
1610
1748
  "bar_values": bar_values_arr,
1611
1749
  "bar_values_eff": bar_values_eff_arr,
1612
- "overall_adm": [float(metrics[i]["overall_admissibility_score"]) for i in range(n)],
1750
+ "overall_adm": [
1751
+ (
1752
+ float(metrics[i]["overall_admissibility_score"])
1753
+ if metrics[i].get("overall_admissibility_score") is not None
1754
+ else float("nan")
1755
+ )
1756
+ for i in range(n)
1757
+ ],
1613
1758
  "cats_fin": cats_fin,
1614
1759
  "cat_colors": cat_colors,
1615
1760
  "spatial": spatial_parsed,
@@ -2560,6 +2705,7 @@ class ResidualEngine:
2560
2705
  nondim_scales: Any = None,
2561
2706
  derivative_metadata: Optional[Dict[str, Any]] = None,
2562
2707
  eval_mask: Any = None,
2708
+ eval_mask_label: Optional[str] = None,
2563
2709
  state_pred_alt: Optional[Dict[str, Any]] = None,
2564
2710
  estimate_derivative_error: Optional[bool] = None,
2565
2711
  derivative_error_threshold: Optional[float] = None,
@@ -2610,6 +2756,8 @@ class ResidualEngine:
2610
2756
  - ``derivative_metadata``: optional per-key metadata for consistency warnings.
2611
2757
  - ``eval_mask``: boolean array (True = keep). When set, RMS/max for this call use
2612
2758
  masked points only.
2759
+ - ``eval_mask_label``: optional string recorded on evaluation-coverage records for
2760
+ this mask (provenance only; does not change scores).
2613
2761
  - ``state_pred_alt`` / ``estimate_derivative_error``: opt-in derivative-error estimates.
2614
2762
  - ``point_weights`` / ``weighted_norms`` (Moju 2.0 default ``True``): area-weighted
2615
2763
  R_eff for scoring, with a percentile safeguard (see ``weighted_safeguard``). Use
@@ -2641,6 +2789,7 @@ class ResidualEngine:
2641
2789
  nondim_scales=nondim_scales,
2642
2790
  derivative_metadata=derivative_metadata,
2643
2791
  eval_mask=eval_mask,
2792
+ eval_mask_label=eval_mask_label,
2644
2793
  state_pred_alt=state_pred_alt,
2645
2794
  estimate_derivative_error=estimate_derivative_error,
2646
2795
  derivative_error_threshold=derivative_error_threshold,
@@ -2681,6 +2830,7 @@ class ResidualEngine:
2681
2830
  nondim_scales: Any = None,
2682
2831
  derivative_metadata: Optional[Dict[str, Any]] = None,
2683
2832
  eval_mask: Any = None,
2833
+ eval_mask_label: Optional[str] = None,
2684
2834
  state_pred_alt: Optional[Dict[str, Any]] = None,
2685
2835
  estimate_derivative_error: Optional[bool] = None,
2686
2836
  derivative_error_threshold: Optional[float] = None,
@@ -3224,12 +3374,22 @@ class ResidualEngine:
3224
3374
  }
3225
3375
 
3226
3376
  flat = _flatten_residual_dict(residuals)
3377
+ unmasked_flat = flat
3227
3378
  mask_meta: Dict[str, Any] = {}
3228
3379
  if eval_mask is not None:
3229
3380
  from moju.monitor.eval_mask import mask_flat_residuals, mask_provenance
3230
3381
 
3231
3382
  flat = mask_flat_residuals(flat, eval_mask)
3232
3383
  mask_meta = mask_provenance(eval_mask)
3384
+ from moju.monitor.evaluation_coverage import coverage_map_from_flat
3385
+
3386
+ evaluation_coverage = coverage_map_from_flat(
3387
+ flat,
3388
+ eval_mask=eval_mask,
3389
+ eval_mask_label=eval_mask_label,
3390
+ mask_meta=mask_meta or None,
3391
+ unmasked_flat=unmasked_flat,
3392
+ )
3233
3393
  rms_per_key = _rms_per_key(flat, to_python=log_to_python)
3234
3394
  r_max_per_key = _max_per_key(flat, to_python=log_to_python, scoring=self._scoring_by_key)
3235
3395
 
@@ -3649,6 +3809,8 @@ class ResidualEngine:
3649
3809
  entry["derivative_error"] = derivative_error_report
3650
3810
  if mask_meta:
3651
3811
  entry["eval_mask"] = mask_meta
3812
+ if evaluation_coverage:
3813
+ entry["evaluation_coverage"] = evaluation_coverage
3652
3814
  if rms_weighted is not None:
3653
3815
  entry["rms_weighted"] = rms_weighted
3654
3816
  entry["weighted_norms"] = {
@@ -3857,6 +4019,8 @@ def evaluate(
3857
4019
  region_names: Optional[Mapping[Any, str]] = None,
3858
4020
  region_min_points: Optional[int] = None,
3859
4021
  evaluate_debug_loop: bool = False,
4022
+ nonfinite_policy: Optional[str] = None,
4023
+ eval_mask_label: Optional[str] = None,
3860
4024
  **compute_kwargs: Any,
3861
4025
  ) -> List[Dict[str, Any]]:
3862
4026
  """
@@ -3953,6 +4117,8 @@ def evaluate(
3953
4117
  implied_percentile=implied_percentile,
3954
4118
  implied_noise_normalization=implied_noise_normalization,
3955
4119
  require_batch=require_batch,
4120
+ nonfinite_policy=nonfinite_policy,
4121
+ eval_mask_label=eval_mask_label,
3956
4122
  )
3957
4123
  if batched is not None:
3958
4124
  for report in batched:
@@ -3973,7 +4139,13 @@ def evaluate(
3973
4139
  **compute_kwargs,
3974
4140
  )
3975
4141
  entry["index"] = i
3976
- report = audit([entry], r_ref=r_ref)
4142
+ report = audit(
4143
+ [entry],
4144
+ r_ref=r_ref,
4145
+ last_residual_dict=residuals if return_residuals else residuals,
4146
+ nonfinite_policy=nonfinite_policy,
4147
+ eval_mask_label=eval_mask_label,
4148
+ )
3977
4149
  if return_residuals:
3978
4150
  report["residuals"] = residuals
3979
4151
  report["execution"] = "loop"
@@ -43,6 +43,8 @@ def evaluate_batched(
43
43
  implied_percentile: Optional[float] = None,
44
44
  implied_noise_normalization: Optional[bool] = None,
45
45
  require_batch: bool = False,
46
+ nonfinite_policy: Optional[str] = None,
47
+ eval_mask_label: Optional[str] = None,
46
48
  ) -> Optional[List[Dict[str, Any]]]:
47
49
  """
48
50
  Score every candidate in one compiled call.
@@ -294,8 +296,24 @@ def evaluate_batched(
294
296
  compact[sk] = jnp.asarray(arr)[i]
295
297
  id_map[str(bn)] = materialize_identifiability_meta(compact)
296
298
  entry["implied_identifiability"] = id_map
299
+ if "coverage_nums" in out:
300
+ from moju.monitor.evaluation_coverage import materialize_coverage_counts
301
+
302
+ cov_map: Dict[str, Any] = {}
303
+ for k, nums in out["coverage_nums"].items():
304
+ host_nums = {sk: jnp.asarray(arr)[i] for sk, arr in nums.items()}
305
+ cov_map[str(k)] = materialize_coverage_counts(
306
+ host_nums, eval_mask_label=eval_mask_label
307
+ )
308
+ entry["evaluation_coverage"] = cov_map
297
309
  entry["monitor_settings"] = ms
298
- report = audit([entry], r_ref=r_ref)
310
+ report = audit(
311
+ [entry],
312
+ r_ref=r_ref,
313
+ last_residual_dict=residuals,
314
+ nonfinite_policy=nonfinite_policy,
315
+ eval_mask_label=eval_mask_label,
316
+ )
299
317
  if return_residuals:
300
318
  report["residuals"] = residuals
301
319
  reports.append(report)
@@ -558,6 +576,11 @@ def _score_one(
558
576
  flat = _flat(residuals)
559
577
  rms = {k: r_eff(v) for k, v in flat.items()}
560
578
  rmax = {k: r_max(v) for k, v in flat.items()}
579
+ from moju.monitor.evaluation_coverage import coverage_counts_arrays
580
+
581
+ coverage_nums: Dict[str, Dict[str, Any]] = {
582
+ k: coverage_counts_arrays(v) for k, v in flat.items()
583
+ }
561
584
  dynamic_scale: Dict[str, Any] = {}
562
585
  recipe_ok: Dict[str, Any] = {}
563
586
  for key, recipe, spec, _is_user in plan.auto_scale:
@@ -582,6 +605,8 @@ def _score_one(
582
605
  "scale": dynamic_scale,
583
606
  "recipe_ok": recipe_ok,
584
607
  }
608
+ if coverage_nums:
609
+ out["coverage_nums"] = coverage_nums
585
610
  if implied_id:
586
611
  out["implied_id"] = implied_id
587
612