fmu_runner/
model_description.rs

1use std::{
2    collections::HashMap,
3    fs,
4    hash::{Hash, Hasher},
5    path::Path,
6};
7
8use quick_xml::{de::from_str, DeError};
9use serde::{Deserialize, Deserializer};
10
11#[derive(Debug, PartialEq, Default, Deserialize)]
12#[serde(default, rename_all = "PascalCase")]
13#[allow(non_snake_case)]
14pub struct BaseUnit {
15    #[serde(rename = "@kg")]
16    pub kg: Option<i32>,
17    #[serde(rename = "@m")]
18    pub m: Option<i32>,
19    #[serde(rename = "@s")]
20    pub s: Option<i32>,
21    #[serde(rename = "@A")]
22    pub A: Option<i32>,
23    #[serde(rename = "@K")]
24    pub K: Option<i32>,
25    #[serde(rename = "@mol")]
26    pub mol: Option<i32>,
27    #[serde(rename = "@cd")]
28    pub cd: Option<i32>,
29    #[serde(rename = "@rad")]
30    pub rad: Option<i32>,
31    #[serde(rename = "@factor")]
32    pub factor: Option<f64>,
33    #[serde(rename = "@offset")]
34    pub offset: Option<f64>,
35}
36
37#[derive(Debug, PartialEq, Default, Deserialize)]
38#[serde(default, rename_all = "PascalCase")]
39pub struct DisplayUnit {
40    #[serde(rename = "@name")]
41    pub name: String,
42    #[serde(rename = "@factor")]
43    pub factor: Option<f64>,
44    #[serde(rename = "@offset")]
45    pub offset: Option<f64>,
46}
47
48#[derive(Debug, PartialEq, Default, Deserialize)]
49#[serde(default, rename_all = "PascalCase")]
50pub struct Unit {
51    #[serde(rename = "@name")]
52    pub name: String,
53    // #[serde(rename = "@BaseUnit")]
54    pub base_unit: Option<BaseUnit>,
55    pub display_unit: Vec<DisplayUnit>,
56    #[serde(rename = "@offset")]
57    pub offset: f64,
58}
59
60#[derive(Debug, PartialEq, Default, Deserialize)]
61#[serde(default, rename_all = "PascalCase")]
62pub struct UnitDefinitions {
63    pub unit: Vec<Unit>,
64}
65
66#[derive(Debug, PartialEq, Default, Deserialize)]
67#[serde(rename_all = "PascalCase")]
68pub struct Real {
69    #[serde(rename = "@declaredType")]
70    declared_type: Option<String>,
71    #[serde(rename = "@start")]
72    start: Option<f64>,
73    #[serde(rename = "@derivative")]
74    derivative: Option<usize>,
75    #[serde(rename = "@reinit")]
76    reinit: Option<bool>,
77}
78
79#[derive(Debug, PartialEq, Default, Deserialize)]
80#[serde(rename_all = "PascalCase")]
81pub struct Boolean {
82    #[serde(rename = "@declaredType")]
83    declared_type: Option<String>,
84    #[serde(rename = "@start")]
85    start: Option<bool>,
86}
87
88#[derive(Debug, PartialEq, Default, Deserialize)]
89#[serde(rename_all = "PascalCase")]
90pub struct Integer {
91    #[serde(rename = "@declaredType")]
92    declared_type: Option<String>,
93    #[serde(rename = "@start")]
94    start: Option<i64>,
95}
96
97#[derive(Debug, Deserialize)]
98#[serde(rename_all = "PascalCase")]
99pub enum SignalType {
100    Real(Real),
101    Integer(Integer),
102    Boolean(Boolean),
103    String,
104    Enumeration,
105}
106
107#[derive(Debug, Deserialize)]
108#[serde(rename_all = "camelCase")]
109pub enum Causality {
110    Parameter,
111    CalculatedParameter,
112    Input,
113    Output,
114    Local,
115    Independent,
116}
117
118impl Default for Causality {
119    fn default() -> Self {
120        Causality::Local
121    }
122}
123
124#[derive(Debug, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub enum Variability {
127    Constant,
128    Fixed,
129    Tunable,
130    Discrete,
131    Continuous,
132}
133
134impl Default for Variability {
135    fn default() -> Self {
136        Variability::Continuous
137    }
138}
139
140#[derive(Debug, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub enum Initial {
143    Exact,
144    Approx,
145    Calculated,
146}
147
148#[derive(Debug, Deserialize)]
149#[serde(rename_all = "PascalCase")]
150pub struct ScalarVariable {
151    #[serde(rename = "@name")]
152    pub name: String,
153    #[serde(rename = "@valueReference")]
154    pub value_reference: ::std::os::raw::c_uint,
155    #[serde(default, rename = "@description")]
156    pub description: String,
157    #[serde(default, rename = "@causality")]
158    pub causality: Causality,
159    #[serde(default, rename = "@variability")]
160    pub variability: Variability,
161    #[serde(rename = "@initial")]
162    pub initial: Option<Initial>,
163    #[serde(rename = "@canHandleMultipleSetPerTimeInstant")]
164    pub can_handle_multiple_set_per_time_instant: Option<bool>,
165    pub annotations: Option<()>,
166    #[serde(rename = "$value")]
167    pub signal_type: SignalType,
168}
169
170impl PartialEq for ScalarVariable {
171    fn eq(&self, other: &Self) -> bool {
172        self.name == other.name
173    }
174}
175impl Eq for ScalarVariable {}
176
177impl Hash for ScalarVariable {
178    fn hash<H: Hasher>(&self, state: &mut H) {
179        self.name.hash(state);
180    }
181}
182
183fn deserialize_to_map<'de, D>(deserializer: D) -> Result<HashMap<String, ScalarVariable>, D::Error>
184where
185    D: Deserializer<'de>,
186{
187    let v = Vec::<ScalarVariable>::deserialize(deserializer)?;
188    let mut map = HashMap::new();
189    for item in v {
190        map.insert(item.name.clone(), item);
191    }
192    Ok(map)
193}
194
195#[derive(Debug, PartialEq, Default, Deserialize)]
196#[serde(default, rename_all = "PascalCase")]
197pub struct ModelVariables {
198    #[serde(deserialize_with = "deserialize_to_map")]
199    pub scalar_variable: HashMap<String, ScalarVariable>,
200}
201
202#[derive(Debug, PartialEq, Default, Deserialize)]
203#[serde(default, rename_all = "PascalCase")]
204pub struct FMIFile {
205    #[serde(rename = "@name")]
206    pub name: String,
207}
208
209#[derive(Debug, PartialEq, Default, Deserialize)]
210#[serde(default, rename_all = "PascalCase")]
211pub struct FMISourceFiles {
212    #[serde(rename = "@name")]
213    pub file: Vec<FMIFile>,
214}
215
216#[derive(Debug, PartialEq, Default, Deserialize)]
217#[serde(default, rename_all = "PascalCase")]
218pub struct Category {
219    #[serde(rename = "@name")]
220    pub name: String,
221    #[serde(rename = "@description")]
222    pub description: String,
223}
224
225#[derive(Debug, PartialEq, Default, Deserialize)]
226#[serde(default, rename_all = "PascalCase")]
227pub struct LogCategories {
228    pub category: Vec<Category>,
229}
230
231#[derive(Debug, PartialEq, Default, Deserialize)]
232#[serde(default, rename_all = "PascalCase")]
233pub struct ModelExchange {
234    pub source_files: FMISourceFiles,
235    #[serde(rename = "@modelIdentifier")]
236    pub model_identifier: String,
237    #[serde(rename = "@needsExecutionTool")]
238    pub needs_execution_tool: bool,
239    #[serde(rename = "@completedIntegratorStepNotNeeded")]
240    pub completed_integrator_step_not_needed: bool,
241    #[serde(rename = "@canBeInstantiatedOnlyOncePerProcess")]
242    pub can_be_instantiated_only_once_per_process: bool,
243    #[serde(rename = "@canNotUseMemoryManagementFunctions")]
244    pub can_not_use_memory_management_functions: bool,
245    #[serde(rename = "@canGetAndSetFMUstate")]
246    pub can_get_and_set_fmustate: bool,
247    #[serde(rename = "@canSerializeFMUstate")]
248    pub can_serialize_fmustate: bool,
249    #[serde(rename = "@providesDirectionalDerivative")]
250    pub provides_directional_derivative: bool,
251}
252
253#[derive(Debug, PartialEq, Default, Deserialize)]
254#[serde(default, rename_all = "PascalCase")]
255pub struct CoSimulation {
256    pub source_files: FMISourceFiles,
257    #[serde(rename = "@modelIdentifier")]
258    pub model_identifier: String,
259    #[serde(rename = "@needsExecutionTool")]
260    pub needs_execution_tool: bool,
261    #[serde(rename = "@canHandleVariableCommunicationStepSize")]
262    pub can_handle_variable_communication_step_size: bool,
263    #[serde(rename = "@canInterpolateInputs")]
264    pub can_interpolate_inputs: bool,
265    #[serde(rename = "@maxOutputDerivativeOrder")]
266    pub max_output_derivative_order: bool,
267    #[serde(rename = "@canRunAsynchronuously")]
268    pub can_run_asynchronuously: bool,
269    #[serde(rename = "@canBeInstantiatedOnlyOncePerProcess")]
270    pub can_be_instantiated_only_once_per_process: bool,
271    #[serde(rename = "@canNotUseMemoryManagementFunctions")]
272    pub can_not_use_memory_management_functions: bool,
273    #[serde(rename = "@canGetAndSetFMUstate")]
274    pub can_get_and_set_fmustate: bool,
275    #[serde(rename = "@canSerializeFMUstate")]
276    pub can_serialize_fmustate: bool,
277    #[serde(rename = "@providesDirectionalDerivative")]
278    pub provides_directional_derivative: bool,
279}
280
281#[derive(Debug, PartialEq, Default, Deserialize)]
282#[serde(default, rename_all = "PascalCase")]
283pub struct DefaultExperiment {
284    #[serde(rename = "@startTime")]
285    pub start_time: f64,
286    #[serde(rename = "@stopTime")]
287    pub stop_time: f64,
288    #[serde(rename = "@tolerance")]
289    pub tolerance: f64,
290    #[serde(rename = "@stepSize")]
291    pub step_size: Option<f64>,
292}
293
294#[derive(Debug, PartialEq, Default, Deserialize)]
295#[serde(default, rename_all = "PascalCase")]
296pub struct FmiModelDescription {
297    pub co_simulation: Option<CoSimulation>,
298    pub model_exchange: Option<ModelExchange>,
299    pub model_variables: ModelVariables,
300    pub unit_definitions: Option<UnitDefinitions>,
301    pub log_categories: Option<LogCategories>,
302    pub default_experiment: Option<DefaultExperiment>,
303    // TypeDefinitions
304    // VendorAnnotations
305    // ModelStructure
306    #[serde(rename = "@fmiVersion")]
307    pub fmi_version: String,
308    #[serde(rename = "@modelName")]
309    pub model_name: String,
310    #[serde(rename = "@guid")]
311    pub guid: String,
312    #[serde(rename = "@description")]
313    pub description: String,
314    #[serde(rename = "@author")]
315    pub author: String,
316    #[serde(rename = "@version")]
317    pub version: String,
318    #[serde(rename = "@copyright")]
319    pub copyright: String,
320    #[serde(rename = "@license")]
321    pub license: String,
322    #[serde(rename = "@generationTool")]
323    pub generation_tool: String,
324    #[serde(rename = "@generationDateAndTime")]
325    pub generation_date_and_time: String,
326    #[serde(rename = "@variableNamingConvention")]
327    pub variable_naming_convention: String,
328    #[serde(rename = "@numberOfEventIndicators")]
329    pub number_of_event_indicators: String,
330}
331
332impl FmiModelDescription {
333    pub fn new(path: &Path) -> Result<Self, DeError> {
334        let text = fs::read_to_string(path).unwrap();
335        from_str(&text)
336    }
337}
338
339// test module
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use rstest::rstest;
344
345    #[rstest]
346    #[case("./tests/parsing/unit-test.xml")]
347    #[case("./tests/parsing/complex-fmi.xml")]
348    #[case("./tests/parsing/bouncing-ball.xml")]
349    fn test_parsing_model_description(#[case] xml: &str) {
350        let text = fs::read_to_string(xml).unwrap();
351        let md: FmiModelDescription = from_str(&text).unwrap();
352
353        println!("{:?}", md.description);
354        println!("{:?}", md.default_experiment);
355        println!("{:?}", md.model_variables);
356        println!("{:?}", md.model_variables.scalar_variable);
357    }
358}