Skip to content

Shift unscalarized array variables with their scalarized elements - #161

Open
baggepinnen wants to merge 1 commit into
mainfrom
shift-unscalarized-array-variables
Open

baggepinnen wants to merge 1 commit into
mainfrom
shift-unscalarized-array-variables

Conversation

@baggepinnen

Copy link
Copy Markdown
Contributor

Fixes SciML/ModelingToolkit.jl#5169.

Problem

shift_discrete_system shifts every discrete variable of a partition forward by one tick
before the partition is compiled. The variables to shift are those that search_variables!
returns for the equations and that are present in fullvars:

for k in discvars
    k in fullvars_set || continue
    MTKBase.isoperator(k, Union{Sample, Hold, Pre}) && continue
    discmap[k] = MTKBase.simplify_shifts(Shift(iv, 1)(k))
end

fullvars only ever contains the scalarized elements of an array variable. The
TearingState constructor searches with SU.Operator atomic, so an occurrence
Shift(t, -1)(x) of an array x is returned whole and scalarized into
Shift(t, -1)(x[i]) entries in fullvars, while shift_discrete_system searches with
only Sample, Hold and Pre atomic and gets back the bare array x, which is not in
fullvars_set.

An array variable used without scalarization, as in f(x(k - 1)) where f is a registered
function of a whole vector and the elements of x are aliased away, is therefore never
entered into the substitution map: its elements are shifted forward while the unscalarized
use is left in place, and the two forms end up one tick apart. For the model in the issue,
the partition equation

0 ~ -Shift(t, 1)(ud(t)) + combine(Shift(t, -1)(xd(t)))*kp

keeps Shift(t, -1)(xd(t)) where combine(xd(t)) was meant, so the compiled system has
unknowns (xdₜ₋₁(t))[1:2] but an observed equation ud(t) ~ combine(xdₜ₋₂(t))*kp.
xdₜ₋₂ is neither an unknown nor a parameter of the system and code generation fails with
UndefVarError: xdₜ₋₂ not defined in ModelingToolkitBase.

This is not specific to the hybrid clock-partition path the issue takes: mtkcompile of a
purely discrete system goes through shift_discrete_system as well and produces the same
result.

Fix

Add the arrays that the scalarized elements of fullvars belong to (with any Shifts
stripped) to fullvars_set, so the unscalarized form is shifted along with the elements.
Entries that are a Sample, Hold or Pre of an array are discarded by the existing
isoperator check.

Tests

Two regression tests, both failing on main and passing here:

  • mtkcompile of a purely discrete system whose array variable is used one tick back as a
    whole: the observed equation must refer to the same shifted variable the unknowns are
    lowered to, and the resulting DiscreteProblem must agree with the one built from the
    element-by-element formulation. On main the first assertion fails and the problem
    construction throws UndefVarError: xdₜ₋₂.
  • The clock-partition path from the issue: after split_system, the unscalarized xd must
    appear at the same tick as its elements.

The existing ModelingToolkitTearing suite and ModelingToolkit's test/clock.jl pass
unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_016KRw9L2c45f9MuTtWcfj8S

`shift_discrete_system` shifts every discrete variable of a partition forward by one
tick. The variables to shift are those that `search_variables!` returns for the
equations and that are present in `fullvars`, but `fullvars` only ever contains the
scalarized elements of an array variable. An array variable used without
scalarization, as in `f(x(k - 1))` where `x` is an array whose elements are aliased
away, therefore never enters the substitution map: its elements are shifted while the
unscalarized use is left in place, which puts the two forms one tick apart.

The compiled system then refers to a variable one step further back than the model
asked for, which is neither an unknown nor a parameter of that system, and code
generation fails with an `UndefVarError`.

Add the arrays that the scalarized elements of `fullvars` belong to, so that the
unscalarized form is shifted along with the elements.

Fixes SciML/ModelingToolkit.jl#5169

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016KRw9L2c45f9MuTtWcfj8S
@baggepinnen
baggepinnen marked this pull request as ready for review September 19, 2026 13:01
baggepinnen added a commit to SciML/ModelingToolkit.jl that referenced this pull request Sep 19, 2026
A clocked array variable whose elements are aliased to sampled inputs and which is
used as a whole one tick back lowered to a variable the compiled partition did not
declare, so code generation for that partition failed
(#5169). JuliaComputing/StateSelection.jl#161 shifts the
unscalarized form along with its scalarized elements.

Add the regression test that fix makes pass — the two spellings of the same control
law must give the same matrices — and raise the ModelingToolkitTearing bound to the
release that will carry it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MxXfPwLvBCg2oeW7jm6MfN
@baggepinnen

Copy link
Copy Markdown
Contributor Author

Verified against the original repro of SciML/ModelingToolkit.jl#5169. With this branch developed into the environment, the compiled partition's observed equation refers to the same shifted variable as its unknowns and the DiscreteProblem builds:

unknowns   = [(xdₜ₋₁(t))[2], (xdₜ₋₁(t))[1]]
observed:
    ud(t) ~ combine(xdₜ₋₁(t))*kp
    ...
DiscreteProblem: ok

SciML/ModelingToolkit.jl#5168 now carries a regression test for the same pattern through linearize_clocked, asserting that the whole-array and the element-by-element spellings of one control law give identical matrices, and raises its ModelingToolkitTearing bound to 1.20.7.

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.

A shift applied to a whole array variable in a clock partition refers to an undeclared variable

1 participant