Skip to content

Commit d1bd6e4

Browse files
committed
feat(core): add symbolic equation isolation engine and alias elimination
1 parent 1204286 commit d1bd6e4

1 file changed

Lines changed: 123 additions & 2 deletions

File tree

packages/core/src/compiler/modelica/symbolic.ts

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@
1919
import type { ModelicaExpression } from "./dae.js";
2020
import {
2121
ModelicaBinaryExpression,
22+
ModelicaDAE,
2223
ModelicaFunctionCallExpression,
2324
ModelicaIntegerLiteral,
2425
ModelicaNameExpression,
2526
ModelicaRealLiteral,
27+
ModelicaRealVariable,
28+
ModelicaSimpleEquation,
2629
ModelicaUnaryExpression,
2730
} from "./dae.js";
2831
import { add, differentiateExpr, div, isOne, isZero, mul, simplifyExpr, sub, ZERO } from "./symbolic-diff.js";
29-
import { ModelicaBinaryOperator, ModelicaUnaryOperator } from "./syntax.js";
32+
import { ModelicaBinaryOperator, ModelicaUnaryOperator, ModelicaVariability } from "./syntax.js";
3033

3134
// ─────────────────────────────────────────────────────────────────────
3235
// Public API
@@ -81,7 +84,7 @@ function countOccurrences(expr: ModelicaExpression, varName: string): number {
8184
/**
8285
* Substitute all occurrences of `varName` with `replacement` in an expression.
8386
*/
84-
function substituteVariable(
87+
export function substituteVariable(
8588
expr: ModelicaExpression,
8689
varName: string,
8790
replacement: ModelicaExpression,
@@ -411,3 +414,121 @@ function isNegOne(expr: ModelicaExpression): boolean {
411414
if (expr instanceof ModelicaIntegerLiteral) return expr.value === -1;
412415
return false;
413416
}
417+
418+
// ─────────────────────────────────────────────────────────────────────
419+
// Alias Elimination
420+
// ─────────────────────────────────────────────────────────────────────
421+
422+
/**
423+
* Substitute all occurrences of `varName` in all expressions of an equation.
424+
*/
425+
function substituteInEquation(
426+
eq: ModelicaSimpleEquation,
427+
varName: string,
428+
replacement: ModelicaExpression,
429+
): ModelicaSimpleEquation {
430+
const e1 = substituteVariable(eq.expression1, varName, replacement);
431+
const e2 = substituteVariable(eq.expression2, varName, replacement);
432+
if (e1 === eq.expression1 && e2 === eq.expression2) return eq;
433+
return new ModelicaSimpleEquation(e1, e2, eq.description);
434+
}
435+
436+
/**
437+
* Detect if a `ModelicaSimpleEquation` is a trivial alias: `a = b`
438+
* where both sides are bare variable references (ModelicaNameExpression).
439+
*
440+
* @returns The pair [aliasVar, targetVar], or null.
441+
*/
442+
function detectTrivialAlias(
443+
eq: ModelicaSimpleEquation,
444+
unknowns: Set<string>,
445+
): { aliasVar: string; targetExpr: ModelicaExpression } | null {
446+
const lhs = eq.expression1;
447+
const rhs = eq.expression2;
448+
449+
// Pattern: name = expr where name is an unknown
450+
if (lhs instanceof ModelicaNameExpression && unknowns.has(lhs.name)) {
451+
// a = b (trivial) or a = expr where expr doesn't contain a
452+
if (!containsVariable(rhs, lhs.name)) {
453+
return { aliasVar: lhs.name, targetExpr: rhs };
454+
}
455+
}
456+
// Pattern: expr = name where name is an unknown
457+
if (rhs instanceof ModelicaNameExpression && unknowns.has(rhs.name)) {
458+
if (!containsVariable(lhs, rhs.name)) {
459+
return { aliasVar: rhs.name, targetExpr: lhs };
460+
}
461+
}
462+
463+
return null;
464+
}
465+
466+
/**
467+
* Perform alias elimination on a DAE.
468+
*
469+
* Scans equations for trivial aliases (`a = b` or `a = expr`) where `a` is
470+
* a continuous unknown and `expr` does not reference `a`. Replaces all
471+
* occurrences of the alias variable with its target expression in all other
472+
* equations, then removes the alias equation and variable.
473+
*
474+
* This reduces the system size and eliminates redundant unknowns before
475+
* BLT analysis.
476+
*/
477+
export function eliminateAliases(dae: ModelicaDAE): void {
478+
// Build the set of continuous unknowns (same logic as BLT)
479+
const unknowns = new Set<string>();
480+
for (const v of dae.variables) {
481+
if (v instanceof ModelicaRealVariable && v.variability === null) {
482+
unknowns.add(v.name);
483+
}
484+
}
485+
486+
// Iterate until no more aliases are found (substitution may reveal new aliases)
487+
let changed = true;
488+
while (changed) {
489+
changed = false;
490+
491+
for (let i = 0; i < dae.equations.length; i++) {
492+
const eq = dae.equations[i];
493+
if (!(eq instanceof ModelicaSimpleEquation)) continue;
494+
495+
const alias = detectTrivialAlias(eq, unknowns);
496+
if (!alias) continue;
497+
498+
const { aliasVar, targetExpr } = alias;
499+
500+
// Don't eliminate parameters, constants, or derivatives
501+
const varDef = dae.variables.find((v) => v.name === aliasVar);
502+
if (!varDef) continue;
503+
if (varDef.variability === ModelicaVariability.PARAMETER || varDef.variability === ModelicaVariability.CONSTANT) {
504+
continue;
505+
}
506+
if (aliasVar.startsWith("der(")) continue;
507+
508+
// Don't eliminate variables that appear in the target expression
509+
// (would create cycles)
510+
if (containsVariable(targetExpr, aliasVar)) continue;
511+
512+
// Substitute aliasVar → targetExpr in all OTHER equations
513+
for (let j = 0; j < dae.equations.length; j++) {
514+
if (j === i) continue;
515+
const otherEq = dae.equations[j];
516+
if (!(otherEq instanceof ModelicaSimpleEquation)) continue;
517+
dae.equations[j] = substituteInEquation(otherEq, aliasVar, targetExpr);
518+
}
519+
520+
// Remove the alias equation
521+
dae.equations.splice(i, 1);
522+
523+
// Remove the alias variable
524+
const varIdx = dae.variables.findIndex((v) => v.name === aliasVar);
525+
if (varIdx >= 0) dae.variables.splice(varIdx, 1);
526+
527+
// Remove from unknowns set
528+
unknowns.delete(aliasVar);
529+
530+
changed = true;
531+
break; // restart scan from the beginning
532+
}
533+
}
534+
}

0 commit comments

Comments
 (0)