moju 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {moju-2.2.0 → moju-2.3.0}/PKG-INFO +1 -1
- {moju-2.2.0 → moju-2.3.0}/moju/__init__.py +1 -1
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/__init__.py +10 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/auditor.py +287 -26
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/closure_registry.py +16 -6
- moju-2.3.0/moju/monitor/evaluate_batch.py +941 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/implied_scoring.py +28 -8
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/law_implied_diagnostics.py +62 -20
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/monitor_log_export.py +21 -0
- moju-2.3.0/moju/monitor/regions.py +807 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/report.py +3 -1
- {moju-2.2.0 → moju-2.3.0}/moju/torch/_engine.py +9 -0
- {moju-2.2.0 → moju-2.3.0}/moju.egg-info/PKG-INFO +1 -1
- {moju-2.2.0 → moju-2.3.0}/moju.egg-info/SOURCES.txt +4 -0
- {moju-2.2.0 → moju-2.3.0}/pyproject.toml +1 -1
- moju-2.3.0/tests/test_evaluate_optin_batched.py +146 -0
- moju-2.3.0/tests/test_evaluate_regions_batched.py +233 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_implied_viscosity_ls.py +5 -1
- {moju-2.2.0 → moju-2.3.0}/tests/test_imports.py +4 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_paper_goldens.py +2 -1
- moju-2.3.0/tests/test_region_aware_scoring.py +583 -0
- moju-2.2.0/moju/monitor/evaluate_batch.py +0 -464
- {moju-2.2.0 → moju-2.3.0}/LICENSE +0 -0
- {moju-2.2.0 → moju-2.3.0}/README.md +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/analytic_flows.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/audit_meta.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/boundary_closures.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/config.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/constitutive_closure_summary.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/constitutive_closures.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/curvilinear.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/derivative_error.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/derivative_keys.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/derivative_metadata.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/derivative_provenance.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/derived_state_chain.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/eval_mask.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/fd_stencils.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/key_declarations.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/law_fd_recipes.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/law_group_inference.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/law_scale_recipes.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/model_derived_registry.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/nondim_inference.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/nondim_scales_parse.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/path_b_derivatives.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/path_b_spectral.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/resolution_sensor.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/scaling_closures.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/spatial_rnorm_panels.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/state_builder.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/state_ref.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/term_breakdown.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/tiers.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/types.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/visualize_components.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/visualize_constitutive.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/visualize_export.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/visualize_labels.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/visualize_plotly.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/visualize_theme.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/weighted_norms.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/monitor/weighted_safeguard.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/piratio/__init__.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/piratio/groups.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/piratio/laws.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/piratio/models.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/piratio/nondim.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/piratio/operators.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/registry.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/torch/__init__.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/torch/_closure.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/torch/_derived.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/torch/_fd_stencils.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/torch/_implied_diagnostics.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/torch/_nondim.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/torch/_path_b.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/torch/_r_eff.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju/torch_interop.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju.egg-info/dependency_links.txt +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju.egg-info/requires.txt +0 -0
- {moju-2.2.0 → moju-2.3.0}/moju.egg-info/top_level.txt +0 -0
- {moju-2.2.0 → moju-2.3.0}/setup.cfg +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_analytic_ns_diagnostics.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_audit_meta.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_auditor.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_builtin_law_evaluate_batch.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_closure_debug_sidecar.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_compat_2_0_defaults.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_config_forms.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_constitutive_closure_summary.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_custom_law_hooks.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_derivative_provenance.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_derived_state_chain.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_evaluate.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_examples_implied_cookbooks.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_examples_path_b_fd_cookbooks.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_examples_turbulence_cookbooks.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_fd_stencils.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_fornberg_and_dual_spacing.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_groups.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_implied_robust_scoring.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_jax_torch_fd_parity.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_law_fd_recipes.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_law_group_inference.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_law_implied_diagnostics.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_law_registry.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_law_scale_recipes.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_laws.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_minimal_input_flow.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_model_derived_registry.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_models.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_monitor_log_export.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_nondim.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_nondim_inference.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_notebooks_media.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_operators.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_path_b_derivatives.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_path_b_spectral.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_phase_a2_derivative_layer.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_phase_a5_periodic.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_readme_examples.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_readme_pypi_links.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_registry.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_report_schema.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_safeguard_diagnostics.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_scoring_and_bounds.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_spatial_rnorm_panels.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_split_admissibility.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_state_ref.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_state_ref_hdf5.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_state_ref_meshio.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_studio_auto_config.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_studio_dependency_planner.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_studio_implied_groups.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_studio_io.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_studio_law_fd_hints.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_studio_model_derived_registry.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_studio_pi_gating.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_studio_plots.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_torch_engine.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_torch_interop.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_visualize_components.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_visualize_constitutive.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_visualize_labels.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_visualize_parity.py +0 -0
- {moju-2.2.0 → moju-2.3.0}/tests/test_visualize_theme.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: moju
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Physics supervision and audit tools for SciML models, with JAX-native residuals and PyTorch support.
|
|
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>
|
|
@@ -45,6 +45,12 @@ from moju.monitor.implied_scoring import (
|
|
|
45
45
|
resolve_implied_scoring_options,
|
|
46
46
|
score_from_r_max,
|
|
47
47
|
)
|
|
48
|
+
from moju.monitor.regions import (
|
|
49
|
+
DEFAULT_REGION_MIN_POINTS,
|
|
50
|
+
normalize_regions,
|
|
51
|
+
percentile_tail_adequacy,
|
|
52
|
+
validate_region_min_points,
|
|
53
|
+
)
|
|
48
54
|
from moju.monitor.law_implied_diagnostics import (
|
|
49
55
|
DEFAULT_IMPLIED_TERM_FRACTION,
|
|
50
56
|
ImpliedPropertyResult,
|
|
@@ -148,6 +154,10 @@ __all__ = [
|
|
|
148
154
|
"parse_implied_scoring_options",
|
|
149
155
|
"resolve_implied_scoring_options",
|
|
150
156
|
"score_from_r_max",
|
|
157
|
+
"DEFAULT_REGION_MIN_POINTS",
|
|
158
|
+
"normalize_regions",
|
|
159
|
+
"percentile_tail_adequacy",
|
|
160
|
+
"validate_region_min_points",
|
|
151
161
|
"ImpliedPropertyResult",
|
|
152
162
|
"effective_audit_specs_for_fragment",
|
|
153
163
|
"law_implied_unsupported_reasons",
|
|
@@ -53,6 +53,8 @@ from moju.monitor.closure_registry import (
|
|
|
53
53
|
from moju.monitor.derived_state_chain import all_ref_keys_from_chain, keys_produced_by_chain
|
|
54
54
|
from moju.monitor.model_derived_registry import enrich_derived_state_from_constitutive_audits
|
|
55
55
|
from moju.monitor.law_implied_diagnostics import (
|
|
56
|
+
is_not_identifiable,
|
|
57
|
+
materialize_identifiability_meta,
|
|
56
58
|
merge_fragment_law_implied_audit_specs,
|
|
57
59
|
merge_law_implied_audit_specs,
|
|
58
60
|
supported_auto_implied_laws_for,
|
|
@@ -776,8 +778,7 @@ def _compute_log_step_metrics(
|
|
|
776
778
|
bn = k[len("constitutive/") : -len("/implied_delta")]
|
|
777
779
|
meta = id_map.get(bn) if isinstance(id_map.get(bn), dict) else None
|
|
778
780
|
if meta and (
|
|
779
|
-
meta
|
|
780
|
-
or meta.get("identifiable") is False
|
|
781
|
+
is_not_identifiable(meta)
|
|
781
782
|
):
|
|
782
783
|
level = "Not identifiable"
|
|
783
784
|
id_meta = meta
|
|
@@ -837,10 +838,7 @@ def _compute_log_step_metrics(
|
|
|
837
838
|
):
|
|
838
839
|
bn = kk[len("constitutive/") : -len("/implied_delta")]
|
|
839
840
|
meta = id_map_entry.get(bn) if isinstance(id_map_entry.get(bn), dict) else None
|
|
840
|
-
if meta and (
|
|
841
|
-
meta.get("status") == "not_identifiable"
|
|
842
|
-
or meta.get("identifiable") is False
|
|
843
|
-
):
|
|
841
|
+
if meta and is_not_identifiable(meta):
|
|
844
842
|
continue
|
|
845
843
|
try:
|
|
846
844
|
vf = float(admissibility[kk])
|
|
@@ -990,6 +988,9 @@ def audit(
|
|
|
990
988
|
implied_percentile: Optional[float] = None,
|
|
991
989
|
implied_noise_normalization: Optional[bool] = None,
|
|
992
990
|
constants: Optional[Mapping[str, Any]] = None,
|
|
991
|
+
regions: Any = None,
|
|
992
|
+
region_names: Optional[Mapping[Any, str]] = None,
|
|
993
|
+
region_min_points: Optional[int] = None,
|
|
993
994
|
) -> Dict[str, Any]:
|
|
994
995
|
"""
|
|
995
996
|
Physics admissibility from logged **R_eff** (field ``rms``) and scales.
|
|
@@ -1013,6 +1014,10 @@ def audit(
|
|
|
1013
1014
|
finite present category score (missing categories excluded); for legacy entries without
|
|
1014
1015
|
``run_mode``, minimum finite score across all present categories.
|
|
1015
1016
|
|
|
1017
|
+
Optional ``regions`` / ``region_names`` / ``region_min_points`` (per-call > any
|
|
1018
|
+
``region_results`` already on the log from the engine) recompute per-region tables
|
|
1019
|
+
when ``last_residual_dict`` is provided. Global scores are unchanged.
|
|
1020
|
+
|
|
1016
1021
|
The returned dict includes ``monitor_run_mode`` from the last log entry when present, plus
|
|
1017
1022
|
``schema_version`` (:data:`moju.monitor.types.REPORT_SCHEMA_VERSION`), ``moju_version``, and
|
|
1018
1023
|
``tier_definition`` (:data:`moju.monitor.tiers.TIER_DEFINITION`).
|
|
@@ -1057,6 +1062,83 @@ def audit(
|
|
|
1057
1062
|
log[-1]["implied_scoring_settings"] = implied_scoring_settings_payload(
|
|
1058
1063
|
_opts, moju_version=_moju_version
|
|
1059
1064
|
)
|
|
1065
|
+
|
|
1066
|
+
# Per-call regions on audit() override / (re)compute region_results.
|
|
1067
|
+
if regions is not None:
|
|
1068
|
+
if last_residual_dict is None:
|
|
1069
|
+
raise ValueError(
|
|
1070
|
+
"audit(..., regions=...) requires last_residual_dict to build per-region tables"
|
|
1071
|
+
)
|
|
1072
|
+
from moju import __version__ as _moju_version
|
|
1073
|
+
from moju.monitor.implied_scoring import resolve_implied_scoring_options
|
|
1074
|
+
from moju.monitor.regions import (
|
|
1075
|
+
DEFAULT_REGION_MIN_POINTS,
|
|
1076
|
+
compute_region_results,
|
|
1077
|
+
normalize_regions,
|
|
1078
|
+
validate_region_min_points,
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
_opts = resolve_implied_scoring_options(
|
|
1082
|
+
constants=constants if constants is not None else (log[-1].get("constants") or {}),
|
|
1083
|
+
engine_defaults={
|
|
1084
|
+
"implied_metric": (log[-1].get("implied_scoring_settings") or {}).get(
|
|
1085
|
+
"implied_metric"
|
|
1086
|
+
),
|
|
1087
|
+
"implied_percentile": (log[-1].get("implied_scoring_settings") or {}).get(
|
|
1088
|
+
"implied_percentile"
|
|
1089
|
+
),
|
|
1090
|
+
"implied_noise_normalization": (log[-1].get("implied_scoring_settings") or {}).get(
|
|
1091
|
+
"implied_noise_normalization"
|
|
1092
|
+
),
|
|
1093
|
+
},
|
|
1094
|
+
implied_metric=implied_metric,
|
|
1095
|
+
implied_percentile=implied_percentile,
|
|
1096
|
+
implied_noise_normalization=implied_noise_normalization,
|
|
1097
|
+
)
|
|
1098
|
+
ref_shape = None
|
|
1099
|
+
for _content in (
|
|
1100
|
+
list((last_residual_dict.get("laws") or {}).values())
|
|
1101
|
+
+ list((last_residual_dict.get("constitutive") or {}).values())
|
|
1102
|
+
):
|
|
1103
|
+
a = jnp.asarray(_content)
|
|
1104
|
+
if a.size > 0:
|
|
1105
|
+
if a.ndim >= 2 and a.shape[-1] in (2, 3):
|
|
1106
|
+
ref_shape = a.shape[:-1]
|
|
1107
|
+
else:
|
|
1108
|
+
ref_shape = a.shape
|
|
1109
|
+
break
|
|
1110
|
+
normalized = normalize_regions(
|
|
1111
|
+
regions,
|
|
1112
|
+
reference_shape=ref_shape,
|
|
1113
|
+
region_names=region_names,
|
|
1114
|
+
)
|
|
1115
|
+
eff_rmin = (
|
|
1116
|
+
validate_region_min_points(region_min_points)
|
|
1117
|
+
if region_min_points is not None
|
|
1118
|
+
else DEFAULT_REGION_MIN_POINTS
|
|
1119
|
+
)
|
|
1120
|
+
# Prefer an existing engine-stored floor when per-call omits region_min_points.
|
|
1121
|
+
if region_min_points is None:
|
|
1122
|
+
prev = (log[-1].get("region_results") or {}).get("provenance") or {}
|
|
1123
|
+
if "region_min_points" in prev:
|
|
1124
|
+
eff_rmin = int(prev["region_min_points"])
|
|
1125
|
+
else:
|
|
1126
|
+
ms_r = ((log[-1].get("monitor_settings") or {}).get("regions") or {})
|
|
1127
|
+
if "region_min_points" in ms_r:
|
|
1128
|
+
eff_rmin = int(ms_r["region_min_points"])
|
|
1129
|
+
region_block = compute_region_results(
|
|
1130
|
+
residual_dict=last_residual_dict,
|
|
1131
|
+
entry=log[-1],
|
|
1132
|
+
normalized_regions=normalized,
|
|
1133
|
+
region_min_points=eff_rmin,
|
|
1134
|
+
implied_scoring_opts=_opts,
|
|
1135
|
+
moju_version=_moju_version,
|
|
1136
|
+
)
|
|
1137
|
+
log[-1]["region_results"] = region_block
|
|
1138
|
+
ms = dict(log[-1].get("monitor_settings") or {})
|
|
1139
|
+
ms["regions"] = region_block.get("provenance")
|
|
1140
|
+
log[-1]["monitor_settings"] = ms
|
|
1141
|
+
|
|
1060
1142
|
step_metrics = _compute_log_step_metrics(log, r_ref)
|
|
1061
1143
|
last_report_per_key: Dict[str, Any] = {}
|
|
1062
1144
|
for entry, m in zip(log, step_metrics):
|
|
@@ -1083,6 +1165,41 @@ def audit(
|
|
|
1083
1165
|
report["implied_identifiability"] = dict(log[-1]["implied_identifiability"])
|
|
1084
1166
|
if log[-1].get("implied_scoring"):
|
|
1085
1167
|
report["implied_scoring"] = dict(log[-1]["implied_scoring"])
|
|
1168
|
+
if log[-1].get("region_results"):
|
|
1169
|
+
report["region_results"] = log[-1]["region_results"]
|
|
1170
|
+
|
|
1171
|
+
# Global percentile adequacy (same arithmetic as per-region flags).
|
|
1172
|
+
if log[-1].get("percentile_adequacy"):
|
|
1173
|
+
report["percentile_adequacy"] = dict(log[-1]["percentile_adequacy"])
|
|
1174
|
+
else:
|
|
1175
|
+
_iss = report.get("implied_scoring_settings") or {}
|
|
1176
|
+
if str(_iss.get("implied_metric", "")) == "percentile":
|
|
1177
|
+
from moju.monitor.regions import percentile_tail_adequacy
|
|
1178
|
+
|
|
1179
|
+
_pct = float(_iss.get("implied_percentile", 95.0))
|
|
1180
|
+
n_global = None
|
|
1181
|
+
if last_residual_dict is not None:
|
|
1182
|
+
for _cat in ("laws", "constitutive"):
|
|
1183
|
+
for _arr in (last_residual_dict.get(_cat) or {}).values():
|
|
1184
|
+
a = jnp.asarray(_arr)
|
|
1185
|
+
if a.size == 0:
|
|
1186
|
+
continue
|
|
1187
|
+
if a.ndim >= 2 and a.shape[-1] in (2, 3):
|
|
1188
|
+
n_global = int(math.prod(a.shape[:-1]))
|
|
1189
|
+
else:
|
|
1190
|
+
n_global = int(a.size)
|
|
1191
|
+
break
|
|
1192
|
+
if n_global is not None:
|
|
1193
|
+
break
|
|
1194
|
+
if n_global is None:
|
|
1195
|
+
rr = report.get("region_results") or {}
|
|
1196
|
+
prov = rr.get("provenance") or {}
|
|
1197
|
+
if prov.get("n_total") is not None:
|
|
1198
|
+
n_global = int(prov["n_total"])
|
|
1199
|
+
if n_global is not None:
|
|
1200
|
+
pa = percentile_tail_adequacy(n_global, _pct)
|
|
1201
|
+
report["percentile_adequacy"] = pa
|
|
1202
|
+
log[-1]["percentile_adequacy"] = pa
|
|
1086
1203
|
for opt_key in (
|
|
1087
1204
|
"term_breakdown",
|
|
1088
1205
|
"derivative_metadata",
|
|
@@ -2172,6 +2289,9 @@ class ResidualEngine:
|
|
|
2172
2289
|
implied_metric: Optional[str] = None,
|
|
2173
2290
|
implied_percentile: Optional[float] = None,
|
|
2174
2291
|
implied_noise_normalization: Optional[bool] = None,
|
|
2292
|
+
regions: Any = None,
|
|
2293
|
+
region_names: Optional[Mapping[Any, str]] = None,
|
|
2294
|
+
region_min_points: Optional[int] = None,
|
|
2175
2295
|
):
|
|
2176
2296
|
law_implied_enabled = bool(law_implied_audits)
|
|
2177
2297
|
cfg_law_scale_mode: Optional[str] = None
|
|
@@ -2277,6 +2397,7 @@ class ResidualEngine:
|
|
|
2277
2397
|
self.implied_percentile = implied_percentile
|
|
2278
2398
|
self.implied_noise_normalization = implied_noise_normalization
|
|
2279
2399
|
from moju.monitor.implied_scoring import resolve_implied_scoring_options
|
|
2400
|
+
from moju.monitor.regions import DEFAULT_REGION_MIN_POINTS, validate_region_min_points
|
|
2280
2401
|
|
|
2281
2402
|
# Validate constructor / constants combination at init (raises on bad values).
|
|
2282
2403
|
resolve_implied_scoring_options(
|
|
@@ -2287,6 +2408,14 @@ class ResidualEngine:
|
|
|
2287
2408
|
"implied_noise_normalization": implied_noise_normalization,
|
|
2288
2409
|
},
|
|
2289
2410
|
)
|
|
2411
|
+
# Regions (Moju 2.3): constructor defaults; not accepted via constants.
|
|
2412
|
+
self.regions = regions
|
|
2413
|
+
self.region_names = dict(region_names) if region_names is not None else None
|
|
2414
|
+
self.region_min_points = (
|
|
2415
|
+
validate_region_min_points(region_min_points)
|
|
2416
|
+
if region_min_points is not None
|
|
2417
|
+
else DEFAULT_REGION_MIN_POINTS
|
|
2418
|
+
)
|
|
2290
2419
|
|
|
2291
2420
|
# Config-time validation (low effort)
|
|
2292
2421
|
def _validate_specs(
|
|
@@ -2434,6 +2563,9 @@ class ResidualEngine:
|
|
|
2434
2563
|
implied_metric: Optional[str] = None,
|
|
2435
2564
|
implied_percentile: Optional[float] = None,
|
|
2436
2565
|
implied_noise_normalization: Optional[bool] = None,
|
|
2566
|
+
regions: Any = None,
|
|
2567
|
+
region_names: Optional[Mapping[Any, str]] = None,
|
|
2568
|
+
region_min_points: Optional[int] = None,
|
|
2437
2569
|
) -> Dict[str, Any]:
|
|
2438
2570
|
"""
|
|
2439
2571
|
Compute residuals.
|
|
@@ -2512,6 +2644,9 @@ class ResidualEngine:
|
|
|
2512
2644
|
implied_metric=implied_metric,
|
|
2513
2645
|
implied_percentile=implied_percentile,
|
|
2514
2646
|
implied_noise_normalization=implied_noise_normalization,
|
|
2647
|
+
regions=regions,
|
|
2648
|
+
region_names=region_names,
|
|
2649
|
+
region_min_points=region_min_points,
|
|
2515
2650
|
)
|
|
2516
2651
|
self._log.append(entry)
|
|
2517
2652
|
self._index += 1
|
|
@@ -2549,6 +2684,9 @@ class ResidualEngine:
|
|
|
2549
2684
|
implied_metric: Optional[str] = None,
|
|
2550
2685
|
implied_percentile: Optional[float] = None,
|
|
2551
2686
|
implied_noise_normalization: Optional[bool] = None,
|
|
2687
|
+
regions: Any = None,
|
|
2688
|
+
region_names: Optional[Mapping[Any, str]] = None,
|
|
2689
|
+
region_min_points: Optional[int] = None,
|
|
2552
2690
|
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
2553
2691
|
"""Compute residuals and one log entry without touching the engine log."""
|
|
2554
2692
|
if run_mode not in ("training", "eval"):
|
|
@@ -3010,6 +3148,22 @@ class ResidualEngine:
|
|
|
3010
3148
|
debug["category"] = category
|
|
3011
3149
|
debug["model_name"] = str(name)
|
|
3012
3150
|
debug["state_map"] = dict(state_map)
|
|
3151
|
+
# Host path: materialize tracer-friendly identifiability scalars.
|
|
3152
|
+
if log_to_python and (
|
|
3153
|
+
"identifiable" in debug or debug.get("status") == "not_identifiable"
|
|
3154
|
+
):
|
|
3155
|
+
debug.update(
|
|
3156
|
+
materialize_identifiability_meta(
|
|
3157
|
+
{
|
|
3158
|
+
"identifiable": debug.get("identifiable", True),
|
|
3159
|
+
"status": debug.get("status"),
|
|
3160
|
+
"excluded_fraction": debug.get("excluded_fraction"),
|
|
3161
|
+
"n_identifiable": debug.get("n_identifiable"),
|
|
3162
|
+
"n_total": debug.get("n_total"),
|
|
3163
|
+
"reason": debug.get("reason"),
|
|
3164
|
+
}
|
|
3165
|
+
)
|
|
3166
|
+
)
|
|
3013
3167
|
debug_collector[base] = debug
|
|
3014
3168
|
|
|
3015
3169
|
return out
|
|
@@ -3385,17 +3539,18 @@ class ResidualEngine:
|
|
|
3385
3539
|
if not isinstance(dbg, dict):
|
|
3386
3540
|
continue
|
|
3387
3541
|
if "identifiable" in dbg or dbg.get("status") == "not_identifiable":
|
|
3388
|
-
compact
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3542
|
+
compact = materialize_identifiability_meta(
|
|
3543
|
+
{
|
|
3544
|
+
"identifiable": dbg.get("identifiable", True),
|
|
3545
|
+
"status": dbg.get("status"),
|
|
3546
|
+
"excluded_fraction": dbg.get("excluded_fraction"),
|
|
3547
|
+
"n_identifiable": dbg.get("n_identifiable"),
|
|
3548
|
+
"n_total": dbg.get("n_total"),
|
|
3549
|
+
"conditioning_threshold": dbg.get("conditioning_threshold"),
|
|
3550
|
+
"fit_quality_threshold": dbg.get("fit_quality_threshold"),
|
|
3551
|
+
"reason": dbg.get("reason"),
|
|
3552
|
+
}
|
|
3553
|
+
)
|
|
3399
3554
|
implied_identifiability[str(bn)] = compact
|
|
3400
3555
|
|
|
3401
3556
|
# Opt-in robust scoring for constitutive implied_delta (defaults = 2.1).
|
|
@@ -3448,6 +3603,7 @@ class ResidualEngine:
|
|
|
3448
3603
|
"scale": scale_per_key,
|
|
3449
3604
|
"scale_source": scale_source_per_key,
|
|
3450
3605
|
"run_mode": run_mode,
|
|
3606
|
+
"constants": dict(self.constants),
|
|
3451
3607
|
"monitor_settings": {
|
|
3452
3608
|
"law_scale_mode": eff_law_scale_mode,
|
|
3453
3609
|
"state_units": eff_state_units,
|
|
@@ -3574,6 +3730,71 @@ class ResidualEngine:
|
|
|
3574
3730
|
cs = _coord_snapshot_from_merged(merged)
|
|
3575
3731
|
if cs:
|
|
3576
3732
|
entry["coord_snapshot"] = cs
|
|
3733
|
+
|
|
3734
|
+
# Per-region fit/score (Moju 2.3). Global scores above are unchanged.
|
|
3735
|
+
eff_regions = self.regions if regions is None else regions
|
|
3736
|
+
eff_region_names = self.region_names if region_names is None else region_names
|
|
3737
|
+
from moju.monitor.regions import (
|
|
3738
|
+
DEFAULT_REGION_MIN_POINTS,
|
|
3739
|
+
compute_region_results,
|
|
3740
|
+
normalize_regions,
|
|
3741
|
+
validate_region_min_points,
|
|
3742
|
+
)
|
|
3743
|
+
|
|
3744
|
+
eff_rmin = (
|
|
3745
|
+
validate_region_min_points(region_min_points)
|
|
3746
|
+
if region_min_points is not None
|
|
3747
|
+
else int(self.region_min_points or DEFAULT_REGION_MIN_POINTS)
|
|
3748
|
+
)
|
|
3749
|
+
if eff_regions is not None:
|
|
3750
|
+
# Infer point count from a primary residual if available.
|
|
3751
|
+
ref_shape = None
|
|
3752
|
+
for _k, _arr in flat.items():
|
|
3753
|
+
a = jnp.asarray(_arr)
|
|
3754
|
+
if a.size > 0:
|
|
3755
|
+
if a.ndim >= 2 and a.shape[-1] in (2, 3):
|
|
3756
|
+
ref_shape = a.shape[:-1]
|
|
3757
|
+
else:
|
|
3758
|
+
ref_shape = a.shape
|
|
3759
|
+
break
|
|
3760
|
+
normalized = normalize_regions(
|
|
3761
|
+
eff_regions,
|
|
3762
|
+
reference_shape=ref_shape,
|
|
3763
|
+
region_names=eff_region_names,
|
|
3764
|
+
)
|
|
3765
|
+
region_block = compute_region_results(
|
|
3766
|
+
residual_dict=residuals,
|
|
3767
|
+
entry=entry,
|
|
3768
|
+
normalized_regions=normalized,
|
|
3769
|
+
region_min_points=eff_rmin,
|
|
3770
|
+
implied_scoring_opts=implied_scoring_opts,
|
|
3771
|
+
moju_version=_moju_version,
|
|
3772
|
+
)
|
|
3773
|
+
entry["region_results"] = region_block
|
|
3774
|
+
ms = dict(entry.get("monitor_settings") or {})
|
|
3775
|
+
ms["regions"] = region_block.get("provenance")
|
|
3776
|
+
entry["monitor_settings"] = ms
|
|
3777
|
+
|
|
3778
|
+
# Global percentile adequacy (independent of whether regions were set).
|
|
3779
|
+
if str(implied_scoring_opts.get("implied_metric", "")) == "percentile":
|
|
3780
|
+
from moju.monitor.regions import percentile_tail_adequacy
|
|
3781
|
+
|
|
3782
|
+
n_global = None
|
|
3783
|
+
for _k, _arr in flat.items():
|
|
3784
|
+
a = jnp.asarray(_arr)
|
|
3785
|
+
if a.size == 0:
|
|
3786
|
+
continue
|
|
3787
|
+
if a.ndim >= 2 and a.shape[-1] in (2, 3):
|
|
3788
|
+
n_global = int(math.prod(a.shape[:-1]))
|
|
3789
|
+
else:
|
|
3790
|
+
n_global = int(a.size)
|
|
3791
|
+
break
|
|
3792
|
+
if n_global is not None:
|
|
3793
|
+
entry["percentile_adequacy"] = percentile_tail_adequacy(
|
|
3794
|
+
n_global,
|
|
3795
|
+
float(implied_scoring_opts.get("implied_percentile", 95.0)),
|
|
3796
|
+
)
|
|
3797
|
+
|
|
3577
3798
|
return residuals, entry
|
|
3578
3799
|
|
|
3579
3800
|
def required_state_keys(
|
|
@@ -3621,20 +3842,25 @@ def evaluate(
|
|
|
3621
3842
|
implied_metric: Optional[str] = None,
|
|
3622
3843
|
implied_percentile: Optional[float] = None,
|
|
3623
3844
|
implied_noise_normalization: Optional[bool] = None,
|
|
3845
|
+
regions: Any = None,
|
|
3846
|
+
region_names: Optional[Mapping[Any, str]] = None,
|
|
3847
|
+
region_min_points: Optional[int] = None,
|
|
3848
|
+
evaluate_debug_loop: bool = False,
|
|
3624
3849
|
**compute_kwargs: Any,
|
|
3625
3850
|
) -> List[Dict[str, Any]]:
|
|
3626
3851
|
"""
|
|
3627
3852
|
One :func:`audit` report per candidate; ``engine.log`` is not modified.
|
|
3628
3853
|
|
|
3629
3854
|
Same-shaped candidates are scored in one :func:`jax.jit` / :func:`jax.vmap` call
|
|
3630
|
-
(``report["execution"] == "batched"``)
|
|
3631
|
-
|
|
3632
|
-
|
|
3855
|
+
(``report["execution"] == "batched"``), including when ``regions`` is set (masks are
|
|
3856
|
+
static across the batch) and when opt-in implied scoring is used without regions.
|
|
3857
|
+
Path B / ``fill_law_fd`` is loop-only. With ``regions`` and/or opt-in scoring,
|
|
3858
|
+
batching is required unless ``evaluate_debug_loop=True`` (no silent fallback).
|
|
3633
3859
|
|
|
3634
3860
|
``state_refs``, when given, must have the same length as ``candidates``. Further keywords are
|
|
3635
3861
|
forwarded to :meth:`ResidualEngine._compute_entry` (``law_scale_mode``, ``state_units``,
|
|
3636
|
-
``implied_metric`` / ``implied_percentile`` / ``implied_noise_normalization``,
|
|
3637
|
-
Explicit implied-scoring kwargs take precedence over ``engine.constants`` (see
|
|
3862
|
+
``implied_metric`` / ``implied_percentile`` / ``implied_noise_normalization``, ``regions``,
|
|
3863
|
+
and so on). Explicit implied-scoring kwargs take precedence over ``engine.constants`` (see
|
|
3638
3864
|
:func:`moju.monitor.implied_scoring.resolve_implied_scoring_options`).
|
|
3639
3865
|
Set ``return_residuals=True`` to attach each candidate's residual dict under ``"residuals"``.
|
|
3640
3866
|
"""
|
|
@@ -3645,10 +3871,19 @@ def evaluate(
|
|
|
3645
3871
|
"pass implied_metric / implied_percentile / implied_noise_normalization as "
|
|
3646
3872
|
"explicit keyword arguments to evaluate(...), not via **compute_kwargs"
|
|
3647
3873
|
)
|
|
3874
|
+
for banned in ("regions", "region_names", "region_min_points", "evaluate_debug_loop"):
|
|
3875
|
+
if banned in compute_kwargs:
|
|
3876
|
+
raise TypeError(
|
|
3877
|
+
f"pass {banned} as an explicit keyword argument to evaluate(...), "
|
|
3878
|
+
"not via **compute_kwargs"
|
|
3879
|
+
)
|
|
3648
3880
|
scoring_kwargs = {
|
|
3649
3881
|
"implied_metric": implied_metric,
|
|
3650
3882
|
"implied_percentile": implied_percentile,
|
|
3651
3883
|
"implied_noise_normalization": implied_noise_normalization,
|
|
3884
|
+
"regions": regions,
|
|
3885
|
+
"region_names": region_names,
|
|
3886
|
+
"region_min_points": region_min_points,
|
|
3652
3887
|
}
|
|
3653
3888
|
from moju.monitor.implied_scoring import (
|
|
3654
3889
|
is_default_implied_scoring,
|
|
@@ -3662,10 +3897,14 @@ def evaluate(
|
|
|
3662
3897
|
"implied_percentile": engine.implied_percentile,
|
|
3663
3898
|
"implied_noise_normalization": engine.implied_noise_normalization,
|
|
3664
3899
|
},
|
|
3665
|
-
|
|
3900
|
+
implied_metric=implied_metric,
|
|
3901
|
+
implied_percentile=implied_percentile,
|
|
3902
|
+
implied_noise_normalization=implied_noise_normalization,
|
|
3666
3903
|
)
|
|
3667
|
-
|
|
3668
|
-
|
|
3904
|
+
eff_regions = engine.regions if regions is None else regions
|
|
3905
|
+
optin = not is_default_implied_scoring(resolved_scoring)
|
|
3906
|
+
# Path B / fill_law_fd is loop-only today (with or without regions). Regions + Path B
|
|
3907
|
+
# raises unless evaluate_debug_loop=True; see MIGRATION_2.2_to_2.3.md.
|
|
3669
3908
|
n = len(candidates)
|
|
3670
3909
|
if state_refs is None:
|
|
3671
3910
|
refs: Sequence[Optional[Mapping[str, Any]]] = (None,) * n
|
|
@@ -3676,9 +3915,17 @@ def evaluate(
|
|
|
3676
3915
|
)
|
|
3677
3916
|
refs = state_refs
|
|
3678
3917
|
fd_fill = bool(compute_kwargs.get("auto_path_b_derivatives") or compute_kwargs.get("fill_law_fd"))
|
|
3679
|
-
|
|
3918
|
+
use_debug_loop = bool(evaluate_debug_loop)
|
|
3919
|
+
if eff_regions is not None and fd_fill and not use_debug_loop:
|
|
3920
|
+
raise RuntimeError(
|
|
3921
|
+
"evaluate(..., regions=...) cannot combine Path B / fill_law_fd with the "
|
|
3922
|
+
"batched region path (Path B evaluate is loop-only regardless of regions). "
|
|
3923
|
+
"Omit derivative fill, or pass evaluate_debug_loop=True."
|
|
3924
|
+
)
|
|
3925
|
+
if not fd_fill and not use_debug_loop:
|
|
3680
3926
|
from moju.monitor.evaluate_batch import evaluate_batched
|
|
3681
3927
|
|
|
3928
|
+
require_batch = (eff_regions is not None) or optin
|
|
3682
3929
|
batched = evaluate_batched(
|
|
3683
3930
|
engine,
|
|
3684
3931
|
candidates,
|
|
@@ -3686,11 +3933,25 @@ def evaluate(
|
|
|
3686
3933
|
run_mode=run_mode,
|
|
3687
3934
|
r_ref=r_ref,
|
|
3688
3935
|
return_residuals=return_residuals,
|
|
3936
|
+
regions=eff_regions,
|
|
3937
|
+
region_names=region_names if regions is not None else (
|
|
3938
|
+
engine.region_names if eff_regions is not None else None
|
|
3939
|
+
),
|
|
3940
|
+
region_min_points=region_min_points,
|
|
3941
|
+
implied_metric=implied_metric,
|
|
3942
|
+
implied_percentile=implied_percentile,
|
|
3943
|
+
implied_noise_normalization=implied_noise_normalization,
|
|
3944
|
+
require_batch=require_batch,
|
|
3689
3945
|
)
|
|
3690
3946
|
if batched is not None:
|
|
3691
3947
|
for report in batched:
|
|
3692
3948
|
report["execution"] = "batched"
|
|
3693
3949
|
return batched
|
|
3950
|
+
if require_batch:
|
|
3951
|
+
raise RuntimeError(
|
|
3952
|
+
"evaluate requires the batched jit/vmap path for regions and/or "
|
|
3953
|
+
"opt-in implied scoring (percentile / noise normalization)"
|
|
3954
|
+
)
|
|
3694
3955
|
reports: List[Dict[str, Any]] = []
|
|
3695
3956
|
for i, (state, ref) in enumerate(zip(candidates, refs)):
|
|
3696
3957
|
residuals, entry = engine._compute_entry(
|
|
@@ -185,21 +185,31 @@ def compute_implied_delta_with_debug(
|
|
|
185
185
|
except (TypeError, ValueError):
|
|
186
186
|
return None, None
|
|
187
187
|
# Drop non-identifiable rows from scoring (all-NaN delta) while keeping debug.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
# ``identifiable`` may be a 0-d JAX bool under jit/vmap — use jnp.where, not Python `if`.
|
|
189
|
+
delta_a = jnp.asarray(delta)
|
|
190
|
+
ident = implied_meta.get("identifiable", True)
|
|
191
|
+
if implied_meta.get("status") == "not_identifiable":
|
|
192
|
+
delta_a = jnp.full_like(delta_a, jnp.nan)
|
|
193
|
+
elif ident is False:
|
|
194
|
+
delta_a = jnp.full_like(delta_a, jnp.nan)
|
|
195
|
+
else:
|
|
196
|
+
try:
|
|
197
|
+
ident_arr = jnp.asarray(ident)
|
|
198
|
+
if ident_arr.dtype == jnp.bool_ or ident_arr.dtype == bool:
|
|
199
|
+
delta_a = jnp.where(ident_arr, delta_a, jnp.full_like(delta_a, jnp.nan))
|
|
200
|
+
except (TypeError, ValueError):
|
|
201
|
+
pass
|
|
192
202
|
debug = {
|
|
193
203
|
"pred": pred_debug,
|
|
194
204
|
"implied": implied_debug,
|
|
195
205
|
"raw": raw,
|
|
196
|
-
"delta":
|
|
206
|
+
"delta": delta_a,
|
|
197
207
|
"mode": "subtract",
|
|
198
208
|
"output_key": output_key,
|
|
199
209
|
}
|
|
200
210
|
if implied_meta:
|
|
201
211
|
debug.update(implied_meta)
|
|
202
|
-
return
|
|
212
|
+
return delta_a, debug
|
|
203
213
|
|
|
204
214
|
|
|
205
215
|
def compute_implied_delta(
|