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" ;
1515import {
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" ;
2531import { 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. */
265372function 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. */
334466function 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+ }
0 commit comments