moju 2.3.0__tar.gz → 2.3.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.3.0 → moju-2.3.2}/PKG-INFO +4 -4
- {moju-2.3.0 → moju-2.3.2}/README.md +1 -1
- {moju-2.3.0 → moju-2.3.2}/moju/__init__.py +1 -1
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/auditor.py +11 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/evaluate_batch.py +38 -4
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/regions.py +4 -4
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/report.py +54 -17
- {moju-2.3.0 → moju-2.3.2}/moju.egg-info/PKG-INFO +4 -4
- {moju-2.3.0 → moju-2.3.2}/moju.egg-info/SOURCES.txt +1 -0
- {moju-2.3.0 → moju-2.3.2}/pyproject.toml +13 -3
- {moju-2.3.0 → moju-2.3.2}/tests/test_evaluate_regions_batched.py +6 -3
- moju-2.3.2/tests/test_not_identifiable_tier.py +247 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_region_aware_scoring.py +3 -1
- {moju-2.3.0 → moju-2.3.2}/LICENSE +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/__init__.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/analytic_flows.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/audit_meta.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/boundary_closures.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/closure_registry.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/config.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/constitutive_closure_summary.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/constitutive_closures.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/curvilinear.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/derivative_error.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/derivative_keys.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/derivative_metadata.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/derivative_provenance.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/derived_state_chain.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/eval_mask.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/fd_stencils.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/implied_scoring.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/key_declarations.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/law_fd_recipes.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/law_group_inference.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/law_implied_diagnostics.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/law_scale_recipes.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/model_derived_registry.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/monitor_log_export.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/nondim_inference.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/nondim_scales_parse.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/path_b_derivatives.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/path_b_spectral.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/resolution_sensor.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/scaling_closures.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/spatial_rnorm_panels.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/state_builder.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/state_ref.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/term_breakdown.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/tiers.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/types.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/visualize_components.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/visualize_constitutive.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/visualize_export.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/visualize_labels.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/visualize_plotly.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/visualize_theme.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/weighted_norms.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/monitor/weighted_safeguard.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/piratio/__init__.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/piratio/groups.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/piratio/laws.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/piratio/models.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/piratio/nondim.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/piratio/operators.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/registry.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/torch/__init__.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/torch/_closure.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/torch/_derived.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/torch/_engine.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/torch/_fd_stencils.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/torch/_implied_diagnostics.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/torch/_nondim.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/torch/_path_b.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/torch/_r_eff.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju/torch_interop.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju.egg-info/dependency_links.txt +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju.egg-info/requires.txt +0 -0
- {moju-2.3.0 → moju-2.3.2}/moju.egg-info/top_level.txt +0 -0
- {moju-2.3.0 → moju-2.3.2}/setup.cfg +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_analytic_ns_diagnostics.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_audit_meta.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_auditor.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_builtin_law_evaluate_batch.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_closure_debug_sidecar.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_compat_2_0_defaults.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_config_forms.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_constitutive_closure_summary.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_custom_law_hooks.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_derivative_provenance.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_derived_state_chain.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_evaluate.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_evaluate_optin_batched.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_examples_implied_cookbooks.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_examples_path_b_fd_cookbooks.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_examples_turbulence_cookbooks.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_fd_stencils.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_fornberg_and_dual_spacing.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_groups.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_implied_robust_scoring.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_implied_viscosity_ls.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_imports.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_jax_torch_fd_parity.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_law_fd_recipes.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_law_group_inference.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_law_implied_diagnostics.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_law_registry.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_law_scale_recipes.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_laws.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_minimal_input_flow.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_model_derived_registry.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_models.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_monitor_log_export.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_nondim.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_nondim_inference.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_notebooks_media.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_operators.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_paper_goldens.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_path_b_derivatives.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_path_b_spectral.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_phase_a2_derivative_layer.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_phase_a5_periodic.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_readme_examples.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_readme_pypi_links.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_registry.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_report_schema.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_safeguard_diagnostics.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_scoring_and_bounds.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_spatial_rnorm_panels.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_split_admissibility.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_state_ref.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_state_ref_hdf5.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_state_ref_meshio.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_studio_auto_config.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_studio_dependency_planner.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_studio_implied_groups.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_studio_io.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_studio_law_fd_hints.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_studio_model_derived_registry.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_studio_pi_gating.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_studio_plots.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_torch_engine.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_torch_interop.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_visualize_components.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_visualize_constitutive.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_visualize_labels.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_visualize_parity.py +0 -0
- {moju-2.3.0 → moju-2.3.2}/tests/test_visualize_theme.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: moju
|
|
3
|
-
Version: 2.3.
|
|
4
|
-
Summary: Physics
|
|
3
|
+
Version: 2.3.2
|
|
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>
|
|
7
7
|
License: MIT
|
|
@@ -12,7 +12,7 @@ Project-URL: Paper, https://zenodo.org/records/20519331
|
|
|
12
12
|
Project-URL: Changelog, https://github.com/IfimoAI/moju/blob/main/CHANGELOG.md
|
|
13
13
|
Project-URL: Citation, https://github.com/IfimoAI/moju/blob/main/CITATION.cff
|
|
14
14
|
Project-URL: Issues, https://github.com/IfimoAI/moju/issues
|
|
15
|
-
Keywords: physics,
|
|
15
|
+
Keywords: physics-informed machine learning,scientific machine learning,surrogate validation,verification and validation,admissibility,JAX,PyTorch,SciML,PINN
|
|
16
16
|
Classifier: Development Status :: 5 - Production/Stable
|
|
17
17
|
Classifier: Intended Audience :: Science/Research
|
|
18
18
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -49,7 +49,7 @@ Dynamic: license-file
|
|
|
49
49
|
|
|
50
50
|
# moju
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Moju audits predicted fields against governing-law residuals, constitutive and implied-parameter checks, and optional per-region diagnostics. It reports admissibility scores, qualitative tiers, and scoring provenance so you can see how well a state satisfies the physics you stated. It is for people validating ML surrogates or simulation fields. Secondary capabilities include composing laws, models, and dimensionless groups, and using governing-law residuals as a training-time loss.
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
55
|
pip install moju
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# moju
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Moju audits predicted fields against governing-law residuals, constitutive and implied-parameter checks, and optional per-region diagnostics. It reports admissibility scores, qualitative tiers, and scoring provenance so you can see how well a state satisfies the physics you stated. It is for people validating ML surrogates or simulation fields. Secondary capabilities include composing laws, models, and dimensionless groups, and using governing-law residuals as a training-time loss.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
pip install moju
|
|
@@ -805,7 +805,9 @@ def _compute_log_step_metrics(
|
|
|
805
805
|
key_report["r_max"] = imp_score.get("r_max_raw")
|
|
806
806
|
if id_meta is not None:
|
|
807
807
|
# Explicit per-key sidecar: never treat as passing; rollup skips this key.
|
|
808
|
+
# Score is null (not 0.0 / NaN) so JSON exports stay strict-compliant.
|
|
808
809
|
key_report["identifiability_status"] = "not_identifiable"
|
|
810
|
+
key_report["admissibility_score"] = None
|
|
809
811
|
key_report["excluded_fraction"] = id_meta.get("excluded_fraction")
|
|
810
812
|
key_report["n_identifiable"] = id_meta.get("n_identifiable")
|
|
811
813
|
key_report["n_total"] = id_meta.get("n_total")
|
|
@@ -831,6 +833,12 @@ def _compute_log_step_metrics(
|
|
|
831
833
|
category_failed = False
|
|
832
834
|
for kk in keys:
|
|
833
835
|
# Skip not-identifiable implied rows — they must not zero the category.
|
|
836
|
+
kr = per_key_report.get(kk) or {}
|
|
837
|
+
if (
|
|
838
|
+
kr.get("identifiability_status") == "not_identifiable"
|
|
839
|
+
or kr.get("admissibility_level") == "Not identifiable"
|
|
840
|
+
):
|
|
841
|
+
continue
|
|
834
842
|
if (
|
|
835
843
|
isinstance(id_map_entry, dict)
|
|
836
844
|
and kk.startswith("constitutive/")
|
|
@@ -1461,6 +1469,9 @@ def _worst_keys_table_rows(
|
|
|
1461
1469
|
row["r_max_raw"] = rep.get("r_max_raw")
|
|
1462
1470
|
if is_ni:
|
|
1463
1471
|
row["admissibility_level"] = "Not identifiable"
|
|
1472
|
+
# Missing score → null in JSON (never NaN / 0.0).
|
|
1473
|
+
row["admissibility"] = None
|
|
1474
|
+
row["admissibility_score"] = None
|
|
1464
1475
|
if rep.get("excluded_fraction") is not None:
|
|
1465
1476
|
row["excluded_fraction"] = rep.get("excluded_fraction")
|
|
1466
1477
|
if rep.get("identifiability_reason"):
|
|
@@ -105,7 +105,10 @@ def evaluate_batched(
|
|
|
105
105
|
resolve_implied_scoring_options,
|
|
106
106
|
score_from_r_max,
|
|
107
107
|
)
|
|
108
|
-
from moju.monitor.law_implied_diagnostics import
|
|
108
|
+
from moju.monitor.law_implied_diagnostics import (
|
|
109
|
+
_gate_params,
|
|
110
|
+
materialize_identifiability_meta,
|
|
111
|
+
)
|
|
109
112
|
from moju.monitor.regions import (
|
|
110
113
|
DEFAULT_REGION_MIN_POINTS,
|
|
111
114
|
normalize_regions,
|
|
@@ -281,6 +284,16 @@ def evaluate_batched(
|
|
|
281
284
|
score_from_r_max=score_from_r_max,
|
|
282
285
|
)
|
|
283
286
|
ms["regions"] = entry["region_results"].get("provenance")
|
|
287
|
+
# Host-side: materialise not-identifiable flags so audit rollup matches
|
|
288
|
+
# the loop / ResidualEngine path (NaN implied scores must not zero the tier).
|
|
289
|
+
if "implied_id" in out:
|
|
290
|
+
id_map: Dict[str, Any] = {}
|
|
291
|
+
for bn, nums in out["implied_id"].items():
|
|
292
|
+
compact: Dict[str, Any] = {}
|
|
293
|
+
for sk, arr in nums.items():
|
|
294
|
+
compact[sk] = jnp.asarray(arr)[i]
|
|
295
|
+
id_map[str(bn)] = materialize_identifiability_meta(compact)
|
|
296
|
+
entry["implied_identifiability"] = id_map
|
|
284
297
|
entry["monitor_settings"] = ms
|
|
285
298
|
report = audit([entry], r_ref=r_ref)
|
|
286
299
|
if return_residuals:
|
|
@@ -437,6 +450,8 @@ def _score_one(
|
|
|
437
450
|
visc_fq = None
|
|
438
451
|
visc_scale = None
|
|
439
452
|
implied_debug: Dict[str, Dict[str, Any]] = {}
|
|
453
|
+
# Tracer-safe identifiability scalars (status strings materialised on the host).
|
|
454
|
+
implied_id: Dict[str, Dict[str, Any]] = {}
|
|
440
455
|
for audit_spec in plan.audits:
|
|
441
456
|
spec = audit_spec["spec"]
|
|
442
457
|
reg = audit_spec["reg"]
|
|
@@ -481,6 +496,23 @@ def _score_one(
|
|
|
481
496
|
side[sk] = jnp.asarray(dbg[sk])
|
|
482
497
|
if side:
|
|
483
498
|
implied_debug[key] = side
|
|
499
|
+
if "identifiable" in dbg or dbg.get("status") == "not_identifiable":
|
|
500
|
+
pack: Dict[str, Any] = {}
|
|
501
|
+
if "identifiable" in dbg:
|
|
502
|
+
pack["identifiable"] = jnp.asarray(dbg["identifiable"])
|
|
503
|
+
elif dbg.get("status") == "not_identifiable":
|
|
504
|
+
pack["identifiable"] = jnp.asarray(False)
|
|
505
|
+
for sk in (
|
|
506
|
+
"excluded_fraction",
|
|
507
|
+
"n_identifiable",
|
|
508
|
+
"n_total",
|
|
509
|
+
"conditioning_threshold",
|
|
510
|
+
"fit_quality_threshold",
|
|
511
|
+
):
|
|
512
|
+
if dbg.get(sk) is not None:
|
|
513
|
+
pack[sk] = jnp.asarray(dbg[sk])
|
|
514
|
+
if pack:
|
|
515
|
+
implied_id[str(base)] = pack
|
|
484
516
|
if (
|
|
485
517
|
dbg.get("noise_rhs") is not None
|
|
486
518
|
and dbg.get("noise_basis") is not None
|
|
@@ -550,6 +582,8 @@ def _score_one(
|
|
|
550
582
|
"scale": dynamic_scale,
|
|
551
583
|
"recipe_ok": recipe_ok,
|
|
552
584
|
}
|
|
585
|
+
if implied_id:
|
|
586
|
+
out["implied_id"] = implied_id
|
|
553
587
|
|
|
554
588
|
if apply_optin:
|
|
555
589
|
# Global opt-in: same arithmetic as score_implied_delta_array, jit-safe.
|
|
@@ -712,9 +746,9 @@ def _assemble_region_results(
|
|
|
712
746
|
key_row.update(
|
|
713
747
|
{
|
|
714
748
|
"status": "insufficient_points",
|
|
715
|
-
"admissibility_score":
|
|
749
|
+
"admissibility_score": None,
|
|
716
750
|
"admissibility_level": "Insufficient points",
|
|
717
|
-
"score_for_admissibility":
|
|
751
|
+
"score_for_admissibility": None,
|
|
718
752
|
"worst_point_index": None,
|
|
719
753
|
"reason": flags.get("reason"),
|
|
720
754
|
}
|
|
@@ -728,7 +762,7 @@ def _assemble_region_results(
|
|
|
728
762
|
key_row.update(
|
|
729
763
|
{
|
|
730
764
|
"status": vis_status,
|
|
731
|
-
"admissibility_score":
|
|
765
|
+
"admissibility_score": None,
|
|
732
766
|
"admissibility_level": (
|
|
733
767
|
"Insufficient points"
|
|
734
768
|
if vis_status == "insufficient_points"
|
|
@@ -671,9 +671,9 @@ def compute_region_results(
|
|
|
671
671
|
key_row.update(
|
|
672
672
|
{
|
|
673
673
|
"status": "insufficient_points",
|
|
674
|
-
"admissibility_score":
|
|
674
|
+
"admissibility_score": None,
|
|
675
675
|
"admissibility_level": "Insufficient points",
|
|
676
|
-
"score_for_admissibility":
|
|
676
|
+
"score_for_admissibility": None,
|
|
677
677
|
"worst_point_index": None,
|
|
678
678
|
"reason": region_reason,
|
|
679
679
|
}
|
|
@@ -692,7 +692,7 @@ def compute_region_results(
|
|
|
692
692
|
key_row.update(
|
|
693
693
|
{
|
|
694
694
|
"status": "insufficient_points",
|
|
695
|
-
"admissibility_score":
|
|
695
|
+
"admissibility_score": None,
|
|
696
696
|
"admissibility_level": "Insufficient points",
|
|
697
697
|
"nu_tilde": viscosity_meta.get("nu_tilde"),
|
|
698
698
|
"mu_tilde": viscosity_meta.get("mu_tilde"),
|
|
@@ -708,7 +708,7 @@ def compute_region_results(
|
|
|
708
708
|
key_row.update(
|
|
709
709
|
{
|
|
710
710
|
"status": "not_identifiable",
|
|
711
|
-
"admissibility_score":
|
|
711
|
+
"admissibility_score": None,
|
|
712
712
|
"admissibility_level": "Not identifiable",
|
|
713
713
|
"nu_tilde": viscosity_meta.get("nu_tilde"),
|
|
714
714
|
"mu_tilde": viscosity_meta.get("mu_tilde"),
|
|
@@ -99,29 +99,60 @@ def _group_keys_by_category(per_key: Dict[str, Any]) -> List[Tuple[str, List[Tup
|
|
|
99
99
|
return result
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
def
|
|
103
|
-
"""
|
|
102
|
+
def _json_safe_value(val: Any) -> Any:
|
|
103
|
+
"""Convert arrays / nested structures to JSON-safe Python; non-finite floats → ``None``."""
|
|
104
104
|
try:
|
|
105
105
|
import jax
|
|
106
106
|
except ImportError:
|
|
107
107
|
jax = None
|
|
108
108
|
import numpy as np
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
if val is None or isinstance(val, (str, bool)):
|
|
111
|
+
return val
|
|
112
|
+
if isinstance(val, dict):
|
|
113
|
+
return {str(k): _json_safe_value(v) for k, v in val.items()}
|
|
114
|
+
if isinstance(val, (list, tuple)):
|
|
115
|
+
return [_json_safe_value(v) for v in val]
|
|
116
|
+
if isinstance(val, (int,)) and not isinstance(val, bool):
|
|
117
|
+
return int(val)
|
|
118
|
+
if isinstance(val, float):
|
|
119
|
+
return val if math.isfinite(val) else None
|
|
120
|
+
if hasattr(val, "tolist"):
|
|
121
|
+
try:
|
|
122
|
+
arr = val
|
|
123
|
+
if jax is not None:
|
|
124
|
+
try:
|
|
118
125
|
arr = jax.device_get(arr)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
# 0-d numpy / jax scalars
|
|
129
|
+
as_np = np.asarray(arr)
|
|
130
|
+
if as_np.shape == ():
|
|
131
|
+
try:
|
|
132
|
+
f = float(as_np.item())
|
|
133
|
+
except (TypeError, ValueError):
|
|
134
|
+
return as_np.item()
|
|
135
|
+
return f if math.isfinite(f) else None
|
|
136
|
+
return _json_safe_value(as_np.tolist())
|
|
137
|
+
except Exception:
|
|
138
|
+
return str(val)
|
|
139
|
+
try:
|
|
140
|
+
f = float(val)
|
|
141
|
+
except (TypeError, ValueError):
|
|
142
|
+
return val
|
|
143
|
+
return f if math.isfinite(f) else None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _residual_dict_to_json_serializable(residual_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
147
|
+
"""Recursively convert residual dict (JAX/NumPy arrays) to JSON-serializable values.
|
|
148
|
+
|
|
149
|
+
Non-finite floats (``NaN`` / ``±Inf``) become ``None`` (JSON ``null``) so
|
|
150
|
+
``json.dumps(..., allow_nan=False)`` succeeds. Used by residual JSON writers
|
|
151
|
+
and :func:`moju.monitor.monitor_log_export.monitor_log_export_to_jsonable`.
|
|
152
|
+
"""
|
|
153
|
+
if not isinstance(residual_dict, dict):
|
|
154
|
+
return _json_safe_value(residual_dict) # type: ignore[return-value]
|
|
155
|
+
return {str(k): _json_safe_value(v) for k, v in residual_dict.items()}
|
|
125
156
|
|
|
126
157
|
|
|
127
158
|
def write_residuals_json(residual_dict: Dict[str, Any], path: str) -> None:
|
|
@@ -515,7 +546,13 @@ def write_audit_pdf(
|
|
|
515
546
|
for label, data in items:
|
|
516
547
|
score = data.get("admissibility_score", 0.0)
|
|
517
548
|
level = data.get("admissibility_level", "Non-Admissible")
|
|
518
|
-
|
|
549
|
+
if (
|
|
550
|
+
level in ("Not identifiable", "Insufficient points")
|
|
551
|
+
or score is None
|
|
552
|
+
):
|
|
553
|
+
score_s = "N/A"
|
|
554
|
+
else:
|
|
555
|
+
score_s = format_admissibility_pct(float(score))
|
|
519
556
|
if level == "Not identifiable" and data.get("excluded_fraction") is not None:
|
|
520
557
|
try:
|
|
521
558
|
excl_pct = 100.0 * float(data["excluded_fraction"])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: moju
|
|
3
|
-
Version: 2.3.
|
|
4
|
-
Summary: Physics
|
|
3
|
+
Version: 2.3.2
|
|
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>
|
|
7
7
|
License: MIT
|
|
@@ -12,7 +12,7 @@ Project-URL: Paper, https://zenodo.org/records/20519331
|
|
|
12
12
|
Project-URL: Changelog, https://github.com/IfimoAI/moju/blob/main/CHANGELOG.md
|
|
13
13
|
Project-URL: Citation, https://github.com/IfimoAI/moju/blob/main/CITATION.cff
|
|
14
14
|
Project-URL: Issues, https://github.com/IfimoAI/moju/issues
|
|
15
|
-
Keywords: physics,
|
|
15
|
+
Keywords: physics-informed machine learning,scientific machine learning,surrogate validation,verification and validation,admissibility,JAX,PyTorch,SciML,PINN
|
|
16
16
|
Classifier: Development Status :: 5 - Production/Stable
|
|
17
17
|
Classifier: Intended Audience :: Science/Research
|
|
18
18
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -49,7 +49,7 @@ Dynamic: license-file
|
|
|
49
49
|
|
|
50
50
|
# moju
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Moju audits predicted fields against governing-law residuals, constitutive and implied-parameter checks, and optional per-region diagnostics. It reports admissibility scores, qualitative tiers, and scoring provenance so you can see how well a state satisfies the physics you stated. It is for people validating ML surrogates or simulation fields. Secondary capabilities include composing laws, models, and dimensionless groups, and using governing-law residuals as a training-time loss.
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
55
|
pip install moju
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "moju"
|
|
7
|
-
version = "2.3.
|
|
8
|
-
description = "Physics
|
|
7
|
+
version = "2.3.2"
|
|
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" }]
|
|
11
11
|
maintainers = [
|
|
@@ -13,7 +13,17 @@ maintainers = [
|
|
|
13
13
|
]
|
|
14
14
|
license = { text = "MIT" }
|
|
15
15
|
requires-python = ">=3.10"
|
|
16
|
-
keywords = [
|
|
16
|
+
keywords = [
|
|
17
|
+
"physics-informed machine learning",
|
|
18
|
+
"scientific machine learning",
|
|
19
|
+
"surrogate validation",
|
|
20
|
+
"verification and validation",
|
|
21
|
+
"admissibility",
|
|
22
|
+
"JAX",
|
|
23
|
+
"PyTorch",
|
|
24
|
+
"SciML",
|
|
25
|
+
"PINN",
|
|
26
|
+
]
|
|
17
27
|
classifiers = [
|
|
18
28
|
"Development Status :: 5 - Production/Stable",
|
|
19
29
|
"Intended Audience :: Science/Research",
|
|
@@ -52,10 +52,13 @@ def _region_snapshot(rep: dict) -> dict:
|
|
|
52
52
|
for name, block in rr.items():
|
|
53
53
|
pk = block["per_key"][_MU_KEY]
|
|
54
54
|
vis = block.get("viscosity") or {}
|
|
55
|
+
raw_score = pk.get("admissibility_score")
|
|
56
|
+
try:
|
|
57
|
+
score_f = float(raw_score) if raw_score is not None else float("nan")
|
|
58
|
+
except (TypeError, ValueError):
|
|
59
|
+
score_f = float("nan")
|
|
55
60
|
out[name] = {
|
|
56
|
-
"score": float(
|
|
57
|
-
if math.isfinite(float(pk.get("admissibility_score", float("nan"))))
|
|
58
|
-
else float("nan"),
|
|
61
|
+
"score": score_f if math.isfinite(score_f) else float("nan"),
|
|
59
62
|
"v_eff": float(pk.get("score_for_admissibility", float("nan")))
|
|
60
63
|
if pk.get("score_for_admissibility") is not None
|
|
61
64
|
else float("nan"),
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Not-identifiable implied laws must not drive the overall tier (2.1 semantics / 2.3.1 fix)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import jax
|
|
10
|
+
|
|
11
|
+
jax.config.update("jax_enable_x64", True)
|
|
12
|
+
|
|
13
|
+
import jax.numpy as jnp
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from moju.monitor import ResidualEngine, audit, evaluate
|
|
18
|
+
from moju.monitor.analytic_flows import ns_state_from_analytic, taylor_green_2d
|
|
19
|
+
|
|
20
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
21
|
+
GOLDEN = ROOT / "tests" / "goldens" / "not_identifiable" / "tg_re100.json"
|
|
22
|
+
|
|
23
|
+
_NI_KEY = "constitutive/dynamic_viscosity_from_re/law_momentum_navier_stokes/implied_delta"
|
|
24
|
+
_NI_BN = "dynamic_viscosity_from_re/law_momentum_navier_stokes"
|
|
25
|
+
_LAW_KEY = "laws/momentum_navier_stokes"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _ns_sm():
|
|
29
|
+
return {
|
|
30
|
+
"u_t": "u_t",
|
|
31
|
+
"u": "u",
|
|
32
|
+
"u_grad": "u_grad",
|
|
33
|
+
"p_grad": "p_grad",
|
|
34
|
+
"u_laplacian": "u_laplacian",
|
|
35
|
+
"re": "re",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ni_state(*, re: float = 100.0, mu: float = 0.01, n: int = 32, t: float = 0.1):
|
|
40
|
+
xs = jnp.linspace(0.0, 2.0 * jnp.pi, n, endpoint=False)
|
|
41
|
+
x, y = jnp.meshgrid(xs, xs, indexing="ij")
|
|
42
|
+
fields = taylor_green_2d(x, y, t, re=re)
|
|
43
|
+
state = ns_state_from_analytic(fields, x, y, t=t)
|
|
44
|
+
state.update({"rho": 1.0, "U": 1.0, "L": 1.0, "mu": mu, "re": re})
|
|
45
|
+
return state, x
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _identifiable_state(*, re: float = 10.0, mu: float = 0.1, n: int = 32, t: float = 0.1):
|
|
49
|
+
return _ni_state(re=re, mu=mu, n=n, t=t)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _engine(**kwargs):
|
|
53
|
+
return ResidualEngine(
|
|
54
|
+
laws=[{"name": "momentum_navier_stokes", "state_map": _ns_sm()}],
|
|
55
|
+
law_implied_audits=True,
|
|
56
|
+
constants={"rho": 1.0, "U": 1.0, "L": 1.0},
|
|
57
|
+
**kwargs,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _assert_ni_semantics(report: dict, *, golden: dict | None = None) -> None:
|
|
62
|
+
exp = (golden or json.loads(GOLDEN.read_text()))["expected"]
|
|
63
|
+
pk = report["per_key"][_NI_KEY]
|
|
64
|
+
assert pk["admissibility_level"] == exp["implied_admissibility_level"]
|
|
65
|
+
assert pk.get("identifiability_status") == exp["identifiability_status"]
|
|
66
|
+
assert pk["admissibility_score"] is None
|
|
67
|
+
assert pk["admissibility_score"] == exp["implied_admissibility_score"]
|
|
68
|
+
assert report["per_key"][_LAW_KEY]["admissibility_level"].startswith(
|
|
69
|
+
exp["law_admissibility_level_prefix"]
|
|
70
|
+
)
|
|
71
|
+
assert report["overall_admissibility_level"].startswith(
|
|
72
|
+
exp["overall_admissibility_level_prefix"]
|
|
73
|
+
)
|
|
74
|
+
id_map = report.get("implied_identifiability") or {}
|
|
75
|
+
assert id_map[_NI_BN]["status"] == "not_identifiable"
|
|
76
|
+
cats = report.get("per_category") or {}
|
|
77
|
+
if exp["constitutive_category_absent_or_nonzero"]:
|
|
78
|
+
assert "constitutive" not in cats or (
|
|
79
|
+
math.isfinite(float(cats["constitutive"])) and float(cats["constitutive"]) != 0.0
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_golden_fixture_documents_expected_tier():
|
|
84
|
+
golden = json.loads(GOLDEN.read_text())
|
|
85
|
+
assert golden["keys"]["implied"] == _NI_KEY
|
|
86
|
+
assert golden["expected"]["overall_admissibility_level_prefix"] == "High"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_audit_not_identifiable_matches_golden():
|
|
90
|
+
golden = json.loads(GOLDEN.read_text())
|
|
91
|
+
state, _ = _ni_state()
|
|
92
|
+
eng = _engine()
|
|
93
|
+
eng.compute_residuals(state, weighted_norms=False)
|
|
94
|
+
_assert_ni_semantics(audit(eng.log), golden=golden)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_evaluate_loop_not_identifiable_matches_golden():
|
|
98
|
+
golden = json.loads(GOLDEN.read_text())
|
|
99
|
+
state, _ = _ni_state()
|
|
100
|
+
reports = evaluate(_engine(), [state], weighted_norms=False, evaluate_debug_loop=True)
|
|
101
|
+
assert reports[0].get("execution") == "loop"
|
|
102
|
+
_assert_ni_semantics(reports[0], golden=golden)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_evaluate_batched_not_identifiable_matches_golden():
|
|
106
|
+
golden = json.loads(GOLDEN.read_text())
|
|
107
|
+
state, _ = _ni_state()
|
|
108
|
+
reports = evaluate(_engine(), [state], weighted_norms=False)
|
|
109
|
+
assert reports[0].get("execution") == "batched"
|
|
110
|
+
_assert_ni_semantics(reports[0], golden=golden)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_evaluate_batched_with_regions_not_identifiable_matches_golden():
|
|
114
|
+
golden = json.loads(GOLDEN.read_text())
|
|
115
|
+
state, x = _ni_state()
|
|
116
|
+
near = np.asarray(x < jnp.pi)
|
|
117
|
+
regions = {"near": near, "far": ~near}
|
|
118
|
+
eng = _engine(regions=regions, region_min_points=8)
|
|
119
|
+
reports = evaluate(
|
|
120
|
+
eng, [state], weighted_norms=False, regions=regions, region_min_points=8
|
|
121
|
+
)
|
|
122
|
+
assert reports[0].get("execution") == "batched"
|
|
123
|
+
_assert_ni_semantics(reports[0], golden=golden)
|
|
124
|
+
for name, blk in reports[0]["region_results"]["regions"].items():
|
|
125
|
+
pk = blk["per_key"][_NI_KEY]
|
|
126
|
+
assert pk["admissibility_level"] == "Not identifiable", name
|
|
127
|
+
assert pk.get("status") == "not_identifiable", name
|
|
128
|
+
assert pk["admissibility_score"] is None, name
|
|
129
|
+
assert blk["viscosity"]["status"] == "not_identifiable", name
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_batched_vs_loop_equality_including_not_identifiable():
|
|
133
|
+
state, _ = _ni_state()
|
|
134
|
+
eng = _engine()
|
|
135
|
+
loop = evaluate(eng, [state], weighted_norms=False, evaluate_debug_loop=True)[0]
|
|
136
|
+
bat = evaluate(eng, [state], weighted_norms=False)[0]
|
|
137
|
+
assert bat["execution"] == "batched"
|
|
138
|
+
assert loop["overall_admissibility_level"] == bat["overall_admissibility_level"]
|
|
139
|
+
assert loop["overall_admissibility_score"] == pytest.approx(
|
|
140
|
+
bat["overall_admissibility_score"], rel=0, abs=1e-12
|
|
141
|
+
)
|
|
142
|
+
assert loop["per_key"][_NI_KEY]["admissibility_level"] == bat["per_key"][_NI_KEY][
|
|
143
|
+
"admissibility_level"
|
|
144
|
+
]
|
|
145
|
+
assert loop["per_category"].keys() == bat["per_category"].keys()
|
|
146
|
+
for cat in loop["per_category"]:
|
|
147
|
+
assert loop["per_category"][cat] == pytest.approx(bat["per_category"][cat])
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_mixed_batch_identifiable_and_not_identifiable():
|
|
151
|
+
ni, _ = _ni_state()
|
|
152
|
+
ok, _ = _identifiable_state()
|
|
153
|
+
reports = evaluate(_engine(), [ni, ok], weighted_norms=False)
|
|
154
|
+
assert reports[0]["execution"] == "batched"
|
|
155
|
+
_assert_ni_semantics(reports[0])
|
|
156
|
+
pk_ok = reports[1]["per_key"][_NI_KEY]
|
|
157
|
+
assert pk_ok["admissibility_level"].startswith("High")
|
|
158
|
+
assert pk_ok.get("identifiability_status") in (None, "ok")
|
|
159
|
+
assert math.isfinite(float(pk_ok["admissibility_score"]))
|
|
160
|
+
assert reports[1]["overall_admissibility_level"].startswith("High")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_insufficient_points_does_not_zero_overall_tier():
|
|
164
|
+
"""Region insufficient_points uses NaN scores locally; global tier stays from scored keys."""
|
|
165
|
+
state, x = _identifiable_state()
|
|
166
|
+
tiny = np.zeros(np.asarray(x).shape, dtype=bool)
|
|
167
|
+
tiny.ravel()[:2] = True
|
|
168
|
+
regions = {"tiny": tiny, "rest": ~tiny}
|
|
169
|
+
eng = _engine(regions=regions, region_min_points=16)
|
|
170
|
+
eng.compute_residuals(state, weighted_norms=False)
|
|
171
|
+
rep = audit(eng.log)
|
|
172
|
+
assert rep["overall_admissibility_level"].startswith("High")
|
|
173
|
+
tiny_blk = rep["region_results"]["regions"]["tiny"]
|
|
174
|
+
assert tiny_blk["status"] == "insufficient_points"
|
|
175
|
+
assert tiny_blk["per_key"][_NI_KEY]["admissibility_level"] == "Insufficient points"
|
|
176
|
+
assert tiny_blk["per_key"][_NI_KEY]["admissibility_score"] is None
|
|
177
|
+
bat = evaluate(
|
|
178
|
+
eng, [state], weighted_norms=False, regions=regions, region_min_points=16
|
|
179
|
+
)[0]
|
|
180
|
+
assert bat["overall_admissibility_level"].startswith("High")
|
|
181
|
+
assert bat["region_results"]["regions"]["tiny"]["status"] == "insufficient_points"
|
|
182
|
+
assert bat["region_results"]["regions"]["tiny"]["per_key"][_NI_KEY][
|
|
183
|
+
"admissibility_score"
|
|
184
|
+
] is None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_json_export_ni_and_insufficient_scores_are_strict_null():
|
|
188
|
+
"""JSON export must use null (allow_nan=False); never NaN or 0.0."""
|
|
189
|
+
from moju.monitor.monitor_log_export import (
|
|
190
|
+
export_monitor_log,
|
|
191
|
+
monitor_log_export_to_jsonable,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
state, x = _ni_state()
|
|
195
|
+
near = np.asarray(x < jnp.pi)
|
|
196
|
+
tiny = np.zeros(np.asarray(x).shape, dtype=bool)
|
|
197
|
+
tiny.ravel()[:2] = True
|
|
198
|
+
regions = {"near": near & ~tiny, "tiny": tiny, "far": ~near}
|
|
199
|
+
eng = _engine(regions=regions, region_min_points=16)
|
|
200
|
+
eng.compute_residuals(state, weighted_norms=False)
|
|
201
|
+
export = export_monitor_log(eng.log, scope="audit", persist=False)
|
|
202
|
+
blob = monitor_log_export_to_jsonable(export)
|
|
203
|
+
text = json.dumps(blob, allow_nan=False)
|
|
204
|
+
parsed = json.loads(text)
|
|
205
|
+
pk = parsed["summary"]["per_key"][_NI_KEY]
|
|
206
|
+
assert pk["admissibility_level"] == "Not identifiable"
|
|
207
|
+
assert pk["admissibility_score"] is None
|
|
208
|
+
assert pk["admissibility_score"] != 0.0
|
|
209
|
+
# Batched evaluate export path
|
|
210
|
+
bat = evaluate(
|
|
211
|
+
eng, [state], weighted_norms=False, regions=regions, region_min_points=16
|
|
212
|
+
)[0]
|
|
213
|
+
# Re-export via a one-step log-shaped summary: dump report through jsonable helper
|
|
214
|
+
from moju.monitor.report import _residual_dict_to_json_serializable
|
|
215
|
+
|
|
216
|
+
bat_json = _residual_dict_to_json_serializable(bat)
|
|
217
|
+
bat_text = json.dumps(bat_json, allow_nan=False)
|
|
218
|
+
bat_parsed = json.loads(bat_text)
|
|
219
|
+
assert bat_parsed["per_key"][_NI_KEY]["admissibility_score"] is None
|
|
220
|
+
tiny_score = bat_parsed["region_results"]["regions"]["tiny"]["per_key"][_NI_KEY][
|
|
221
|
+
"admissibility_score"
|
|
222
|
+
]
|
|
223
|
+
assert tiny_score is None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_equivalent_to_worst_point_does_not_zero_overall_tier():
|
|
227
|
+
"""percentile_status=equivalent_to_worst_point is advisory; scores stay finite."""
|
|
228
|
+
state, x = _identifiable_state()
|
|
229
|
+
small = np.zeros(np.asarray(x).shape, dtype=bool)
|
|
230
|
+
small.ravel()[:10] = True
|
|
231
|
+
regions = {"small": small, "big": ~small}
|
|
232
|
+
eng = _engine(
|
|
233
|
+
regions=regions,
|
|
234
|
+
region_min_points=8,
|
|
235
|
+
implied_metric="percentile",
|
|
236
|
+
implied_percentile=95.0,
|
|
237
|
+
)
|
|
238
|
+
eng.compute_residuals(
|
|
239
|
+
state, weighted_norms=False, implied_metric="percentile", implied_percentile=95.0
|
|
240
|
+
)
|
|
241
|
+
rep = audit(eng.log)
|
|
242
|
+
assert rep["overall_admissibility_level"].startswith("High")
|
|
243
|
+
small_blk = rep["region_results"]["regions"]["small"]
|
|
244
|
+
assert small_blk.get("percentile_status") == "equivalent_to_worst_point"
|
|
245
|
+
assert math.isfinite(float(small_blk["per_key"][_NI_KEY]["admissibility_score"]))
|
|
246
|
+
assert "constitutive" in (rep.get("per_category") or {})
|
|
247
|
+
assert float(rep["per_category"]["constitutive"]) != 0.0
|
|
@@ -489,7 +489,9 @@ def test_evaluate_with_regions_uses_batched_jit_vmap():
|
|
|
489
489
|
assert prov["region_min_points"] == 8
|
|
490
490
|
assert set(prov["region_names"]) == {"far", "near"}
|
|
491
491
|
assert "mask_sha256" in prov
|
|
492
|
-
|
|
492
|
+
import moju
|
|
493
|
+
|
|
494
|
+
assert prov["moju_version"] == moju.__version__
|
|
493
495
|
|
|
494
496
|
|
|
495
497
|
def test_mask_hashes_and_region_provenance_in_report_and_json():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|