Repository navigation
engine: extend positive-value convention to Mul co-factors - #1008
Conversation
The static polarity analyzer already applied the SD positive-value
labeling convention in three places -- the Div one-side arm (share =
pop / total labels total -> share Negative without proving pop > 0),
both both-sides-dependent arms (operand_positive_by_convention), and
the agg feeder-hop analysis (the mul_convention flag) -- but not in the
general Mul one-side arm. The asymmetry meant the flagship logistic
model (net_growth = population * fractional_growth) could not sign
population -> net_growth, so the compounding loop every CLD labels R
was named u1 with Undetermined polarity.
Make the feeder-hop rule the general rule and delete the mul_convention
flag: a bare named co-factor independent of the link source is positive
by convention (passing the dependent side's derivative sign through),
a provable co-factor sign is used exactly, and a compound co-factor
(1 - pop/K -- the logistic class whose value sign genuinely flips
mid-run) stays Unknown. The convention is a labeling decision, not a
proof: runtime loop-score reclassification remains the ground truth and
overrides it, which the Rux fixtures now demonstrate directly (their
bare co-factor g flips sign mid-run, and the runtime pass downgrades
the conventional R to MostlyReinforcing).
The provable-sign predicates are rebuilt on literal_sign /
provable_value_sign, which see through unary negation. That closes a
real mislabel that shipped: the lexer takes no leading sign, so k = -5
parses as Op1(Negative, Const(5)) and the old Const-only predicates
were blind to every negative constant a user can write -- x / k was
labeled Positive by the Div convention when it is provably Negative.
Loop ids derived from newly-signed links churn u{n} -> r{n}/b{n}
(logistic growth, fishbanks harvest); the fishbanks regeneration loop
pins that a non-monotone graphical function still yields Undetermined.
The pysimlin README quickstart now prints r1 (R) and its loop-id
paragraph explains structural ids vs runtime polarity instead of
apologizing for u1.
Code ReviewReviewed the changes to The refactor is sound: Test coverage on the shape-level decisions is comprehensive: compound co-factor stays Unknown (both one-side and single-equation logistic both-sides), provably negative co-factor flips (Mul and Div), inline negated bare co-factor flips by convention, Overall correctness: correct. No findings. 🤖 Reviewed with claude-opus-4-7 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1008 +/- ##
=======================================
Coverage 91.86% 91.86%
=======================================
Files 247 247
Lines 159174 159159 -15
=======================================
- Hits 146224 146211 -13
+ Misses 12950 12948 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 54761f4cdc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Expr2::Op1(crate::ast::UnaryOp::Negative, inner, _, _) => { | ||
| cofactor_value_sign(inner, variables).map(|sign| !sign) | ||
| } | ||
| Expr2::Var(..) | Expr2::Subscript(..) => Some(true), |
There was a problem hiding this comment.
Resolve subscripted constants before applying the convention
For a model such as arr[dim] = -2 and y = x * arr[d1], provable_value_sign never examines an Expr2::Subscript, so this fallback treats the co-factor as positive and reports x -> y as Positive even though its derivative is negative. This produces confidently inverted structural link and loop polarities (and potentially different loop IDs) for negative constant-valued array parameters; resolve the referenced array equation's sign before falling back to the positive-value convention.
Useful? React with 👍 / 👎.
| Expr2::Var(ident, _, _) => { | ||
| let var = variables?.get(ident)?; | ||
| if let Some(Ast::Scalar(var_expr)) = var.ast() { | ||
| literal_sign(var_expr) |
There was a problem hiding this comment.
Exclude stock initializers from constant sign proofs
When ident names a stock, Variable::ast() returns the stock's init_ast, not an equation proving its value is constant. Consequently, a stock initialized with -1 is treated as a provably negative co-factor for the entire model, even if its inflows later make it positive, and links such as x -> y in y = x * stock receive a confidently wrong structural polarity. Restrict this proof to value-bearing Variable::Var equations, or separately prove that a referenced stock cannot change sign.
Useful? React with 👍 / 👎.
Why
The pysimlin README quickstart -- logistic growth, the flagship example -- printed its compounding loop as
u1 (R)and needed a paragraph explaining why structural analysis could not signpopulation -> net_growth. The gap was an asymmetry inside the polarity analyzer, not a fundamental limit: the SD positive-value labeling convention (named quantities are conventionally positive-valued) was already applied by the Div one-side arm (share = pop / totalreads astotal -> shareNegative on every SD diagram), by both both-sides-dependent arms, and by the agg feeder-hop analysis behind amul_conventionflag -- but not by the general Mul one-side arm, which is exactly wherenet_growth = population * fractional_growthlands.What
mul_conventionflag is deleted: a bare named co-factor independent of the link source is positive by convention; a provable co-factor sign (a literal, or a variable whose whole equation is one) is used exactly and can flip; a compound co-factor (1 - pop/K, the logistic class whose value sign genuinely flips mid-run) stays Unknown. The single-equation logistic and non-monotone graphical functions remain Undetermined -- pinned by new tests.literal_sign/provable_value_sign, which see through unary negation. This closes a shipped mislabel: the lexer takes no leading sign, sok = -5parses asOp1(Negative, Const(5))and the old Const-only predicates were blind to every negative constant a user can actually write --x / kwas labeled Positive when it is provably Negative (now pinned in both Div directions).gflips sign mid-run; the structural label is the conventional Reinforcing and the runtime pass downgrades it to MostlyReinforcing).u{n}->r{n}/b{n}(logistic growth, fishbanks harvest). Amusingly, the checked-in logistic LTM reference TSV's original export labeled that rowR1; it had been renamed tou1to match our engine, and now matches the source again.r1 (R), and the loop-id paragraph explains structural ids vs runtime-classified polarity (including when they can legitimately disagree) instead of apologizing foru1.Non-obvious decisions
is_positive_variable/is_negative_variable/is_positive_constant/is_negative_constanthad no production callers after the rework and were deleted rather than kept as wrappers; their tests moved to the new helpers.🤖 Generated with Claude Code
https://claude.ai/code/session_015KzVZykXRTnehv6UY6WZrJ