Skip to content

Commit b9f95ae

Browse files
committed
feat(core): implement FMI 3.0 dual-standard API, Native Clocks, and XML Dimension grouping
1 parent ff79649 commit b9f95ae

5 files changed

Lines changed: 554 additions & 10 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export class ModelicaDAE {
2323
initialEquations: ModelicaEquation[] = [];
2424
/** Algorithm sections from `initial algorithm` sections. */
2525
initialAlgorithms: ModelicaStatement[][] = [];
26+
/** Algebraic loops (SCCs) detected during flattening. */
27+
algebraicLoops: { variables: string[]; equations: ModelicaEquation[] }[] = [];
2628
variables: ModelicaVariable[] = [];
2729
stateMachines: ModelicaStateMachine[] = [];
2830
/** Clock partitions identified by the synchronous clock inference pass. */

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7666,5 +7666,6 @@ export function findAlgebraicLoops(dae: ModelicaDAE): void {
76667666
// (In a full BLT solver, we'd replace dae.equations completely. For now, we just log them or store them).
76677667
if (algebraicLoops.length > 0) {
76687668
console.log(`[DAE] Found ${algebraicLoops.length} algebraic loop(s) in ${dae.name}`);
7669+
dae.algebraicLoops = algebraicLoops;
76697670
}
76707671
}

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

Lines changed: 198 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ModelicaArray,
1717
ModelicaBinaryExpression,
1818
ModelicaBooleanVariable,
19+
ModelicaClockVariable,
1920
ModelicaEnumerationVariable,
2021
ModelicaFunctionCallExpression,
2122
ModelicaIfElseExpression,
@@ -49,8 +50,8 @@ export interface FmiScalarVariable {
4950
variability: FmiVariability;
5051
/** Description string (optional). */
5152
description?: string;
52-
/** Data type: Real, Integer, Boolean, String. */
53-
type: "Real" | "Integer" | "Boolean" | "String";
53+
/** Data type: Real, Integer, Boolean, String, Clock. */
54+
type: "Real" | "Integer" | "Boolean" | "String" | "Clock";
5455
/** Start value (initial condition or default). */
5556
start?: number;
5657
/** SI unit string (optional). */
@@ -427,12 +428,13 @@ function mapVariable(v: ModelicaVariable, valueRef: number): FmiScalarVariable {
427428
}
428429

429430
/** Determine the FMI type from a Modelica variable. */
430-
function mapType(v: ModelicaVariable): "Real" | "Integer" | "Boolean" | "String" {
431+
function mapType(v: ModelicaVariable): "Real" | "Integer" | "Boolean" | "String" | "Clock" {
431432
if (v instanceof ModelicaRealVariable) return "Real";
432433
if (v instanceof ModelicaIntegerVariable) return "Integer";
433434
if (v instanceof ModelicaBooleanVariable) return "Boolean";
434435
if (v instanceof ModelicaStringVariable) return "String";
435436
if (v instanceof ModelicaEnumerationVariable) return "Integer";
437+
if (v instanceof ModelicaClockVariable) return "Clock";
436438
return "Real";
437439
}
438440

@@ -681,3 +683,196 @@ function formatUnknown(
681683
const kindsAttr = ` dependenciesKind="${depItems.map((d) => d.kind).join(" ")}"`;
682684
return ` <Unknown index="${index}"${depsAttr}${kindsAttr} />`;
683685
}
686+
interface Fmi3ArrayVariable {
687+
baseName: string;
688+
startRef: number;
689+
dimensions: number[];
690+
sv: FmiScalarVariable;
691+
elements: FmiScalarVariable[];
692+
}
693+
694+
function groupFmi3Variables(scalarVariables: FmiScalarVariable[]): (FmiScalarVariable | Fmi3ArrayVariable)[] {
695+
const result: (FmiScalarVariable | Fmi3ArrayVariable)[] = [];
696+
const arrayMap = new Map<string, Fmi3ArrayVariable>();
697+
698+
for (const sv of scalarVariables) {
699+
const match = sv.name.match(/^(.+)\[([\d, ]+)\]$/);
700+
if (match) {
701+
const baseName = match[1] as string;
702+
const indices = (match[2] as string).split(",").map((s) => parseInt(s.trim(), 10));
703+
704+
let arr = arrayMap.get(baseName);
705+
if (!arr) {
706+
arr = {
707+
baseName,
708+
startRef: sv.valueReference,
709+
dimensions: indices.map(() => 0),
710+
sv: { ...sv, name: baseName },
711+
elements: [],
712+
};
713+
arrayMap.set(baseName, arr);
714+
result.push(arr);
715+
}
716+
717+
const currentArr = arr as Fmi3ArrayVariable;
718+
for (let i = 0; i < indices.length; i++) {
719+
const ind = indices[i] as number;
720+
const currentDim = currentArr.dimensions[i] as number;
721+
if (ind !== undefined && currentDim !== undefined) {
722+
if (ind > currentDim) {
723+
currentArr.dimensions[i] = ind;
724+
}
725+
}
726+
}
727+
currentArr.elements.push(sv);
728+
} else {
729+
result.push(sv);
730+
}
731+
}
732+
return result;
733+
}
734+
735+
export function generateFmi3ModelDescriptionXml(
736+
variables: FmiScalarVariable[],
737+
opts: FmuOptions & {
738+
guid: string;
739+
outputRefs: number[];
740+
derivativeRefs: number[];
741+
initialUnknownRefs: number[];
742+
fmuType: FmuTypeFlags;
743+
nEventIndicators: number;
744+
deps: Map<number, DepEntry2[]>;
745+
aliasMap: Map<string, string>;
746+
enumTypes: Map<string, { name: string; description: string | null }[]>;
747+
},
748+
): string {
749+
const lines: string[] = [];
750+
751+
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
752+
lines.push("<fmiModelDescription");
753+
lines.push(' fmiVersion="3.0"');
754+
lines.push(` modelName="${escapeXml(opts.modelIdentifier)}"`);
755+
lines.push(` instantiationToken="${escapeXml(opts.guid)}"`);
756+
if (opts.description) lines.push(` description="${escapeXml(opts.description)}"`);
757+
if (opts.author) lines.push(` author="${escapeXml(opts.author)}"`);
758+
lines.push(` generationTool="${escapeXml(opts.generationTool ?? "ModelScript")}"`);
759+
lines.push(` generationDateAndTime="${new Date().toISOString()}"`);
760+
lines.push(' variableNamingConvention="structured">');
761+
762+
if (opts.fmuType.modelExchange) {
763+
lines.push("");
764+
lines.push(
765+
` <ModelExchange modelIdentifier="${escapeXml(opts.modelIdentifier)}" providesDirectionalDerivative="true" />`,
766+
);
767+
}
768+
769+
if (opts.fmuType.coSimulation) {
770+
lines.push("");
771+
lines.push(
772+
` <CoSimulation modelIdentifier="${escapeXml(opts.modelIdentifier)}" canHandleVariableCommunicationStepSize="true" providesDirectionalDerivative="true" />`,
773+
);
774+
}
775+
776+
lines.push("");
777+
lines.push(" <DefaultExperiment");
778+
lines.push(` startTime="${opts.startTime ?? 0}"`);
779+
lines.push(` stopTime="${opts.stopTime ?? 1}"`);
780+
lines.push(` stepSize="${opts.stepSize ?? 0.001}" />`);
781+
782+
lines.push("");
783+
lines.push(" <ModelVariables>");
784+
const groupedVars = groupFmi3Variables(variables);
785+
for (const item of groupedVars) {
786+
let sv: FmiScalarVariable;
787+
let dimensions: number[] | null = null;
788+
if ("elements" in item) {
789+
sv = item.sv;
790+
dimensions = item.dimensions;
791+
} else {
792+
sv = item;
793+
}
794+
795+
lines.push(` <!-- ${escapeXml(sv.name)} -->`);
796+
const descAttr = sv.description ? ` description="${escapeXml(sv.description)}"` : "";
797+
const causalityAttr = sv.causality === "independent" ? ' causality="independent"' : ` causality="${sv.causality}"`;
798+
const variabilityAttr = ` variability="${sv.variability}"`;
799+
const initialAttr = sv.initial ? ` initial="${sv.initial}"` : "";
800+
801+
let fmi3Type = sv.type as string;
802+
if (fmi3Type === "Real") fmi3Type = "Float64";
803+
else if (fmi3Type === "Integer") fmi3Type = "Int32";
804+
else if (fmi3Type === "Clock") {
805+
lines.push(
806+
` <Clock name="${escapeXml(sv.name)}" valueReference="${sv.valueReference}"${causalityAttr}${descAttr}`,
807+
);
808+
if (sv.start !== undefined || sv.derivative !== undefined || sv.declaredType || dimensions) {
809+
lines.push(`>`);
810+
if (dimensions) {
811+
for (const d of dimensions) lines.push(` <Dimension start="${d}" />`);
812+
}
813+
lines.push(` </Clock>`);
814+
} else {
815+
lines[lines.length - 1] += " />";
816+
}
817+
continue;
818+
}
819+
820+
lines.push(
821+
` <${fmi3Type} name="${escapeXml(sv.name)}" valueReference="${sv.valueReference}"${causalityAttr}${variabilityAttr}${initialAttr}${descAttr}`,
822+
);
823+
824+
if (sv.start !== undefined || sv.derivative !== undefined || sv.declaredType || dimensions) {
825+
const startAttr = sv.start !== undefined ? ` start="${sv.start}"` : "";
826+
const derivAttr = sv.derivative !== undefined ? ` derivative="${sv.derivative}"` : "";
827+
const declTypeAttr = sv.declaredType ? ` declaredType="${escapeXml(sv.declaredType)}"` : "";
828+
829+
if (dimensions) {
830+
lines.push(`>`);
831+
for (const d of dimensions) lines.push(` <Dimension start="${d}" />`);
832+
lines.push(` </${fmi3Type}>`);
833+
} else {
834+
lines.push(` ${startAttr}${derivAttr}${declTypeAttr} />`);
835+
}
836+
} else {
837+
lines[lines.length - 1] += " />";
838+
}
839+
}
840+
lines.push(" </ModelVariables>");
841+
842+
lines.push("");
843+
lines.push(" <ModelStructure>");
844+
845+
if (opts.outputRefs.length > 0) {
846+
for (const ref of opts.outputRefs) {
847+
lines.push(formatFmi3Unknown("Output", ref, opts.deps));
848+
}
849+
}
850+
851+
if (opts.derivativeRefs.length > 0) {
852+
for (const ref of opts.derivativeRefs) {
853+
lines.push(formatFmi3Unknown("ContinuousStateDerivative", ref, opts.deps));
854+
}
855+
}
856+
857+
if (opts.initialUnknownRefs.length > 0) {
858+
for (const ref of opts.initialUnknownRefs) {
859+
lines.push(formatFmi3Unknown("InitialUnknown", ref, opts.deps));
860+
}
861+
}
862+
863+
lines.push(" </ModelStructure>");
864+
lines.push("");
865+
lines.push("</fmiModelDescription>");
866+
867+
return lines.join("\n");
868+
}
869+
870+
function formatFmi3Unknown(tagName: string, ref: number, deps: Map<number, DepEntry2[]>): string {
871+
const entries = deps.get(ref);
872+
if (!entries || entries.length === 0) {
873+
return ` <${tagName} valueReference="${ref}" />`;
874+
}
875+
const depsAttr = ` dependencies="${entries.map((e) => e.vr).join(" ")}"`;
876+
const kindsAttr = ` dependenciesKind="${entries.map((e) => e.kind).join(" ")}"`;
877+
return ` <${tagName} valueReference="${ref}"${depsAttr}${kindsAttr} />`;
878+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export function buildFmuArchive(
7272
files.set(`sources/${id}_model.h`, encoder.encode(sources.modelH));
7373
files.set(`sources/${id}_model.c`, encoder.encode(sources.modelC));
7474
files.set("sources/fmi2Functions.c", encoder.encode(sources.fmi2FunctionsC));
75+
files.set("sources/fmi3Functions.c", encoder.encode(sources.fmi3FunctionsC));
7576

7677
// Include FMI 2.0 headers (minimal subset for compilation)
7778
files.set("sources/fmi2Functions.h", encoder.encode(FMI2_FUNCTIONS_H));
@@ -102,6 +103,8 @@ export function buildFmuArchive(
102103
` <BuildConfiguration modelIdentifier="${id}">`,
103104
' <SourceFileSet language="C">',
104105
` <SourceFile name="${id}_model.c" />`,
106+
` <SourceFile name="fmi2Functions.c" />`,
107+
` <SourceFile name="fmi3Functions.c" />`,
105108
" </SourceFileSet>",
106109
" </BuildConfiguration>",
107110
"</fmiBuildDescription>",

0 commit comments

Comments
 (0)