Summary
analyze_link_polarity in src/simlin-engine/src/ltm/polarity.rs (Lookup arm, lines 134-170) analyzes the polarity of Lookup builtins by combining the argument's polarity with the table's monotonicity (via analyze_graphical_function_polarity). The table is found by matching the lookup's table_expr against Expr2::Var(name):
let table_name = match table_expr.as_ref() {
Expr2::Var(name, _, _) => Some(name.as_str()),
_ => None,
};
Per-element graphical functions (where the table is itself an arrayed variable, referenced as arr[D]) appear as Expr2::Subscript(name, indices, ...) rather than Expr2::Var(name). The current code has an explicit TODO at line 148 acknowledging this:
// TODO: support Expr2::Subscript for subscripted lookup tables (per-element gf)
When this case fires, polarity analysis falls through to LinkPolarity::Unknown, which conservatively classifies any loop containing such a link as Undetermined.
Why this matters
Models that use per-element graphical functions (e.g., a region-specific dose-response curve effect[Region] = LOOKUP(curve[Region], dose)) lose static polarity detection on every link through the lookup. Loop polarity falls back to runtime classification via LoopPolarity::from_runtime_scores, which works but loses information when the simulation never actually exercises the polarity (a parameter never crosses an inflection, etc.).
Static polarity is preferred over runtime polarity wherever possible -- static covers the entire feedback structure (including loops the simulation never activates), feeds into the structural classification of loop ID assignment (r1, b1, u1), and is used by downstream consumers like the simplified-CLD layer in src/diagram. An Unknown static polarity forces a u-prefixed loop ID even when runtime classification could later refine it.
Proposal
- Extend the
match table_expr.as_ref() to handle Expr2::Subscript(name, indices, _, _).
- For each subscript shape:
- FixedIndex (e.g.,
LOOKUP(curve[NYC], x)): resolve the specific element's table at compile time. The table for that element is in the variable's tables Vec.
- Bare A2A (e.g., inside
effect[D] = LOOKUP(curve[D], dose[D])): determine that all elements' tables share the same monotonicity (or return Unknown if they differ), reusing the existing arrayed-equation aggregation pattern from analyze_link_polarity's Ast::Arrayed arm.
- Wildcard / DynamicIndex: conservatively return
Unknown (current behavior).
- Add tests covering: same-monotonicity per-element tables, mixed-monotonicity per-element tables, fixed-index references, A2A references.
File references
- TODO marker:
src/simlin-engine/src/ltm/polarity.rs:148
- Lookup polarity logic:
src/simlin-engine/src/ltm/polarity.rs:134-170
- Graphical function monotonicity helper:
analyze_graphical_function_polarity in same file
- Variable tables storage:
src/simlin-engine/src/variable.rs Variable::Var.tables
Component(s) affected
simlin-engine (LTM polarity analysis); downstream effect on loop ID classification and the simplified-CLD layer in src/diagram.
Discovery context
Identified during a deep design review of LTM completeness. Distinct from #480 (which covers array reducers SUM/MEAN/etc. returning Unknown) and #492 (GF strict-monotonicity EPSILON precision); this entry is specifically about Expr2::Subscript handling in the Lookup arm of analyze_link_polarity. Tech-debt.md item 21 mirrors #480 and does not cover this case.
Labels / epic
Labels: ltm, polarity-analysis, completeness
Epic: #488 (LTM)
Summary
analyze_link_polarityinsrc/simlin-engine/src/ltm/polarity.rs(Lookup arm, lines 134-170) analyzes the polarity ofLookupbuiltins by combining the argument's polarity with the table's monotonicity (viaanalyze_graphical_function_polarity). The table is found by matching the lookup'stable_expragainstExpr2::Var(name):Per-element graphical functions (where the table is itself an arrayed variable, referenced as
arr[D]) appear asExpr2::Subscript(name, indices, ...)rather thanExpr2::Var(name). The current code has an explicit TODO at line 148 acknowledging this:// TODO: support Expr2::Subscript for subscripted lookup tables (per-element gf)When this case fires, polarity analysis falls through to
LinkPolarity::Unknown, which conservatively classifies any loop containing such a link asUndetermined.Why this matters
Models that use per-element graphical functions (e.g., a region-specific dose-response curve
effect[Region] = LOOKUP(curve[Region], dose)) lose static polarity detection on every link through the lookup. Loop polarity falls back to runtime classification viaLoopPolarity::from_runtime_scores, which works but loses information when the simulation never actually exercises the polarity (a parameter never crosses an inflection, etc.).Static polarity is preferred over runtime polarity wherever possible -- static covers the entire feedback structure (including loops the simulation never activates), feeds into the structural classification of loop ID assignment (
r1,b1,u1), and is used by downstream consumers like the simplified-CLD layer insrc/diagram. AnUnknownstatic polarity forces au-prefixed loop ID even when runtime classification could later refine it.Proposal
match table_expr.as_ref()to handleExpr2::Subscript(name, indices, _, _).LOOKUP(curve[NYC], x)): resolve the specific element's table at compile time. The table for that element is in the variable'stablesVec.effect[D] = LOOKUP(curve[D], dose[D])): determine that all elements' tables share the same monotonicity (or returnUnknownif they differ), reusing the existing arrayed-equation aggregation pattern fromanalyze_link_polarity'sAst::Arrayedarm.Unknown(current behavior).File references
src/simlin-engine/src/ltm/polarity.rs:148src/simlin-engine/src/ltm/polarity.rs:134-170analyze_graphical_function_polarityin same filesrc/simlin-engine/src/variable.rsVariable::Var.tablesComponent(s) affected
simlin-engine(LTM polarity analysis); downstream effect on loop ID classification and the simplified-CLD layer insrc/diagram.Discovery context
Identified during a deep design review of LTM completeness. Distinct from #480 (which covers array reducers SUM/MEAN/etc. returning
Unknown) and #492 (GF strict-monotonicity EPSILON precision); this entry is specifically aboutExpr2::Subscripthandling in theLookuparm ofanalyze_link_polarity. Tech-debt.md item 21 mirrors #480 and does not cover this case.Labels / epic
Labels:
ltm,polarity-analysis,completenessEpic: #488 (LTM)