Julia 1.13 release: OMBackend 0.5.1 (step 7) - #79
Merged
Merged
Conversation
* Adapt to compacted NFEquation/NFStatement
EQUATION_IF and ALG_WHEN became @CUniontype constructor functions, not types.
Type the STRUCTURAL_IF_EQUATION / DOCC if-equation fields as the concrete
backing struct EquationImpl (was EQUATION_IF, which broke precompile), and
replace the three stmt isa ALG_WHEN checks with isvariant (isa needs a type,
not a constructor function).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Complete DAE.ASUB.sub -> List{Subscript} migration
DAE.ASUB.sub changed from List{Exp} to List{Subscript} (DAE.jl, OMC-correct),
but consumers still treated subscripts as expressions. Migrate every layer:
- Construction: wrap indices in DAE.INDEX (Causalize, BDAECreate,
simCodeFunctions; _iconstExpList -> _iconstIndexSubList).
- Generic DAE traversal (Util.jl): subscript-aware traverse helpers; the ASUB
case uses fresh locals (the shared expl_1 is typed List{DAE.Exp}) and makeASUB
takes untyped sub (Cons{DAE.INDEX} is not <: List{DAE.Subscript} for dispatch;
the DAE.ASUB constructor widens via convert).
- Consumer matches (simCodeUtil): match DAE.INDEX(DAE.ICONST(i)) for constant
subscript suffixes; unwrap inner exprs for cref collection.
- DAE<->SimCode bridge (simCodeStructureUtil): toSimExp extracts each
subscript's inner exp; toDAEExp wraps SimCode ASUB exprs in DAE.INDEX.
- Dump (backendDump): ASUB case uses a fresh local, not the typed expl.
- Codegen (MTK_CodeGeneration, MTK_CodeGenerationUtil): unwrap subscript inner
exprs before lowering.
OM.jl light regression: 297/297 (was 277/1/15), including the previously
@test_broken EnumForTableLookup, which was itself an ASUB-subscript issue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ASUB migration: recurse into subscript inner exprs in resolveConstantIfExp
The sixth ASUB-subscript consumer, reached only by larger MSL models (Engine1a,
AxisRotation, PointGravity, OneWayClutch): resolveConstantIfExp's DAE.ASUB case
passed each sub (now a DAE.INDEX) to itself as if it were a DAE.Exp. Recurse
into each subscript's inner expression and rebuild the subscript, matching the
rest of the DAE.ASUB.sub -> List{Subscript} migration.
Heavy-suite MSL tiers: cleared 6 of 10 failures (all the resolveConstantIfExp
errors); the remaining 4 are pre-existing solver-convergence (MaxIters) on hard
hybrid models and a separate flat-export issue, not ASUB-related.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Follow the NFEquationImpl rename and List{Subscript} ASUB subscripts
OMFrontend names the compacted equation struct NFEquationImpl, and DAE.ASUB.sub is now a List{DAE.Subscript}. Subscripts are wrapped in DAE.INDEX when built and unwrapped when matched.
Co-Authored-By: JKRT <jtinnerholm@gmail.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Version 0.5.1
Use a full semver version.
Co-Authored-By: JKRT <jtinnerholm@gmail.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: JKRT <jtinnerholm@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: SVAGEN26 <247156613+SVAGEN26@users.noreply.github.com>
DAE.ASUB.sub is a List{Subscript} since DAE 1.2.0, but two expression
walkers still treated each subscript as an expression:
- simCodeCheck _walkExpChildren: every subscripted residual raised
"unsupported DAE.Exp variant: DAE.INDEX" (108 warnings on MSL
Digital.Examples.Counter);
- simCodeUtil _containsDerCallDAE: a der() inside a subscript was never
found (the subscript fell through to `false`).
Both now look at the expression inside INDEX/SLICE/WHOLE_NONEXP;
WHOLEDIM has none.
Assisted-by: Claude Opus 5.5
The alias passes iterated Dict{String, ...} containers, so their choices
followed the hash of the variable names, and Julia 1.13 changed
hash(::String):
- eliminateAliasVariables started its component search from
keys(adjList), so ties between equally ranked variables picked the
representative by hash, and it walked aliasMap to build the eliminated
variables and equations;
- eliminateRHSEquivalentEquations processed pairs(rhsGroups) and walked
its aliasMap the same way.
MSL OvervoltageProtection got a different (equivalent) system on 1.13
than on 1.12. These containers are OrderedDicts now, so the order follows
the equations; for OvervoltageProtection the generated MTK code is the
same on 1.12 and 1.13.
Assisted-by: Claude Opus 5.5
isvariant and the compacted-uniontype @match support (including qualified OMFrontend.Frontend.VARIANT(...) patterns) come from MetaModelica 0.4.0. Assisted-by: Claude Opus 5.5
- .github/workflows/omjl.yml calls OM.jl's integration workflow (JKRT/OM.jl .github/workflows/omjl-ci.yml) on every pull request and on pushes to master and 1.13: the OMBackend tests against the other OM.jl packages at the matching branch, then OM.jl's test suite, on Julia 1.12 and 1.13. - CI.yml: the sibling checkout uses OM.jl's checkout-siblings action (the PR branch if a sibling has it, else the PR base branch, else master) instead of cloning every sibling's default branch; pushes to 1.13 run it too. The om-rt job is removed: omjl.yml replaces it, and it tested JKRT/OM.jl master against master siblings whatever the change targeted. Assisted-by: Claude Opus 5.5
Relations with the same zero set (`w_rel <= 0`, `w_rel > 0`) had one event callback each. Both sat on the same root with opposite signs; where the integrator stopped on it they fired alternately and flipped the cluster without time advancing (MSL Rotational OneWayClutchDisengaged, maxiters with Rodas5 at tight tolerances). They now share one callback whose crossing pins all of them. That second callback also stood in for Modelica event iteration: in the OneWayClutch the crossing sets `stuck`, and `locked = pre(stuck) and not startForward` follows only on a second evaluation. A cluster synthesized from discrete equations (condition: change(<relation>) terms) that reads its own pre() values now gets an ImperativeAffect that evaluates the body until a pass changes nothing (§8.6). User whens and other clusters keep the one-pass equation affects, and so does a cluster whose body cannot be lowered into an ImperativeAffect (typed _UnsupportedInAffect, e.g. sample()). The lowering gained casts, and edge()/change() of a variable or of a crossing relation. Assisted-by: Claude Opus 5.5
OM.jl folds parameter values into the equations at compile time: the lowering replaces a binding that refers to another parameter by that parameter's value, eliminateConstantParameters inlines every parameter with a constant binding, and the code generator assigns each parameter's value to a Julia variable of the same name, so the equations capture numbers even for the parameters MTK still lists. Changing a parameter meant recompiling. OMBackend.withTunableParameters(names) do ... end (a scoped value, read by the lowering, the SimCode passes and the code generator) keeps the named parameters: Causalize.resolveCrefBindings! keeps references to them (`ramp.height = Va`), _boundParameterExpression, the lookup behind every compile-time evaluation of a parameter, reports no binding for them, eliminateConstantParameters protects them, and the code generator leaves the symbolic parameter in the equations (in chunked parameter declarations bound as a local: the eval'd global is too new for the running function). A parameter bound to a tunable one (rate = 3*k, loadInertia.J = JLoad) follows it. Simulating with other values: simulateModel / resimulateModel (whose `parameters` argument was ignored) / OM.simulate(...; parameters) set them on the cached build after restoring its pristine parameter vector (a solved problem carries it as the event callbacks left it), re-solve a DAE's consistent initial state for them (the build keeps its initialization per DAE problem; a warm mode of _solveDAEInitialization! starts from the compiled state and skips the cold-start phases: MSL fullRobot 1.3 s -> 0.09 s), and run the normal simulate path. pristineParameters(model) and isTunable(model, names) expose the state. A re-simulation matches a recompiled model (MSL RobotR3.fullRobot with another payload and axis-2 gains: to 1e-9). Default behaviour is unchanged. Assisted-by: Claude Opus 5.5
withTunableParameters(["nn.W1"]) kept nothing: OM.jl scalarizes array parameters (nn_W1[1][2]) and the tunable set held the array's name only. An array is now tunable as a whole. isTunableParameter matches its elements by removing trailing subscripts one at a time (also inside arrays of components, gs[1]_g[2] -> gs[1]_g, and record fields); eliminateConstantParameters protects every element, and eliminateDeadParameters keeps unused ones (a network output the model does not use would otherwise make the array one row short). Each IMTK build records the tunable set it was compiled with, and simulate(...; parameters) and isTunable accept only those names. Being a parameter of the problem is not enough: a parameter referenced by a start attribute stays one, but its value is compiled into the equations, so setting it silently did nothing. A build that fails or is skipped (another mode, structural models) forgets the model's previous build instead of letting it answer for the new one. A tunable parameter read in an initial algorithm, which is evaluated when the module loads, is an error instead of a silently constant value. Also: eventCallbacks(model) returns the cached build's callbacks without building the problem again; A[2,1] is accepted for A[2][1]. Assisted-by: Claude Opus 5.5
…ucts
Two gaps found with the MSL V6 engine (EngineV6_analytic, CylinderBase):
A record-typed parameter binding that ends up among the initial
equations (FixedRotation's R_rel_inv = Frames.from_T(transpose(R_rel.T),
zeros(3))) reached SimCode as a COMPLEX_EQUATION, which it has no form
for ("toSim: unsupported BDAE.Equation variant"). expandComplexEquations
now expands record equations in initialEqs as it does in orderedEqs.
A vector scalar product the frontend could not expand element by element
(e * (frame_ib.f - Frames.resolveRelative(...)): a vector-valued call as
an operand) was collapsed to OP_MUL and generated as Julia's *, which
has no vector-vector method. It is now its own OpKind, OP_DOT, generated
as vectorDot (as the algorithm path already did) and mapped back to
MUL_SCALAR_PRODUCT.
Assisted-by: Claude Opus 5.5
Asserts outside when-clauses were dropped: BDAECreate lowered them to ASSERT_EQUATION, but the SimCode split into residual/when/if lists had no place for them, and the lifters for regular algorithms skipped STMT_ASSERT. SIM_CODE now carries them (asserts), with non-tunable parameters inlined, canonical names, and the constant, alias and parameter substitutions of the elimination passes; the variables they read count as used, so no pass drops them. The MTK code generator adds one DiscreteCallback that checks them after initialization and after each accepted step, as OpenModelica does, not in the right-hand side. Conditions and messages are lowered like event affects and read variables by name through the problem (states, observed and eliminated variables, parameters). AssertionLevel.error throws ModelicaAssertionError(time, message, condition), with the time located within the step by bisection on its interpolation; AssertionLevel.warning warns once each time the condition becomes false. Parameter-only asserts are checked once. A violated assert is rethrown without the iMTK fallback retry or a failure log. Function asserts read AssertionLevel by index with 1 as error, but AssertionLevel = enumeration(warning, error): error-level asserts only warned and warning-level ones threw. The level is now matched by name. Assisted-by: Claude Opus 5.5
…me conditions - An if-expression whose condition has its relations inside noEvent stays inline (literal, no event) instead of being lifted to an event if-equation (MLS 8.5). - A branch event of a multi-branch if-equation wrote the firing branch's initial value into every other branch's ifCond (`if x<0 ... elseif x<1 ... else` went back to the first branch at x = 1). The branch conditions nest first-true-wins, so the other ifConds keep their value. - If-equations whose conditions are all discrete or parameters gate on the condition directly already; they no longer get a crossing function too (e.g. MSL positiveBranch, fixed at initialization). - Pure-time conditions set their ifCond by the crossing's direction: a single constant value (with MTK's affect_neg defaulting to affect) made `sin(time) > 0.5` switch on and never off. Assisted-by: Claude Opus 5.5
A relation keeps its value between events and changes only at events; its crossing function had no hysteresis, so a function starting exactly at its threshold (MSL EngineV6: two cylinders start at a dead centre) produced no event (DiffEq needs a strictly signed previous value) and the branch chosen by rounding stayed, and two coinciding crossings (cylinder pairs, `x>0` next to `x<=0`) re-fired each other at one instant until MaxIters. - If-equation branch events use g = zc + H*scale*(1 - 2*ifCond), H = 1e-4*reltol (parameter _zcHysteresis, set per solve), scale = 1 + max(|operands|): OpenModelica's LessZC. g is never zero after initialization or an event (FMI 3.0 3.1.1). - Every relation branch gets a literal initialize affect (per relation, strict or closed, and/or/not) from the solved initial state; if one differs from the compiled value the algebraic unknowns are solved again. - Event iteration: after any event every relation is evaluated again with OpenModelica's inclusive rule (withRelationRefresh). - Branch events re-solve the algebraic unknowns (BrownFullBasicInit, not NoInit): `y = if ... then 1 else 2` kept its old value until the end of the next step and interpolated wrongly in between. - Discrete branch conditions gate per branch (an elseif chain with a Boolean set by a when next to relations). - The duplicate t0 point saved after initialize affects is dropped (sol(0.0) was NaN). Callbacks no longer clear the modified flag an event set. Assisted-by: Claude Opus 5.5
getIrreducibleVars marked every state and every variable of every if-equation irreducible, so MTK had to keep them all as unknowns: the MSL EngineV6 had 169 unknowns (OpenModelica: 4 states), positions and velocities were solved as algebraic unknowns, and start values that are not fixed acted as fixed. Now irreducible are only: - the variable an if-equation assigns when all its branches assign one variable (every lifted if-expression); the rule now also reads the residual form the branches have by then, so it never matched before; - the states when-equations read or write (their callbacks index the state vector by name); - the discretes in when conditions, and THETA. What relied on the old marking: - The initial-value map came from the irreducible list: a reducible state got no start value (VSS sub-models: "Initialization incomplete"). startValueVariableNames adds every state. - identifyOutputOnlyVariables follows residual equations only, so a variable read only inside an if-equation was dropped; it now starts from what if-equations read. - The relation getters used SII getu on expressions; over observed variables SII builds a parameter-only getter whose function reads the unknowns as undefined globals. They build the observed function directly, and the relation entries are built in the latest world. - splitInitialValues ran simplify on alias candidates with few variables; with few unknowns every full equation qualifies, however large (the V6 cylinder rig ran out of memory). Capped at 200 nodes. - checkNamedStateLookups warns when a name the legacy callbacks index the state vector by is not an unknown. The V6 cylinder rig: 5 unknowns (was 8), 0.5 s in 0.9 s, matching OpenModelica (phi 31.885989 vs 31.885991) with its 10 event times. A pendulum with start values only on x and y now rests at the bottom with phi, phid as states, as in OpenModelica. Assisted-by: Claude Opus 5.5
… state splitInitialValues kept the start of a differential state as a hard u0 unless the state appears in an algebraic equation. An initialization equation can determine it too: MSL filters start in steady state, `der(x) = 0`, which fixes x through its differential equation. Pinning x at its start as well over-determined the initialization; the DirectRHS init solve then freed every variable and moved the fixed=true ones. In the MSL EngineV6 (filter.x no longer in an algebraic equation now that the compiler chooses the states) the crank started at phi = 0.0021 and w = 9.9917 instead of 0 and 10, and every later value was off by ~2e-4. Now a non-fixed start is also demoted to a guess when an initialization equation determines the state (its left-hand side, or x in D(x)). The V6 then matches OpenModelica from the start (load.w(0.02) = 15.03401, OpenModelica 15.033995; the pressures to 7 digits). Assisted-by: Claude Opus 5.5
A when on one relation went to a ContinuousCallback on `a - b` with only `affect!`, which SciML also calls on the falling edge (`affect_neg!` defaults to `affect!`; the comment on the zero-crossing transformation had the directions the wrong way round), and a relation that starts at its threshold never fired (DiffEq ignores a condition that starts at 0). Such a when now goes through relationWhenCallback, the same semantics as the if-equation relations (MLS 8.5, OpenModelica's relationhysteresis): - the relation is buffered, set literally at the start (or when a mode of a variable-structure model becomes active), and its crossing function is shifted by the hysteresis relative to it; - the body runs only when the relation becomes true (MLS 8.3.5); - after the body the relation is evaluated again, so `reinit(x, 0)` in `when x > 0.8` makes it false at once; - operands are read by name (unknowns, observed variables, parameters), per system, so a mode's callbacks stay inactive in the other modes; - the generated closures are called in the latest world (a dynamic VSS model compiles a mode while it runs). Checked against OpenModelica 1.27.1 on six models (start at a threshold, a closed relation true at the start, rising edges only, elsewhen, a when on a Boolean, a bouncing ball). Fixed-start discretes also get their initialization equation (getFixedStartConstraintsMTK was given states, OCC and algebraic variables only): `Boolean b(start = true, fixed = true)` set only by a when started at false. Assisted-by: Claude Opus 5.5
An event can move a relation's operand without that relation's crossing function crossing zero (a reinit, a branch switch that changes an algebraic variable, complementary relations on one zero set). After every step one DiscreteCallback now checks every buffered relation against the state with OpenModelica's hysteresis rule and, where one disagrees, iterates in sweeps as OpenModelica's updateDiscreteSystem: 1. every relation (if-equation relations and whens on a relation) is evaluated on the same state; 2. the whens on a relation that became true run their bodies, with pre() read from a snapshot of the state before the bodies; then the whens on discrete conditions, each checked just before it runs; 3. the algebraic unknowns are solved again (initialize_dae! for DAEs, also DAEFunction problems), until nothing changes. A when's continuous callback now only locates the event. A model that does not settle in max(20, 3 per relation or when) sweeps stops with an error and ReturnCode.Failure, as OpenModelica does; a failed re-solve reports an error. Asserts are checked after the iteration. The iteration is emitted for models without if-equation relations too. Whens on discrete conditions (a changed discrete, an algorithm section's inputs) are emitted through discreteWhenCallback and, where a model has buffered relations, run inside the iteration: before, they ran ahead of it and saw a discrete set by a relation's when one step late (AlgorithmArrayIfChain kept the old branch until t = 1). The condition reset of a discrete when (a de-bounce of bare Boolean triggers) was computed on the zero-crossing form, which unwraps a top-level change(k) to k: k was reset to 0 and the when fired again. It now uses the condition as written. relationRefresh.jl is organised as IfRelations, the whens on a relation, the whens on a discrete condition, and EventIteration with small functions; pre() from the snapshot is a ScopedValue (PRE_FROM_SNAPSHOT) set while a relation when's body is lowered. Assisted-by: Claude Opus 5.5
Rodas5's embedded error estimate misses a right-hand side that depends on time explicitly: on x' = cos(39t) it took 10 steps at reltol 1e-8 and ended far from the solution; Rodas5P is right (SciML recommends it over Rodas5). OMBackend.defaultSolver() (Rodas5P) and daeFallbackSolver() (FBDF, what a mass-matrix system switches to when Rosenbrock methods handle its shape badly) are now the only places that name the solvers: simulateModel, resimulateModel, the generated simulate and simulateFromBuild, and the precompile workloads use them. Finite differences are asked for with ADTypes.AutoFiniteDiff(), which OrdinaryDiffEq 6 and 7 both accept (7 no longer takes autodiff = false); ADTypes is a direct dependency now. On the current SciML stack (OrdinaryDiffEqCore 3.33) Rodas5P runs into the known DAE event bug fixed upstream in OrdinaryDiffEqCore 4.17.1: MSL HeatingRectifier on Julia 1.12 reaches Capacitor1.v(3) = 0.2898 instead of 0.3087. So this lands with the SciML upgrade, on 1.13-sciml. Assisted-by: Claude Opus 5.5
The upgrade that brings the fix of the DAE interpolation after events (OrdinaryDiffEqCore >= 4.17.1): ModelingToolkit 11.45.1, SciMLBase 3.56, DiffEqBase 7.21, OrdinaryDiffEq 7.8.1 (Core 4.18.0). ModelingToolkit's compat is relaxed from = 11.21.0 to 11.21. What the new stack needed: - Names that moved: BrownFullBasicInit from DiffEqBase, NewtonRaphson from NonlinearSolve (new dependency), DFBDF/QNDF from OrdinaryDiffEqBDF (new dependency), OrderedDict from OrderedCollections, lower_varname and var_from_nested_derivative from Symbolics. - ModelingToolkit 11.45 turns a state with der(v) = 0 into a parameter unless it is irreducible: every discrete variable, and every state whose equation is der(v) = 0 (zeroDerivativeSymbols), is irreducible now. Events and initial algorithms set them and read them by name. - Integer, Boolean and enumeration variables are rounded back to integers when a step leaves round-off in them (withIntegralDiscretes): Rosenbrock steps moved a counter to 3.0000000000000004. - The older when/elsewhen callbacks locate events with RightRootFind: from the left of the root, DiffEqBase 7 finds the same crossing again (ElseWhenBasic looped at x = 0.3 until MaxIters). - defaultInitializeKwargs: BrownFullBasicInit at the solve's abstol when the caller chose no initialization, as OrdinaryDiffEq did before OrdinaryDiffEqCore 4 (which only checks u0). OM.jl's suite on Julia 1.13 with this commit: 478 pass, 1 fail, 5 broken. Rotational.Friction fails: after a lock the pre-memory friction path left the stuck torque stale. The discrete clusters later on this branch fix it. Assisted-by: Claude Opus 5.5
SciMLBase 3 dropped the affect_neg! of VectorContinuousCallback: its affect! is called once per event instant with the events of all components, 0, +1 (upcrossing) or -1 (downcrossing). The type-erased merge of the continuous callbacks (_eraseContinuousCallbacks, used by the cached-build solves of OMSurrogates) built the old form and failed with a MethodError. _MergedContinuousEvents dispatches the events to each sub-callback (affect! or affect_neg! by the sign, a vector sub its slice); the old form stays for SciMLBase 2. Assisted-by: Claude Opus 5.5
MSL fullRobot never finished building. Since the compiler chooses the states, it reduces to 119 unknowns with 1809 observed equations. With the observed equations substituted, the right-hand sides are 11.6 million nodes as a tree but only 1793 distinct ones. The direct RHS is built with common subexpression elimination and compiles in 0.2 s. The symbolic initialization rows were built without it, and Julia never finished lowering that function. Build them with CSE as well. fullRobot validates 42/42 against the Dymola reference again, built and solved in about a minute. Assisted-by: Claude Opus 5.5
The whens over the relations of coupled discretes, lifted from their discrete equations: PartialFriction's free, startForward, startBackward, locked and mode, and the Digital table clusters. They were MTK callbacks with ImperativeAffects: pre() from a module-level memory (DISCRETE_PRE_MEM), a second pass, a "carry" of the mode at a breakaway, and chained affects inside if-equation flips. After a flip, NoInit left the algebraic unknowns stale. MSL Rotational.Friction then locked on the sliding torque, and a crossing of the stuck torque was found on the Rosenbrock interpolant of that inconsistent state (review s4a). Now each cluster is a DiscreteCluster (discreteClusters.jl): - A VectorContinuousCallback only locates crossings. It has one component per distinct relation, shifted by eps = H * scale relative to the relation's buffer, as for the if-equation relations and the whens on a relation. Relations without a continuous-time operand, and == and <>, are exact. - The event iteration (relationRefresh.jl) evaluates the clusters. When a continuous event fired, it first solves the algebraic variables again, because an if-equation flip leaves them stale. Each sweep then updates every buffer by the hysteresis rule and solves each cluster as a mixed system: the body with pre() fixed, write, solve, relations again, until the discretes stay. - Where that cycles, _search! tries values of the relations that flipped, fewest changes first, from the same state each time. It judges consistency by the rule from the values at the start of the pass, and twins vary together. A friction element locking from a slide cycles between a forward and a backward breakaway; the search finds the stuck state, as OpenModelica's nonlinear solver does with the discrete equations inside its residual. - At the start: relations literal, the start bodies, one solve, then the relations literal again. Removed: the pre-memory affects, DISCRETE_PRE_MEM (LIFTED_DISCRETES keeps the names for the direct-RHS initialization), the second pass, the carry, the chained-cluster affects, and the variables OMBACKEND_FSM_REINIT, OMBACKEND_PREMEM_EVENT_ITERATION and OMBACKEND_PREMEM_TRACE. pre(x) in an affect without a substitution now reads observed.x. The locate-only callbacks of the clusters and of the whens on a relation use initializealg = NoInit(); the event iteration does the one solve. OMBACKEND_EVENT_TRACE=true prints every cluster evaluation. Rotational.Friction against OpenModelica 1.27.1 at reltol 1e-6: every event to 5 digits (0.00678, 0.05934, 0.10929, 0.5, 0.53425, 0.64061, 1.66364) and none after; the stuck torque within 0.01; |w_rel| = 1e-10 while locked. Open (a separate task): crossings of algebraic unknowns are located on Rodas5P's dense output, which nothing controls while the states are frozen. A short excess over a breakaway limit can be missed. Assisted-by: Claude Opus 5.5
A Rosenbrock method (not a W-method) needs an accurate Jacobian for its order and its error estimate. Where a model has no symbolic Jacobian (MSL HeatingRectifier: the generated one keeps an unresolved term), the default used forward differences. Rodas5P then accepted a step across a diode's conduction onset whose error was 25 times the tolerance: in the step 3.841 -> 4.069, Capacitor1.v ended 2.5 % off. Central differences give the reference, as ForwardDiff does: max error 5.6e-4 against 7.4e-3. How far off depended on the step sequence, which followed the equation order, so the model passed or failed by process on Julia 1.12 and 1.13. Now it is identical and accurate in 12 fresh processes of both versions. Finite differences rather than ForwardDiff, because external C functions take no dual numbers. Assisted-by: Claude Opus 5.5
A stiffly accurate Rosenbrock step satisfies the algebraic equations only in their linearization and only at its end, so its error estimate is zero for the algebraic unknowns: - Steps grow by 10 while only algebraic unknowns move. x' = 0, 0 = y - 100 sin t takes 6 steps over [0, 10], with a dense output wrong by 200. - Events on algebraic operands are located on that dense output. A friction element held by a time-varying torque missed a short excess over its breakaway limit. - At step ends the algebraic unknowns carry the local error. EngineV6 at the default tolerance saw a piston 6.6e-9 past its stroke, and the MSL assert s_rel <= L + 1e-12 stopped it. withAlgebraicStepControl (algebraicStepControl.jl) is added to every mass-matrix build, first among its discrete callbacks. After each accepted step of a Rodas method with adaptive steps: - The dense output of the algebraic unknowns at 0.2, 0.5 and 0.8 of the step is compared with one Newton correction on the algebraic rows. The Jacobian is the problem's symbolic one, or finite differences of the algebraic block. The largest error in the solver's norm caps the next step. - The step end is projected onto the algebraic equations, to 1e-6 of the tolerance or round-off. A Rodas step keeps no FSAL derivative or history, so it needs no restart. - The work is done in the callback's condition. DiffEqBase saves a step before a discrete callback's affect, which would keep the unprojected state. - It is off after a continuous event (the event iteration solves the algebraic unknowns then), for other methods (BDF controls the algebraic error itself), for fixed steps, and for the residual form. isStepControl tells it from event callbacks. Results: - the plain DAE: 52 steps, dense error 8.8e-5 at reltol 1e-6; - the friction model Narrow: OpenModelica's six events to 5 digits; - EngineV6 runs through at the default tolerance. Assisted-by: Claude Opus 5.5
A Complex equation with no record reference on either side, such as the
Kirchhoff equation `pin_p.i + pin_n.i = Complex(0)` of the QuasiStationary
two-pins (a `'+'` operator call against `Complex(0)`), is expanded field by
field through makeScalarElement. Its fallback built DAE.ASUB with bare
ICONST subscripts, but DAE.jl's ASUB holds a List{Subscript}, so the backend
stopped with "Cannot convert ICONST to Subscript". This stopped 59 MSL 3.2.3
examples (FundamentalWave, QuasiStatic, QuasiStationary). Wrap the index in
DAE.INDEX as every other ASUB does. With the registry skips removed, 51 of the
59 now pass the backend, 25 simulate and 16 validate against the MAP-LIB
references.
simplifyASUBofARRAY lost its branch for ASUB with a bare ICONST subscript,
which the typed list can never hold; the two arms of makeScalarElement's
CREF case that both produced DAE.INDEX(s) are folded.
Assisted-by: Claude Opus 5.5
Each pure-time if-condition (time >= startTime) has its own event callback; its affect sets its own condition by the crossing direction and re-derived every other one from the sign of its crossing function at t. The events are located on the left of their roots, so an event coinciding with the firing one still showed its pre-crossing sign there: each affect reset the others and only the last stayed switched. The MSL machine examples close the phases of an ideal switch with BooleanSteps sharing startTime; phase 1 never closed, and the machines, on one phase, never started (AIMC_DOL stayed at rest). The other conditions are now read just after the event, at t + 1e-10 (1 + |t|): a coincident crossing counts as happened, a condition away from its crossing keeps its sign, one that only touches zero keeps its value. The crossing functions depend on t and parameters only, so shifting t needs no derivative (mod and floor included). EventSemantics.CoincidentTimeEvents (four conditions at 0.1) gave 0, 0, 0, 0.1 and now 0.9, 0.9, 0.9, 0.1 as OpenModelica; AIMC_DOL's FundamentalWave machine now matches the Dymola reference to 6 digits. Assisted-by: Claude Opus 5.5
Functions with Complex array inputs (Complex.'*'.scalarProduct behind the
ComplexBlocks Sum, QuasiStationary quasiRMS(input Complex u[:])) stopped the
QuasiStationary and QuasiStatic sensor models at "unresolved component
references", or, with the SimCode check off, crashed with a generated
function called with too few arguments. flattenRecordVar split a record
input into fields but dropped its dimensions (the record is ty, the array
dims), statements inside for/while/if were not transformed, and equation
call sites passed {k[1], k[2]} where the function takes one array per field.
- Record fields keep the record's dimensions (u_re[:], u_im[:]); dimension
expressions and other parameters' dims and bindings are rewritten
(size(u, 1) -> size(u_re, 1)).
- transformExpForFlattenedRecords is a traversal: element field access
(u[k].re -> u_re[k]), size of a record array, record arguments of calls
(with their subscripts; record-valued calls as TSUB per field) anywhere in
an expression; for/while/if bodies, locals, asserts, no-return calls and
tuple assignments are transformed.
- An element assignment v[i] := value goes through a temporary record, so a
call is evaluated once and a value reading v[i] sees the old element; a
whole record array y := f(...) stays one statement that codegen scatters
in one evaluation (_recordCrefFields accepts arrays of records); other
record-array assignments raise an error instead of leaving defaults.
- At equation call sites an array of records becomes one array per field
(the scalarized names, k[1]_re); builtins are left alone.
Reviewed in three rounds (agent); the aliasing of a per-field split of
y := f(y) was caught by the new test and replaced by the scatter.
Assisted-by: Claude Opus 5.5
MTK compiled a when-cluster's t0 initialize into an implicit affect over the parent's algebraic equations; at t0 it sat on singular points (a mean block's sqrt(0), a thyristor's off from s < 0) and raised UnsolvableCallbackError for 37 MSL models. - The t0 initialize is an ImperativeAffect wherever the body lowers (_eventIterAffectParts: initVal, one pass unless the cluster reads its own pre()). Crossings keep the equation form and the shared callback settings, so coincident crossings stay in one vector callback. - Models with ideal switches (a cluster relation reads an algebraic unknown that only the switch's own equations read) take the discrete-cluster path (_modelHasSwitchClusters). - Discrete-cluster path: a body-read Boolean defined by a relation is replaced by the relation and becomes an event source; a crossing located by the cluster's callback flips its relation (DiscreteCluster.crossed). - Discrete whens on a Boolean: an edge latch, seeded at the start of every solve, instead of resetting the condition variable; typed caches. - Initial-algorithm fetch: Booleans are not clamped to >= 1. Assisted-by: Claude Opus 5.5
At a commutation of a six-pulse thyristor bridge, Brown's Newton stalls at a residual of 4e-9 with values of 1e6 and a near-singular Jacobian; BrownFullBasicInit() asks for 1e-10, so the event iteration's re-solve and the if-equation branch callbacks stopped the simulation with InitialFailure. EventReinit: BrownFullBasicInit at its own tolerance first (relations are decided on the solved values; a looser tolerance skips the solve where a switched diode barely changes the residual, and a diode bridge chattered); only if that fails, the result stands if it lowered the largest algebraic residual to within the solve's abstol, as the initialization's. Used by _resolveAlgebraics! and the branch callbacks' reinitializealg. Assisted-by: Claude Opus 5.5
Stage 2 of the event work. The discrete-cluster path (MLS 8.6 event iteration) is now the default for every model (OMBACKEND_DISCRETE_PRE_MEMORY=false opts out). Lifter (BDAECreate): - It runs after the binding equations: a `Boolean open = time > 0.5` binding made no event, so the switch it drives never opened. - A discrete alias `a = b` (a connect, in either orientation) is not a definition. The definitions read through the alias sets (_aliasSets, _aliasDefinitions, _readThroughAliases!), so a Greater block's relation next to `y = fire` makes its event. - pre() at initialization takes the values the initial equations fix (_initialConstants, _initialPreValues): `pre(y) = pre_y_start`, a parameter as well as a literal, and `m = pre(n)` with m fixed directly or through an alias. Also for an n outside the cluster: the StateGraph InitialStep starts active (newActive's start alone left every step inactive). A start only an alias carries is the definition's. Event iteration (relationRefresh, discreteClusters): - Discrete-when and elsewhen-arm bodies read pre() from the sweep's snapshot (PRE_FROM_SNAPSHOT; the affect takes it as an optional second argument): `iAtOpen = pre(i)` read i after the re-solve. - A relation when's discretes and a cluster that no re-solve moves (DiscreteCluster.coupled, from the equation graph) leave the algebraic unknowns stale: they are solved again before a coupled cluster or a discrete when reads them. A failed stale solve puts the return code back. - An exact relation at a crossing is read at the right limit (_eventCrossingValues!). - After an iteration that changed something the step size starts again (_restartStepSize!; ODE problems, 1e-6 of the time span where there are algebraic rows): one step crossed an ideal switch's transient and its interpolation was far off (SwitchWithArc -4.5 A for 0.0005 A; CauerLowPassSC 5-10 % off, now matching the reference to 6 digits). Code generation: - An if-equation whose branches define different variables takes the residual form (a switch with an arc: `i = 0` in one branch, `v = 0` in the other). - The names the callbacks index in the state vector are irreducible: a when body reading an explicitly solved algebraic failed with a KeyError. - The operands of not/and/or are Booleans (`when not active`: a Boolean state is a Float64). - The default solver choice runs in defaultSolverFor, outside the generated module, where a model variable `count` shadowed Base.count. - Integer and enumeration cluster members stay integral (_isIntegralValued); cluster whens skip the periodic sample refresh. propagateConstants: an unknown bound to a parameter and still read keeps its equation with the other substitutions only (`y - u` read `R - R`). Assisted-by: Claude Opus 5.5
A table or time when (a PresetTimeCallback) sets a discrete that the discrete clusters read, but nothing ran the event iteration at that instant: the MSL JK flip-flop's K from a table at t = 22, while the clock is high, reached the latches only at the next clock edge (t = 25). The other discrete callbacks now mark the iteration pending (_PendingAffect); it comes last in the callback set, so it runs in the same step, with the algebraic unknowns solved again first. Assisted-by: Claude Opus 5.5
…assignments DirectRHS built the problem of a system that structural_simplify left without unknowns from a dummy state alone: no system (none of its observed variables could be read from the solution: MSL Inverse_sine, IdealGasH2O, readRealParameterModel), no parameter values, none of its own events (an if-equation on time stayed in its first branch) and no initial parameter assignments (`k(fixed = false)` with `initial equation k = 2` kept its start). The empty system now goes through the same parameter logic as any other: `sys`, the parameter vector, the assignments (and DAE_REINIT for other parameter values) and the system's events. A free parameter is refused: it is an unknown of an init solve the empty system does not run. When process_events threw, the system's own events were dropped with a warning (MSL CauerLowPassSC on 2026-09-27): refused now, unless the system has no events of its own. Assisted-by: Claude Opus 5.5
…expression The runtime ignored significantDigits, minimumLength and format (Julia's string(x)), and the lowerings passed the values as they arrive: a Boolean or Integer read from the integrator is a Float64 (`b=1.0`), and an enumeration value its index. The assert-message lowering formatted the first argument only. - modelica_String formats as OpenModelica's runtime (C's snprintf: `%-*.*g`, padding, a format checked for f, e, E, g, G). - modelicaStringCall lowers the call by its argument count (the frontend fills in the defaults: 4 for a Real, 3 for an Integer, Boolean or enumeration, 2 for a Real and a format), the types from the DAE expression: Integers rounded, Booleans compared with 0, an enumeration value as its literal's name; anything else is refused. All four lowerings use it (event affects, when statements, MTK expressions, function bodies); the builtin table entry is gone. - SimulationCode's TYPE_ENUM keeps the literal names (they were dropped on the way through SimulationCode). - An assert message reading a String parameter reads its module-level binding: through `observed` the assert was left out as reading a variable the simulation does not keep. A message that cannot be lowered is reported with impact :result. As OpenModelica: `r=0.8 r6= 0.8 n=4 p=[ 4] b=true e=two f= 0.800`. Assisted-by: Claude Opus 5.5
The STRING kind covers String parameters and String variables alike, and both were assigned once at module level: a String variable reading time or a variable kept its first value (or failed with an UndefVarError `t`). Refused now: one that reads time, a state, derivative, algebraic, discrete, input or array variable, or a variable the SimCode passes eliminated. Assisted-by: Claude Opus 5.5
A when on a relation whose operand names the system lacks was disabled without a word at run time (meant for the other modes of a variable-structure model). The names are now checked once against the model's own system in withRelationRefresh: refused if missing. At run time the other modes' systems stay silent, as before. Assisted-by: Claude Opus 5.5
Each affect took its own snapshot of the state on entry, and the event iteration's sweep the state before it: a when triggered at an instant by a discrete that another callback had just changed read pre() after that change (`when sample(0.5, 1) then n = pre(n) + 1` and `when n > 0 then m = pre(n) * 10`: m = 10, OpenModelica 0; a time when on a T the sample body set: k = 0.2, OpenModelica 10). A per-integrator holder (instantPre) records the state before the instant at the first affect at a new time. Every generated affect and the event iteration's first sweep (the clusters' pre values too) read it; a later sweep is a later iteration, with pre() the state before it. When the callbacks before the iteration changed the state, a second sweep follows even if the first changed nothing, as OpenModelica iterates while a value differs from its pre(): the MSL Digital gates (`y = pre(auxiliary_n)`) move one gate per iteration. A reinit() takes effect after the iteration (OpenModelica's needToIterate): the generated reinit records its state, and a sweep evaluates its relations with the states reinitialized since it began at their values in its pre(). A when on a relation of the state fires in the next sweep, with pre() after the reinit (a bouncing ball's `when v > 0 then up = pre(v)`: 3.54, not -4.43); a when on a discrete the same body sets fires in the first. Assisted-by: Claude Opus 5.5
… located Within a Rosenbrock step an Integer state drifts (3.0000000000000004: the linear solve's pivoting mixes rows). The cluster's crossing callback located `mod(n, 2) == 1` flipping and back, and an edge() of it fired at every step after n reached 3 (rises = 7 at 1, OpenModelica 3; 158 by t = 0.51 in a trace). A cluster relation that reads neither a continuous-time variable nor time changes only at events, where the event iteration evaluates it (after the rounding callback): its crossing function is a constant now (eventOnly). Assisted-by: Claude Opus 5.5
A when is active at the initialization only as `when initial()` or
`when {..., initial(), ...}`. `when initial() or x > 0.6` never fired at x's
crossing: the legacy continuous path's crossing function was
`0 - (x > 0.6)`, 0 then -1, without a sign change; the algorithm form fired at
the initialization too (OpenModelica: n = 0, then 1 after 0.6). An initial()
disjunct of an `or` is dropped from an equation when-condition
(dropInitialDisjuncts), and an algorithm when is an initial one only in the two
forms.
Assisted-by: Claude Opus 5.5
_mergeAliasAttrs returned the representative's attributes unless the eliminated variable's were Real: `k(start = 3, fixed = true)` with `k2 = k` read 0 until its first event (OpenModelica 3). The discrete kinds merge start and fixed (with min and max) the same way; a negated Boolean alias takes `not start`, a negated enumeration alias keeps the representative's. Assisted-by: Claude Opus 5.5
…d it is idempotent
The SymbolicContinuousCallback of a `when time >= pre(next)` ran the body in
its initialize: a when without initial() fired at the start (n = 1 from 0,
OpenModelica first at 0.5), and `{time >= pre(next), initial()}` ran twice,
in the initial algorithm too (n = 2, OpenModelica 1).
The runtime arm of a when whose condition had initial() is marked now
(BDAE.ALSO_INITIAL_EQUATION, SimulationCode EQ_ATTR.alsoInitial), and the
start-of-solve run is kept for such a when whose body reads no pre() of what
it sets: the time tables' getNextTimeEvent, which resets the table's C object
for a solve of the problem itself (remake + solve kept its end state:
z(1) = 2.77, OpenModelica 2.47).
The affect records the state before the instant (instantPre): a when that
edge() or change() of what it sets triggers read pre() after its write and
never fired.
Assisted-by: Claude Opus 5.5
`Boolean ch = change(k)` of a discrete has no continuous relation and no pre() loop, so it stayed a residual, where change() and edge() are constant false: a `when ch` never fired (OpenModelica q = 1). A relation-free group whose body calls change() or edge() is lifted now; the event iteration evaluates it at every pass, true at k's event and false at the next. A sibling is no longer inlined inside edge() or change(): `edge(trig.y)` became `edge(sample(...))` (nothing left to follow: a BoundsError), and `edge(b > 0.5)` could not be lowered (the MSL Edge and Change blocks on a BooleanTable were refused; they give OpenModelica's counts now). A cluster without relations or followed variables is refused, not indexed. Assisted-by: Claude Opus 5.5
- A relation-when that changes a discrete: the sweep's pass loop continues, as after a discrete when, so a relation it flips fires in the same sweep with the same pre() (`when x > 0.5 then d = 1` and `when x + d > 1.2 then y = pre(d)`: y = 0, OpenModelica; it fired a sweep later, y = 1). - A sample(0, ...) tick at the start (PeriodicCallback's initial_affect) ran before the iteration's initialize, whose discrete-when latches took its values: `when n > 0` never fired (m = -1, OpenModelica 0). The latches start from the initialized state with the tick's changes put back (the instant holder), and the iteration runs at the start. Assisted-by: Claude Opus 5.5
…refused - initialValues evaluated integer() with round (integer(2.7) = 3); it is floor (MLS 3.7.1.1). - A fixed start `integer(p27)` started at 0.0 with a warning: after the parameter substitution floor(2.7) stayed a symbolic call. A constant term is evaluated by applying its operations (_evalConstantTerm). - A fixed start that still cannot be evaluated and reads no other variable is refused, not 0.0. One that reads other variables keeps 0.0 until the initialization solves it, with a warning. Assisted-by: Claude Opus 5.5
`Real v(fixed = true); v = xa + 1; der(xa) = -xa` gave xa(0) = 0 (OpenModelica -1): v was folded away by the explicit fold and its pin lost. fixed = true fixes the start, the default one where none is given (MLS 4.9.1): the fold and the output-only rescue (_hasFixedStart, was _hasExplicitFixedStart), the irreducible set (fixedStartVarNames) and the initialization constraints (getFixedStartConstraintsMTK: `v ~ 0`) take it. Assisted-by: Claude Opus 5.5
An eliminated variable defined through der() (`a = der(x)`, an acceleration sensor's output nothing else reads) got no observed equation and was in no result. Its residual has der(x) replaced by the right side of x's explicit equation `D(x) ~ f` (substituteDerivatives, added with the start values) and is solved for it; without such an equation it stays out, as before. Assisted-by: Claude Opus 5.5
…ot pinned - `initial equation der(x) = der(y)`: the row read der(y), which no observed equation reduces to the states, and it was dropped at @debug (x(0) = 0, OpenModelica 1). A derivative in an initialization row is expanded (`D(2y)` = 2 D(y): an observed variable's definition inside D()) and replaced by the right side of its equation. A derivative row on an observed variable or an algebraic unknown is a row of its own (it was skipped or vanished). A row that still does not reduce is refused: a wrong initial state otherwise. - An initial equation on der(der(x)) (Differential(t, 2)) was taken as one on der(x): refused, as by OpenModelica. - A pure ODE pinned every start for its init solve, and phase 3 then freed every variable and moved the fixed ones (`x = 2y + 1` with y fixed at 2: y = 0, OpenModelica 2). It pins only what a DAE does: a non-fixed start an initialization equation involves (left side, an expression's variables, or its right side) is a guess. Assisted-by: Claude Opus 5.5
Phase 3 of the DAE init solve frees every variable, the user's fixed ones too, when the pinned phases fail, and the root it finds was accepted: `x(start = 1, fixed = true)` with an initial equation that wants another x gave x = 0.48, and `v(fixed = true)` with `v = xa^2 + 1` gave v = 1, without a word. OpenModelica refuses both. Codegen emits USER_PINS: the continuous Real variables with fixed = true and the left sides of initial equations `v = literal or parameter` (after the if-equation relay renaming; not discretes, whose fixed is pre(v) = start). buildDirectRHSProblem refuses when one of them moved from its entry value in the accepted initial state. Internal pins (if-equation temporaries, latches) may still move, as before. - Names are matched by _plainVariableName, so array elements (var"x[1]") are checked too; mergeSoftGuesses and the discrete and exclude checks use the same matching. - A pin whose start reads a free parameter (fixed = false) is no user value to hold and is not checked: x(start = p, fixed = true) gives x = p. - A fixed literal start and an initial equation `x = other literal` are refused at codegen (two values for x), on the pure-ODE path as well. Assisted-by: Claude Opus 5.5
Tunable runs (withTunableParameters, `parameters = Dict(...)`) kept the compiled initial state wherever the initialization read a tunable parameter. OpenModelica's `-override` and a compile at the new values agree; OM.jl did not: - Start values and literal initial equations were evaluated once: x(start = p, fixed = true) and `initial equation x = p` kept x(0) = 1 for p = 2. `_runEntry` evaluates the entry state again for the run's values, resolving only the parameters it reads, and only when one changed. - Codegen folded p into initial equations (`x ~ 1.0`); `keepTunable` keeps a tunable parameter, or a bound parameter reading one, a reference (`_initialEquationRhs`, shared by both initial-equation generators). - A free parameter computed from a tunable one was solved at the build; alone on a side it is bound to the other side now, else the initialization computes it. - Derivative targets (`der(x) = p`) are computed for the run, also in its settles. - Only parameters without a value are assigned by initial equations (ASSIGNED_PARAMETERS): a tunable p alone on the left of `p = 2 * q` was overwritten with 2q from q's start, and q never solved. - Parameters bound to tunable ones (`q = 2 * p`) are set in the run's parameter vector (`followTunable!`): a when-condition read q's compiled value. - A pure ODE registers a re-initialization too; the user-pin check runs after every re-initialization. - DAE_REINIT was keyed by the generated RHS, which two models of the same equations share: one model's re-initialization answered for the other. It is keyed by the problem's reduced system and freed with the build. - `_constantNumber`: SymbolicUtils keeps constants as Const terms, so `isa Number` missed them and every parameter was resolved by substitution (DoublePendulum: 572 ms to 192 ms a build). - The observed-alias scan computes the equations' strings once. Initialization reads that were silently wrong (UNCLEAR batches 6, 7): - An initial algorithm section reading a variable the initialization determines (its start was read), one the backend eliminated (0.0) or a parameter the initialization computes (the section was dropped): refused. A fixed variable's start is evaluated (an expression was read as 0.0), and a String parameter is read as its literal or module-level value. - Constant-parameter elimination substitutes into String bindings (`name = String(k)` was an UndefVarError at module load). - The runtime when initial() pass read discretes rounded and clamped to at least 1 (`n = m + 5`, m = -2: n = 6); Integers and discrete Reals are read as they are now (enumerations and discretes of unknown type, the MSL Digital Logic indices, keep the clamp). - A when initial() body reading a variable the problem does not have (0.0): refused when read. - Two literal initialization values for one variable (an initial algorithm's x := 2 and `y = 3` with y = x): refused, except discretes (DISCRETE_VARIABLES: their fixed start is pre(v) = start). - An unbound parameter alone on the right of an initial equation (`x = q`) is free now; it was neither free nor assigned. - Output-only elimination keeps what initial equations read (to a fixed point): `z = b` with b eliminated was an UndefVarError. - A String variable set by an equation and reinit in when initial() are refused (an UndefVarError and a MethodError). Assisted-by: Claude Opus 5.5
B16 matched the rows the symbolic initialization leaves out (lifted discretes, assigned parameters) by the plain name, so an array element (`var"jointRRP_e_im[1]"`, an assigned parameter of the MSL analytic loops) was no longer a residual row too. The V6 cylinder's top dead centre then passed the GasForce assert's 1e-12 margin (s_rel = L + 1.4e-12 at 0.151 s; OM.jl's test of it, verify-all after B16). As before B16: phi(0.5) = 31.88598943, OpenModelica 31.88599150. Assisted-by: Claude Opus 5.5
homotopy(actual, simplified) was lowered to `actual`: the simplified expression was dropped, and the initialization took whatever root Newton found from the start values (`0 = homotopy(x^3 - 3x - 1, x - 2)`: x = -0.347; OpenModelica 1.879; MSL SignalGenerator's op-amps at their unstable equilibrium, 0 V, OpenModelica -15 V). OpenModelica runs the homotopy method on the first try when a model contains homotopy(). - homotopy(a, s) is `ifelse(λ >= 1, a, λ a + (1 - λ) s)` with a parameter λ (`_homotopyLambda`, the if-condition parameters' declaration), 1 in a simulation; `_usesHomotopy` adds it when an equation calls homotopy. - `_homotopyContinuation` solves the DAE initialization at λ = 0, 1/3, 2/3 and 1, each from the previous solution, with the relations' literals set at each step's solution (kept at the entry's, the limiters stayed linear and the path returned to the unstable equilibrium). On a failure it falls back to the direct solve, reported. - The blend is lowered only in the continuous and initialization equations (HOMOTOPY_BLEND); elsewhere (when bodies and conditions, relations, bindings, if-equation branches) homotopy is the actual expression. - The simulation's RHS is built at λ = 1 (the simplified expression folds away and is not evaluated during the run); the initialization's reads λ. - The discrete clusters' start path and a tunable run's re-initialization go through the continuation too. SignalGenerator now starts as OpenModelica does; its first switch (a comparator with positive feedback) does not settle in the event iteration and the run stops (it ran at the wrong equilibrium before). Assisted-by: Claude Opus 5.5
A comparator with positive feedback (MSL SignalGenerator's opAmp1) cycled
at each switch: low saturation -> the linear branch, whose solution is the
unstable midpoint where the relation flips back -> low again, and the event
iteration stopped ("did not settle", t = 0.025). OpenModelica solves the
loop with the live if-expression and lands on the other saturated side.
- When the iteration does not settle, `_consistentIfRelations!` tries the
combinations of the if-relations' literals, fewest flips first (at most
10 relations): the algebraic unknowns solved for each, the first where
every relation evaluates to its literal is taken, and the instant is
iterated again.
- When one instant is iterated 3 times in a row (the linear branch settled
at the fold where it meets the saturated one, and its crossing fired at
once: SignalGenerator's fifth switch with stopTime 2), the combination
that holds clearly, beyond the hysteresis, is taken; a relation the solves
do not move (an independent one at its threshold) keeps its literal, and
a current combination that holds clearly stays.
The count starts again with each solve (a re-simulation reuses the
callbacks).
- Only where the iteration has nothing but if-relations: a when body or a
cluster fired at every pass of a cycle (a when on the comparator's
relation counted 347 switches, OpenModelica 20); such a model stops as
before.
SignalGenerator runs to 2 s (40 switches) and matches the MSL reference at
every sampled point.
Assisted-by: Claude Opus 5.5
MSL DifferenceAmplifier went Unstable or InitialFailure in 4 of 52 verify runs, and with the default solver it stopped at its ramp's end (t = 1e-9). - Generated functions hash by content (mtkExternals.jl). A RuntimeGeneratedFunction hashed by its body Expr's address, and so did ModelicaFunctionWrapper and ConstTableLookupFn (an Array): every term calling a Modelica function hashed differently by process, and MTK chose other aliases and summation orders. Byte-identical generated code took 13940..15560 steps; nine processes now build the same system. - The merged continuous callback keeps its components' re-initialization (DirectRHSGeneration.jl `_mergeableReinit`). It was created with `initializealg = nothing` for MTK's NoInit events: OrdinaryDiffEqCore re-solved the DAE with the integrator's BrownFullBasicInit at every event, also where it only moved to the event time. Merged only where the components agree (nothing, NoInit or EventReinit). - A DAE with continuous events and no relations gets the event iteration (relationRefresh.jl `_eventsOnAlgebraicUnknowns`): it solves the algebraic unknowns again after an if-equation's time event, after the step-end recorder took the left limit. Before, only the merged callback's re-solve did, inside the callback. - An event re-solve stands when one more Newton correction of the algebraic unknowns is within the solver's error norm (`_withinSolveTolerance`), and when it did not raise the residual (a second re-solve at the same event). DifferenceAmplifier's ramp end: condition 7e26, residual stalled at 1e-4, correction 0.08 of the tolerance. - Algebraic unknowns that are derivatives (MTK's dummy derivatives, `xˍt`) are left out of the error control: abstol Inf for them (MTK_CodeGeneration.jl `defaultToleranceKwargs`), as OpenModelica's ODE mode integrates the states only. The transistors' der(vbc) (1e9 V/s, V1's 0.2 GHz sine) held the steps at 1e-11 s until the solve stopped near 2e-6 s. DifferenceAmplifier now runs to 1 s in 6000 steps and ends at OpenModelica's values, with every solver tried. - A caller's abstol per unknown reaches the initialization as its minimum (BrownFullBasicInit passed it to NonlinearSolve, which takes a scalar: a MethodError). MSL coverage (all 425, against b25e): validate 303 (+1, ChopperStepUp_R), simulate 327. HBridge_DC_Drive now hits the harness's iteration cap: the build every process now makes takes ~31000 steps per simulated second (its 1 kHz PWM); it matches OpenModelica to 6 digits at 0.5-2 s. Its earlier "simulate" rows came from other processes' builds and never validated. Tests: two processes hash a wrapper, its RGF, a term and a table lookup the same; merged callbacks keep NoInit, EventReinit and nothing, mixed ones are not merged; a vector abstol in the solve keyword arguments. Assisted-by: Claude Opus 5.5
…n does not The test held the re-solve of 0 = 1e8 (y^2 - 2) at abstol 1e-12 to a failure: its residual stays at 4e-8 after rounding. Since a7f4fba the result stands when one more Newton correction is within the solve's tolerance, and y is right to 2e-16 there. The failing case is now 0 = y^2 + 1, which has no real solution. Assisted-by: Claude Opus 5.5
The OM.jl packages are verified on Julia 1.13 only (since 2026-09-28): compat julia = "1.13" (added where it was missing), and every workflow tests on 1.13. Assisted-by: Claude Opus 5.5
One version per package for the release (bumped where the version was already registered in the OpenModelica registry), and every OM.jl sibling dependency bounded below by its release version: the packages are developed in lockstep, and the registry must not pair this version with an older sibling. Assisted-by: Claude Opus 5.5
…its siblings The OpenModelica registry's auto-update skips a Project.toml with a [sources] block. The siblings were path sources for development; OM.jl's ci/ scripts develop them into a temporary environment (as for every sibling without [sources]), and this CI now develops its sibling checkouts into the project before the build, so the matching branches stay what is tested. OMBackend 376/376 both ways (ci/test-package.jl, own environment). Assisted-by: Claude Opus 5.5
Pkg rewrote the file with absolute [sources] paths into a local checkout during a test run between the edit and the commit, and 34b2792 committed them: CI failed with "expected package OMParser to exist at path /Users/...". The release has no [sources] block. Assisted-by: Claude Opus 5.5
It ran with --project=<this package>, whose project is not instantiated where the siblings are developed into a temporary environment (OM.jl's ci/test-package.jl, since [sources] is gone): "Package Plots is required but does not seem to be installed" in CI. Base.active_project() has the package's dependencies. 376/376 without a local Manifest.toml. Assisted-by: Claude Opus 5.5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges the 1.13 line into master for the Julia 1.13 release of the OM.jl packages: OMBackend 0.5.1.
Changes (159 commits)
Merge order
The OM.jl packages are released in lockstep. Merge in this order, and register each package in the
OpenModelica registry right after its merge, so that the next one resolves against it:
Please use Create a merge commit (not squash): OM.jl's submodule pointers and the sibling checkouts in CI
refer to these commits.
Verification (local, macOS, Julia 1.13 with threads)
OMFrontend 378/378, OMLibraryTesting 25/25, MetaModelica 262/262, OMRuntimeExternalC 274/274).
release-1.13, and OM.jl's integrationworkflow checks out the siblings at that branch.
Notes
julia = "1.13"in compat, and every workflow tests on 1.13.🤖 Generated with Claude Code