Skip to main content

fmi_schema/fmi2/
model_description.rs

1use crate::{Error, traits::FmiModelDescription};
2
3use super::{
4    CoSimulation, Fmi2Unit, Fmi2VariableDependency, ModelExchange, ScalarVariable, SimpleType,
5};
6
7#[derive(Default, Debug, hard_xml::XmlRead, hard_xml::XmlWrite)]
8#[xml(
9    tag = "fmiModelDescription",
10    strict(unknown_attribute, unknown_element)
11)]
12pub struct Fmi2ModelDescription {
13    /// Version of FMI (Clarification for FMI 2.0.2: for FMI 2.0.x revisions fmiVersion is defined
14    /// as "2.0").
15    #[xml(attr = "fmiVersion")]
16    pub fmi_version: String,
17
18    /// The name of the model as used in the modeling environment that generated the XML file, such
19    /// as Modelica.Mechanics.Rotational.Examples.CoupledClutches.
20    #[xml(attr = "modelName")]
21    pub model_name: String,
22
23    /// Fingerprint of xml-file content to verify that xml-file and C-functions are compatible to
24    /// each other
25    #[xml(attr = "guid")]
26    pub guid: String,
27
28    #[xml(attr = "description")]
29    pub description: Option<String>,
30
31    /// Version of FMU, e.g., "1.4.1"
32    #[xml(attr = "version")]
33    pub version: Option<String>,
34
35    /// Information on intellectual property copyright for this FMU, such as "© MyCompany 2011"
36    #[xml(attr = "copyright")]
37    pub copyright: Option<String>,
38
39    /// Information on intellectual property licensing for this FMU, such as "BSD license",
40    /// "Proprietary", or "Public Domain"
41    #[xml(attr = "license")]
42    pub license: Option<String>,
43
44    /// Name of the tool that generated the XML file.
45    #[xml(attr = "generationTool")]
46    pub generation_tool: Option<String>,
47
48    /// time/date of database creation according to ISO 8601 (preference: YYYY-MM-DDThh:mm:ss)
49    /// Date and time when the XML file was generated. The format is a subset of dateTime and
50    /// should be: YYYY-MM-DDThh:mm:ssZ (with one T between date and time; Z characterizes the
51    /// Zulu time zone, in other words, Greenwich meantime) [for example 2009-12-08T14:33:22Z].
52    #[xml(attr = "generationDateAndTime")]
53    pub generation_date_and_time: Option<String>,
54
55    /// Defines whether the variable names in `<ModelVariables>` and in `<TypeDefinitions>` follow a
56    /// particular convention.
57    #[xml(attr = "variableNamingConvention")]
58    pub variable_naming_convention: Option<String>,
59
60    /// FMI 2.0: Required for ModelExchange, ignored for Co-Simulation (may be absent).
61    #[xml(attr = "numberOfEventIndicators")]
62    pub number_of_event_indicators: Option<u32>,
63
64    /// If present, the FMU is based on FMI for Model Exchange
65    #[xml(child = "ModelExchange")]
66    pub model_exchange: Option<ModelExchange>,
67
68    /// If present, the FMU is based on FMI for Co-Simulation
69    #[xml(child = "CoSimulation")]
70    pub co_simulation: Option<CoSimulation>,
71
72    #[xml(child = "LogCategories")]
73    pub log_categories: Option<LogCategories>,
74
75    #[xml(child = "DefaultExperiment")]
76    pub default_experiment: Option<DefaultExperiment>,
77
78    #[xml(child = "UnitDefinitions")]
79    pub unit_definitions: Option<UnitDefinitions>,
80
81    #[xml(child = "TypeDefinitions")]
82    pub type_definitions: Option<TypeDefinitions>,
83
84    #[xml(child = "ModelVariables", default)]
85    pub model_variables: ModelVariables,
86
87    #[xml(child = "ModelStructure", default)]
88    pub model_structure: ModelStructure,
89}
90
91impl Fmi2ModelDescription {
92    /// Total number of variables
93    pub fn num_variables(&self) -> usize {
94        self.model_variables.variables.len()
95    }
96
97    /// Get the number of continuous states (and derivatives)
98    pub fn num_states(&self) -> usize {
99        self.model_structure.derivatives.unknowns.len()
100    }
101
102    pub fn num_event_indicators(&self) -> usize {
103        self.number_of_event_indicators.unwrap_or(0) as usize
104    }
105
106    /// Get a iterator of the SalarVariables
107    pub fn get_model_variables(&self) -> impl Iterator<Item = &ScalarVariable> {
108        self.model_variables.variables.iter()
109    }
110
111    #[cfg(false)]
112    pub fn get_model_variable_by_vr(&self, vr: u32) -> Option<&ScalarVariable> {
113        self.model_variables.map.get(&vr)
114    }
115
116    /// Turns an UnknownList into a nested Vector of ScalarVariables and their Dependencies
117    #[cfg(false)]
118    fn map_unknowns(
119        &self,
120        list: &UnknownList,
121    ) -> Result<Vec<UnknownsTuple>, ModelDescriptionError> {
122        list.unknowns
123            .iter()
124            .map(|unknown| {
125                self.model_variables
126                    .by_index
127                    // Variable indices start at 1 in the modelDescription
128                    .get(unknown.index as usize - 1)
129                    .map(|vr| &self.model_variables.map[vr])
130                    .ok_or_else(|| {
131                        ModelDescriptionError::VariableAtIndexNotFound(
132                            self.model_name.clone(),
133                            unknown.index as usize,
134                        )
135                    })
136                    .and_then(|var| {
137                        let deps = unknown
138                            .dependencies
139                            .iter()
140                            .map(|dep| {
141                                self.model_variables
142                                    .by_index
143                                    .get(*dep as usize - 1)
144                                    .map(|vr| &self.model_variables.map[vr])
145                                    .ok_or_else(|| {
146                                        ModelDescriptionError::VariableAtIndexNotFound(
147                                            self.model_name.clone(),
148                                            *dep as usize,
149                                        )
150                                    })
151                            })
152                            .collect::<Result<Vec<_>, ModelDescriptionError>>()?;
153
154                        Ok((var, deps))
155                    })
156            })
157            .collect()
158    }
159
160    /// Get a reference to the vector of Unknowns marked as outputs
161    #[cfg(false)]
162    pub fn outputs(&self) -> Result<Vec<UnknownsTuple>, ModelDescriptionError> {
163        self.map_unknowns(&self.model_structure.outputs)
164    }
165
166    /// Get a reference to the vector of Unknowns marked as derivatives
167    #[cfg(false)]
168    pub fn derivatives(&self) -> Result<Vec<UnknownsTuple>, ModelDescriptionError> {
169        self.map_unknowns(&self.model_structure.derivatives)
170    }
171
172    /// Get a reference to the vector of Unknowns marked as initial_unknowns
173    #[cfg(false)]
174    pub fn initial_unknowns(&self) -> Result<Vec<UnknownsTuple>, ModelDescriptionError> {
175        self.map_unknowns(&self.model_structure.initial_unknowns)
176    }
177
178    /// Get a reference to the model variable with the given name
179    pub fn model_variable_by_name(&self, name: &str) -> Result<&ScalarVariable, Error> {
180        self.model_variables
181            .variables
182            .iter()
183            .find(|var| var.name == name)
184            .ok_or_else(|| Error::VariableNotFound(name.to_owned()))
185    }
186
187    /// This private function is used to de-reference variable indices from the UnknownList and
188    /// Real{derivative}
189    #[cfg(false)]
190    fn model_variable_by_index(
191        &self,
192        idx: usize,
193    ) -> Result<&ScalarVariable, ModelDescriptionError> {
194        self.model_variables
195            .by_index
196            .get(idx - 1)
197            .map(|vr| &self.model_variables.map[vr])
198            .ok_or_else(|| {
199                ModelDescriptionError::VariableAtIndexNotFound(
200                    self.model_name.clone(),
201                    idx as usize,
202                )
203            })
204    }
205
206    /// Return a vector of tuples `(&ScalarVariable, &ScalarVariabel)`, where the 1st is a
207    /// continuous-time state, and the 2nd is its derivative.
208    #[cfg(false)]
209    pub fn continuous_states(
210        &self,
211    ) -> Result<Vec<(&ScalarVariable, &ScalarVariable)>, ModelDescriptionError> {
212        self.model_structure
213            .derivatives
214            .unknowns
215            .iter()
216            .map(|unknown| {
217                self.model_variable_by_index(unknown.index as usize)
218                    .and_then(|der| {
219                        if let ScalarVariableElement::Real { derivative, .. } = der.elem {
220                            derivative
221                                .ok_or_else(|| {
222                                    ModelDescriptionError::VariableDerivativeMissing(
223                                        der.name.clone(),
224                                    )
225                                })
226                                .and_then(|der_idx| {
227                                    self.model_variable_by_index(der_idx as usize)
228                                        .map(|state| (state, der))
229                                })
230                        } else {
231                            Err(ModelDescriptionError::VariableTypeMismatch(
232                                ScalarVariableElementBase::Real,
233                                ScalarVariableElementBase::from(&der.elem),
234                            ))
235                        }
236                    })
237            })
238            .collect()
239    }
240}
241
242impl FmiModelDescription for Fmi2ModelDescription {
243    fn model_name(&self) -> &str {
244        &self.model_name
245    }
246
247    fn version_string(&self) -> &str {
248        &self.fmi_version
249    }
250
251    fn deserialize(xml: &str) -> Result<Self, crate::Error> {
252        hard_xml::XmlRead::from_str(xml).map_err(crate::Error::XmlParse)
253    }
254
255    fn serialize(&self) -> Result<String, crate::Error> {
256        hard_xml::XmlWrite::to_string(self).map_err(crate::Error::XmlParse)
257    }
258}
259
260#[derive(Clone, Default, PartialEq, Debug, hard_xml::XmlRead, hard_xml::XmlWrite)]
261#[xml(tag = "LogCategories", strict(unknown_attribute, unknown_element))]
262pub struct LogCategories {
263    #[xml(child = "Category")]
264    pub categories: Vec<Category>,
265}
266
267#[derive(Clone, Default, PartialEq, Debug, hard_xml::XmlRead, hard_xml::XmlWrite)]
268#[xml(tag = "Category", strict(unknown_attribute, unknown_element))]
269pub struct Category {
270    #[xml(attr = "name")]
271    pub name: String,
272    #[xml(attr = "description")]
273    pub description: String,
274}
275
276#[derive(Clone, Default, PartialEq, Debug, hard_xml::XmlRead, hard_xml::XmlWrite)]
277#[xml(tag = "DefaultExperiment")]
278pub struct DefaultExperiment {
279    /// Default start time of simulation
280    #[xml(attr = "startTime")]
281    pub start_time: Option<f64>,
282    /// Default stop time of simulation
283    #[xml(attr = "stopTime")]
284    pub stop_time: Option<f64>,
285    /// Default relative integration tolerance
286    #[xml(attr = "tolerance")]
287    pub tolerance: Option<f64>,
288    /// ModelExchange: Default step size for fixed step integrators.
289    /// CoSimulation: Preferred communicationStepSize.
290    #[xml(attr = "stepSize")]
291    pub step_size: Option<f64>,
292}
293
294impl DefaultExperiment {
295    pub fn start_time(&self) -> f64 {
296        self.start_time.unwrap_or(0.0)
297    }
298
299    pub fn stop_time(&self) -> f64 {
300        self.stop_time.unwrap_or(10.0)
301    }
302
303    pub fn tolerance(&self) -> f64 {
304        self.tolerance.unwrap_or(1e-3)
305    }
306
307    pub fn step_size(&self) -> Option<f64> {
308        self.step_size
309    }
310}
311
312#[derive(Default, Debug, hard_xml::XmlRead, hard_xml::XmlWrite)]
313#[xml(tag = "UnitDefinitions", strict(unknown_attribute, unknown_element))]
314pub struct UnitDefinitions {
315    #[xml(child = "Unit")]
316    pub units: Vec<Fmi2Unit>,
317}
318
319#[derive(Default, Debug, hard_xml::XmlRead, hard_xml::XmlWrite)]
320#[xml(tag = "TypeDefinitions", strict(unknown_attribute, unknown_element))]
321pub struct TypeDefinitions {
322    #[xml(child = "SimpleType")]
323    pub types: Vec<SimpleType>,
324}
325
326#[derive(Default, Debug, hard_xml::XmlRead, hard_xml::XmlWrite)]
327#[xml(tag = "ModelVariables", strict(unknown_attribute, unknown_element))]
328pub struct ModelVariables {
329    #[xml(child = "ScalarVariable")]
330    pub variables: Vec<ScalarVariable>,
331}
332
333#[derive(Default, PartialEq, Debug, hard_xml::XmlRead, hard_xml::XmlWrite)]
334#[xml(tag = "ModelStructure", strict(unknown_attribute, unknown_element))]
335pub struct ModelStructure {
336    #[xml(child = "Outputs", default)]
337    pub outputs: Outputs,
338
339    #[xml(child = "Derivatives", default)]
340    pub derivatives: Derivatives,
341
342    #[xml(child = "InitialUnknowns", default)]
343    pub initial_unknowns: InitialUnknowns,
344}
345
346#[derive(Default, PartialEq, Debug, hard_xml::XmlRead, hard_xml::XmlWrite)]
347#[xml(tag = "Outputs")]
348pub struct Outputs {
349    #[xml(child = "Unknown")]
350    pub unknowns: Vec<Fmi2VariableDependency>,
351}
352
353#[derive(Default, PartialEq, Debug, hard_xml::XmlRead, hard_xml::XmlWrite)]
354#[xml(tag = "Derivatives")]
355pub struct Derivatives {
356    #[xml(child = "Unknown")]
357    pub unknowns: Vec<Fmi2VariableDependency>,
358}
359
360#[derive(Default, PartialEq, Debug, hard_xml::XmlRead, hard_xml::XmlWrite)]
361#[xml(tag = "InitialUnknowns")]
362pub struct InitialUnknowns {
363    #[xml(child = "Unknown")]
364    pub unknowns: Vec<Fmi2VariableDependency>,
365}
366
367#[cfg(test)]
368mod tests {
369    use hard_xml::XmlRead;
370
371    use super::*;
372
373    #[test]
374    fn test_model_description() {
375        let s = r##"<?xml version="1.0" encoding="UTF8"?>
376<fmiModelDescription
377 fmiVersion="2.0"
378 modelName="MyLibrary.SpringMassDamper"
379 guid="{8c4e810f-3df3-4a00-8276-176fa3c9f9e0}"
380 description="Rotational Spring Mass Damper System"
381 version="1.0"
382 generationDateAndTime="2011-09-23T16:57:33Z"
383 variableNamingConvention="structured"
384 numberOfEventIndicators="2">
385 <ModelVariables>
386    <ScalarVariable name="x[1]" valueReference="0" initial="exact"> <Real/> </ScalarVariable> <!-- idex="5" -->
387    <ScalarVariable name="x[2]" valueReference="1" initial="exact"> <Real/> </ScalarVariable> <!-- index="6" -->
388    <ScalarVariable name="PI.x" valueReference="46" description="State of block" causality="local" variability="continuous" initial="calculated">
389        <Real relativeQuantity="false" />
390    </ScalarVariable>
391    <ScalarVariable name="der(PI.x)" valueReference="45" causality="local" variability="continuous" initial="calculated">
392        <Real relativeQuantity="false" derivative="3" />
393    </ScalarVariable>
394 </ModelVariables>
395 <ModelStructure>
396    <Outputs><Unknown index="1" dependencies="1 2" /><Unknown index="2" /></Outputs>
397    <Derivatives><Unknown index="4" dependencies="1 2" /></Derivatives>
398    <InitialUnknowns />
399</ModelStructure>
400</fmiModelDescription>"##;
401        let md = Fmi2ModelDescription::from_str(&s).unwrap();
402        assert_eq!(md.fmi_version, "2.0");
403        assert_eq!(md.model_name, "MyLibrary.SpringMassDamper");
404        assert_eq!(md.guid, "{8c4e810f-3df3-4a00-8276-176fa3c9f9e0}");
405        assert_eq!(
406            md.description.as_deref(),
407            Some("Rotational Spring Mass Damper System")
408        );
409        assert_eq!(md.version.as_deref(), Some("1.0"));
410        // assert_eq!(x.generation_date_and_time, chrono::DateTime<chrono::Utc>::from)
411        assert_eq!(md.variable_naming_convention, Some("structured".to_owned()));
412        assert_eq!(md.number_of_event_indicators, Some(2));
413        assert_eq!(md.model_variables.variables.len(), 4);
414
415        let outputs = &md.model_structure.outputs.unknowns;
416        assert_eq!(outputs.len(), 2);
417        assert_eq!(outputs[0].index, 1);
418        assert_eq!(outputs[0].dependencies, vec![1, 2]);
419        assert_eq!(outputs[1].index, 2);
420        assert!(outputs[1].dependencies.is_empty());
421
422        let derivatives = &md.model_structure.derivatives.unknowns;
423        assert_eq!(derivatives.len(), 1);
424        assert_eq!(derivatives[0].index, 4);
425        assert_eq!(derivatives[0].dependencies, vec![1, 2]);
426    }
427}