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