Skip to content

Commit a632a87

Browse files
committed
feat(core): modelstructure dependencies, unit/type definitions, initial attribute, log categories, derivative interpolation, resource packaging
1 parent ec95ddd commit a632a87

3 files changed

Lines changed: 244 additions & 8 deletions

File tree

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

Lines changed: 205 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,21 @@
1111
* FMI 2.0 specification: https://fmi-standard.org/
1212
*/
1313

14-
import type { ModelicaDAE, ModelicaVariable } from "./dae.js";
14+
import type { ModelicaDAE, ModelicaExpression, ModelicaVariable } from "./dae.js";
1515
import {
16+
ModelicaArray,
17+
ModelicaBinaryExpression,
1618
ModelicaBooleanVariable,
1719
ModelicaEnumerationVariable,
20+
ModelicaFunctionCallExpression,
21+
ModelicaIfElseExpression,
1822
ModelicaIntegerVariable,
1923
ModelicaNameExpression,
2024
ModelicaRealVariable,
2125
ModelicaSimpleEquation,
2226
ModelicaStringVariable,
27+
ModelicaSubscriptedExpression,
28+
ModelicaUnaryExpression,
2329
ModelicaWhenEquation,
2430
} from "./dae.js";
2531
import { ModelicaVariability } from "./syntax.js";
@@ -50,10 +56,16 @@ export interface FmiScalarVariable {
5056
start?: number;
5157
/** SI unit string (optional). */
5258
unit?: string;
59+
/** Display unit (optional). */
60+
displayUnit?: string;
5361
/** For state variables: index of the corresponding derivative variable. */
5462
derivative?: number;
5563
/** Alias type: "noAlias" (default), "alias", or "negatedAlias". */
5664
alias?: "noAlias" | "alias" | "negatedAlias";
65+
/** FMI 2.0 initial attribute. */
66+
initial?: "exact" | "approx" | "calculated";
67+
/** Declared type name (for TypeDefinitions linkage). */
68+
declaredType?: string;
5769
}
5870

5971
/** FMU type support flags. */
@@ -136,11 +148,24 @@ export function generateFmu(dae: ModelicaDAE, options: FmuOptions, stateVars?: S
136148
// Map from state variable name → its valueReference (for derivative linkage)
137149
const stateVarRefs = new Map<string, number>();
138150
const states = stateVars ?? new Set<string>();
151+
/** Enumeration type definitions: typeName → literals. */
152+
const enumTypes = new Map<string, { name: string; description: string | null }[]>();
139153

140154
for (const v of dae.variables) {
141155
const sv = mapVariable(v, valueRef++);
142156
scalarVariables.push(sv);
143157

158+
// Track enumeration type names for TypeDefinitions
159+
if (v instanceof ModelicaEnumerationVariable && v.enumerationLiterals.length > 0) {
160+
const typeName = v.enumerationLiterals[0]?.typeName;
161+
if (typeName && !enumTypes.has(typeName)) {
162+
enumTypes.set(
163+
typeName,
164+
v.enumerationLiterals.map((lit) => ({ name: lit.stringValue, description: lit.description })),
165+
);
166+
}
167+
if (typeName) sv.declaredType = typeName;
168+
}
144169
// Track state variable references for derivative linkage
145170
if (states.has(v.name)) {
146171
stateVarRefs.set(v.name, sv.valueReference);
@@ -180,6 +205,9 @@ export function generateFmu(dae: ModelicaDAE, options: FmuOptions, stateVars?: S
180205
}
181206
}
182207

208+
// ── Compute dependencies ──
209+
const deps = computeDependencies(dae, scalarVariables, outputRefs, derivativeRefs, initialUnknownRefs);
210+
183211
// ── Generate modelDescription.xml ──
184212
const fmuType = options.fmuType ?? { modelExchange: true, coSimulation: true };
185213
const nEventIndicators = countEventIndicators(dae);
@@ -192,6 +220,8 @@ export function generateFmu(dae: ModelicaDAE, options: FmuOptions, stateVars?: S
192220
fmuType,
193221
nEventIndicators,
194222
aliasMap,
223+
deps,
224+
enumTypes,
195225
});
196226

197227
return {
@@ -261,6 +291,83 @@ function detectAliases(dae: ModelicaDAE, scalarVariables: FmiScalarVariable[]):
261291
return aliasMap;
262292
}
263293

294+
/**
295+
* Compute variable dependency graph for ModelStructure.
296+
* For each output/derivative/initial-unknown, find which inputs/states it depends on.
297+
*/
298+
function computeDependencies(
299+
dae: ModelicaDAE,
300+
scalarVariables: FmiScalarVariable[],
301+
outputRefs: number[],
302+
derivativeRefs: number[],
303+
initialUnknownRefs: number[],
304+
): Map<number, number[]> {
305+
const deps = new Map<number, number[]>();
306+
const svByName = new Map<string, FmiScalarVariable>();
307+
for (const sv of scalarVariables) svByName.set(sv.name, sv);
308+
309+
// Build a map from LHS variable name → equation RHS names
310+
const equationDeps = new Map<string, Set<string>>();
311+
for (const eq of dae.equations) {
312+
if (!(eq instanceof ModelicaSimpleEquation)) continue;
313+
const lhs = eq.expression1;
314+
if (lhs instanceof ModelicaNameExpression) {
315+
const names = new Set<string>();
316+
collectExpressionNames(eq.expression2, names);
317+
equationDeps.set(lhs.name, names);
318+
}
319+
}
320+
321+
const allUnknownRefs = [...outputRefs, ...derivativeRefs, ...initialUnknownRefs];
322+
for (const ref of allUnknownRefs) {
323+
const sv = scalarVariables.find((v) => v.valueReference === ref);
324+
if (!sv) continue;
325+
326+
const rhsNames = equationDeps.get(sv.name);
327+
if (!rhsNames) continue;
328+
329+
// Map referenced names to their value references
330+
const depRefs: number[] = [];
331+
for (const name of rhsNames) {
332+
const depSv = svByName.get(name);
333+
if (depSv && depSv.valueReference !== ref) {
334+
depRefs.push(depSv.valueReference);
335+
}
336+
}
337+
if (depRefs.length > 0) {
338+
deps.set(
339+
ref,
340+
depRefs.sort((a, b) => a - b),
341+
);
342+
}
343+
}
344+
345+
return deps;
346+
}
347+
348+
/** Recursively collect all variable name references from an expression. */
349+
function collectExpressionNames(expr: ModelicaExpression, names: Set<string>): void {
350+
if (expr instanceof ModelicaNameExpression) {
351+
names.add(expr.name);
352+
} else if (expr instanceof ModelicaBinaryExpression) {
353+
collectExpressionNames(expr.operand1, names);
354+
collectExpressionNames(expr.operand2, names);
355+
} else if (expr instanceof ModelicaUnaryExpression) {
356+
collectExpressionNames(expr.operand, names);
357+
} else if (expr instanceof ModelicaSubscriptedExpression) {
358+
collectExpressionNames(expr.base, names);
359+
for (const sub of expr.subscripts) collectExpressionNames(sub, names);
360+
} else if (expr instanceof ModelicaArray) {
361+
for (const el of expr.elements) collectExpressionNames(el, names);
362+
} else if (expr instanceof ModelicaIfElseExpression) {
363+
collectExpressionNames(expr.condition, names);
364+
collectExpressionNames(expr.thenExpression, names);
365+
collectExpressionNames(expr.elseExpression, names);
366+
} else if (expr instanceof ModelicaFunctionCallExpression) {
367+
for (const arg of expr.args) collectExpressionNames(arg, names);
368+
}
369+
}
370+
264371
/** Map a Modelica variable to an FMI scalar variable. */
265372
function mapVariable(v: ModelicaVariable, valueRef: number): FmiScalarVariable {
266373
const sv: FmiScalarVariable = {
@@ -293,6 +400,16 @@ function mapVariable(v: ModelicaVariable, valueRef: number): FmiScalarVariable {
293400
const unitVal = extractStringLiteral(unitAttr);
294401
if (unitVal) sv.unit = unitVal;
295402
}
403+
// Extract displayUnit
404+
const displayUnitAttr = v.attributes.get("displayUnit");
405+
if (displayUnitAttr) {
406+
const duVal = extractStringLiteral(displayUnitAttr);
407+
if (duVal) sv.displayUnit = duVal;
408+
}
409+
410+
// Determine initial attribute per FMI 2.0 spec
411+
const initialVal = mapInitial(sv.causality, sv.variability);
412+
if (initialVal) sv.initial = initialVal;
296413

297414
return sv;
298415
}
@@ -330,6 +447,21 @@ function mapVariability(v: ModelicaVariable): FmiVariability {
330447
}
331448
}
332449

450+
/** Determine the FMI 2.0 `initial` attribute from causality + variability. */
451+
function mapInitial(
452+
causality: FmiCausality,
453+
variability: FmiVariability,
454+
): "exact" | "approx" | "calculated" | undefined {
455+
if (causality === "parameter") return "exact";
456+
if (causality === "input") return undefined; // inputs have no initial
457+
if (causality === "independent") return undefined;
458+
if (causality === "output") return "calculated";
459+
// causality === "local"
460+
if (variability === "constant") return "exact";
461+
if (variability === "fixed" || variability === "tunable") return "calculated";
462+
return "calculated";
463+
}
464+
333465
/** Extract a numeric literal value from a DAE expression. */
334466
function extractNumericLiteral(expr: unknown): number | null {
335467
if (!expr || typeof expr !== "object") return null;
@@ -374,7 +506,9 @@ function generateModelDescriptionXml(
374506
initialUnknownRefs: number[];
375507
fmuType: FmuTypeFlags;
376508
nEventIndicators: number;
509+
deps: Map<number, number[]>;
377510
aliasMap: Map<string, string>;
511+
enumTypes: Map<string, { name: string; description: string | null }[]>;
378512
},
379513
): string {
380514
const lines: string[] = [];
@@ -403,31 +537,77 @@ function generateModelDescriptionXml(
403537
if (opts.fmuType.coSimulation) {
404538
lines.push("");
405539
lines.push(
406-
` <CoSimulation modelIdentifier="${escapeXml(opts.modelIdentifier)}" canGetAndSetFMUstate="true" canSerializeFMUstate="true" providesDirectionalDerivative="true" />`,
540+
` <CoSimulation modelIdentifier="${escapeXml(opts.modelIdentifier)}" canGetAndSetFMUstate="true" canSerializeFMUstate="true" providesDirectionalDerivative="true" canInterpolateInputs="true" />`,
407541
);
408542
}
409543

544+
// LogCategories
545+
lines.push("");
546+
lines.push(" <LogCategories>");
547+
lines.push(' <Category name="logAll" />');
548+
lines.push(' <Category name="logError" />');
549+
lines.push(' <Category name="logEvents" />');
550+
lines.push(' <Category name="logStatusWarning" />');
551+
lines.push(' <Category name="logStatusDiscard" />');
552+
lines.push(' <Category name="logStatusPending" />');
553+
lines.push(" </LogCategories>");
554+
410555
// Default experiment
411556
lines.push("");
412557
lines.push(" <DefaultExperiment");
413558
lines.push(` startTime="${opts.startTime ?? 0}"`);
414559
lines.push(` stopTime="${opts.stopTime ?? 1}"`);
415560
lines.push(` stepSize="${opts.stepSize ?? 0.001}" />`);
416561

562+
// UnitDefinitions
563+
const units = new Set<string>();
564+
for (const sv of variables) {
565+
if (sv.unit) units.add(sv.unit);
566+
if (sv.displayUnit) units.add(sv.displayUnit);
567+
}
568+
if (units.size > 0) {
569+
lines.push("");
570+
lines.push(" <UnitDefinitions>");
571+
for (const u of units) {
572+
lines.push(` <Unit name="${escapeXml(u)}" />`);
573+
}
574+
lines.push(" </UnitDefinitions>");
575+
}
576+
577+
// TypeDefinitions (enumerations)
578+
if (opts.enumTypes.size > 0) {
579+
lines.push("");
580+
lines.push(" <TypeDefinitions>");
581+
for (const [typeName, literals] of opts.enumTypes) {
582+
lines.push(` <SimpleType name="${escapeXml(typeName)}">`);
583+
lines.push(" <Enumeration>");
584+
for (const lit of literals) {
585+
const descAttr = lit.description ? ` description="${escapeXml(lit.description)}"` : "";
586+
lines.push(` <Item name="${escapeXml(lit.name)}"${descAttr} />`);
587+
}
588+
lines.push(" </Enumeration>");
589+
lines.push(" </SimpleType>");
590+
}
591+
lines.push(" </TypeDefinitions>");
592+
}
593+
417594
// Model variables
418595
lines.push("");
419596
lines.push(" <ModelVariables>");
420597
for (const sv of variables) {
421598
lines.push(` <!-- ${escapeXml(sv.name)} -->`);
422599
const descAttr = sv.description ? ` description="${escapeXml(sv.description)}"` : "";
423600
const aliasAttr = sv.alias && sv.alias !== "noAlias" ? ` alias="${sv.alias}"` : "";
601+
const initialAttr = sv.initial ? ` initial="${sv.initial}"` : "";
424602
lines.push(
425-
` <ScalarVariable name="${escapeXml(sv.name)}" valueReference="${sv.valueReference}" causality="${sv.causality}" variability="${sv.variability}"${descAttr}${aliasAttr}>`,
603+
` <ScalarVariable name="${escapeXml(sv.name)}" valueReference="${sv.valueReference}" causality="${sv.causality}" variability="${sv.variability}"${descAttr}${aliasAttr}${initialAttr}>`,
426604
);
427605
const startAttr = sv.start !== undefined ? ` start="${sv.start}"` : "";
428606
const unitAttr = sv.unit ? ` unit="${escapeXml(sv.unit)}"` : "";
607+
const duAttr = sv.displayUnit ? ` displayUnit="${escapeXml(sv.displayUnit)}"` : "";
429608
const derivAttr = sv.derivative !== undefined ? ` derivative="${sv.derivative}"` : "";
430-
lines.push(` <${sv.type}${startAttr}${unitAttr}${derivAttr} />`);
609+
const declTypeAttr = sv.declaredType ? ` declaredType="${escapeXml(sv.declaredType)}"` : "";
610+
lines.push(` <${sv.type}${startAttr}${unitAttr}${duAttr}${derivAttr}${declTypeAttr} />`);
431611
lines.push(" </ScalarVariable>");
432612
}
433613
lines.push(" </ModelVariables>");
@@ -440,7 +620,7 @@ function generateModelDescriptionXml(
440620
lines.push(" <Outputs>");
441621
for (const ref of opts.outputRefs) {
442622
const idx = variables.findIndex((v) => v.valueReference === ref);
443-
if (idx >= 0) lines.push(` <Unknown index="${idx + 1}" />`);
623+
if (idx >= 0) lines.push(formatUnknown(idx + 1, ref, opts.deps, variables));
444624
}
445625
lines.push(" </Outputs>");
446626
}
@@ -449,7 +629,7 @@ function generateModelDescriptionXml(
449629
lines.push(" <Derivatives>");
450630
for (const ref of opts.derivativeRefs) {
451631
const idx = variables.findIndex((v) => v.valueReference === ref);
452-
if (idx >= 0) lines.push(` <Unknown index="${idx + 1}" />`);
632+
if (idx >= 0) lines.push(formatUnknown(idx + 1, ref, opts.deps, variables));
453633
}
454634
lines.push(" </Derivatives>");
455635
}
@@ -458,7 +638,7 @@ function generateModelDescriptionXml(
458638
lines.push(" <InitialUnknowns>");
459639
for (const ref of opts.initialUnknownRefs) {
460640
const idx = variables.findIndex((v) => v.valueReference === ref);
461-
if (idx >= 0) lines.push(` <Unknown index="${idx + 1}" />`);
641+
if (idx >= 0) lines.push(formatUnknown(idx + 1, ref, opts.deps, variables));
462642
}
463643
lines.push(" </InitialUnknowns>");
464644
}
@@ -469,3 +649,21 @@ function generateModelDescriptionXml(
469649

470650
return lines.join("\n");
471651
}
652+
653+
/** Format an <Unknown> element with optional dependency attributes. */
654+
function formatUnknown(
655+
index: number,
656+
ref: number,
657+
deps: Map<number, number[]>,
658+
variables: FmiScalarVariable[],
659+
): string {
660+
const depRefs = deps.get(ref);
661+
if (!depRefs || depRefs.length === 0) {
662+
return ` <Unknown index="${index}" />`;
663+
}
664+
// Convert VRs to 1-based indices
665+
const depIndices = depRefs.map((vr) => variables.findIndex((v) => v.valueReference === vr) + 1).filter((i) => i > 0);
666+
const depsAttr = ` dependencies="${depIndices.join(" ")}"`;
667+
const kindsAttr = ` dependenciesKind="${depIndices.map(() => "dependent").join(" ")}"`;
668+
return ` <Unknown index="${index}"${depsAttr}${kindsAttr} />`;
669+
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export interface FmuArchiveOptions extends FmuOptions {
2929
includeSources?: boolean;
3030
/** Include serialized model.json (default: true). */
3131
includeModelJson?: boolean;
32+
/** Additional resource files to bundle in `resources/` (filename → contents). */
33+
resourceFiles?: Map<string, Uint8Array>;
3234
}
3335

3436
/** Result of FMU archive generation. */
@@ -86,6 +88,13 @@ export function buildFmuArchive(
8688
files.set("resources/model.json", encoder.encode(modelJson));
8789
}
8890

91+
// ── Additional resource files ──
92+
if (options.resourceFiles) {
93+
for (const [name, data] of options.resourceFiles) {
94+
files.set(`resources/${name}`, data);
95+
}
96+
}
97+
8998
// ── Build ZIP archive ──
9099
const archive = createZip(files);
91100

@@ -334,6 +343,8 @@ fmi2Status fmi2GetRealStatus(fmi2Component, const fmi2StatusKind, fmi2Real*);
334343
fmi2Status fmi2GetIntegerStatus(fmi2Component, const fmi2StatusKind, fmi2Integer*);
335344
fmi2Status fmi2GetBooleanStatus(fmi2Component, const fmi2StatusKind, fmi2Boolean*);
336345
fmi2Status fmi2GetStringStatus(fmi2Component, const fmi2StatusKind, fmi2String*);
346+
fmi2Status fmi2SetRealInputDerivatives(fmi2Component, const fmi2ValueReference[], size_t, const fmi2Integer[], const fmi2Real[]);
347+
fmi2Status fmi2GetRealOutputDerivatives(fmi2Component, const fmi2ValueReference[], size_t, const fmi2Integer[], fmi2Real[]);
337348
338349
#endif
339350
`;

0 commit comments

Comments
 (0)