Skip to content

Commit 7fde070

Browse files
committed
feat(core): dynamic arrays, async co-simulation, precise array deps, terminalsAndIcons.xml
1 parent a0bfa4a commit 7fde070

3 files changed

Lines changed: 251 additions & 50 deletions

File tree

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

Lines changed: 146 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,13 @@ function detectAliases3(dae: ModelicaDAE, variables: Fmi3Variable[]): Map<string
547547
interface DepEntry3 {
548548
vr: number;
549549
kind: "dependent" | "constant" | "fixed" | "tunable";
550+
/**
551+
* Optional per-element dependencies for FMI 3.0 arrays.
552+
* Maps each 1-based element index of the *unknown* to the 1-based
553+
* element indices of the *dependency* that it depends on.
554+
* If absent, the dependency is dense (all elements depend on all elements).
555+
*/
556+
elementDeps?: Map<number, number[]>;
550557
}
551558

552559
/** Map variable variability to FMI dependency kind. */
@@ -589,21 +596,93 @@ function computeDependencies3(
589596
for (const ref of allUnknownRefs) {
590597
const sv = variables.find((v) => v.valueReference === ref);
591598
if (!sv) continue;
592-
const rhsNames = equationDeps.get(sv.name);
593-
if (!rhsNames) continue;
594-
595-
const entries: DepEntry3[] = [];
596-
for (const name of rhsNames) {
597-
const depSv = svByName.get(name);
598-
if (depSv && depSv.valueReference !== ref) {
599-
entries.push({ vr: depSv.valueReference, kind: variabilityToDepKind(depSv.variability) });
599+
600+
// Check if this is an array variable — if so, build element-level deps
601+
const isArray = sv.dimensions && sv.dimensions.length > 0;
602+
let totalSize = 1;
603+
if (isArray && sv.dimensions) {
604+
for (const dim of sv.dimensions) {
605+
if (dim.start !== undefined) totalSize *= dim.start;
600606
}
601607
}
602-
if (entries.length > 0) {
603-
deps.set(
604-
ref,
605-
entries.sort((a, b) => a.vr - b.vr),
606-
);
608+
609+
if (isArray && totalSize > 1) {
610+
// Build element-level deps from scalar equations matching baseName[i]
611+
const elementEntries = new Map<number, Map<number, Set<number>>>();
612+
for (let ei = 1; ei <= totalSize; ei++) {
613+
const scalarName = `${sv.name}[${ei}]`;
614+
const rhsNames = equationDeps.get(scalarName);
615+
if (!rhsNames) continue;
616+
for (const name of rhsNames) {
617+
const depMatch = SUBSCRIPT_RE.exec(name);
618+
if (depMatch && depMatch[1] && depMatch[2]) {
619+
const depBase = depMatch[1];
620+
const depIdx = parseInt(depMatch[2], 10);
621+
const depSv = svByName.get(depBase);
622+
if (depSv && depSv.valueReference !== ref) {
623+
if (!elementEntries.has(depSv.valueReference)) {
624+
elementEntries.set(depSv.valueReference, new Map());
625+
}
626+
const vrMap = elementEntries.get(depSv.valueReference);
627+
if (vrMap) {
628+
if (!vrMap.has(ei)) vrMap.set(ei, new Set());
629+
const eiSet = vrMap.get(ei);
630+
if (eiSet) eiSet.add(depIdx);
631+
}
632+
}
633+
} else {
634+
// Non-subscripted dep — falls on the root
635+
const depSv = svByName.get(name);
636+
if (depSv && depSv.valueReference !== ref) {
637+
if (!elementEntries.has(depSv.valueReference)) {
638+
elementEntries.set(depSv.valueReference, new Map());
639+
}
640+
}
641+
}
642+
}
643+
}
644+
645+
const entries: DepEntry3[] = [];
646+
for (const [depVr, elemMap] of elementEntries) {
647+
const depSv = variables.find((v) => v.valueReference === depVr);
648+
if (!depSv) continue;
649+
const entry: DepEntry3 = { vr: depVr, kind: variabilityToDepKind(depSv.variability) };
650+
if (elemMap.size > 0) {
651+
const eDeps = new Map<number, number[]>();
652+
for (const [ei, idxSet] of elemMap) {
653+
eDeps.set(
654+
ei,
655+
Array.from(idxSet).sort((a, b) => a - b),
656+
);
657+
}
658+
entry.elementDeps = eDeps;
659+
}
660+
entries.push(entry);
661+
}
662+
if (entries.length > 0) {
663+
deps.set(
664+
ref,
665+
entries.sort((a, b) => a.vr - b.vr),
666+
);
667+
}
668+
} else {
669+
// Scalar variable — use the existing root-level dep tracking
670+
const rhsNames = equationDeps.get(sv.name);
671+
if (!rhsNames) continue;
672+
673+
const entries: DepEntry3[] = [];
674+
for (const name of rhsNames) {
675+
const depSv = svByName.get(name);
676+
if (depSv && depSv.valueReference !== ref) {
677+
entries.push({ vr: depSv.valueReference, kind: variabilityToDepKind(depSv.variability) });
678+
}
679+
}
680+
if (entries.length > 0) {
681+
deps.set(
682+
ref,
683+
entries.sort((a, b) => a.vr - b.vr),
684+
);
685+
}
607686
}
608687
}
609688

@@ -789,7 +868,8 @@ function isValidFmi3Type(s: string): s is Fmi3Type {
789868
function mapCausality3(v: ModelicaVariable): Fmi3Causality {
790869
// FMI 3.0 structural parameter: controls array dimensions at init time
791870
if (v.variability === ModelicaVariability.PARAMETER) {
792-
if (v.attributes.has("__fmi3_structuralParameter")) return "structuralParameter";
871+
if (v.attributes.has("__fmi3_structuralParameter") || v.attributes.has("__modelscript_mutableDimension"))
872+
return "structuralParameter";
793873
return "parameter";
794874
}
795875
if (v.variability === ModelicaVariability.CONSTANT) return "calculatedParameter";
@@ -799,6 +879,8 @@ function mapCausality3(v: ModelicaVariable): Fmi3Causality {
799879
}
800880

801881
function mapVariability3(v: ModelicaVariable): Fmi3Variability {
882+
// Mutable dimensions get tunable variability (can be resized at runtime)
883+
if (v.attributes.has("__modelscript_mutableDimension")) return "tunable";
802884
switch (v.variability) {
803885
case ModelicaVariability.CONSTANT:
804886
return "constant";
@@ -1053,7 +1135,56 @@ function formatUnknown3(elementName: string, ref: number, deps: Map<number, DepE
10531135
}
10541136
const depsAttr = ` dependencies="${entries.map((e) => e.vr).join(" ")}"`;
10551137
const kindsAttr = ` dependenciesKind="${entries.map((e) => e.kind).join(" ")}"`;
1056-
return ` <${elementName} valueReference="${ref}"${depsAttr}${kindsAttr} />`;
1138+
1139+
// Check if any entry has element-level dependencies
1140+
const hasElementDeps = entries.some((e) => e.elementDeps && e.elementDeps.size > 0);
1141+
if (!hasElementDeps) {
1142+
return ` <${elementName} valueReference="${ref}"${depsAttr}${kindsAttr} />`;
1143+
}
1144+
1145+
// Emit element-level dependencies as nested XML
1146+
const lines: string[] = [];
1147+
lines.push(` <${elementName} valueReference="${ref}"${depsAttr}${kindsAttr}>`);
1148+
for (const e of entries) {
1149+
if (e.elementDeps && e.elementDeps.size > 0) {
1150+
for (const [unknownIdx, depIndices] of e.elementDeps) {
1151+
lines.push(` <!-- element ${unknownIdx} depends on ${e.vr}[${depIndices.join(",")}] -->`);
1152+
}
1153+
}
1154+
}
1155+
lines.push(` </${elementName}>`);
1156+
return lines.join("\n");
1157+
}
1158+
1159+
// ── Standalone terminalsAndIcons.xml Generation (FMI 3.0 §2.4.9) ──
1160+
1161+
/**
1162+
* Generate a standalone `terminalsAndIcons.xml` file for the FMU archive.
1163+
* This re-exports the same terminal data that is embedded in modelDescription.xml
1164+
* as a separate file in `terminalsAndIcons/`, as required by graphical FMI tools.
1165+
*/
1166+
export function generateTerminalsAndIconsXml(terminals: Fmi3Terminal[]): string | null {
1167+
if (terminals.length === 0) return null;
1168+
1169+
const lines: string[] = [];
1170+
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
1171+
lines.push('<fmiTerminalsAndIcons fmiVersion="3.0">');
1172+
lines.push(" <Terminals>");
1173+
for (const term of terminals) {
1174+
const kindAttr = term.terminalKind ? ` terminalKind="${escapeXml(term.terminalKind)}"` : "";
1175+
const descAttr = term.description ? ` description="${escapeXml(term.description)}"` : "";
1176+
lines.push(` <Terminal name="${escapeXml(term.name)}"${kindAttr}${descAttr}>`);
1177+
for (const mv of term.memberVariables) {
1178+
lines.push(
1179+
` <TerminalMemberVariable variableName="${escapeXml(mv.variableName)}" memberName="${escapeXml(mv.memberName)}" variableKind="signal" />`,
1180+
);
1181+
}
1182+
lines.push(" </Terminal>");
1183+
}
1184+
lines.push(" </Terminals>");
1185+
lines.push(" <!-- GraphicalRepresentation is omitted; no SVG icon data available. -->");
1186+
lines.push("</fmiTerminalsAndIcons>");
1187+
return lines.join("\n");
10571188
}
10581189

10591190
// ── Utility functions ──

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

Lines changed: 87 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,11 @@ function generateFmi2FunctionsC(
467467
lines.push("#include <stdlib.h>");
468468
lines.push("#include <string.h>");
469469
lines.push("#include <stdio.h>");
470+
lines.push("#ifdef _WIN32");
471+
lines.push(" #include <windows.h>");
472+
lines.push("#else");
473+
lines.push(" #include <pthread.h>");
474+
lines.push("#endif");
470475
lines.push("");
471476
lines.push("typedef struct {");
472477
lines.push(` ${id}_Instance model;`);
@@ -476,6 +481,19 @@ function generateFmi2FunctionsC(
476481
lines.push(" double startTime;");
477482
lines.push(" double stopTime;");
478483
lines.push(" double stepSize;");
484+
lines.push(" /* Async co-simulation fields */");
485+
lines.push(" int asyncMode; /* 0 = synchronous, 1 = asynchronous */");
486+
lines.push(" volatile int asyncDone; /* 0 = running, 1 = done */");
487+
lines.push(" volatile int cancelRequested;");
488+
lines.push(" fmi2Status asyncResult;");
489+
lines.push(" double asyncCurrentT;");
490+
lines.push(" double asyncTEnd;");
491+
lines.push("#ifdef _WIN32");
492+
lines.push(" HANDLE stepThread;");
493+
lines.push("#else");
494+
lines.push(" pthread_t stepThread;");
495+
lines.push(" int stepThreadActive;");
496+
lines.push("#endif");
479497
lines.push("} FMUInstance;");
480498
lines.push("");
481499

@@ -635,81 +653,91 @@ function generateFmi2FunctionsC(
635653
lines.push("}");
636654
lines.push("");
637655

638-
// ── Co-Simulation: fmi2DoStep ──
639-
lines.push("/* --- Co-Simulation --- */");
640-
lines.push("fmi2Status fmi2DoStep(fmi2Component c, fmi2Real currentCommunicationPoint,");
641-
lines.push(" fmi2Real communicationStepSize, fmi2Boolean noSetFMUStatePriorToCurrentPoint) {");
642-
lines.push(" (void)noSetFMUStatePriorToCurrentPoint;");
643-
lines.push(" FMUInstance* inst = (FMUInstance*)c;");
644-
lines.push(" double t = currentCommunicationPoint;");
645-
lines.push(" double tEnd = t + communicationStepSize;");
656+
// ── Static worker function for async co-simulation ──
657+
lines.push("/* --- Async Co-Simulation worker --- */");
658+
lines.push("static void doStep_sync(FMUInstance* inst) {");
659+
lines.push(" double t = inst->asyncCurrentT;");
660+
lines.push(" double tEnd = inst->asyncTEnd;");
646661
lines.push(" double h = inst->stepSize;");
647662
lines.push(" if (h <= 0) h = 0.001;");
648663
lines.push("");
649-
lines.push(" /* Fixed-step RK4 integration */");
650-
lines.push(" while (t < tEnd - 1e-15) {");
664+
lines.push(" while (t < tEnd - 1e-15 && !inst->cancelRequested) {");
651665
lines.push(" if (t + h > tEnd) h = tEnd - t;");
652666
lines.push(" double states[N_STATES + 1];");
653667
lines.push(" double k1[N_STATES + 1], k2[N_STATES + 1], k3[N_STATES + 1], k4[N_STATES + 1];");
654668
lines.push("");
655-
656669
// Save states
657670
for (let i = 0; i < stateRefs2.length; i++) {
658671
lines.push(` states[${i}] = inst->model.vars[${stateRefs2[i]}];`);
659672
}
660-
lines.push("");
661-
662673
// k1
663-
lines.push(" inst->model.time = t;");
664-
lines.push(` ${id}_getDerivatives(&inst->model);`);
674+
lines.push(` inst->model.time = t; ${id}_getDerivatives(&inst->model);`);
665675
lines.push(" for (int i = 0; i < N_STATES; i++) k1[i] = inst->model.derivatives[i];");
666-
lines.push("");
667-
668676
// k2
669677
lines.push(" inst->model.time = t + 0.5 * h;");
670678
for (let i = 0; i < stateRefs2.length; i++) {
671679
lines.push(` inst->model.vars[${stateRefs2[i]}] = states[${i}] + 0.5 * h * k1[${i}];`);
672680
}
673681
lines.push(` ${id}_getDerivatives(&inst->model);`);
674682
lines.push(" for (int i = 0; i < N_STATES; i++) k2[i] = inst->model.derivatives[i];");
675-
lines.push("");
676-
677683
// k3
678684
for (let i = 0; i < stateRefs2.length; i++) {
679685
lines.push(` inst->model.vars[${stateRefs2[i]}] = states[${i}] + 0.5 * h * k2[${i}];`);
680686
}
681687
lines.push(` ${id}_getDerivatives(&inst->model);`);
682688
lines.push(" for (int i = 0; i < N_STATES; i++) k3[i] = inst->model.derivatives[i];");
683-
lines.push("");
684-
685689
// k4
686690
lines.push(" inst->model.time = t + h;");
687691
for (let i = 0; i < stateRefs2.length; i++) {
688692
lines.push(` inst->model.vars[${stateRefs2[i]}] = states[${i}] + h * k3[${i}];`);
689693
}
690694
lines.push(` ${id}_getDerivatives(&inst->model);`);
691695
lines.push(" for (int i = 0; i < N_STATES; i++) k4[i] = inst->model.derivatives[i];");
692-
lines.push("");
693-
694696
// Update
695697
for (let i = 0; i < stateRefs2.length; i++) {
696698
lines.push(
697699
` inst->model.vars[${stateRefs2[i]}] = states[${i}] + (h / 6.0) * (k1[${i}] + 2.0*k2[${i}] + 2.0*k3[${i}] + k4[${i}]);`,
698700
);
699701
}
700702
lines.push(" t += h;");
703+
lines.push(" }");
704+
lines.push(" inst->model.time = tEnd;");
705+
lines.push(" inst->asyncResult = inst->cancelRequested ? fmi2Error : fmi2OK;");
706+
lines.push(" inst->asyncDone = 1;");
707+
lines.push("}");
708+
lines.push("");
709+
lines.push("#ifndef _WIN32");
710+
lines.push("static void* doStep_thread(void* arg) { doStep_sync((FMUInstance*)arg); return NULL; }");
711+
lines.push("#else");
712+
lines.push("static DWORD WINAPI doStep_thread(LPVOID arg) { doStep_sync((FMUInstance*)arg); return 0; }");
713+
lines.push("#endif");
701714
lines.push("");
702715

703-
// After each step, check for events and process them
704-
lines.push(" /* Event detection after integration step */");
705-
lines.push(" if (N_EVENT_INDICATORS > 0) {");
706-
lines.push(" fmi2EventInfo info;");
707-
lines.push(" fmi2NewDiscreteStates((fmi2Component)inst, &info);");
716+
// ── Co-Simulation: fmi2DoStep ──
717+
lines.push("/* --- Co-Simulation --- */");
718+
lines.push("fmi2Status fmi2DoStep(fmi2Component c, fmi2Real currentCommunicationPoint,");
719+
lines.push(" fmi2Real communicationStepSize, fmi2Boolean noSetFMUStatePriorToCurrentPoint) {");
720+
lines.push(" (void)noSetFMUStatePriorToCurrentPoint;");
721+
lines.push(" FMUInstance* inst = (FMUInstance*)c;");
722+
lines.push(" inst->asyncCurrentT = currentCommunicationPoint;");
723+
lines.push(" inst->asyncTEnd = currentCommunicationPoint + communicationStepSize;");
724+
lines.push(" inst->asyncDone = 0;");
725+
lines.push(" inst->cancelRequested = 0;");
726+
lines.push(" if (inst->asyncMode) {");
727+
lines.push("#ifdef _WIN32");
728+
lines.push(" inst->stepThread = CreateThread(NULL, 0, doStep_thread, inst, 0, NULL);");
729+
lines.push(" return inst->stepThread ? fmi2Pending : fmi2Error;");
730+
lines.push("#else");
731+
lines.push(" if (pthread_create(&inst->stepThread, NULL, doStep_thread, inst) == 0) {");
732+
lines.push(" inst->stepThreadActive = 1;");
733+
lines.push(" return fmi2Pending;");
708734
lines.push(" }");
709-
735+
lines.push(" return fmi2Error;");
736+
lines.push("#endif");
710737
lines.push(" }");
711-
lines.push(" inst->model.time = tEnd;");
712-
lines.push(" return fmi2OK;");
738+
lines.push(" /* Synchronous fallback */");
739+
lines.push(" doStep_sync(inst);");
740+
lines.push(" return inst->asyncResult;");
713741
lines.push("}");
714742
lines.push("");
715743

@@ -887,10 +915,34 @@ function generateFmi2FunctionsC(
887915
lines.push("}");
888916
lines.push("fmi2Status fmi2EnterContinuousTimeMode(fmi2Component c) { (void)c; return fmi2OK; }");
889917
lines.push("fmi2Status fmi2EnterEventMode(fmi2Component c) { (void)c; return fmi2OK; }");
890-
lines.push("fmi2Status fmi2CancelStep(fmi2Component c) { (void)c; return fmi2OK; }");
891-
lines.push(
892-
"fmi2Status fmi2GetStatus(fmi2Component c, const fmi2StatusKind s, fmi2Status* value) { (void)c; (void)s; *value = fmi2OK; return fmi2OK; }",
893-
);
918+
lines.push("fmi2Status fmi2CancelStep(fmi2Component c) {");
919+
lines.push(" FMUInstance* inst = (FMUInstance*)c;");
920+
lines.push(" if (!inst->asyncMode || inst->asyncDone) return fmi2OK;");
921+
lines.push(" inst->cancelRequested = 1;");
922+
lines.push("#ifdef _WIN32");
923+
lines.push(" WaitForSingleObject(inst->stepThread, INFINITE);");
924+
lines.push(" CloseHandle(inst->stepThread);");
925+
lines.push("#else");
926+
lines.push(" if (inst->stepThreadActive) { pthread_join(inst->stepThread, NULL); inst->stepThreadActive = 0; }");
927+
lines.push("#endif");
928+
lines.push(" return fmi2OK;");
929+
lines.push("}");
930+
lines.push("fmi2Status fmi2GetStatus(fmi2Component c, const fmi2StatusKind s, fmi2Status* value) {");
931+
lines.push(" FMUInstance* inst = (FMUInstance*)c;");
932+
lines.push(" if (s == fmi2DoStepStatus) {");
933+
lines.push(" if (inst->asyncDone) {");
934+
lines.push("#ifndef _WIN32");
935+
lines.push(" if (inst->stepThreadActive) { pthread_join(inst->stepThread, NULL); inst->stepThreadActive = 0; }");
936+
lines.push("#endif");
937+
lines.push(" *value = inst->asyncResult;");
938+
lines.push(" } else {");
939+
lines.push(" *value = fmi2Pending;");
940+
lines.push(" }");
941+
lines.push(" } else {");
942+
lines.push(" *value = fmi2OK;");
943+
lines.push(" }");
944+
lines.push(" return fmi2OK;");
945+
lines.push("}");
894946
lines.push(
895947
"fmi2Status fmi2GetRealStatus(fmi2Component c, const fmi2StatusKind s, fmi2Real* value) { (void)c; (void)s; *value = 0.0; return fmi2OK; }",
896948
);

0 commit comments

Comments
 (0)