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.
- {moju-2.4.0 → moju-2.4.2}/PKG-INFO +2 -2
- {moju-2.4.0 → moju-2.4.2}/README.md +1 -1
- {moju-2.4.0 → moju-2.4.2}/moju/__init__.py +1 -1
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/auditor.py +4 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/evaluate_batch.py +34 -3
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/evaluation_coverage.py +228 -16
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/law_implied_diagnostics.py +35 -3
- {moju-2.4.0 → moju-2.4.2}/moju.egg-info/PKG-INFO +2 -2
- {moju-2.4.0 → moju-2.4.2}/pyproject.toml +1 -1
- moju-2.4.2/tests/test_evaluation_coverage.py +691 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_paper_goldens.py +19 -0
- moju-2.4.0/tests/test_evaluation_coverage.py +0 -353
- {moju-2.4.0 → moju-2.4.2}/LICENSE +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/__init__.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/analytic_flows.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/audit_meta.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/boundary_closures.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/closure_registry.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/config.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/constitutive_closure_summary.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/constitutive_closures.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/curvilinear.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/derivative_error.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/derivative_keys.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/derivative_metadata.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/derivative_provenance.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/derived_state_chain.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/eval_mask.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/fd_stencils.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/implied_scoring.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/key_declarations.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/law_fd_recipes.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/law_group_inference.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/law_scale_recipes.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/model_derived_registry.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/monitor_log_export.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/nondim_inference.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/nondim_scales_parse.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/path_b_derivatives.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/path_b_spectral.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/regions.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/report.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/resolution_sensor.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/scaling_closures.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/spatial_rnorm_panels.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/state_builder.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/state_ref.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/term_breakdown.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/tiers.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/types.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/visualize_components.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/visualize_constitutive.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/visualize_export.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/visualize_labels.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/visualize_plotly.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/visualize_theme.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/weighted_norms.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/monitor/weighted_safeguard.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/piratio/__init__.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/piratio/groups.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/piratio/laws.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/piratio/models.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/piratio/nondim.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/piratio/operators.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/registry.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/torch/__init__.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/torch/_closure.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/torch/_derived.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/torch/_engine.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/torch/_fd_stencils.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/torch/_implied_diagnostics.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/torch/_nondim.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/torch/_path_b.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/torch/_r_eff.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju/torch_interop.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju.egg-info/SOURCES.txt +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju.egg-info/dependency_links.txt +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju.egg-info/requires.txt +0 -0
- {moju-2.4.0 → moju-2.4.2}/moju.egg-info/top_level.txt +0 -0
- {moju-2.4.0 → moju-2.4.2}/setup.cfg +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_analytic_ns_diagnostics.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_audit_meta.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_auditor.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_builtin_law_evaluate_batch.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_closure_debug_sidecar.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_compat_2_0_defaults.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_config_forms.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_constitutive_closure_summary.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_custom_law_hooks.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_derivative_provenance.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_derived_state_chain.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_evaluate.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_evaluate_optin_batched.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_evaluate_regions_batched.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_examples_implied_cookbooks.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_examples_path_b_fd_cookbooks.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_examples_turbulence_cookbooks.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_fd_stencils.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_fornberg_and_dual_spacing.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_groups.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_implied_robust_scoring.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_implied_viscosity_ls.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_imports.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_jax_torch_fd_parity.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_law_fd_recipes.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_law_group_inference.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_law_implied_diagnostics.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_law_registry.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_law_scale_recipes.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_laws.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_minimal_input_flow.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_model_derived_registry.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_models.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_monitor_log_export.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_nondim.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_nondim_inference.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_not_identifiable_tier.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_notebooks_media.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_operators.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_path_b_derivatives.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_path_b_spectral.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_phase_a2_derivative_layer.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_phase_a5_periodic.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_readme_examples.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_readme_pypi_links.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_region_aware_scoring.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_registry.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_report_schema.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_safeguard_diagnostics.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_scoring_and_bounds.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_spatial_rnorm_panels.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_split_admissibility.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_state_ref.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_state_ref_hdf5.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_state_ref_meshio.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_studio_auto_config.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_studio_dependency_planner.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_studio_implied_groups.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_studio_io.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_studio_law_fd_hints.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_studio_model_derived_registry.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_studio_pi_gating.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_studio_plots.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_torch_engine.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_torch_interop.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_visualize_components.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_visualize_constitutive.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_visualize_labels.py +0 -0
- {moju-2.4.0 → moju-2.4.2}/tests/test_visualize_parity.py +0 -0
- {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.
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
|
72
|
-
"""
|
|
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 {
|
|
77
|
-
|
|
78
|
-
|
|
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":
|
|
151
|
+
"n_selected": n_sel,
|
|
81
152
|
"n_finite": n_finite,
|
|
82
|
-
"n_nonfinite":
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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" }]
|