Skip to content

Add params support to Contractor and Separator#229

Open
dpsanders wants to merge 5 commits intomasterfrom
add-params-to-contractor-separator
Open

Add params support to Contractor and Separator#229
dpsanders wants to merge 5 commits intomasterfrom
add-params-to-contractor-separator

Conversation

@dpsanders
Copy link
Copy Markdown
Member

Summary

Wire ReversePropagation's parameter support through Contractor and Separator so that the same compiled separator can be re-used at runtime with different parameter values, instead of recompiling one separator per parameter value.

ReversePropagation.forward_backward_contractor(ssa, vars, params) already emits a contractor with signature (__inputs, __constraint, __params) -> .... Until now, ICP's Contractor / Separator exposed no way to use it.

Motivation

Periodic Lorentz gas "first-hit" probabilities, computed via interval-validated paving: each blocker disc contributes a not_blocked separator with the disc centre baked in as a numeric literal. With ~127 blockers along a corridor of length 100, that's ~500 atomic separators each going through Symbolics → ReversePropagation → @RuntimeGeneratedFunction. Build cost is ~10 minutes and grows linearly in k.

Rebuilding not_blocked parametrically in (Bx, By) and reusing it across blockers measured ~16 s build at k = 20 (vs ~190 s for the baked-in version), and stays constant in k.

Changes

  • Contractor and Separator gain a params field (default []) and a params kwarg on every constructor.
  • For the exact = true codepath in build_fb_contractor, emit the parametric outer wrapper symmetrically (this codepath rebuilds the wrapper itself for decoration / exact-literal handling, so it had to be taught about params explicitly).
  • (CC::Contractor)(X, constraint, param_vals::Tuple) and (SS::Separator)(X, param_vals::Tuple) are new call methods.
  • Atomic separators built without params silently ignore an incoming param_vals, so parametric and non-parametric separators compose cleanly through / / ¬.
  • CombinationSeparator gains a second closure fp for the parametric call; / / ¬ build both closures so the same composite can be invoked either way.
  • make_function and the separator(...) factory thread params through to Symbolics.build_function and to leaf Separator construction respectively.

Compatibility

Existing 1-arg / 2-arg call signatures are unchanged. New behaviour only kicks in when params=[...] is passed at construction or when param_vals::Tuple is passed at call time.

Tests

Three new testsets (Parametric Contractor, Parametric Separator, mixed composition):

@variables x y r
C = Contractor(x^2 + y^2, [x, y]; params = [r])
C(X, -Inf..1, (interval(1.0),))  # x, y ∈ [-1, 1]
C(X, -Inf..4, (interval(2.0),))  # same compiled C, x, y ∈ [-2, 2]
@variables x y cx cy
S = Separator((x - cx)^2 + (y - cy)^2 <= 1, [x, y]; params = [cx, cy])
S(X, (interval(0.0), interval(0.0)))   # disc at origin
S(X, (interval(5.0), interval(0.0)))   # same compiled S, disc at (5, 0)
Sx = Separator(x >= 0, [x, y])         # no params
Scomb = S  Sx                          # mixed composition
Scomb(X, (interval(0.0), interval(0.0)))  # works; non-param leaf ignores params

Test plan

  • Parametric Contractor testset passes (same compiled C reused at two different r).
  • Parametric Separator testset passes (same compiled S reused at two different (cx, cy)).
  • Mixed parametric ⊓ non-parametric composition passes.
  • End-to-end smoke in downstream Lorentz gas project: parametric not_blocked separator built once, applied across multiple blocker disc centres, returns sensible contractions per blocker.
  • Existing Contractors / Separators testsets — currently fail on this branch with add_rev not defined in IntervalArithmetic, but this is a pre-existing test-environment issue (the test env doesn't dev the local IntervalArithmetic that has the new reverse functions); not introduced by this PR.

Note on commits

This branch is stacked on lorentz-no-icontractors-style preparatory work (RP 0.8 / SSAFunction adoption, exact = true decoration handling, IntervalContractors removal). Those changes weren't on master at PR time, so they appear in the diff as separate commits — happy to rebase / split / target a different base branch if any of them have landed in the meantime.

🤖 Generated with Claude Code

dpsanders and others added 5 commits April 20, 2026 09:52
Contractor now lowers its expression to an SSAFunction once via
cse_equations and passes that SSA to forward_backward_contractor. The
SSA is stored on the contractor so it can be reused across multiple
constraints over the same expression. Separator shares the SSA between
its forward evaluator and its HC4Revise contractor, so each constructor
does the symbolic CSE pass only once.

make_function switches from eval to @RuntimeGeneratedFunction, matching
RP's own shift: generated functions can now be built and invoked within
the same dynamic extent without hitting Julia world-age errors, with
much faster build time.

Bumps compat to ReversePropagation = "0.8" and adds
RuntimeGeneratedFunctions as a direct dep. ICP package version bumped
to 0.16.0. Drive-by fix to a garbled .gitignore entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
IntervalArithmetic v1 taints any interval that touches a plain (non-
ExactReal) Real with the "not guaranteed" (NG) flag. ICP's generated
code is full of those — the literals from the user's expression (the 1
in x^2+y^2==1) and the integer exponents CSE inserts (the 2 in x^2) —
so C.f([3..4, 5..6]) came back as [33.0, 51.0]_com_NG.

Wrapping the literals inside the symbolic expression doesn't survive:
Symbolics normalises ExactReal back to a plain number during its own
simplification. The fix has to happen on the generated Expr.

Add exactify(ex), which walks an Expr and wraps every non-Bool Real
literal in IntervalArithmetic.exact. Thread an exact::Bool = false
keyword through Contractor, Separator, and separator/constraint:

- make_function applies exactify to the build_function output before
  @RuntimeGeneratedFunction when exact=true.
- build_fb_contractor replicates RP's outer wrapper locally so it can
  exactify the body before RGF. When exact=false it forwards to
  forward_backward_contractor unchanged (no overhead in the default).

Default behaviour is unchanged; opting in gives
  julia> constraint(x^2+y^2==1, vars; exact=true).f([3..4, 5..6])
  [33.0, 51.0]_com

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
IEEE 1788 weakens intersect_interval(a, b) to the trv decoration
because, in general, the result may not be a subset of the range of
any function. Every reverse op in IntervalContractors is built on that
same intersect, so both the outer intersect_interval(_value,
_constraint) that RP emits and every narrowing step in the reverse
sweep drag trv through the contractor output. In HC4Revise, however,
each intersection/reverse-op is narrowing *within* an already-evaluated
function range, so min(decoration(inputs)...) is the valid decoration
— not trv.

Add preserve_decorations(ex) in src/utils.jl: a walker that rewrites
the body returned by ReversePropagation.forward_backward_expr so that
every IntervalArithmetic.intersect_interval call and every
ReversePropagation._rev_* reverse-op call has its output re-decorated
with min(decoration(inputs)...). Because Symbolics' toexpr emits the
call head as the actual function value (not Expr(:., Module, :name)),
the matchers compare ex.args[1] === IntervalArithmetic.intersect_interval
and parentmodule(f) === ReversePropagation && startswith(nameof(f), "_rev_")
directly.

_redecorate(x, d) unconditionally replaces the decoration (not min
with the op's own output decoration, which is precisely the weakened
trv we're overriding). Empty intervals stay trv (empty semantics).

build_fb_contractor now runs preserve_decorations alongside exactify
under exact=true. With both on:

  julia> constraint(x^2+y^2==1, vars; exact=true).contractor((-10..10, -10..10))
  [-1.0, 1.0]_com²   # was [-1.0, 1.0]_trv_NG²

(The outer of the Separator triple can still end up _trv because
Separator computes it via boundary ⊔ corner and hull intrinsically
weakens under IEEE 1788 — intended semantics.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverse contractors now live in IntervalArithmetic; ICP imports them via
ReversePropagation. The only remaining IntervalContractors usage in this
package was a `const reverse_operations = IntervalContractors.reverse_operations`
binding that is never referenced internally — drop it together with the
`using IntervalContractors` import and the dependency entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`ReversePropagation.forward_backward_contractor` already accepts a
`params` list and emits a contractor with signature
`(__inputs, __constraint, __params)`. Wire that capability through
`Contractor` and `Separator` so the same compiled separator can be
re-used at runtime with different parameter values, instead of
recompiling one separator per parameter value.

Concrete motivating use case: the periodic Lorentz-gas "first-hit"
problem builds a `not_blocked` separator per blocker disc, with the
disc centre baked into the polynomial. At k = 100 along a corridor
this is ~127 blockers × 4 cases = ~500 separators, each going
through Symbolics → ReversePropagation → @RuntimeGeneratedFunction.
Build cost is ~10 min and grows linearly in k. With (Bx, By) as
parameters, the same `not_blocked` separator is built once (4 cases
total) and re-used per blocker — measured build cost drops from
~190s to ~16s at k = 20, and stays constant in k.

Changes:

* `Contractor` and `Separator` gain a `params` field (default `[]`)
  and a `params` keyword on every constructor. When non-empty, the
  contractor is built with RP's parametric path and the generated
  function takes a third positional `__params` tuple at call time.
* For the `exact = true` codepath in `build_fb_contractor`, emit
  the parametric wrapper symmetrically (this codepath rebuilds the
  outer wrapper itself for decoration / exact-literal handling, so
  it had to be taught about params explicitly).
* `(CC::Contractor)(X, constraint, param_vals::Tuple)` and
  `(SS::Separator)(X, param_vals::Tuple)` are new call methods.
  Atomic separators built without params silently ignore an incoming
  `param_vals`, which lets parametric and non-parametric separators
  compose cleanly through `⊓` / `⊔` / `¬`.
* `CombinationSeparator` gains a second closure `fp` for the
  parametric call; `⊓` / `⊔` / `¬` build both closures so the same
  composite can be invoked either way.
* `make_function` and the `separator(...)` factory thread params
  through to `Symbolics.build_function` and to leaf `Separator`
  construction respectively.
* Tests: parametric Contractor, parametric Separator, and a mixed
  composition (parametric ⊓ non-parametric) — all reuse a single
  compiled object across multiple parameter values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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