Skip to content

Fix reaction propensities that reference a kernel via apply_along - #187

Merged
jc-macdonald merged 1 commit into
mainfrom
feature/fix-reaction-reduce-propensity
Aug 25, 2026
Merged

Fix reaction propensities that reference a kernel via apply_along#187
jc-macdonald merged 1 commit into
mainfrom
feature/fix-reaction-reduce-propensity

Conversation

@jc-macdonald

Copy link
Copy Markdown
Member

Closes #186.

Summary

_build_reaction_artifacts compiled propensity_ir_full — the pre-expanded (via expand_reduce_pointwise, empty lhs_assignment) template-symbolic form. For a propensity whose rate reduces over an axis coinciding with the target's own free axis (e.g. a spatial force-of-infection term: apply_along(k[age, age:ap] * S[age:ap], age=ap)), that expansion produces malformed AxisIndex(axis='', ...) subscripts, and compiling them raised ValueError: axes don't match array.

Root cause

Confirmed empirically, not just by inspection: the deterministic equation path never hits this, because it compiles Reduce-bearing expressions via the Reduce-preserving form through lower_to_vector_ast's native Reduce-node lowering — not through expand_reduce_pointwise's template-symbolic expansion. Verified by:

  1. Compiling the exact same rate expression as a normal (non-reaction) transition and confirming the deterministic path produces correct output once the kernel is supplied.
  2. Switching _build_reaction_artifacts to compile propensity_ir_reduce instead of propensity_ir_full.
  3. Confirming both the propensity value and its cross-check against the deterministic path now match a hand-computed expectation for a real (non-degenerate) kernel contraction — not just the earlier * 0 sanity check that happened to cancel the broken term.

Fix

One-line change: compile r.propensity_ir_reduce instead of r.propensity_ir_full in _build_reaction_artifacts. No other code paths touched.

Why this matters for the motivating use case

Found while testing flepimop2-op_system's new reactions adapter wiring against a spatial force-of-infection-style rate — the exact pattern diphtheria_outbreakvacc's own S -> E transition uses. Without this fix, that transition would have silently produced no working reaction artifact (or a confusing runtime error at whatever point downstream a consumer first called propensity_fn) once named for stochastic treatment.

Verification

  • New regression test test_reduce_bearing_kernel_rate_propensity in tests/op_system/test_op_system_reactions.py: covers both propensity correctness and the deterministic-path cross-check for a real kernel contraction.
  • Full test suite: 451 passed (450 existing + 1 new).
  • mypy --strict clean.
  • ruff clean (one genuinely-introduced unused-noqa from a no-longer-needed suppression, removed; remaining findings confirmed pre-existing repo-wide noqa-comments style noise, unrelated to this change).

Test plan

  • pytest — 451 passed
  • mypy --strict — clean
  • ruff check — clean (modulo pre-existing noise)
  • Manual: same rate expression produces identical results via the reaction artifact and the deterministic pytree_eval_fn, for a real (non-degenerate) kernel matrix

_build_reaction_artifacts compiled propensity_ir_full -- the
pre-expanded (via expand_reduce_pointwise, empty lhs_assignment)
template-symbolic form. For a propensity whose rate reduces over an
axis coinciding with the target's own free axis (e.g. a spatial
force-of-infection term, apply_along(k[age,age:ap]*S[age:ap], age=ap)),
that expansion produces malformed AxisIndex(axis='', ...) subscripts,
and compiling them raised "axes don't match array".

Root cause, confirmed empirically: the deterministic equation path
never hits this, because it compiles Reduce-bearing expressions via
the Reduce-preserving form (propensity_ir_reduce's equivalent) through
lower_to_vector_ast's native Reduce-node lowering -- not through
expand_reduce_pointwise's template-symbolic expansion. Verified by
compiling the exact same rate expression as a normal (non-reaction)
transition and confirming it produces correct deterministic output,
then switching _build_reaction_artifacts to compile
propensity_ir_reduce instead of propensity_ir_full and confirming both
the propensity value and its cross-check against the deterministic
path now match a hand-computed expectation for a real (non-degenerate)
kernel contraction.

New regression test (test_reduce_bearing_kernel_rate_propensity):
covers exactly this case, both propensity correctness and the
deterministic-path cross-check.

Found while testing the flepimop2-op_system adapter's reactions
wiring against a spatial force-of-infection-style rate -- the pattern
the diphtheria_outbreakvacc model's own S -> E transition uses, which
would have silently produced no working reaction artifact for that
transition (or a confusing runtime error) once named for stochastic
treatment.

Verified: full test suite (451 = 450 + 1 new) passes; mypy --strict
clean; ruff clean (one genuinely-introduced unused-noqa, now removed;
remaining findings confirmed pre-existing repo-wide noqa-comments
style noise, unrelated).
@jc-macdonald
jc-macdonald merged commit f35d5fc into main Aug 25, 2026
3 of 4 checks passed
@jc-macdonald
jc-macdonald deleted the feature/fix-reaction-reduce-propensity branch August 25, 2026 18:11
jc-macdonald added a commit that referenced this pull request Aug 25, 2026
The kernel-reduction case (apply_along over a mixing kernel, matching
a spatial force-of-infection term) needed the fix in #187
(propensity_ir_reduce instead of propensity_ir_full) to compile at
all. Left as a documented known-limitation comment when this branch
was first opened, since #187 hadn't merged yet and the test would
have been red on this PR alone. Now that #187 is merged into main and
this branch is rebased onto it, restore the real test:
test_option_reactions_merges_mixing_kernels exercises the fix end to
end through the adapter (mixing_kernels merged into propensity_fn
params the same way the other steppers already do), not just at the
op_system compile layer.

Verified: flepimop2-op_system full suite (59 = 58 + 1) passes; ruff
and mypy clean on the touched lines.
jc-macdonald added a commit that referenced this pull request Aug 25, 2026
* Expose CompiledRhs.reactions via the flepimop2-op_system adapter

PR #185 added CompiledRhs.reactions but the flepimop2-op_system
adapter (OpSystemSystem) builds a curated options dict listing
specific compiled fields by name -- it doesn't auto-forward new
CompiledRhs fields, so .reactions was compiled but unreachable by any
engine plugin via stepper.option("reactions"). Also exports
CompiledReaction/ReactionPropensityFn from op_system's public __init__
(missed in #185 -- they existed on compile.py but weren't re-exported,
same gap CompiledRhs/compile_spec already have going the other way,
see the pre-existing mypy note below).

Adds a _make_reaction_steppers helper mirroring
_maybe_make_pytree_stepper's exact pattern: wraps each
CompiledReaction.propensity_fn so mixing_kernels get merged into
params the same way every other stepper already does (via the shared
OpSystemSystem._merged_params), returning a same-shape tuple of
CompiledReaction with only propensity_fn replaced (dataclasses.replace).
Wired into options["reactions"] alongside the other stepper options.

Found and documented (not fixed, out of scope here) a real limitation
while testing: a transition rate that references a mixing kernel
through an apply_along reduction (e.g. a spatial force-of-infection
term) isn't supported by the v1 reaction-artifact scope --
_compile_ir_expr raises "axes don't match array" trying to lower a
Reduce-bearing propensity against a from-axes-only target shape. Rates
that only reference from-side axis-indexed shaped params work fine.
Left a detailed comment in tests/test_system.py where this was found;
opening a tracking issue against op_system for it.

Also noted, not touched: op_system's own __init__ re-export of
CompiledRhs/compile_spec (now also CompiledReaction/ReactionPropensityFn)
doesn't satisfy mypy's explicit-reexport check from a consuming
package's perspective -- confirmed pre-existing (2 of the resulting 3
errors already existed before this commit, via git stash) as a
harmless mismatch between this __all__-based re-export pattern and
mypy's stricter check; not fixed here since a real fix touches
op_system's export mechanism broadly, outside this PR's scope.

Verified: flepimop2-op_system's full test suite (58 tests, 4 new)
passes; ruff clean on all touched lines (pre-existing repo-wide
noqa-comments style noise confirmed unrelated, same as op_system
proper); mypy clean on all touched lines (pre-existing unrelated
errors elsewhere in both touched files confirmed via git stash
before/after comparison, not introduced here). Manually verified
end-to-end: OpSystemSystem(spec=...).options["reactions"] surfaces a
working, correctly-valued propensity_fn for a named collapse-to-
pinned-target transition, matching the diphtheria model's real usage
shape.

* Restore reactions/mixing_kernels confirming test now that #187 is merged

The kernel-reduction case (apply_along over a mixing kernel, matching
a spatial force-of-infection term) needed the fix in #187
(propensity_ir_reduce instead of propensity_ir_full) to compile at
all. Left as a documented known-limitation comment when this branch
was first opened, since #187 hadn't merged yet and the test would
have been red on this PR alone. Now that #187 is merged into main and
this branch is rebased onto it, restore the real test:
test_option_reactions_merges_mixing_kernels exercises the fix end to
end through the adapter (mixing_kernels merged into propensity_fn
params the same way the other steppers already do), not just at the
op_system compile layer.

Verified: flepimop2-op_system full suite (59 = 58 + 1) passes; ruff
and mypy clean on the touched lines.
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.

Reaction propensities that reference a mixing kernel via apply_along fail to compile

1 participant