Most constraints do not need Condition. The choice depends on what pylcm must know
in order to express the restriction along the selected solver’s candidate route.
Use an ordinary callable when the Boolean result is enough¶
import jax.numpy as jnp
def feasible_consumption(*, consumption, resources):
return (consumption > 0) & (consumption <= resources) & jnp.isfinite(consumption)GridSearch evaluates this predicate on complete state-action candidates. A specialized
solver can also evaluate it at any route stage where every argument exists.
Use Condition when the comparison structure must survive¶
A callable exposes inputs and an output, but not the fact that it means savings >= 0
or wealth < asset_limit. Retain that structure when a specialized solver must:
prove that its construction already enforces the constraint;
compile a named boundary into its candidate partition;
determine strict versus inclusive boundary ownership;
explain precisely why the constraint cannot be evaluated on its route.
You may also choose Condition simply because its declarative form is clearer. It does
not expand the mathematical set of Booleans available to grid search, and it does not
make an unsupported constraint supported by an EGM solver.
Syntax¶
ref(name) refers to a state, action, DAG output, parameter, or declared margin role.
import lcm
# Named value and literal.
nonnegative_savings = lcm.ref("savings") >= 0.0
# Named value and named value.
below_asset_limit = lcm.ref("wealth") < lcm.ref("asset_limit")
# Intersection and union.
working_age = (lcm.ref("age") >= 18) & (lcm.ref("age") < 67)
insured_or_eligible = (lcm.ref("insured") == 1) | (
lcm.ref("income") <= lcm.ref("eligibility_limit")
)
# Complement.
not_retired = ~(lcm.ref("retired") == 1)
# Conditional requirement.
hours_if_working = lcm.implies(
premise=lcm.ref("working") == 1,
consequent=lcm.ref("hours") <= 40,
)Use &, |, and ~, not Python’s and, or, and not. Comparisons may use <,
<=, ==, !=, >=, and >. Strictness determines which side owns an exact boundary
point.
When a particular solver needs retained structure¶
| Restriction | Broad callable evaluation | Why retained structure may be needed |
|---|---|---|
| Consumption is finite and below resources | Complete grid-search candidate | Usually not needed |
| Savings lower bound | Savings may be constructed rather than exposed at every EGM stage | Solver proves it from its grid |
| Liquid-state interval | NBEGM partitions work by declared liquid boundaries | Compiler needs names, thresholds, and strictness |
| Arbitrary Boolean using unavailable intermediate values | Grid search may expose them | Structure cannot help unless the solver implements a proof/compiler |
The last row is important: Condition is information, not magic.
Prefer a margin-specific lower bound¶
For the borrowing constraint of a declared liquid margin, prefer:
borrowing_limit = lcm.post_decision_lower_bound(
margin=liquid,
lower=0.0,
)It prevents the name in the condition from drifting away from the
LiquidMargin.post_decision_state name and lets the solver compare the lower bound with
its savings grid.
State and discrete-action predicates in EGM continuation queries¶
DC-EGM and admitted NB-EGM routes evaluate eligible current-state and discrete-action predicates at the actual continuation query. They retain each conditional alternative, apply its budget and predicates, then aggregate the surviving choices. This also preserves a feasible region lying between stored nodes. Predicates depending on a continuous action still require an existing solver proof or compilation route.
Generic value-function interpolation, including a GridSearch parent and canonical simulation continuation valuation, uses values masked at stored nodes. Its off-node value can therefore differ from an EGM-family conditional carry query. Simulation checks current-action constraints on the actual subjects and candidate actions.
Budget kinks and cliffs are not ordinary feasibility constraints. Declare them with case pieces or a piecewise-affine schedule.