Skip to content

Commit 4e4c318

Browse files
committed
feat(core): implement FMI string state deep-copy and analytical jacobians
1 parent 6d377c5 commit 4e4c318

5 files changed

Lines changed: 305 additions & 87 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export class ModelicaDAE {
4040
diagnostics: ModelicaDiagnostic[] = [];
4141
/** Experiment annotation data (StartTime, StopTime, Tolerance, etc.). */
4242
experiment: { startTime?: number; stopTime?: number; tolerance?: number; interval?: number } = {};
43+
/** Event indicators (zero-crossing functions) for state events. */
44+
eventIndicators: ModelicaExpression[] = [];
45+
/** Discrete state updates extracted from `when` clauses. */
46+
whenClauses: ModelicaWhenEquation[] = [];
4347

4448
constructor(name: string, description?: string | null) {
4549
this.name = name;

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

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,7 @@ export class ModelicaFlattener extends ModelicaModelVisitor<[string, ModelicaDAE
817817
if (this.activeClassStack.length === 0) {
818818
this.#assembleStateMachines(args[1]);
819819
this.#partitionClocks(args[1]);
820+
this.#extractEventIndicators(args[1]);
820821

821822
// Extract experiment annotation (StartTime, StopTime, Tolerance, Interval)
822823
for (const ann of node.annotations) {
@@ -2440,6 +2441,81 @@ export class ModelicaFlattener extends ModelicaModelVisitor<[string, ModelicaDAE
24402441
partitionMap.set(i, new ModelicaClockPartition(i));
24412442
}
24422443

2444+
// Pass 2.5: Base-Clock Inference (GCD/LCM)
2445+
// Extract Clock(num, den) arguments from sample() calls and compute the global base clock
2446+
const clockExprs = new Map<number, ModelicaFunctionCallExpression>();
2447+
for (const eq of dae.equations) {
2448+
if (eq.clockDomain !== undefined && eq instanceof ModelicaSimpleEquation) {
2449+
if (!clockExprs.has(eq.clockDomain)) {
2450+
const findSample = (expr: ModelicaExpression): ModelicaFunctionCallExpression | null => {
2451+
if (expr instanceof ModelicaFunctionCallExpression && clockOps.has(expr.functionName)) return expr;
2452+
if (expr instanceof ModelicaFunctionCallExpression) {
2453+
for (const arg of expr.args) {
2454+
const found = findSample(arg);
2455+
if (found) return found;
2456+
}
2457+
}
2458+
if ("expression1" in expr && expr.expression1) {
2459+
const f = findSample(expr.expression1 as ModelicaExpression);
2460+
if (f) return f;
2461+
}
2462+
if ("expression2" in expr && expr.expression2) {
2463+
const f = findSample(expr.expression2 as ModelicaExpression);
2464+
if (f) return f;
2465+
}
2466+
if ("expression" in expr && expr.expression && expr.expression !== expr) {
2467+
const f = findSample(expr.expression as ModelicaExpression);
2468+
if (f) return f;
2469+
}
2470+
return null;
2471+
};
2472+
const call = findSample(eq.expression1) ?? findSample(eq.expression2);
2473+
if (call) clockExprs.set(eq.clockDomain, call);
2474+
}
2475+
}
2476+
}
2477+
2478+
// Helper math functions for base-clock inference
2479+
const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b));
2480+
const lcm = (a: number, b: number): number => (a * b) / gcd(a, b);
2481+
2482+
// Compute base clock: GCD(nums) / LCM(dens)
2483+
let baseNum = 0;
2484+
let baseDen = 1;
2485+
let hasFractionalClocks = false;
2486+
2487+
for (const call of clockExprs.values()) {
2488+
// Look for Clock(num, den) in the arguments of sample/hold
2489+
const clockArg = call.args.find((a) => a instanceof ModelicaFunctionCallExpression && a.functionName === "Clock");
2490+
if (clockArg instanceof ModelicaFunctionCallExpression && clockArg.args.length === 2) {
2491+
const numExpr = clockArg.args[0];
2492+
const denExpr = clockArg.args[1];
2493+
if (numExpr instanceof ModelicaIntegerLiteral && denExpr instanceof ModelicaIntegerLiteral) {
2494+
const num = numExpr.value;
2495+
const den = denExpr.value;
2496+
if (hasFractionalClocks) {
2497+
baseNum = gcd(baseNum, num);
2498+
baseDen = lcm(baseDen, den);
2499+
} else {
2500+
baseNum = num;
2501+
baseDen = den;
2502+
hasFractionalClocks = true;
2503+
}
2504+
}
2505+
}
2506+
}
2507+
2508+
const globalBaseClock = hasFractionalClocks
2509+
? new ModelicaFunctionCallExpression("Clock", [
2510+
new ModelicaIntegerLiteral(baseNum),
2511+
new ModelicaIntegerLiteral(baseDen),
2512+
])
2513+
: null;
2514+
2515+
for (const partition of partitionMap.values()) {
2516+
partition.baseClock = globalBaseClock;
2517+
}
2518+
24432519
// Assign equations to partitions
24442520
for (const eq of dae.equations) {
24452521
if (eq.clockDomain !== undefined) {
@@ -2478,6 +2554,80 @@ export class ModelicaFlattener extends ModelicaModelVisitor<[string, ModelicaDAE
24782554

24792555
dae.clockPartitions = [...partitionMap.values()].filter((p) => p.equations.length > 0);
24802556
}
2557+
2558+
/**
2559+
* Scans the fully assembled DAE for relational operators and `when` clauses.
2560+
* Extracts zero-crossing expressions into DAE `eventIndicators` and discrete
2561+
* updates into `whenClauses`.
2562+
*/
2563+
#extractEventIndicators(dae: ModelicaDAE): void {
2564+
const indicators = new Set<string>();
2565+
2566+
const extractExpr = (expr: ModelicaExpression) => {
2567+
if (expr instanceof ModelicaBinaryExpression) {
2568+
if (
2569+
expr.operator === ModelicaBinaryOperator.LESS_THAN ||
2570+
expr.operator === ModelicaBinaryOperator.LESS_THAN_OR_EQUAL ||
2571+
expr.operator === ModelicaBinaryOperator.GREATER_THAN ||
2572+
expr.operator === ModelicaBinaryOperator.GREATER_THAN_OR_EQUAL ||
2573+
expr.operator === ModelicaBinaryOperator.EQUALITY ||
2574+
expr.operator === ModelicaBinaryOperator.INEQUALITY
2575+
) {
2576+
// Add `expr.operand1 - expr.operand2` as an event indicator
2577+
const diff = new ModelicaBinaryExpression(ModelicaBinaryOperator.SUBTRACTION, expr.operand1, expr.operand2);
2578+
const hash = diff.hash;
2579+
if (!indicators.has(hash)) {
2580+
indicators.add(hash);
2581+
dae.eventIndicators.push(diff);
2582+
}
2583+
}
2584+
extractExpr(expr.operand1);
2585+
extractExpr(expr.operand2);
2586+
} else if (expr instanceof ModelicaUnaryExpression) {
2587+
extractExpr(expr.operand);
2588+
} else if (expr instanceof ModelicaFunctionCallExpression) {
2589+
for (const arg of expr.args) extractExpr(arg);
2590+
} else if (expr instanceof ModelicaIfElseExpression) {
2591+
extractExpr(expr.condition);
2592+
extractExpr(expr.thenExpression);
2593+
for (const clause of expr.elseIfClauses) {
2594+
extractExpr(clause.condition);
2595+
extractExpr(clause.expression);
2596+
}
2597+
extractExpr(expr.elseExpression);
2598+
} else if (expr instanceof ModelicaArray) {
2599+
for (const el of expr.elements) extractExpr(el);
2600+
}
2601+
};
2602+
2603+
const extractEq = (eq: ModelicaEquation) => {
2604+
if (eq instanceof ModelicaSimpleEquation) {
2605+
extractExpr(eq.expression1);
2606+
extractExpr(eq.expression2);
2607+
} else if (eq instanceof ModelicaIfEquation) {
2608+
extractExpr(eq.condition);
2609+
for (const e of eq.equations) extractEq(e);
2610+
for (const elseIf of eq.elseIfClauses) {
2611+
extractExpr(elseIf.condition);
2612+
for (const e of elseIf.equations) extractEq(e);
2613+
}
2614+
for (const e of eq.elseEquations) extractEq(e);
2615+
} else if (eq instanceof ModelicaWhenEquation) {
2616+
dae.whenClauses.push(eq);
2617+
extractExpr(eq.condition);
2618+
for (const e of eq.equations) extractEq(e);
2619+
for (const elseWhen of eq.elseWhenClauses) {
2620+
extractExpr(elseWhen.condition);
2621+
for (const e of elseWhen.equations) extractEq(e);
2622+
}
2623+
} else if (eq instanceof ModelicaForEquation) {
2624+
for (const e of eq.equations) extractEq(e);
2625+
}
2626+
};
2627+
2628+
for (const eq of dae.equations) extractEq(eq);
2629+
for (const eq of dae.initialEquations) extractEq(eq);
2630+
}
24812631
}
24822632

24832633
/**

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
ModelicaStringVariable,
2727
ModelicaSubscriptedExpression,
2828
ModelicaUnaryExpression,
29-
ModelicaWhenEquation,
3029
} from "./dae.js";
3130
import { ModelicaVariability } from "./syntax.js";
3231

@@ -244,14 +243,7 @@ export function generateFmu(dae: ModelicaDAE, options: FmuOptions, stateVars?: S
244243
* Each when-equation condition (main + elseWhen) becomes one event indicator.
245244
*/
246245
function countEventIndicators(dae: ModelicaDAE): number {
247-
let count = 0;
248-
for (const eq of dae.equations) {
249-
if (eq instanceof ModelicaWhenEquation) {
250-
count++; // Main condition
251-
count += eq.elseWhenClauses.length; // Each elsewhen clause
252-
}
253-
}
254-
return count;
246+
return dae.eventIndicators.length;
255247
}
256248

257249
/**

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import {
3131
ModelicaStringVariable,
3232
ModelicaSubscriptedExpression,
3333
ModelicaUnaryExpression,
34-
ModelicaWhenEquation,
3534
} from "./dae.js";
3635
import { ModelicaVariability } from "./syntax.js";
3736

@@ -504,14 +503,7 @@ function detectTerminals3(variables: Fmi3Variable[]): Fmi3Terminal[] {
504503
// ── Internal helpers ──
505504

506505
function countEventIndicators3(dae: ModelicaDAE): number {
507-
let count = 0;
508-
for (const eq of dae.equations) {
509-
if (eq instanceof ModelicaWhenEquation) {
510-
count++;
511-
count += eq.elseWhenClauses.length;
512-
}
513-
}
514-
return count;
506+
return dae.eventIndicators.length;
515507
}
516508

517509
function detectAliases3(dae: ModelicaDAE, variables: Fmi3Variable[]): Map<string, string> {

0 commit comments

Comments
 (0)