Skip to content

Commit dae7a94

Browse files
committed
feat(core): AD-based initial equation solver, exact AD jacobians in FMU algebraic loops and BDF
1 parent 373048c commit dae7a94

4 files changed

Lines changed: 644 additions & 29 deletions

File tree

packages/core/src/compiler/modelica/ad-jacobian.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function extractDer(expr: ModelicaExpression): string | null {
3131
/**
3232
* Evaluate a tape forward pass at runtime, returning the value array.
3333
*/
34-
function evaluateTapeForward(ops: TapeOp[], varValues: Map<string, number>): Float64Array {
34+
export function evaluateTapeForward(ops: TapeOp[], varValues: Map<string, number>): Float64Array {
3535
const t = new Float64Array(ops.length);
3636
for (let i = 0; i < ops.length; i++) {
3737
const op = ops[i]!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
@@ -86,7 +86,7 @@ function evaluateTapeForward(ops: TapeOp[], varValues: Map<string, number>): Flo
8686
/**
8787
* Evaluate the reverse-mode AD sweep on a tape, returning gradients for all variables.
8888
*/
89-
function evaluateTapeReverse(ops: TapeOp[], t: Float64Array, outputIndex: number): Map<string, number> {
89+
export function evaluateTapeReverse(ops: TapeOp[], t: Float64Array, outputIndex: number): Map<string, number> {
9090
const dt = new Float64Array(ops.length);
9191
dt[outputIndex] = 1.0;
9292

packages/core/src/compiler/modelica/fmu-codegen.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,153 @@ function generateAlgebraicLoopSolvers(id: string, dae: ModelicaDAE, result: FmuR
606606
return lines.join("\n");
607607
}
608608

609+
/**
610+
* Generate _initializeSolve() — Newton-Raphson solver for initial equations
611+
* using exact AD Jacobians from StaticTapeBuilder.
612+
*/
613+
function generateInitializeSolve(id: string, dae: ModelicaDAE, result: FmuResult): string[] {
614+
const lines: string[] = [];
615+
616+
if (dae.initialEquations.length === 0) {
617+
lines.push(`void ${id}_initializeSolve(${id}_Instance* inst) { (void)inst; }`);
618+
return lines;
619+
}
620+
621+
// Collect initial equations that are simple equations (LHS = RHS)
622+
const initEqs: { lhs: ModelicaExpression; rhs: ModelicaExpression }[] = [];
623+
for (const eq of dae.initialEquations) {
624+
if ("expression1" in eq && "expression2" in eq) {
625+
const se = eq as { expression1: ModelicaExpression; expression2: ModelicaExpression };
626+
initEqs.push({ lhs: se.expression1, rhs: se.expression2 });
627+
}
628+
}
629+
630+
if (initEqs.length === 0) {
631+
lines.push(`void ${id}_initializeSolve(${id}_Instance* inst) { (void)inst; }`);
632+
return lines;
633+
}
634+
635+
const N = initEqs.length;
636+
637+
// Identify unknowns: variables referenced in initial equations that are not parameters
638+
const paramNames = new Set<string>();
639+
for (const v of dae.variables) {
640+
if (v.variability === ModelicaVariability.PARAMETER || v.variability === ModelicaVariability.CONSTANT) {
641+
paramNames.add(v.name);
642+
}
643+
}
644+
645+
const referencedNames = new Set<string>();
646+
for (const eq of initEqs) {
647+
collectReferencedNames(eq.lhs, referencedNames);
648+
collectReferencedNames(eq.rhs, referencedNames);
649+
}
650+
651+
const unknowns: string[] = [];
652+
for (const name of referencedNames) {
653+
if (!paramNames.has(name) && name !== "time") {
654+
const sv = result.scalarVariables.find((v) => v.name === name);
655+
if (sv) unknowns.push(name);
656+
}
657+
}
658+
659+
const nUnknowns = Math.min(unknowns.length, N);
660+
if (nUnknowns === 0) {
661+
lines.push(`void ${id}_initializeSolve(${id}_Instance* inst) { (void)inst; }`);
662+
return lines;
663+
}
664+
665+
// Build AD tapes for each residual: R_i = LHS_i - RHS_i
666+
const tapes: StaticTapeBuilder[] = [];
667+
const residualOutputIndices: number[] = [];
668+
for (const eq of initEqs) {
669+
const tape = new StaticTapeBuilder();
670+
const lhsIdx = tape.walk(eq.lhs);
671+
const rhsIdx = tape.walk(eq.rhs);
672+
residualOutputIndices.push(tape.pushOp({ type: "sub", a: lhsIdx, b: rhsIdx }));
673+
tapes.push(tape);
674+
}
675+
676+
// Variable name -> inst->vars[VR] resolver
677+
const varResolver = (name: string): string => {
678+
if (name === "time") return "inst->time";
679+
const sv = result.scalarVariables.find((v) => v.name === name);
680+
return sv ? `inst->vars[${sv.valueReference}]` : `0.0 /* ${name} */`;
681+
};
682+
683+
lines.push(`/* Initial Equation Solver with Exact AD Jacobian */`);
684+
lines.push(`void ${id}_initializeSolve(${id}_Instance* inst) {`);
685+
lines.push(` double R[${N}], J[${N * nUnknowns}], dx[${nUnknowns}];`);
686+
lines.push(` int iter;`);
687+
lines.push(` for (iter = 0; iter < 50; iter++) {`);
688+
689+
// Forward pass: compute residuals R[i]
690+
for (let i = 0; i < N; i++) {
691+
const tape = tapes[i];
692+
if (!tape) continue;
693+
const outIdx = residualOutputIndices[i];
694+
if (outIdx === undefined) continue;
695+
696+
lines.push(` { /* Residual ${i} */`);
697+
const fwdCode = tape.emitForwardC(varResolver);
698+
lines.push(...fwdCode.map((c) => " " + c));
699+
lines.push(` R[${i}] = t[${outIdx}];`);
700+
lines.push(` }`);
701+
}
702+
703+
// Convergence check
704+
lines.push(` double err = 0.0;`);
705+
lines.push(` for (int i = 0; i < ${N}; i++) err += fabs(R[i]);`);
706+
lines.push(` if (err < 1e-10) break;`);
707+
708+
// Reverse pass: compute Jacobian J[row * nUnknowns + col]
709+
lines.push(` /* Exact Jacobian via Reverse-Mode AD */`);
710+
for (let row = 0; row < N; row++) {
711+
const tape = tapes[row];
712+
if (!tape) continue;
713+
const outIdx = residualOutputIndices[row];
714+
if (outIdx === undefined) continue;
715+
716+
lines.push(` { /* J row ${row} */`);
717+
const fwdCode = tape.emitForwardC(varResolver);
718+
lines.push(...fwdCode.map((c) => " " + c));
719+
const { code: revCode, gradients } = tape.emitReverseC(outIdx);
720+
lines.push(...revCode.map((c) => " " + c));
721+
722+
for (let col = 0; col < nUnknowns; col++) {
723+
const varName = unknowns[col];
724+
if (!varName) continue;
725+
const gradIdx = gradients.get(varName);
726+
if (gradIdx !== undefined) {
727+
lines.push(` J[${row * nUnknowns + col}] = dt[${gradIdx}];`);
728+
} else {
729+
lines.push(` J[${row * nUnknowns + col}] = 0.0;`);
730+
}
731+
}
732+
lines.push(` }`);
733+
}
734+
735+
// Newton update: solve J * dx = -R
736+
lines.push(` for (int i = 0; i < ${N}; i++) R[i] = -R[i];`);
737+
lines.push(` solve_linear_sys(inst, ${nUnknowns}, J, R, dx);`);
738+
739+
// Apply dx to unknowns
740+
for (let j = 0; j < nUnknowns; j++) {
741+
const varName = unknowns[j];
742+
if (!varName) continue;
743+
const sv = result.scalarVariables.find((v) => v.name === varName);
744+
if (sv) {
745+
lines.push(` inst->vars[${sv.valueReference}] += dx[${j}]; /* ${varName} */`);
746+
}
747+
}
748+
749+
lines.push(` }`);
750+
lines.push(`}`);
751+
lines.push(``);
752+
753+
return lines;
754+
}
755+
609756
function generateModelC(id: string, dae: ModelicaDAE, result: FmuResult): string {
610757
// Reset buffer counters for this codegen invocation
611758
delayBufferCounter = 0;
@@ -762,9 +909,16 @@ function generateModelC(id: string, dae: ModelicaDAE, result: FmuResult): string
762909
}
763910
}
764911
}
912+
// Call the initial equation solver (generated below) after start values
913+
if (dae.initialEquations.length > 0) {
914+
lines.push(` ${id}_initializeSolve(inst);`);
915+
}
765916
lines.push("}");
766917
lines.push("");
767918

919+
// ── Initial equation solver via Newton-Raphson with exact AD Jacobian ──
920+
lines.push(...generateInitializeSolve(id, dae, result));
921+
768922
// ── getDerivatives function ──
769923
lines.push(`void ${id}_getDerivatives(${id}_Instance* inst) {`);
770924
lines.push(` ${id}_solveAlgebraicLoops(inst);`);

0 commit comments

Comments
 (0)