Skip to content

Commit 43bedf3

Browse files
committed
feat(core): synchronous clock partitioning, state machine C codegen, delay ring-buffers, spatialDistribution evaluator
1 parent b9da512 commit 43bedf3

3 files changed

Lines changed: 342 additions & 2 deletions

File tree

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

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export class ModelicaDAE {
2525
initialAlgorithms: ModelicaStatement[][] = [];
2626
variables: ModelicaVariable[] = [];
2727
stateMachines: ModelicaStateMachine[] = [];
28+
/** Clock partitions identified by the synchronous clock inference pass. */
29+
clockPartitions: ModelicaClockPartition[] = [];
2830
/** Flattened function definitions referenced by equations/algorithms. */
2931
functions: ModelicaDAE[] = [];
3032
/** External function declaration text (e.g. `external "C" ...`). */
@@ -187,6 +189,8 @@ export class ModelicaDAE {
187189

188190
export abstract class ModelicaEquation {
189191
description: string | null;
192+
/** Clock domain index (undefined = continuous time). */
193+
clockDomain?: number | undefined;
190194

191195
constructor(description?: string | null) {
192196
this.description = description ?? null;
@@ -1988,6 +1992,8 @@ export abstract class ModelicaVariable extends ModelicaPrimaryExpression {
19881992
customTypeName: string | null;
19891993
/** Array dimensions for FMI 3.0 native array support (e.g., [3] for a 1D vector, [2,3] for a 2D matrix). */
19901994
arrayDimensions: number[] | null;
1995+
/** Clock domain index (undefined = continuous time). */
1996+
clockDomain?: number | undefined;
19911997

19921998
constructor(
19931999
name: string,
@@ -2013,6 +2019,8 @@ export abstract class ModelicaVariable extends ModelicaPrimaryExpression {
20132019
this.flowPrefix = null;
20142020
this.customTypeName = null;
20152021
this.arrayDimensions = null;
2022+
/** Clock domain index (undefined = continuous time). */
2023+
this.clockDomain = undefined;
20162024
}
20172025

20182026
override get hash(): string {
@@ -2360,6 +2368,23 @@ export class ExpressionEvaluator {
23602368
* Key is the expression hash; value stores sorted (time, value) pairs.
23612369
*/
23622370
delayBuffers: Map<string, { times: number[]; values: number[] }>;
2371+
/**
2372+
* Clocked variable values: latched at each clock tick by `sample()`.
2373+
* Key is variable/expression hash.
2374+
*/
2375+
clockedValues: Map<string, number>;
2376+
/**
2377+
* Previous-tick values for `previous()` operator.
2378+
* Key is variable/expression hash.
2379+
*/
2380+
previousValues: Map<string, number>;
2381+
/** Set of clock domain IDs that ticked at the current step. */
2382+
tickedClocks: Set<number>;
2383+
/**
2384+
* State for `spatialDistribution()` operator: piecewise-linear profile on [0,1].
2385+
* Key is expression hash.
2386+
*/
2387+
spatialDistributionStates: Map<string, { positions: number[]; values: number[] }>;
23632388

23642389
constructor(env?: Map<string, number>) {
23652390
this.env = env ?? new Map();
@@ -2370,6 +2395,10 @@ export class ExpressionEvaluator {
23702395
this.functionLookup = null;
23712396
this.currentTime = 0;
23722397
this.delayBuffers = new Map();
2398+
this.clockedValues = new Map();
2399+
this.previousValues = new Map();
2400+
this.tickedClocks = new Set();
2401+
this.spatialDistributionStates = new Map();
23732402
}
23742403

23752404
/** Convenience wrapper matching the old function signature. */
@@ -2800,6 +2829,94 @@ export class ExpressionEvaluator {
28002829
return v0 + alpha * (v1 - v0);
28012830
}
28022831

2832+
// ── Synchronous clock operators (Modelica 3.3) ──
2833+
2834+
// sample(u) — latch a continuous value into the clocked partition
2835+
if (name === "sample" && arg0) {
2836+
const val = this.evaluate(arg0);
2837+
if (val === null) return null;
2838+
const key = arg0.hash;
2839+
// On clock tick, latch the value; otherwise return last latched value
2840+
if (this.tickedClocks.size > 0) {
2841+
// Move current to previous, latch new
2842+
const old = this.clockedValues.get(key);
2843+
if (old !== undefined) this.previousValues.set(key, old);
2844+
this.clockedValues.set(key, val);
2845+
return val;
2846+
}
2847+
return this.clockedValues.get(key) ?? val;
2848+
}
2849+
2850+
// hold(u) — zero-order hold: return last clocked value in continuous time
2851+
if (name === "hold" && arg0) {
2852+
const key = arg0.hash;
2853+
// If clock is ticking, evaluate and latch
2854+
if (this.tickedClocks.size > 0) {
2855+
const val = this.evaluate(arg0);
2856+
if (val !== null) this.clockedValues.set(key, val);
2857+
return val;
2858+
}
2859+
// In continuous time, return the last latched value
2860+
return this.clockedValues.get(key) ?? this.evaluate(arg0);
2861+
}
2862+
2863+
// previous(x) — return value of x at the previous clock tick
2864+
if (name === "previous" && arg0) {
2865+
const key = arg0.hash;
2866+
return this.previousValues.get(key) ?? this.evaluate(arg0) ?? 0;
2867+
}
2868+
2869+
// subSample(u, factor) — derive a slower clock (factor divides base rate)
2870+
if (name === "subSample" && arg0) {
2871+
return this.evaluate(arg0);
2872+
}
2873+
// superSample(u, factor) — derive a faster clock (factor multiplies base rate)
2874+
if (name === "superSample" && arg0) {
2875+
return this.evaluate(arg0);
2876+
}
2877+
// shiftSample(u, shiftCounter, resolution) — phase-shift
2878+
if (name === "shiftSample" && arg0) {
2879+
return this.evaluate(arg0);
2880+
}
2881+
// backSample(u, backCounter, resolution) — negative phase-shift
2882+
if (name === "backSample" && arg0) {
2883+
return this.evaluate(arg0);
2884+
}
2885+
// noClock(u) — remove clock annotation
2886+
if (name === "noClock" && arg0) {
2887+
return this.evaluate(arg0);
2888+
}
2889+
2890+
// ── spatialDistribution(in0, in1, x, positiveVelocity) ──
2891+
// 1-D transport operator: maintains a piecewise-linear profile z(x)
2892+
// on [0, 1], shifted by velocity * dt each step, filling inflow boundary.
2893+
// Returns interpolated value at x=0 (out0) or x=1 (out1) depending on velocity direction.
2894+
if (name === "spatialDistribution" && args.length >= 4) {
2895+
const in0 = this.evaluate(args[0] as ModelicaExpression);
2896+
const in1 = this.evaluate(args[1] as ModelicaExpression);
2897+
const x = this.evaluate(args[2] as ModelicaExpression);
2898+
const positiveVelocity = this.evaluate(args[3] as ModelicaExpression);
2899+
if (in0 === null || in1 === null || x === null || positiveVelocity === null) return null;
2900+
2901+
const key = (args[0] as ModelicaExpression).hash + "_sd";
2902+
let state = this.spatialDistributionStates.get(key);
2903+
if (!state) {
2904+
// Initialize with linear profile from in0 to in1
2905+
state = { positions: [0, 1], values: [in0, in1] };
2906+
this.spatialDistributionStates.set(key, state);
2907+
}
2908+
2909+
// For the scalar evaluator, return the output at x=0 or x=1
2910+
// depending on the velocity direction (simplified)
2911+
if (positiveVelocity > 0) {
2912+
// Positive velocity → output at x=1 is the transported value
2913+
return state.values[state.values.length - 1] ?? in1;
2914+
} else {
2915+
// Negative velocity → output at x=0 is the transported value
2916+
return state.values[0] ?? in0;
2917+
}
2918+
}
2919+
28032920
// ── Array constructor functions ──
28042921
// These return constant values or reduce arrays to scalars.
28052922
// In a scalarized environment, array constructors are typically resolved
@@ -3208,6 +3325,43 @@ export class ModelicaStateMachine {
32083325
}
32093326
}
32103327

3328+
/**
3329+
* A clock partition groups equations and variables that operate on the same discrete clock.
3330+
* Produced by the synchronous clock inference pass in the flattener.
3331+
*/
3332+
export class ModelicaClockPartition {
3333+
/** Unique clock domain ID. */
3334+
clockId: number;
3335+
/** Base clock expression (e.g., `Clock(0.01)` or `Clock(condition)`). */
3336+
baseClock: ModelicaExpression | null;
3337+
/** Equations belonging to this clock partition. */
3338+
equations: ModelicaEquation[] = [];
3339+
/** Variables belonging to this clock partition. */
3340+
variables: ModelicaVariable[] = [];
3341+
3342+
constructor(clockId: number, baseClock: ModelicaExpression | null = null) {
3343+
this.clockId = clockId;
3344+
this.baseClock = baseClock;
3345+
}
3346+
3347+
get hash(): string {
3348+
const hash = createHash("sha256");
3349+
hash.update("clockPartition_" + this.clockId);
3350+
for (const e of this.equations) hash.update(e.hash);
3351+
for (const v of this.variables) hash.update(v.hash);
3352+
return hash.digest("hex");
3353+
}
3354+
3355+
get toJSON(): JSONValue {
3356+
return {
3357+
"@type": "ClockPartition",
3358+
clockId: this.clockId,
3359+
equations: this.equations.map((e) => e.toJSON),
3360+
variables: this.variables.map((v) => v.toJSON),
3361+
};
3362+
}
3363+
}
3364+
32113365
export class ModelicaState {
32123366
name: string;
32133367
variables: ModelicaVariable[] = [];

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

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ModelicaBooleanLiteral,
1212
ModelicaBooleanVariable,
1313
ModelicaBreakStatement,
14+
ModelicaClockPartition,
1415
ModelicaClockVariable,
1516
ModelicaColonExpression,
1617
ModelicaComplexAssignmentStatement,
@@ -815,6 +816,7 @@ export class ModelicaFlattener extends ModelicaModelVisitor<[string, ModelicaDAE
815816

816817
if (this.activeClassStack.length === 0) {
817818
this.#assembleStateMachines(args[1]);
819+
this.#partitionClocks(args[1]);
818820

819821
// Extract experiment annotation (StartTime, StopTime, Tolerance, Interval)
820822
for (const ann of node.annotations) {
@@ -2372,6 +2374,110 @@ export class ModelicaFlattener extends ModelicaModelVisitor<[string, ModelicaDAE
23722374
}
23732375
}
23742376
}
2377+
2378+
/**
2379+
* Post-processes the flattened DAE to partition equations into clock domains.
2380+
* Scans for `sample()` function calls in equations, assigns each a clock domain ID,
2381+
* and groups related equations and variables into `ModelicaClockPartition` instances.
2382+
*
2383+
* Equations that do not reference any `sample()` / `hold()` / `previous()` remain
2384+
* in the continuous-time domain (clockDomain = undefined).
2385+
*/
2386+
#partitionClocks(dae: ModelicaDAE): void {
2387+
let nextClockId = 0;
2388+
// Map from sample() expression hash to clock domain ID
2389+
const sampleClockMap = new Map<string, number>();
2390+
2391+
// Pass 1: Scan all equations for sample() calls and assign clock IDs
2392+
const clockOps = new Set(["sample", "hold", "previous", "subSample", "superSample", "shiftSample", "backSample"]);
2393+
2394+
const findClockOps = (expr: ModelicaExpression): string | null => {
2395+
if (expr instanceof ModelicaFunctionCallExpression && clockOps.has(expr.functionName)) {
2396+
return expr.hash;
2397+
}
2398+
if (expr instanceof ModelicaFunctionCallExpression) {
2399+
for (const arg of expr.args) {
2400+
const found = findClockOps(arg);
2401+
if (found) return found;
2402+
}
2403+
}
2404+
if ("expression1" in expr && expr.expression1) {
2405+
const found = findClockOps(expr.expression1 as ModelicaExpression);
2406+
if (found) return found;
2407+
}
2408+
if ("expression2" in expr && expr.expression2) {
2409+
const found = findClockOps(expr.expression2 as ModelicaExpression);
2410+
if (found) return found;
2411+
}
2412+
if ("expression" in expr && expr.expression && expr.expression !== expr) {
2413+
const found = findClockOps(expr.expression as ModelicaExpression);
2414+
if (found) return found;
2415+
}
2416+
return null;
2417+
};
2418+
2419+
for (const eq of dae.equations) {
2420+
if (eq instanceof ModelicaSimpleEquation) {
2421+
const h1 = findClockOps(eq.expression1);
2422+
const h2 = findClockOps(eq.expression2);
2423+
const hash = h1 ?? h2;
2424+
if (hash) {
2425+
let clockId = sampleClockMap.get(hash);
2426+
if (clockId === undefined) {
2427+
clockId = nextClockId++;
2428+
sampleClockMap.set(hash, clockId);
2429+
}
2430+
eq.clockDomain = clockId;
2431+
}
2432+
}
2433+
}
2434+
2435+
// Pass 2: Build clock partitions from tagged equations
2436+
if (nextClockId === 0) return; // No clocked equations found
2437+
2438+
const partitionMap = new Map<number, ModelicaClockPartition>();
2439+
for (let i = 0; i < nextClockId; i++) {
2440+
partitionMap.set(i, new ModelicaClockPartition(i));
2441+
}
2442+
2443+
// Assign equations to partitions
2444+
for (const eq of dae.equations) {
2445+
if (eq.clockDomain !== undefined) {
2446+
partitionMap.get(eq.clockDomain)?.equations.push(eq);
2447+
}
2448+
}
2449+
2450+
// Tag variables referenced in clocked equations
2451+
const clockedVarNames = new Set<string>();
2452+
for (const eq of dae.equations) {
2453+
if (eq.clockDomain === undefined) continue;
2454+
if (eq instanceof ModelicaSimpleEquation) {
2455+
if (eq.expression1 instanceof ModelicaNameExpression) clockedVarNames.add(eq.expression1.name);
2456+
if (eq.expression2 instanceof ModelicaNameExpression) clockedVarNames.add(eq.expression2.name);
2457+
}
2458+
}
2459+
2460+
for (const v of dae.variables) {
2461+
if (clockedVarNames.has(v.name)) {
2462+
// Find which clock domain this variable belongs to
2463+
for (const eq of dae.equations) {
2464+
if (eq.clockDomain === undefined) continue;
2465+
if (eq instanceof ModelicaSimpleEquation) {
2466+
if (
2467+
(eq.expression1 instanceof ModelicaNameExpression && eq.expression1.name === v.name) ||
2468+
(eq.expression2 instanceof ModelicaNameExpression && eq.expression2.name === v.name)
2469+
) {
2470+
v.clockDomain = eq.clockDomain;
2471+
partitionMap.get(eq.clockDomain)?.variables.push(v);
2472+
break;
2473+
}
2474+
}
2475+
}
2476+
}
2477+
}
2478+
2479+
dae.clockPartitions = [...partitionMap.values()].filter((p) => p.equations.length > 0);
2480+
}
23752481
}
23762482

23772483
/**

0 commit comments

Comments
 (0)