Skip to content

Validation cleanup — delete error_handling.py and validation.py#359

Open
hmgaudecker wants to merge 9 commits into
mainfrom
refactor/phase-1-validation-cleanup
Open

Validation cleanup — delete error_handling.py and validation.py#359
hmgaudecker wants to merge 9 commits into
mainfrom
refactor/phase-1-validation-cleanup

Conversation

@hmgaudecker
Copy link
Copy Markdown
Member

@hmgaudecker hmgaudecker commented May 18, 2026

Summary

Our validation strategy was not clear. Most of the time we had it coupled in the modules where the stuff was happening, but we also had two umbrella modules whose names misled about what lived inside (lcm/utils/error_handling.py and lcm/regime_building/validation.py). This PR absorbs each validator into the module it actually serves. Furthermore, it streamlines validation by removing redundancies and fixing a couple of inconsistencies.

File-level outcome

File Action
lcm/utils/error_handling.py deleted
lcm/regime_building/validation.py deleted
lcm/regime_building/transitions.py new (collect_state_transitions + 2 helpers)
lcm/_transition_checks.py new (regime-prob family, top-level private; pre-flight transition-probability validation)
lcm/solution/validate_V.py new (per-period value-function NaN check + diagnostic-attachment helpers)
lcm/utils/ast_inspection.py new (4 AST helpers)
lcm/user_regime.py absorbs 8 regime-spec validators + slimmed validate_transition_probs
lcm/pandas_utils.py drops the import of AST helpers (now imports from utils/ast_inspection)
lcm/regime_building/diagnostics.py unchanged

Note: lcm/user_regime.py blows up to 744 LOC and the public validate_transition_probs sits below the eight private Regime.__post_init__ validators — a temporary violation of the "public API first" layout rule. This is purely transient if as PR #360 removes validate_transition_probs (-175 LOC). If we decide that PR #361 has the right architecture (it splits this file into api/regime.py (class + public function) and lcm/_regime/ (validators, transition-probs helpers), that would reduce further.

Behaviour changes

  • validate_transition_probs is now state-probs-only. The old function was overloaded: with state_name it validated a state-transition array, without it (state_name=None) it validated a regime-transition array. The regime-probs overload is redundant with validate_regime_transitions_all_periods, which runs unconditionally on every model.solve() / model.simulate() and additionally checks inactive-regime probability and reachability. The slimmed version requires state_name. The overload's TypeError("does not have a stochastic regime transition") guard is removed along with it — that error surface no longer has a reachable code path; misuse is caught instead by validate_regime_transitions_all_periods at solve/simulate time.
  • validate_regime_transition_probs is now private (_validate_regime_transition_probs). Tests adjust their imports; no other src caller exists.
  • One pre-existing fix folded in: _validate_no_reachable_incomplete_targets (relocated into _transition_checks.py) had an override that, when a target regime was entirely absent from the source's state_transitions, listed all of the target's states as "missing" — including non-stochastic ones that need no explicit entry. The override is dropped; the error now lists only the stochastic transitions actually required.
  • Three tests in tests/test_pandas_utils.py were dropped: _valid and _wrong_shape are subsumed by validate_regime_transitions_all_periods; _not_markov_raises asserted the TypeError guard that is removed above, so it goes with the overload. Docs updated accordingly. Everything else is pure mechanical absorption + import-path updates.

🤖 Generated with Claude Code

hmgaudecker and others added 7 commits May 18, 2026 18:57
Move `collect_state_transitions`, `_make_identity_fn`, and
`_add_raw_transition` from `regime_building/validation.py` to a new
focused module. Update imports in 5 callers. Drop unused imports from
`validation.py`. Part of the Phase 1 effort to delete the
"validation" and "error_handling" umbrellas; see
`Phase 1 — Validation Cleanup.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move 8 validators from `regime_building/validation.py` into
`user_regime.py` and privatize the two formerly-public ones:
- `validate_mapping_contents` -> `_validate_mapping_contents`
- `validate_logical_consistency` -> `_validate_logical_consistency`
- `_validate_distributed_grids`, `_validate_function_output_grid_indexing`,
  `_find_function_output_grid_indexing`, `_validate_active`,
  `_validate_state_transitions`, `_validate_per_target_dict`

The validators are sole-called from `UserRegime.__post_init__`;
co-locating them with the class eliminates a misleading umbrella
module and the cross-module delayed import in `__post_init__`. Delete
`regime_building/validation.py`. Part of Phase 1 — see
`Phase 1 — Validation Cleanup.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move `_get_func_indexing_params`, `_slice_references_params`,
`_collect_subscripts`, `_extract_bare_names` from
`utils/error_handling.py` into a new focused module. Update imports
in `pandas_utils.py` and `tests/test_validate_array_indexing.py`.
`error_handling.py::validate_transition_probs` still uses
`_get_func_indexing_params` and now imports it from the new module
(deferred to M5' for full extraction). Part of Phase 1 — see
`Phase 1 — Validation Cleanup.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Create `regime_building/runtime_checks.py` and absorb two families
from `utils/error_handling.py`:
- V family (`validate_V`, `_enrich_with_diagnostics`,
  `_summarize_diagnostics`, `_format_diagnostic_summary`)
- regime-prob family (`validate_regime_transition_probs` ->
  `_validate_regime_transition_probs`, `_format_sum_violation`,
  `validate_regime_transitions_all_periods`,
  `_validate_regime_transition_single`,
  `_validate_no_reachable_incomplete_targets`)

Both families fit the unifying concept "defensive checks on JAX
arrays produced during solve/simulate." Privatize
`validate_regime_transition_probs` (only tests call it directly).
Update imports in 6 callers (3 src, 3 test). `diagnostics.py` keeps
its name. Part of Phase 1 — see `Phase 1 — Validation Cleanup.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_regime.py

Drop the regime-probs overload — that mode is redundant with
`validate_regime_transitions_all_periods` which runs unconditionally
during `model.solve()` and `model.simulate()` and additionally
checks inactive-regime probability and reachability.

Keep the state-probs mode. It is the only defence against four
silent-correctness bug classes in user-written MarkovTransition
functions for states: wrong-shape broadcasting, values outside
[0, 1], rows not summing to 1, and subscript-order swaps relative
to the function signature.

Move slimmed function + helpers (`_extract_markov_transition`,
`_build_grids`, `_build_expected_shape`) to `user_regime.py` next to
the `Regime` class they operate on. Update `lcm/__init__.py` import.
Drop three regime-probs tests from `tests/test_pandas_utils.py`.
Update `docs/user_guide/pandas_interop.md` accordingly. Part of
Phase 1 — see `Phase 1 — Validation Cleanup.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All contents have been absorbed: V family + regime-prob family into
`regime_building/runtime_checks.py` (M4); AST helpers into
`utils/ast_inspection.py` (M3); slimmed `validate_transition_probs`
into `user_regime.py` (M5'). The "error_handling" umbrella was
misleading from the start — three unrelated concerns under one
name. Part of Phase 1 — see `Phase 1 — Validation Cleanup.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…me.py

Three renames driven by Phase 1's src moves:
- `tests/test_error_handling_invalid_vf.py` → `tests/test_invalid_vf.py`
  (the "error_handling" concept is gone from src).
- `tests/test_validate_array_indexing.py` → `tests/test_ast_inspection.py`
  (AST helpers moved to `lcm/utils/ast_inspection.py`).
- Extract the four `validate_transition_probs` state-probs tests from
  `tests/test_pandas_utils.py` into `tests/test_regime.py` (the
  function now lives in `lcm/user_regime.py`). Duplicate the
  three-line `_make_partner_probs_array` helper into the new
  location rather than imposing a cross-file import.

Closes the Phase 1 plan in `Phase 1 — Validation Cleanup.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@read-the-docs-community
Copy link
Copy Markdown

read-the-docs-community Bot commented May 18, 2026

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 18, 2026

Benchmark comparison (main → HEAD)

Comparing 629ac442 (main) → 0052a422 (HEAD)

Benchmark Statistic before after Ratio Alert
aca-baseline execution time 26.281 s 27.115 s 1.03
peak GPU mem 639 MB 581 MB 0.91
compilation time 297.53 s 305.19 s 1.03
peak CPU mem 7.40 GB 7.65 GB 1.03
Mahler-Yum execution time 4.679 s 4.594 s 0.98
peak GPU mem 529 MB 529 MB 1.00
compilation time 14.15 s 13.87 s 0.98
peak CPU mem 1.71 GB 1.72 GB 1.00
Precautionary Savings - Solve execution time 46.3 ms 53.7 ms 1.16
peak GPU mem 101 MB 101 MB 1.00
compilation time 2.66 s 2.82 s 1.06
peak CPU mem 1.14 GB 1.14 GB 1.00
Precautionary Savings - Simulate execution time 120.2 ms 122.5 ms 1.02
peak GPU mem 349 MB 349 MB 1.00
compilation time 5.01 s 5.20 s 1.04
peak CPU mem 1.34 GB 1.33 GB 1.00
Precautionary Savings - Solve & Simulate execution time 159.7 ms 155.8 ms 0.98
peak GPU mem 586 MB 586 MB 1.00
compilation time 7.22 s 6.94 s 0.96
peak CPU mem 1.30 GB 1.30 GB 1.00
Precautionary Savings - Solve & Simulate (irreg) execution time 288.0 ms 291.2 ms 1.01
peak GPU mem 2.20 GB 2.20 GB 1.00
compilation time 7.53 s 7.34 s 0.98
peak CPU mem 1.36 GB 1.35 GB 1.00

@hmgaudecker hmgaudecker changed the title Phase 1: Validation cleanup — delete error_handling.py and validation.py Validation cleanup — delete error_handling.py and validation.py May 19, 2026
hmgaudecker and others added 2 commits May 19, 2026 14:40
…cks.py

regime_building/ is the build-time pipeline (UserRegime → engine.Regime).
The runtime checks fire on solve / simulate, not at build, so they don't
belong here. Split by caller:

- validate_V (+ its diagnostic-attachment helpers) is a tight subroutine
  of the backward-induction loop in solve_brute.py and the V handed to
  simulate.py. Moves to solution/validate_V.py (solve owns producing V;
  simulation imports from there).
- The regime-transition-probability pre-flight sweep is called from
  Model.solve()/simulate() before backward induction runs. Moves to a
  top-level lcm/_transition_checks.py — orthogonal to solution/ and
  simulation/, sibling of engine.py and model_processing.py.

Imports updated in model.py, solve_brute.py, simulate.py and three tests.
Docstring on user_regime.validate_transition_probs updated to point at the
new location.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `_transition_checks.py` module docstring no longer references a
  non-existent `regime_building/static_checks.py`; it describes the
  actual distinction (runtime numerical checks vs. construction-time
  regime-spec validators).
- `regime_building/transitions.py`: the `collect_state_transitions`
  docstring said ShockGrid states get a `lambda: None` stub; the code
  skips them entirely. Docstring corrected.
- `solution/validate_V.py`: `Dict` → `dict` in a Returns section to
  match the `dict[str, Any]` annotation.
- `_transition_checks.py` `_validate_no_reachable_incomplete_targets`:
  drop the override that, when a target regime was absent from the
  source's `state_transitions`, listed every state of the target as
  "missing" — including non-stochastic states that need no explicit
  entry. The preceding line already computes the correct stochastic-
  only set for that case.
- `user_regime.py`: add a scope-boundary note to
  `_validate_function_output_grid_indexing` — the AST check is
  deliberately best-effort and should be deleted rather than hardened
  if it ever produces false positives.
- Replace the two contentless happy-path validator tests with boundary
  inputs: values at the inclusive [0, 1] bounds and row sums just
  inside the sum-to-1 tolerance, so "does not raise" pins the
  tolerance/bound logic instead of being a bare smoke check.
Copy link
Copy Markdown
Member Author

@hmgaudecker hmgaudecker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Autoreview.

@hmgaudecker hmgaudecker requested review from mj023 and timmens May 21, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant