Skip to content

Commit 37e04d6

Browse files
committed
feat(core,cosim): fmi 2.0 state save/restore, event handling, string variable support
1 parent f52621b commit 37e04d6

2 files changed

Lines changed: 206 additions & 9 deletions

File tree

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
ModelicaIntegerVariable,
1919
ModelicaRealVariable,
2020
ModelicaStringVariable,
21+
ModelicaWhenEquation,
2122
} from "./dae.js";
2223
import { ModelicaVariability } from "./syntax.js";
2324

@@ -172,13 +173,15 @@ export function generateFmu(dae: ModelicaDAE, options: FmuOptions, stateVars?: S
172173

173174
// ── Generate modelDescription.xml ──
174175
const fmuType = options.fmuType ?? { modelExchange: true, coSimulation: true };
176+
const nEventIndicators = countEventIndicators(dae);
175177
const xml = generateModelDescriptionXml(scalarVariables, {
176178
...options,
177179
guid,
178180
outputRefs,
179181
derivativeRefs,
180182
initialUnknownRefs,
181183
fmuType,
184+
nEventIndicators,
182185
});
183186

184187
return {
@@ -190,12 +193,27 @@ export function generateFmu(dae: ModelicaDAE, options: FmuOptions, stateVars?: S
190193
initialUnknowns: initialUnknownRefs,
191194
},
192195
guid,
193-
numberOfEventIndicators: 0,
196+
numberOfEventIndicators: nEventIndicators,
194197
};
195198
}
196199

197200
// ── Internal helpers ──
198201

202+
/**
203+
* Count event indicators from when-equations in the DAE.
204+
* Each when-equation condition (main + elseWhen) becomes one event indicator.
205+
*/
206+
function countEventIndicators(dae: ModelicaDAE): number {
207+
let count = 0;
208+
for (const eq of dae.equations) {
209+
if (eq instanceof ModelicaWhenEquation) {
210+
count++; // Main condition
211+
count += eq.elseWhenClauses.length; // Each elsewhen clause
212+
}
213+
}
214+
return count;
215+
}
216+
199217
/** Map a Modelica variable to an FMI scalar variable. */
200218
function mapVariable(v: ModelicaVariable, valueRef: number): FmiScalarVariable {
201219
const sv: FmiScalarVariable = {
@@ -308,6 +326,7 @@ function generateModelDescriptionXml(
308326
derivativeRefs: number[];
309327
initialUnknownRefs: number[];
310328
fmuType: FmuTypeFlags;
329+
nEventIndicators: number;
311330
},
312331
): string {
313332
const lines: string[] = [];
@@ -322,7 +341,7 @@ function generateModelDescriptionXml(
322341
lines.push(` generationTool="${escapeXml(opts.generationTool ?? "ModelScript")}"`);
323342
lines.push(` generationDateAndTime="${new Date().toISOString()}"`);
324343
lines.push(' variableNamingConvention="structured"');
325-
lines.push(' numberOfEventIndicators="0">');
344+
lines.push(` numberOfEventIndicators="${opts.nEventIndicators}">`);
326345

327346
// ModelExchange element
328347
if (opts.fmuType.modelExchange) {

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

Lines changed: 185 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import {
2323
ModelicaIntegerLiteral,
2424
ModelicaNameExpression,
2525
ModelicaRealLiteral,
26+
ModelicaSimpleEquation,
2627
ModelicaStringLiteral,
2728
ModelicaUnaryExpression,
29+
ModelicaWhenEquation,
2830
} from "./dae.js";
2931
import type { FmuOptions, FmuResult } from "./fmi.js";
3032
import { ModelicaBinaryOperator, ModelicaUnaryOperator, ModelicaVariability } from "./syntax.js";
@@ -58,7 +60,7 @@ export function generateFmuCSources(dae: ModelicaDAE, fmuResult: FmuResult, opti
5860
const modelC = generateModelC(id, dae, fmuResult);
5961

6062
// ── fmi2Functions.c ──
61-
const fmi2FunctionsC = generateFmi2FunctionsC(id, nVars, nStates, nStringVars, fmuResult);
63+
const fmi2FunctionsC = generateFmi2FunctionsC(id, nVars, nStates, nStringVars, dae, fmuResult);
6264

6365
// ── CMakeLists.txt ──
6466
const cmakeLists = generateCMakeLists(id);
@@ -236,6 +238,38 @@ function escapeCString(s: string): string {
236238
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
237239
}
238240

241+
/**
242+
* Convert a when-equation condition to a C zero-crossing expression.
243+
* For relational operators (x < threshold), returns `(LHS) - (RHS)`.
244+
* For boolean conditions, returns the expression directly (non-zero = active).
245+
*/
246+
function conditionToZeroCrossingC(condition: ModelicaExpression): string {
247+
if (condition instanceof ModelicaBinaryExpression) {
248+
const op = condition.operator;
249+
if (
250+
op === ModelicaBinaryOperator.LESS_THAN ||
251+
op === ModelicaBinaryOperator.LESS_THAN_OR_EQUAL ||
252+
op === ModelicaBinaryOperator.GREATER_THAN ||
253+
op === ModelicaBinaryOperator.GREATER_THAN_OR_EQUAL
254+
) {
255+
return `(${exprToC(condition.operand1)}) - (${exprToC(condition.operand2)})`;
256+
}
257+
}
258+
// Fallback: treat as boolean condition (1.0 if true, -1.0 if false)
259+
return `(${exprToC(condition)} ? 1.0 : -1.0)`;
260+
}
261+
262+
/**
263+
* Extract the assignment target variable name from the LHS of a simple equation.
264+
*/
265+
function extractAssignmentTarget(expr: ModelicaExpression): string | null {
266+
if (expr instanceof ModelicaNameExpression) return expr.name;
267+
if (expr && typeof expr === "object" && "name" in expr) {
268+
return (expr as { name: string }).name;
269+
}
270+
return null;
271+
}
272+
239273
// ── File generators ──
240274

241275
function generateModelH(id: string, nVars: number, nStates: number, nStringVars: number, result: FmuResult): string {
@@ -268,6 +302,7 @@ function generateModelH(id: string, nVars: number, nStates: number, nStringVars:
268302
lines.push(" double states[N_STATES + 1];");
269303
lines.push(" double derivatives[N_STATES + 1];");
270304
lines.push(" char* stringVars[N_STRING_VARS + 1]; /* string variable storage */");
305+
lines.push(" double eventPrev[N_EVENT_INDICATORS + 1]; /* previous event indicator values */");
271306
lines.push(" double time;");
272307
lines.push(" int isDirtyValues;");
273308
lines.push(`} ${id}_Instance;`);
@@ -370,10 +405,45 @@ function generateModelC(id: string, dae: ModelicaDAE, result: FmuResult): string
370405
lines.push("}");
371406
lines.push("");
372407

373-
// ── getEventIndicators function (stub) ──
408+
// ── getEventIndicators function ──
409+
// Each when-equation condition becomes an event indicator
410+
const whenEqs = dae.equations.filter((eq): eq is ModelicaWhenEquation => eq instanceof ModelicaWhenEquation);
374411
lines.push(`void ${id}_getEventIndicators(${id}_Instance* inst, double* indicators) {`);
375-
lines.push(" (void)inst; (void)indicators;");
376-
lines.push(" /* No event indicators yet */");
412+
413+
if (whenEqs.length === 0) {
414+
lines.push(" (void)inst; (void)indicators;");
415+
} else {
416+
// Emit local variable aliases for referenced names
417+
const eventReferencedNames = new Set<string>();
418+
for (const weq of whenEqs) {
419+
collectReferencedNames(weq.condition, eventReferencedNames);
420+
for (const clause of weq.elseWhenClauses) {
421+
collectReferencedNames(clause.condition, eventReferencedNames);
422+
}
423+
}
424+
if (eventReferencedNames.has("time")) {
425+
lines.push(" double time = inst->time;");
426+
}
427+
for (const sv of result.scalarVariables) {
428+
if (sv.causality === "independent") continue;
429+
if (!eventReferencedNames.has(sv.name)) continue;
430+
const cName = varToC(sv.name);
431+
lines.push(` double ${cName} = inst->vars[${sv.valueReference}];`);
432+
}
433+
lines.push("");
434+
435+
let indicatorIdx = 0;
436+
for (const weq of whenEqs) {
437+
const zc = conditionToZeroCrossingC(weq.condition);
438+
lines.push(` indicators[${indicatorIdx}] = ${zc}; /* when condition */`);
439+
indicatorIdx++;
440+
for (const clause of weq.elseWhenClauses) {
441+
const zcElse = conditionToZeroCrossingC(clause.condition);
442+
lines.push(` indicators[${indicatorIdx}] = ${zcElse}; /* elsewhen condition */`);
443+
indicatorIdx++;
444+
}
445+
}
446+
}
377447
lines.push("}");
378448

379449
return lines.join("\n");
@@ -384,8 +454,11 @@ function generateFmi2FunctionsC(
384454
nVars: number,
385455
nStates: number,
386456
nStringVars: number,
457+
dae: ModelicaDAE,
387458
result: FmuResult,
388459
): string {
460+
// Extract when-equations for event handling
461+
const whenEqs = dae.equations.filter((eq): eq is ModelicaWhenEquation => eq instanceof ModelicaWhenEquation);
389462
const lines: string[] = [];
390463
lines.push("/* Auto-generated by ModelScript — FMI 2.0 API implementation */");
391464
lines.push(`#include "${id}_model.h"`);
@@ -624,6 +697,15 @@ function generateFmi2FunctionsC(
624697
);
625698
}
626699
lines.push(" t += h;");
700+
lines.push("");
701+
702+
// After each step, check for events and process them
703+
lines.push(" /* Event detection after integration step */");
704+
lines.push(" if (N_EVENT_INDICATORS > 0) {");
705+
lines.push(" fmi2EventInfo info;");
706+
lines.push(" fmi2NewDiscreteStates((fmi2Component)inst, &info);");
707+
lines.push(" }");
708+
627709
lines.push(" }");
628710
lines.push(" inst->model.time = tEnd;");
629711
lines.push(" return fmi2OK;");
@@ -703,9 +785,105 @@ function generateFmi2FunctionsC(
703785
lines.push(
704786
"fmi2Status fmi2GetNominalsOfContinuousStates(fmi2Component c, fmi2Real nominals[], size_t nx) { for (size_t i = 0; i < nx; i++) nominals[i] = 1.0; (void)c; return fmi2OK; }",
705787
);
706-
lines.push(
707-
"fmi2Status fmi2NewDiscreteStates(fmi2Component c, fmi2EventInfo* info) { info->newDiscreteStatesNeeded = fmi2False; info->terminateSimulation = fmi2False; info->nominalsOfContinuousStatesChanged = fmi2False; info->valuesOfContinuousStatesChanged = fmi2False; info->nextEventTimeDefined = fmi2False; (void)c; return fmi2OK; }",
708-
);
788+
// ── fmi2NewDiscreteStates ──
789+
// Evaluate when-equation conditions and execute body assignments on rising edge
790+
lines.push("fmi2Status fmi2NewDiscreteStates(fmi2Component c, fmi2EventInfo* info) {");
791+
lines.push(" FMUInstance* inst = (FMUInstance*)c;");
792+
lines.push(" info->newDiscreteStatesNeeded = fmi2False;");
793+
lines.push(" info->terminateSimulation = fmi2False;");
794+
lines.push(" info->nominalsOfContinuousStatesChanged = fmi2False;");
795+
lines.push(" info->valuesOfContinuousStatesChanged = fmi2False;");
796+
lines.push(" info->nextEventTimeDefined = fmi2False;");
797+
798+
if (whenEqs.length > 0) {
799+
lines.push("");
800+
lines.push(" /* Evaluate current event indicators */");
801+
lines.push(" double indicators[N_EVENT_INDICATORS];");
802+
lines.push(` ${id}_getEventIndicators(&inst->model, indicators);`);
803+
lines.push("");
804+
805+
// Emit local variable aliases for when-equation body assignments
806+
const bodyReferencedNames = new Set<string>();
807+
for (const weq of whenEqs) {
808+
for (const bodyEq of weq.equations) {
809+
if (bodyEq instanceof ModelicaSimpleEquation) {
810+
collectReferencedNames(bodyEq.expression1, bodyReferencedNames);
811+
collectReferencedNames(bodyEq.expression2, bodyReferencedNames);
812+
}
813+
}
814+
for (const clause of weq.elseWhenClauses) {
815+
for (const bodyEq of clause.equations) {
816+
if (bodyEq instanceof ModelicaSimpleEquation) {
817+
collectReferencedNames(bodyEq.expression1, bodyReferencedNames);
818+
collectReferencedNames(bodyEq.expression2, bodyReferencedNames);
819+
}
820+
}
821+
}
822+
}
823+
if (bodyReferencedNames.has("time")) {
824+
lines.push(" double time = inst->model.time;");
825+
}
826+
for (const sv of result.scalarVariables) {
827+
if (sv.causality === "independent") continue;
828+
if (!bodyReferencedNames.has(sv.name)) continue;
829+
const cName = varToC(sv.name);
830+
lines.push(` double ${cName} = inst->model.vars[${sv.valueReference}];`);
831+
}
832+
lines.push("");
833+
834+
let eventIdx = 0;
835+
for (const weq of whenEqs) {
836+
// Detect rising edge: indicator crossed from negative/zero to positive (or vice versa)
837+
lines.push(` /* when-equation ${eventIdx} */`);
838+
lines.push(` if ((indicators[${eventIdx}] > 0.0 && inst->model.eventPrev[${eventIdx}] <= 0.0) ||`);
839+
lines.push(` (indicators[${eventIdx}] <= 0.0 && inst->model.eventPrev[${eventIdx}] > 0.0)) {`);
840+
841+
// Execute the when-equation body assignments
842+
for (const bodyEq of weq.equations) {
843+
if (bodyEq instanceof ModelicaSimpleEquation) {
844+
const lhsName = extractAssignmentTarget(bodyEq.expression1);
845+
if (lhsName) {
846+
// Find the VR for this variable
847+
const sv = result.scalarVariables.find((v) => v.name === lhsName);
848+
if (sv) {
849+
lines.push(
850+
` inst->model.vars[${sv.valueReference}] = ${exprToC(bodyEq.expression2)}; /* ${lhsName} */`,
851+
);
852+
}
853+
}
854+
}
855+
}
856+
lines.push(" }");
857+
lines.push(` inst->model.eventPrev[${eventIdx}] = indicators[${eventIdx}];`);
858+
eventIdx++;
859+
860+
// elseWhen clauses
861+
for (const clause of weq.elseWhenClauses) {
862+
lines.push(` /* elsewhen-clause ${eventIdx} */`);
863+
lines.push(` if ((indicators[${eventIdx}] > 0.0 && inst->model.eventPrev[${eventIdx}] <= 0.0) ||`);
864+
lines.push(` (indicators[${eventIdx}] <= 0.0 && inst->model.eventPrev[${eventIdx}] > 0.0)) {`);
865+
for (const bodyEq of clause.equations) {
866+
if (bodyEq instanceof ModelicaSimpleEquation) {
867+
const lhsName = extractAssignmentTarget(bodyEq.expression1);
868+
if (lhsName) {
869+
const sv = result.scalarVariables.find((v) => v.name === lhsName);
870+
if (sv) {
871+
lines.push(
872+
` inst->model.vars[${sv.valueReference}] = ${exprToC(bodyEq.expression2)}; /* ${lhsName} */`,
873+
);
874+
}
875+
}
876+
}
877+
}
878+
lines.push(" }");
879+
lines.push(` inst->model.eventPrev[${eventIdx}] = indicators[${eventIdx}];`);
880+
eventIdx++;
881+
}
882+
}
883+
}
884+
885+
lines.push(" return fmi2OK;");
886+
lines.push("}");
709887
lines.push("fmi2Status fmi2EnterContinuousTimeMode(fmi2Component c) { (void)c; return fmi2OK; }");
710888
lines.push("fmi2Status fmi2EnterEventMode(fmi2Component c) { (void)c; return fmi2OK; }");
711889
lines.push("fmi2Status fmi2CancelStep(fmi2Component c) { (void)c; return fmi2OK; }");

0 commit comments

Comments
 (0)