moju 2.4.0__tar.gz → 2.4.2__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 (150) hide show
  1. {moju-2.4.0 → moju-2.4.2}/PKG-INFO +2 -2
  2. {moju-2.4.0 → moju-2.4.2}/README.md +1 -1
  3. {moju-2.4.0 → moju-2.4.2}/moju/__init__.py +1 -1
  4. {moju-2.4.0 → moju-2.4.2}/moju/monitor/auditor.py +4 -0
  5. {moju-2.4.0 → moju-2.4.2}/moju/monitor/evaluate_batch.py +34 -3
  6. {moju-2.4.0 → moju-2.4.2}/moju/monitor/evaluation_coverage.py +228 -16
  7. {moju-2.4.0 → moju-2.4.2}/moju/monitor/law_implied_diagnostics.py +35 -3
  8. {moju-2.4.0 → moju-2.4.2}/moju.egg-info/PKG-INFO +2 -2
  9. {moju-2.4.0 → moju-2.4.2}/pyproject.toml +1 -1
  10. moju-2.4.2/tests/test_evaluation_coverage.py +691 -0
  11. {moju-2.4.0 → moju-2.4.2}/tests/test_paper_goldens.py +19 -0
  12. moju-2.4.0/tests/test_evaluation_coverage.py +0 -353
  13. {moju-2.4.0 → moju-2.4.2}/LICENSE +0 -0
  14. {moju-2.4.0 → moju-2.4.2}/moju/monitor/__init__.py +0 -0
  15. {moju-2.4.0 → moju-2.4.2}/moju/monitor/analytic_flows.py +0 -0
  16. {moju-2.4.0 → moju-2.4.2}/moju/monitor/audit_meta.py +0 -0
  17. {moju-2.4.0 → moju-2.4.2}/moju/monitor/boundary_closures.py +0 -0
  18. {moju-2.4.0 → moju-2.4.2}/moju/monitor/closure_registry.py +0 -0
  19. {moju-2.4.0 → moju-2.4.2}/moju/monitor/config.py +0 -0
  20. {moju-2.4.0 → moju-2.4.2}/moju/monitor/constitutive_closure_summary.py +0 -0
  21. {moju-2.4.0 → moju-2.4.2}/moju/monitor/constitutive_closures.py +0 -0
  22. {moju-2.4.0 → moju-2.4.2}/moju/monitor/curvilinear.py +0 -0
  23. {moju-2.4.0 → moju-2.4.2}/moju/monitor/derivative_error.py +0 -0
  24. {moju-2.4.0 → moju-2.4.2}/moju/monitor/derivative_keys.py +0 -0
  25. {moju-2.4.0 → moju-2.4.2}/moju/monitor/derivative_metadata.py +0 -0
  26. {moju-2.4.0 → moju-2.4.2}/moju/monitor/derivative_provenance.py +0 -0
  27. {moju-2.4.0 → moju-2.4.2}/moju/monitor/derived_state_chain.py +0 -0
  28. {moju-2.4.0 → moju-2.4.2}/moju/monitor/eval_mask.py +0 -0
  29. {moju-2.4.0 → moju-2.4.2}/moju/monitor/fd_stencils.py +0 -0
  30. {moju-2.4.0 → moju-2.4.2}/moju/monitor/implied_scoring.py +0 -0
  31. {moju-2.4.0 → moju-2.4.2}/moju/monitor/key_declarations.py +0 -0
  32. {moju-2.4.0 → moju-2.4.2}/moju/monitor/law_fd_recipes.py +0 -0
  33. {moju-2.4.0 → moju-2.4.2}/moju/monitor/law_group_inference.py +0 -0
  34. {moju-2.4.0 → moju-2.4.2}/moju/monitor/law_scale_recipes.py +0 -0
  35. {moju-2.4.0 → moju-2.4.2}/moju/monitor/model_derived_registry.py +0 -0
  36. {moju-2.4.0 → moju-2.4.2}/moju/monitor/monitor_log_export.py +0 -0
  37. {moju-2.4.0 → moju-2.4.2}/moju/monitor/nondim_inference.py +0 -0
  38. {moju-2.4.0 → moju-2.4.2}/moju/monitor/nondim_scales_parse.py +0 -0
  39. {moju-2.4.0 → moju-2.4.2}/moju/monitor/path_b_derivatives.py +0 -0
  40. {moju-2.4.0 → moju-2.4.2}/moju/monitor/path_b_spectral.py +0 -0
  41. {moju-2.4.0 → moju-2.4.2}/moju/monitor/regions.py +0 -0
  42. {moju-2.4.0 → moju-2.4.2}/moju/monitor/report.py +0 -0
  43. {moju-2.4.0 → moju-2.4.2}/moju/monitor/resolution_sensor.py +0 -0
  44. {moju-2.4.0 → moju-2.4.2}/moju/monitor/scaling_closures.py +0 -0
  45. {moju-2.4.0 → moju-2.4.2}/moju/monitor/spatial_rnorm_panels.py +0 -0
  46. {moju-2.4.0 → moju-2.4.2}/moju/monitor/state_builder.py +0 -0
  47. {moju-2.4.0 → moju-2.4.2}/moju/monitor/state_ref.py +0 -0
  48. {moju-2.4.0 → moju-2.4.2}/moju/monitor/term_breakdown.py +0 -0
  49. {moju-2.4.0 → moju-2.4.2}/moju/monitor/tiers.py +0 -0
  50. {moju-2.4.0 → moju-2.4.2}/moju/monitor/types.py +0 -0
  51. {moju-2.4.0 → moju-2.4.2}/moju/monitor/visualize_components.py +0 -0
  52. {moju-2.4.0 → moju-2.4.2}/moju/monitor/visualize_constitutive.py +0 -0
  53. {moju-2.4.0 → moju-2.4.2}/moju/monitor/visualize_export.py +0 -0
  54. {moju-2.4.0 → moju-2.4.2}/moju/monitor/visualize_labels.py +0 -0
  55. {moju-2.4.0 → moju-2.4.2}/moju/monitor/visualize_plotly.py +0 -0
  56. {moju-2.4.0 → moju-2.4.2}/moju/monitor/visualize_theme.py +0 -0
  57. {moju-2.4.0 → moju-2.4.2}/moju/monitor/weighted_norms.py +0 -0
  58. {moju-2.4.0 → moju-2.4.2}/moju/monitor/weighted_safeguard.py +0 -0
  59. {moju-2.4.0 → moju-2.4.2}/moju/piratio/__init__.py +0 -0
  60. {moju-2.4.0 → moju-2.4.2}/moju/piratio/groups.py +0 -0
  61. {moju-2.4.0 → moju-2.4.2}/moju/piratio/laws.py +0 -0
  62. {moju-2.4.0 → moju-2.4.2}/moju/piratio/models.py +0 -0
  63. {moju-2.4.0 → moju-2.4.2}/moju/piratio/nondim.py +0 -0
  64. {moju-2.4.0 → moju-2.4.2}/moju/piratio/operators.py +0 -0
  65. {moju-2.4.0 → moju-2.4.2}/moju/registry.py +0 -0
  66. {moju-2.4.0 → moju-2.4.2}/moju/torch/__init__.py +0 -0
  67. {moju-2.4.0 → moju-2.4.2}/moju/torch/_closure.py +0 -0
  68. {moju-2.4.0 → moju-2.4.2}/moju/torch/_derived.py +0 -0
  69. {moju-2.4.0 → moju-2.4.2}/moju/torch/_engine.py +0 -0
  70. {moju-2.4.0 → moju-2.4.2}/moju/torch/_fd_stencils.py +0 -0
  71. {moju-2.4.0 → moju-2.4.2}/moju/torch/_implied_diagnostics.py +0 -0
  72. {moju-2.4.0 → moju-2.4.2}/moju/torch/_nondim.py +0 -0
  73. {moju-2.4.0 → moju-2.4.2}/moju/torch/_path_b.py +0 -0
  74. {moju-2.4.0 → moju-2.4.2}/moju/torch/_r_eff.py +0 -0
  75. {moju-2.4.0 → moju-2.4.2}/moju/torch_interop.py +0 -0
  76. {moju-2.4.0 → moju-2.4.2}/moju.egg-info/SOURCES.txt +0 -0
  77. {moju-2.4.0 → moju-2.4.2}/moju.egg-info/dependency_links.txt +0 -0
  78. {moju-2.4.0 → moju-2.4.2}/moju.egg-info/requires.txt +0 -0
  79. {moju-2.4.0 → moju-2.4.2}/moju.egg-info/top_level.txt +0 -0
  80. {moju-2.4.0 → moju-2.4.2}/setup.cfg +0 -0
  81. {moju-2.4.0 → moju-2.4.2}/tests/test_analytic_ns_diagnostics.py +0 -0
  82. {moju-2.4.0 → moju-2.4.2}/tests/test_audit_meta.py +0 -0
  83. {moju-2.4.0 → moju-2.4.2}/tests/test_auditor.py +0 -0
  84. {moju-2.4.0 → moju-2.4.2}/tests/test_builtin_law_evaluate_batch.py +0 -0
  85. {moju-2.4.0 → moju-2.4.2}/tests/test_closure_debug_sidecar.py +0 -0
  86. {moju-2.4.0 → moju-2.4.2}/tests/test_compat_2_0_defaults.py +0 -0
  87. {moju-2.4.0 → moju-2.4.2}/tests/test_config_forms.py +0 -0
  88. {moju-2.4.0 → moju-2.4.2}/tests/test_constitutive_closure_summary.py +0 -0
  89. {moju-2.4.0 → moju-2.4.2}/tests/test_custom_law_hooks.py +0 -0
  90. {moju-2.4.0 → moju-2.4.2}/tests/test_derivative_provenance.py +0 -0
  91. {moju-2.4.0 → moju-2.4.2}/tests/test_derived_state_chain.py +0 -0
  92. {moju-2.4.0 → moju-2.4.2}/tests/test_evaluate.py +0 -0
  93. {moju-2.4.0 → moju-2.4.2}/tests/test_evaluate_optin_batched.py +0 -0
  94. {moju-2.4.0 → moju-2.4.2}/tests/test_evaluate_regions_batched.py +0 -0
  95. {moju-2.4.0 → moju-2.4.2}/tests/test_examples_implied_cookbooks.py +0 -0
  96. {moju-2.4.0 → moju-2.4.2}/tests/test_examples_path_b_fd_cookbooks.py +0 -0
  97. {moju-2.4.0 → moju-2.4.2}/tests/test_examples_turbulence_cookbooks.py +0 -0
  98. {moju-2.4.0 → moju-2.4.2}/tests/test_fd_stencils.py +0 -0
  99. {moju-2.4.0 → moju-2.4.2}/tests/test_fornberg_and_dual_spacing.py +0 -0
  100. {moju-2.4.0 → moju-2.4.2}/tests/test_groups.py +0 -0
  101. {moju-2.4.0 → moju-2.4.2}/tests/test_implied_robust_scoring.py +0 -0
  102. {moju-2.4.0 → moju-2.4.2}/tests/test_implied_viscosity_ls.py +0 -0
  103. {moju-2.4.0 → moju-2.4.2}/tests/test_imports.py +0 -0
  104. {moju-2.4.0 → moju-2.4.2}/tests/test_jax_torch_fd_parity.py +0 -0
  105. {moju-2.4.0 → moju-2.4.2}/tests/test_law_fd_recipes.py +0 -0
  106. {moju-2.4.0 → moju-2.4.2}/tests/test_law_group_inference.py +0 -0
  107. {moju-2.4.0 → moju-2.4.2}/tests/test_law_implied_diagnostics.py +0 -0
  108. {moju-2.4.0 → moju-2.4.2}/tests/test_law_registry.py +0 -0
  109. {moju-2.4.0 → moju-2.4.2}/tests/test_law_scale_recipes.py +0 -0
  110. {moju-2.4.0 → moju-2.4.2}/tests/test_laws.py +0 -0
  111. {moju-2.4.0 → moju-2.4.2}/tests/test_minimal_input_flow.py +0 -0
  112. {moju-2.4.0 → moju-2.4.2}/tests/test_model_derived_registry.py +0 -0
  113. {moju-2.4.0 → moju-2.4.2}/tests/test_models.py +0 -0
  114. {moju-2.4.0 → moju-2.4.2}/tests/test_monitor_log_export.py +0 -0
  115. {moju-2.4.0 → moju-2.4.2}/tests/test_nondim.py +0 -0
  116. {moju-2.4.0 → moju-2.4.2}/tests/test_nondim_inference.py +0 -0
  117. {moju-2.4.0 → moju-2.4.2}/tests/test_not_identifiable_tier.py +0 -0
  118. {moju-2.4.0 → moju-2.4.2}/tests/test_notebooks_media.py +0 -0
  119. {moju-2.4.0 → moju-2.4.2}/tests/test_operators.py +0 -0
  120. {moju-2.4.0 → moju-2.4.2}/tests/test_path_b_derivatives.py +0 -0
  121. {moju-2.4.0 → moju-2.4.2}/tests/test_path_b_spectral.py +0 -0
  122. {moju-2.4.0 → moju-2.4.2}/tests/test_phase_a2_derivative_layer.py +0 -0
  123. {moju-2.4.0 → moju-2.4.2}/tests/test_phase_a5_periodic.py +0 -0
  124. {moju-2.4.0 → moju-2.4.2}/tests/test_readme_examples.py +0 -0
  125. {moju-2.4.0 → moju-2.4.2}/tests/test_readme_pypi_links.py +0 -0
  126. {moju-2.4.0 → moju-2.4.2}/tests/test_region_aware_scoring.py +0 -0
  127. {moju-2.4.0 → moju-2.4.2}/tests/test_registry.py +0 -0
  128. {moju-2.4.0 → moju-2.4.2}/tests/test_report_schema.py +0 -0
  129. {moju-2.4.0 → moju-2.4.2}/tests/test_safeguard_diagnostics.py +0 -0
  130. {moju-2.4.0 → moju-2.4.2}/tests/test_scoring_and_bounds.py +0 -0
  131. {moju-2.4.0 → moju-2.4.2}/tests/test_spatial_rnorm_panels.py +0 -0
  132. {moju-2.4.0 → moju-2.4.2}/tests/test_split_admissibility.py +0 -0
  133. {moju-2.4.0 → moju-2.4.2}/tests/test_state_ref.py +0 -0
  134. {moju-2.4.0 → moju-2.4.2}/tests/test_state_ref_hdf5.py +0 -0
  135. {moju-2.4.0 → moju-2.4.2}/tests/test_state_ref_meshio.py +0 -0
  136. {moju-2.4.0 → moju-2.4.2}/tests/test_studio_auto_config.py +0 -0
  137. {moju-2.4.0 → moju-2.4.2}/tests/test_studio_dependency_planner.py +0 -0
  138. {moju-2.4.0 → moju-2.4.2}/tests/test_studio_implied_groups.py +0 -0
  139. {moju-2.4.0 → moju-2.4.2}/tests/test_studio_io.py +0 -0
  140. {moju-2.4.0 → moju-2.4.2}/tests/test_studio_law_fd_hints.py +0 -0
  141. {moju-2.4.0 → moju-2.4.2}/tests/test_studio_model_derived_registry.py +0 -0
  142. {moju-2.4.0 → moju-2.4.2}/tests/test_studio_pi_gating.py +0 -0
  143. {moju-2.4.0 → moju-2.4.2}/tests/test_studio_plots.py +0 -0
  144. {moju-2.4.0 → moju-2.4.2}/tests/test_torch_engine.py +0 -0
  145. {moju-2.4.0 → moju-2.4.2}/tests/test_torch_interop.py +0 -0
  146. {moju-2.4.0 → moju-2.4.2}/tests/test_visualize_components.py +0 -0
  147. {moju-2.4.0 → moju-2.4.2}/tests/test_visualize_constitutive.py +0 -0
  148. {moju-2.4.0 → moju-2.4.2}/tests/test_visualize_labels.py +0 -0
  149. {moju-2.4.0 → moju-2.4.2}/tests/test_visualize_parity.py +0 -0
  150. {moju-2.4.0 → moju-2.4.2}/tests/test_visualize_theme.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moju
3
- Version: 2.4.0
3
+ Version: 2.4.2
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>
@@ -87,7 +87,7 @@ An evaluation mask excludes all components at each excluded spatial point.
87
87
  Audit-scope monitor-log exports preserve the non-finite policy recorded by
88
88
  `audit`, including incomplete per-key verdicts and null scores in JSON series.
89
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).
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_not_identifiable`, `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). Points excluded by implied-audit identifiability (finite required inputs where the scoring-path conditioning test fails or is undefined) are counted as `n_not_identifiable` (not `n_nonfinite`), so `n_selected = n_total − n_masked − n_not_identifiable`. A point is non-finite only when a required input (or residual component) at that point is non-finite — never because the conditioning ratio alone is non-finite.
91
91
 
92
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
93
 
@@ -38,7 +38,7 @@ An evaluation mask excludes all components at each excluded spatial point.
38
38
  Audit-scope monitor-log exports preserve the non-finite policy recorded by
39
39
  `audit`, including incomplete per-key verdicts and null scores in JSON series.
40
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).
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_not_identifiable`, `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). Points excluded by implied-audit identifiability (finite required inputs where the scoring-path conditioning test fails or is undefined) are counted as `n_not_identifiable` (not `n_nonfinite`), so `n_selected = n_total − n_masked − n_not_identifiable`. A point is non-finite only when a required input (or residual component) at that point is non-finite — never because the conditioning ratio alone is non-finite.
42
42
 
43
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
44
 
@@ -1,3 +1,3 @@
1
1
  """Moju: Physics-AI supervision for engineering-grade simulations."""
2
2
 
3
- __version__ = "2.4.0"
3
+ __version__ = "2.4.2"
@@ -1028,6 +1028,9 @@ def _apply_evaluation_coverage_policy(
1028
1028
  eval_mask_label=eval_mask_label
1029
1029
  or (entry.get("eval_mask") or {}).get("label"),
1030
1030
  mask_meta=entry.get("eval_mask") if isinstance(entry.get("eval_mask"), dict) else None,
1031
+ closure_debug=last_residual_dict.get("closure_debug")
1032
+ if isinstance(last_residual_dict.get("closure_debug"), dict)
1033
+ else None,
1031
1034
  )
1032
1035
  per_key = report.get("per_key") or {}
1033
1036
  incomplete_keys: List[str] = []
@@ -3389,6 +3392,7 @@ class ResidualEngine:
3389
3392
  eval_mask_label=eval_mask_label,
3390
3393
  mask_meta=mask_meta or None,
3391
3394
  unmasked_flat=unmasked_flat,
3395
+ closure_debug=closure_debug or None,
3392
3396
  )
3393
3397
  rms_per_key = _rms_per_key(flat, to_python=log_to_python)
3394
3398
  r_max_per_key = _max_per_key(flat, to_python=log_to_python, scoring=self._scoring_by_key)
@@ -470,6 +470,10 @@ def _score_one(
470
470
  implied_debug: Dict[str, Dict[str, Any]] = {}
471
471
  # Tracer-safe identifiability scalars (status strings materialised on the host).
472
472
  implied_id: Dict[str, Dict[str, Any]] = {}
473
+ # Pointwise NI masks / row-NI flags for coverage (keyed by residual basename).
474
+ batch_id_masks: Dict[str, Any] = {}
475
+ batch_ni_masks: Dict[str, Any] = {}
476
+ batch_id_row_ni: Dict[str, Any] = {}
473
477
  for audit_spec in plan.audits:
474
478
  spec = audit_spec["spec"]
475
479
  reg = audit_spec["reg"]
@@ -531,6 +535,19 @@ def _score_one(
531
535
  pack[sk] = jnp.asarray(dbg[sk])
532
536
  if pack:
533
537
  implied_id[str(base)] = pack
538
+ if dbg.get("identifiable_mask") is not None:
539
+ batch_id_masks[str(base)] = jnp.asarray(dbg["identifiable_mask"])
540
+ if dbg.get("not_identifiable_mask") is not None:
541
+ batch_ni_masks[str(base)] = jnp.asarray(
542
+ dbg["not_identifiable_mask"]
543
+ )
544
+ # Row-level wipe (tracer-safe 0-d bool): entire δ is NaN for scoring.
545
+ if "identifiable" in dbg:
546
+ batch_id_row_ni[str(base)] = jnp.logical_not(
547
+ jnp.asarray(dbg["identifiable"])
548
+ )
549
+ elif dbg.get("status") == "not_identifiable":
550
+ batch_id_row_ni[str(base)] = jnp.asarray(True)
534
551
  if (
535
552
  dbg.get("noise_rhs") is not None
536
553
  and dbg.get("noise_basis") is not None
@@ -578,9 +595,23 @@ def _score_one(
578
595
  rmax = {k: r_max(v) for k, v in flat.items()}
579
596
  from moju.monitor.evaluation_coverage import coverage_counts_arrays
580
597
 
581
- coverage_nums: Dict[str, Dict[str, Any]] = {
582
- k: coverage_counts_arrays(v) for k, v in flat.items()
583
- }
598
+ coverage_nums: Dict[str, Dict[str, Any]] = {}
599
+ for k, v in flat.items():
600
+ id_mask = None
601
+ ni_mask = None
602
+ row_ni: Any = False
603
+ if str(k).startswith("constitutive/") and str(k).endswith("/implied_delta"):
604
+ bn = str(k)[len("constitutive/") : -len("/implied_delta")]
605
+ id_mask = batch_id_masks.get(bn)
606
+ ni_mask = batch_ni_masks.get(bn)
607
+ if bn in batch_id_row_ni:
608
+ row_ni = batch_id_row_ni[bn]
609
+ coverage_nums[k] = coverage_counts_arrays(
610
+ v,
611
+ identifiable_mask=id_mask,
612
+ not_identifiable_mask=ni_mask,
613
+ row_not_identifiable=row_ni,
614
+ )
584
615
  dynamic_scale: Dict[str, Any] = {}
585
616
  recipe_ok: Dict[str, Any] = {}
586
617
  for key, recipe, spec, _is_user in plan.auto_scale:
@@ -4,6 +4,13 @@ Evaluation coverage records for admissibility verdicts.
4
4
  Non-finite residual samples (NaN / ±inf) are tolerated by NaN-aware reductions so
5
5
  scoring can proceed on the finite subset. Coverage records make that subset
6
6
  explicit on every scored item and on the aggregate report.
7
+
8
+ Identifiability exclusions for implied audits are counted via Moju's own
9
+ identifiability mask (``n_not_identifiable``), not via NaN presence in the
10
+ implied-δ array. A point is not identifiable when required inputs are finite but
11
+ conditioning fails or is undefined; non-finite required inputs stay in the
12
+ selected set and count as ``n_nonfinite``. Selected samples are those that remain
13
+ after ``eval_mask`` and identifiability exclusions.
7
14
  """
8
15
 
9
16
  from __future__ import annotations
@@ -68,18 +75,83 @@ def _sample_flat(arr: Any) -> jnp.ndarray:
68
75
  return jnp.asarray(arr).ravel()
69
76
 
70
77
 
71
- def count_finite_samples(arr: Any) -> Dict[str, int]:
72
- """Host counts of finite / non-finite elements in a residual array."""
78
+ def _align_identifiable_mask(arr: Any, identifiable_mask: Any) -> Optional[jnp.ndarray]:
79
+ """Broadcast / reshape a spatial identifiability mask onto residual elements."""
80
+ if identifiable_mask is None:
81
+ return None
82
+ a = jnp.asarray(arr)
83
+ m = jnp.asarray(identifiable_mask).astype(bool)
84
+ if m.size == a.size:
85
+ return m.reshape(a.shape)
86
+ if a.ndim == m.ndim + 1 and a.shape[:-1] == m.shape:
87
+ return jnp.broadcast_to(m[..., None], a.shape)
88
+ if a.ndim > m.ndim and a.shape[: m.ndim] == m.shape:
89
+ trail = a.shape[m.ndim :]
90
+ return jnp.broadcast_to(m.reshape(m.shape + (1,) * len(trail)), a.shape)
91
+ return None
92
+
93
+
94
+ def count_finite_samples(
95
+ arr: Any,
96
+ *,
97
+ identifiable_mask: Any = None,
98
+ not_identifiable_mask: Any = None,
99
+ ) -> Dict[str, int]:
100
+ """
101
+ Host counts of finite / non-finite elements in a residual array.
102
+
103
+ ``not_identifiable_mask`` (preferred) marks scoring-path exclusions where
104
+ required inputs are finite but conditioning fails or is undefined; those
105
+ points become ``n_not_identifiable``. Remaining points are selected: residual
106
+ non-finites count as ``n_nonfinite`` (typically non-finite required inputs).
107
+
108
+ Legacy ``identifiable_mask`` (True = scoring-identifiable) is still accepted:
109
+ ``n_not_identifiable`` is then ``~identifiable_mask``, which conflates NI with
110
+ input non-finites — prefer ``not_identifiable_mask`` when available.
111
+ """
73
112
  a = _sample_flat(arr)
74
113
  n = int(a.size)
75
114
  if n == 0:
76
- return {"n_selected": 0, "n_finite": 0, "n_nonfinite": 0}
77
- finite = jnp.isfinite(a)
78
- n_finite = int(jnp.sum(finite))
115
+ return {
116
+ "n_selected": 0,
117
+ "n_finite": 0,
118
+ "n_nonfinite": 0,
119
+ "n_not_identifiable": 0,
120
+ }
121
+ ni_aligned = _align_identifiable_mask(arr, not_identifiable_mask)
122
+ if ni_aligned is not None:
123
+ ni = _sample_flat(ni_aligned)
124
+ n_ni = int(jnp.sum(ni))
125
+ selected = a[~ni]
126
+ n_sel = int(selected.size)
127
+ n_finite = int(jnp.sum(jnp.isfinite(selected))) if n_sel else 0
128
+ return {
129
+ "n_selected": n_sel,
130
+ "n_finite": n_finite,
131
+ "n_nonfinite": n_sel - n_finite,
132
+ "n_not_identifiable": n_ni,
133
+ }
134
+ # Legacy fallback: identifiable_mask True = selected.
135
+ aligned = _align_identifiable_mask(arr, identifiable_mask)
136
+ if aligned is None:
137
+ finite = jnp.isfinite(a)
138
+ n_finite = int(jnp.sum(finite))
139
+ return {
140
+ "n_selected": n,
141
+ "n_finite": n_finite,
142
+ "n_nonfinite": n - n_finite,
143
+ "n_not_identifiable": 0,
144
+ }
145
+ m = _sample_flat(aligned)
146
+ n_ni = int(jnp.sum(~m))
147
+ selected = a[m]
148
+ n_sel = int(selected.size)
149
+ n_finite = int(jnp.sum(jnp.isfinite(selected))) if n_sel else 0
79
150
  return {
80
- "n_selected": n,
151
+ "n_selected": n_sel,
81
152
  "n_finite": n_finite,
82
- "n_nonfinite": n - n_finite,
153
+ "n_nonfinite": n_sel - n_finite,
154
+ "n_not_identifiable": n_ni,
83
155
  }
84
156
 
85
157
 
@@ -89,12 +161,14 @@ def coverage_record_from_counts(
89
161
  n_masked: int,
90
162
  n_finite: int,
91
163
  n_nonfinite: int,
164
+ n_not_identifiable: int = 0,
92
165
  eval_mask_label: Optional[str] = None,
93
166
  ) -> Dict[str, Any]:
94
167
  """Build a JSON-serialisable coverage record from integer counts."""
95
168
  n_total_i = int(n_total)
96
169
  n_masked_i = max(0, int(n_masked))
97
- n_selected = n_total_i - n_masked_i
170
+ n_ni_i = max(0, int(n_not_identifiable))
171
+ n_selected = n_total_i - n_masked_i - n_ni_i
98
172
  if n_selected < 0:
99
173
  n_selected = 0
100
174
  # Prefer explicit finite/nonfinite; clamp to selected.
@@ -110,6 +184,7 @@ def coverage_record_from_counts(
110
184
  rec: Dict[str, Any] = {
111
185
  "n_total": n_total_i,
112
186
  "n_masked": n_masked_i,
187
+ "n_not_identifiable": n_ni_i,
113
188
  "n_selected": int(n_selected),
114
189
  "n_finite": int(n_fin),
115
190
  "n_nonfinite": int(n_non),
@@ -127,6 +202,9 @@ def coverage_from_residual(
127
202
  eval_mask_label: Optional[str] = None,
128
203
  n_total: Optional[int] = None,
129
204
  n_masked: Optional[int] = None,
205
+ identifiable_mask: Any = None,
206
+ not_identifiable_mask: Any = None,
207
+ row_not_identifiable: bool = False,
130
208
  ) -> Dict[str, Any]:
131
209
  """
132
210
  Coverage for one residual array.
@@ -135,31 +213,95 @@ def coverage_from_residual(
135
213
  size / excluded count; ``arr`` must already be restricted to selected points
136
214
  (as produced by :func:`moju.monitor.eval_mask.mask_flat_residuals`).
137
215
  Masked samples never count as non-finite.
216
+
217
+ ``not_identifiable_mask`` (from the scoring conditioning path) marks points that
218
+ are not identifiable (finite inputs, conditioning fails or undefined); residual
219
+ non-finites among the remaining points count as ``n_nonfinite``. When
220
+ ``row_not_identifiable`` is True (row-level wipe), every remaining sample after
221
+ ``eval_mask`` is counted as not-identifiable.
138
222
  """
139
- counts = count_finite_samples(arr)
140
223
  m = _as_bool_mask(eval_mask)
141
224
  if m is not None:
142
225
  n_tot = int(n_total) if n_total is not None else int(m.size)
143
226
  n_msk = int(n_masked) if n_masked is not None else int(n_tot - int(jnp.sum(m)))
144
227
  else:
145
- n_tot = int(n_total) if n_total is not None else int(counts["n_selected"])
228
+ base_n = int(jnp.asarray(arr).size)
229
+ n_tot = int(n_total) if n_total is not None else base_n
146
230
  n_msk = int(n_masked) if n_masked is not None else 0
231
+
232
+ if row_not_identifiable:
233
+ n_remain = max(0, n_tot - n_msk)
234
+ return coverage_record_from_counts(
235
+ n_total=n_tot,
236
+ n_masked=n_msk,
237
+ n_finite=0,
238
+ n_nonfinite=0,
239
+ n_not_identifiable=n_remain,
240
+ eval_mask_label=eval_mask_label,
241
+ )
242
+
243
+ counts = count_finite_samples(
244
+ arr,
245
+ identifiable_mask=identifiable_mask,
246
+ not_identifiable_mask=not_identifiable_mask,
247
+ )
147
248
  return coverage_record_from_counts(
148
249
  n_total=n_tot,
149
250
  n_masked=n_msk,
150
251
  n_finite=counts["n_finite"],
151
252
  n_nonfinite=counts["n_nonfinite"],
253
+ n_not_identifiable=counts["n_not_identifiable"],
152
254
  eval_mask_label=eval_mask_label,
153
255
  )
154
256
 
155
257
 
156
- def coverage_counts_arrays(arr: Any) -> Dict[str, Any]:
258
+ def coverage_counts_arrays(
259
+ arr: Any,
260
+ *,
261
+ identifiable_mask: Any = None,
262
+ not_identifiable_mask: Any = None,
263
+ row_not_identifiable: Any = False,
264
+ ) -> Dict[str, Any]:
157
265
  """Tracer-safe finite / size counts for jit/vmap (0-d arrays)."""
158
266
  a = _sample_flat(arr)
159
267
  n = jnp.asarray(a.size, dtype=jnp.int32)
160
- n_finite = jnp.sum(jnp.isfinite(a)).astype(jnp.int32)
161
- n_nonfinite = (n - n_finite).astype(jnp.int32)
162
- return {"n_selected": n, "n_finite": n_finite, "n_nonfinite": n_nonfinite}
268
+ zero = jnp.asarray(0, dtype=jnp.int32)
269
+ row_ni = jnp.asarray(row_not_identifiable, dtype=bool)
270
+
271
+ ni_aligned = _align_identifiable_mask(arr, not_identifiable_mask)
272
+ if ni_aligned is not None:
273
+ ni = _sample_flat(ni_aligned)
274
+ n_ni_raw = jnp.sum(ni).astype(jnp.int32)
275
+ selected = ~ni
276
+ n_sel_raw = jnp.sum(selected).astype(jnp.int32)
277
+ n_finite_raw = jnp.sum(jnp.isfinite(a) & selected).astype(jnp.int32)
278
+ n_non_raw = (n_sel_raw - n_finite_raw).astype(jnp.int32)
279
+ else:
280
+ aligned = _align_identifiable_mask(arr, identifiable_mask)
281
+ if aligned is None:
282
+ n_finite_raw = jnp.sum(jnp.isfinite(a)).astype(jnp.int32)
283
+ n_non_raw = (n - n_finite_raw).astype(jnp.int32)
284
+ n_sel_raw = n
285
+ n_ni_raw = zero
286
+ else:
287
+ m = _sample_flat(aligned)
288
+ n_ni_raw = jnp.sum(~m).astype(jnp.int32)
289
+ selected_finite = jnp.isfinite(a) & m
290
+ n_finite_raw = jnp.sum(selected_finite).astype(jnp.int32)
291
+ n_sel_raw = jnp.sum(m).astype(jnp.int32)
292
+ n_non_raw = (n_sel_raw - n_finite_raw).astype(jnp.int32)
293
+
294
+ # Row-level NI wipe: every element is not-identifiable for scoring.
295
+ n_sel = jnp.where(row_ni, zero, n_sel_raw)
296
+ n_finite = jnp.where(row_ni, zero, n_finite_raw)
297
+ n_nonfinite = jnp.where(row_ni, zero, n_non_raw)
298
+ n_ni = jnp.where(row_ni, n, n_ni_raw)
299
+ return {
300
+ "n_selected": n_sel,
301
+ "n_finite": n_finite,
302
+ "n_nonfinite": n_nonfinite,
303
+ "n_not_identifiable": n_ni,
304
+ }
163
305
 
164
306
 
165
307
  def materialize_coverage_counts(
@@ -173,16 +315,48 @@ def materialize_coverage_counts(
173
315
  n_sel = int(jnp.asarray(nums["n_selected"]).item())
174
316
  n_fin = int(jnp.asarray(nums["n_finite"]).item())
175
317
  n_non = int(jnp.asarray(nums["n_nonfinite"]).item())
176
- n_tot = int(n_total) if n_total is not None else n_sel + int(n_masked)
318
+ n_ni = int(jnp.asarray(nums.get("n_not_identifiable", 0)).item())
319
+ n_tot = int(n_total) if n_total is not None else n_sel + int(n_masked) + n_ni
177
320
  return coverage_record_from_counts(
178
321
  n_total=n_tot,
179
322
  n_masked=n_masked,
180
323
  n_finite=n_fin,
181
324
  n_nonfinite=n_non,
325
+ n_not_identifiable=n_ni,
182
326
  eval_mask_label=eval_mask_label,
183
327
  )
184
328
 
185
329
 
330
+ def identifiable_masks_from_closure_debug(
331
+ closure_debug: Optional[Mapping[str, Any]],
332
+ ) -> Dict[str, Any]:
333
+ """
334
+ Map flat implied_delta keys to pointwise identifiable masks / row-NI flags.
335
+
336
+ Returns ``{flat_key: {"mask": array|None, "row_not_identifiable": bool}}``.
337
+ """
338
+ from moju.monitor.law_implied_diagnostics import is_not_identifiable
339
+
340
+ out: Dict[str, Any] = {}
341
+ if not isinstance(closure_debug, Mapping):
342
+ return out
343
+ for bn, dbg in closure_debug.items():
344
+ if not isinstance(dbg, Mapping):
345
+ continue
346
+ flat_key = f"constitutive/{bn}/implied_delta"
347
+ row_ni = is_not_identifiable(dbg)
348
+ out[flat_key] = {
349
+ # Scoring-identifiable points (legacy fallback for coverage).
350
+ "mask": None if row_ni else dbg.get("identifiable_mask"),
351
+ # Finite conditioning failures from the same scoring function.
352
+ "not_identifiable_mask": None
353
+ if row_ni
354
+ else dbg.get("not_identifiable_mask"),
355
+ "row_not_identifiable": bool(row_ni),
356
+ }
357
+ return out
358
+
359
+
186
360
  def coverage_map_from_flat(
187
361
  flat: Mapping[str, Any],
188
362
  *,
@@ -190,12 +364,21 @@ def coverage_map_from_flat(
190
364
  eval_mask_label: Optional[str] = None,
191
365
  mask_meta: Optional[Mapping[str, Any]] = None,
192
366
  unmasked_flat: Optional[Mapping[str, Any]] = None,
367
+ identifiable_by_key: Optional[Mapping[str, Any]] = None,
368
+ closure_debug: Optional[Mapping[str, Any]] = None,
193
369
  ) -> Dict[str, Dict[str, Any]]:
194
370
  """Build per-key coverage for a flat residual map (post-mask if applicable).
195
371
 
196
372
  Supply ``unmasked_flat`` to count exclusions in residual-element units,
197
373
  including trailing components and keys to which the mask did not apply.
374
+
375
+ ``identifiable_by_key`` or ``closure_debug`` supplies per-key identifiability
376
+ masks so NI-gated NaNs are not counted as ``n_nonfinite``.
198
377
  """
378
+ id_info = dict(identifiable_by_key or {})
379
+ if closure_debug is not None:
380
+ id_info.update(identifiable_masks_from_closure_debug(closure_debug))
381
+
199
382
  n_total: Optional[int] = None
200
383
  n_masked = 0
201
384
  if mask_meta:
@@ -223,12 +406,37 @@ def coverage_map_from_flat(
223
406
  if unmasked_flat is not None and key in unmasked_flat:
224
407
  key_total = int(jnp.asarray(unmasked_flat[key]).size)
225
408
  key_masked = key_total - int(jnp.asarray(arr).size)
409
+
410
+ id_meta = id_info.get(str(key)) if id_info else None
411
+ id_mask = None
412
+ ni_mask = None
413
+ row_ni = False
414
+ if isinstance(id_meta, Mapping):
415
+ row_ni = bool(id_meta.get("row_not_identifiable"))
416
+
417
+ def _maybe_eval_mask(raw: Any) -> Any:
418
+ if raw is None or row_ni:
419
+ return None
420
+ if eval_mask is None:
421
+ return raw
422
+ try:
423
+ from moju.monitor.eval_mask import apply_mask_to_residual
424
+
425
+ return apply_mask_to_residual(raw, eval_mask)
426
+ except (ValueError, TypeError):
427
+ return raw
428
+
429
+ id_mask = _maybe_eval_mask(id_meta.get("mask"))
430
+ ni_mask = _maybe_eval_mask(id_meta.get("not_identifiable_mask"))
226
431
  out[str(key)] = coverage_from_residual(
227
432
  arr,
228
433
  eval_mask=eval_mask,
229
434
  eval_mask_label=eval_mask_label,
230
435
  n_total=key_total,
231
436
  n_masked=key_masked,
437
+ identifiable_mask=id_mask,
438
+ not_identifiable_mask=ni_mask,
439
+ row_not_identifiable=row_ni,
232
440
  )
233
441
  return out
234
442
 
@@ -237,10 +445,11 @@ def aggregate_coverage(per_key: Mapping[str, Mapping[str, Any]]) -> Dict[str, An
237
445
  """Roll up per-key coverage into one aggregate record."""
238
446
  if not per_key:
239
447
  return coverage_record_from_counts(
240
- n_total=0, n_masked=0, n_finite=0, n_nonfinite=0
448
+ n_total=0, n_masked=0, n_finite=0, n_nonfinite=0, n_not_identifiable=0
241
449
  )
242
450
  n_total = 0
243
451
  n_masked = 0
452
+ n_ni = 0
244
453
  n_finite = 0
245
454
  n_nonfinite = 0
246
455
  statuses = []
@@ -250,6 +459,7 @@ def aggregate_coverage(per_key: Mapping[str, Mapping[str, Any]]) -> Dict[str, An
250
459
  continue
251
460
  n_total += int(rec.get("n_total") or 0)
252
461
  n_masked += int(rec.get("n_masked") or 0)
462
+ n_ni += int(rec.get("n_not_identifiable") or 0)
253
463
  n_finite += int(rec.get("n_finite") or 0)
254
464
  n_nonfinite += int(rec.get("n_nonfinite") or 0)
255
465
  statuses.append(str(rec.get("coverage_status") or "ok"))
@@ -263,6 +473,7 @@ def aggregate_coverage(per_key: Mapping[str, Mapping[str, Any]]) -> Dict[str, An
263
473
  n_masked=n_masked,
264
474
  n_finite=n_finite,
265
475
  n_nonfinite=n_nonfinite,
476
+ n_not_identifiable=n_ni,
266
477
  eval_mask_label=label,
267
478
  )
268
479
  out["coverage_status"] = status
@@ -331,6 +542,7 @@ __all__ = [
331
542
  "coverage_record_from_counts",
332
543
  "coverage_status",
333
544
  "finite_score_or_none",
545
+ "identifiable_masks_from_closure_debug",
334
546
  "is_incomplete_key",
335
547
  "materialize_coverage_counts",
336
548
  "validate_nonfinite_policy",
@@ -104,6 +104,15 @@ def _vec_norm_last(x: Any) -> jnp.ndarray:
104
104
  return jnp.sqrt(jnp.sum(a**2, axis=-1) + 1e-30)
105
105
 
106
106
 
107
+ def _inputs_finite_at_points(arr: Any, point_ndim: int) -> jnp.ndarray:
108
+ """True where every component of ``arr`` is finite at each leading point index."""
109
+ a = jnp.asarray(arr)
110
+ fin = jnp.isfinite(a)
111
+ if a.ndim > point_ndim:
112
+ fin = fin.all(axis=tuple(range(point_ndim, a.ndim)))
113
+ return fin
114
+
115
+
107
116
  def _re_from_viscosity(
108
117
  U: Any,
109
118
  L: Any,
@@ -232,13 +241,32 @@ def _apply_viscosity_conditioning(
232
241
  tmax = jnp.maximum(tmax, _vec_norm_last(term))
233
242
  term_fraction = jnp.where(tmax > 0, pt / tmax, jnp.zeros_like(pt))
234
243
 
235
- mask = (
244
+ # Classify by inputs first, then by the conditioning test (2.4.2):
245
+ # - non-finite: any required input (rhs / basis / balance terms) is non-finite
246
+ # - not identifiable: inputs finite, but conditioning fails *or* is undefined
247
+ # (e.g. 0/0 → non-finite β / fit_quality from perfectly finite inputs)
248
+ # - selected: inputs finite and conditioning defined and passes thresholds
249
+ # β itself is *derived* and must not drive the non-finite bucket.
250
+ point_ndim = int(b.ndim)
251
+ inputs_finite = _inputs_finite_at_points(rhs_a, point_ndim) & _inputs_finite_at_points(
252
+ ba, point_ndim
253
+ )
254
+ for term in balance_terms:
255
+ if term is None:
256
+ continue
257
+ inputs_finite = inputs_finite & _inputs_finite_at_points(term, point_ndim)
258
+ conditioning_defined = (
236
259
  jnp.isfinite(fit_quality)
237
260
  & jnp.isfinite(term_fraction)
238
261
  & jnp.isfinite(b)
239
- & (fit_quality >= float(fit_threshold))
240
- & (term_fraction >= float(term_threshold))
241
262
  )
263
+ passes_thresholds = (fit_quality >= float(fit_threshold)) & (
264
+ term_fraction >= float(term_threshold)
265
+ )
266
+ # Scoring gate (same points used for β wipe / n_identifiable).
267
+ mask = inputs_finite & conditioning_defined & passes_thresholds
268
+ # Coverage NI exclusions (same function; not a parallel reimplementation).
269
+ not_identifiable_mask = inputs_finite & ~(conditioning_defined & passes_thresholds)
242
270
  # Keep counts / flags as JAX arrays so this path is jit/vmap-safe. Host code
243
271
  # converts with bool()/int() when building reports outside a transform.
244
272
  n_tot = int(mask.size) # static shape → concrete Python int
@@ -260,6 +288,10 @@ def _apply_viscosity_conditioning(
260
288
  "n_identifiable": n_ok,
261
289
  "n_total": n_tot,
262
290
  "identifiable": identifiable,
291
+ # Scoring-identifiable points (finite inputs ∧ defined conditioning ∧ thresholds).
292
+ "identifiable_mask": mask,
293
+ # Finite inputs where conditioning fails or is undefined — n_not_identifiable.
294
+ "not_identifiable_mask": not_identifiable_mask,
263
295
  # Status string is derived on the host from ``identifiable`` (see
264
296
  # ``materialize_identifiability_meta``); do not branch on tracers here.
265
297
  "min_identifiable_fraction": float(min_frac),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moju
3
- Version: 2.4.0
3
+ Version: 2.4.2
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>
@@ -87,7 +87,7 @@ An evaluation mask excludes all components at each excluded spatial point.
87
87
  Audit-scope monitor-log exports preserve the non-finite policy recorded by
88
88
  `audit`, including incomplete per-key verdicts and null scores in JSON series.
89
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).
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_not_identifiable`, `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). Points excluded by implied-audit identifiability (finite required inputs where the scoring-path conditioning test fails or is undefined) are counted as `n_not_identifiable` (not `n_nonfinite`), so `n_selected = n_total − n_masked − n_not_identifiable`. A point is non-finite only when a required input (or residual component) at that point is non-finite — never because the conditioning ratio alone is non-finite.
91
91
 
92
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
93
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "moju"
7
- version = "2.4.0"
7
+ version = "2.4.2"
8
8
  description = "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."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Ifimo Lab (Ifimo Global Solutions LLC)", email = "lab@ifimoanalytics.com" }]