fmu_runner/
fmu.rs

1use crate::model_description::{FmiModelDescription, ScalarVariable};
2use itertools::Itertools;
3use libfmi::{
4    fmi2Boolean, fmi2Byte, fmi2CallbackFunctions, fmi2Component, fmi2FMUstate, fmi2Integer,
5    fmi2Real, fmi2Status, fmi2Type, fmi2ValueReference, Fmi2Dll,
6};
7use std::{
8    borrow::Borrow,
9    collections::HashMap,
10    env,
11    ffi::CString,
12    fmt::Display,
13    fs, io,
14    iter::zip,
15    ops::Deref,
16    os,
17    path::PathBuf,
18    sync::atomic::{AtomicUsize, Ordering},
19};
20use thiserror::Error;
21use zip::result::ZipError;
22
23/// A unpacked FMU with a parsed model description.
24#[derive(Debug)]
25pub struct Fmu {
26    #[allow(dead_code)]
27    /// Optional tempdir that's used to hold the unpacked FMU.
28    temp_dir: Option<tempfile::TempDir>,
29    /// The directory of the unpacked FMU files.
30    unpacked_dir: PathBuf,
31    /// Parsed model description XML.
32    pub model_description: FmiModelDescription,
33}
34
35/// An instance of a loaded FMU dynamic library.
36pub struct FmuLibrary {
37    /// The loaded dll library.
38    fmi: Fmi2Dll,
39    /// The simulation type of the loaded dll.
40    ///
41    /// Note that FMI specifies different libraries for CoSimulation vs ModelExchange
42    /// so we must keep track of which library we loaded from this FMU.
43    simulation_type: fmi2Type,
44    /// The unpacked FMU. The FmuLibrary needs to take ownership of it to keep
45    /// the tempdir alive.
46    pub fmu: Fmu,
47    /// Generates unique instance names for starting new FMU instances.
48    instance_name_factory: InstanceNameFactory,
49}
50
51/// A simulation "instance", ready to execute.
52pub struct FmuInstance<C: Borrow<FmuLibrary>> {
53    /// The loaded dll library.
54    ///
55    /// This is generic behind the [`Borrow`] trait so that the user can pass in
56    /// a reference or different Cell types such as [`Arc`].
57    ///
58    /// The presence of this enforces that the [`FmuLibrary`] will outlive the
59    /// [`FmuInstance`].
60    pub lib: C,
61    /// A pointer to the "instance" we created by calling [`fmi2Instantiate`].
62    instance: *mut os::raw::c_void,
63    #[allow(dead_code)]
64    callbacks: Box<fmi2CallbackFunctions>,
65}
66
67pub struct FmuState<'fmu, C: Borrow<FmuLibrary>>(fmi2FMUstate, &'fmu FmuInstance<C>);
68
69impl<'fmu, C: Borrow<FmuLibrary>> Drop for FmuState<'fmu, C> {
70    fn drop(&mut self) {
71        unsafe {
72            self.1
73                .lib
74                .borrow()
75                .fmi
76                .fmi2FreeFMUstate(self.1.instance, &mut self.0);
77        }
78    }
79}
80
81pub struct FmuGetSetStateCapability<'fmu, C: Borrow<FmuLibrary>>(&'fmu FmuInstance<C>);
82
83impl<'fmu, C: Borrow<FmuLibrary>> FmuGetSetStateCapability<'fmu, C> {
84    pub fn get_state(&self) -> Result<FmuState<'fmu, C>, FmuError> {
85        let mut fmu2state: fmi2FMUstate = std::ptr::null_mut();
86        let pfmu2state = std::ptr::addr_of_mut!(fmu2state);
87        FmuInstance::<C>::ok_or_err(unsafe {
88            self.0
89                .lib
90                .borrow()
91                .fmi
92                .fmi2GetFMUstate(self.0.instance, pfmu2state)
93        })?;
94        Ok(FmuState(fmu2state, self.0))
95    }
96
97    pub fn set_state(&self, mut state: FmuState<'fmu, C>) -> Result<(), FmuError> {
98        let pfmu2state = std::ptr::addr_of_mut!(state.0);
99        FmuInstance::<C>::ok_or_err(unsafe {
100            self.0
101                .lib
102                .borrow()
103                .fmi
104                .fmi2SetFMUstate(self.0.instance, *pfmu2state)
105        })?;
106        Ok(())
107    }
108}
109
110pub struct FmuSerializeStateCapability<'fmu, C: Borrow<FmuLibrary>>(&'fmu FmuInstance<C>);
111
112impl<'fmu, C: Borrow<FmuLibrary>> FmuSerializeStateCapability<'fmu, C> {
113    pub fn serialize_state(&self, state: &FmuState<'fmu, C>) -> Result<Vec<u8>, FmuError> {
114        let mut size: usize = 0;
115        let pfmu2state = std::ptr::addr_of!(state.0);
116        FmuInstance::<C>::ok_or_err(unsafe {
117            self.0.lib.borrow().fmi.fmi2SerializedFMUstateSize(
118                self.0.instance,
119                *pfmu2state,
120                &mut size,
121            )
122        })?;
123        let mut serialized_state = vec![0u8; size];
124        let raw_serialized_state: *mut fmi2Byte = serialized_state.as_mut_ptr() as *mut fmi2Byte;
125        FmuInstance::<C>::ok_or_err(unsafe {
126            self.0.lib.borrow().fmi.fmi2SerializeFMUstate(
127                self.0.instance,
128                *pfmu2state,
129                raw_serialized_state,
130                size,
131            )
132        })?;
133        Ok(serialized_state)
134    }
135
136    pub fn deserialize_state(
137        &self,
138        serialized_state: &[u8],
139    ) -> Result<FmuState<'fmu, C>, FmuError> {
140        let mut fmu2state: fmi2FMUstate = std::ptr::null_mut();
141        let pfmu2state = std::ptr::addr_of_mut!(fmu2state);
142        let raw_serialized_state: *const fmi2Byte = serialized_state.as_ptr() as *const fmi2Byte;
143        FmuInstance::<C>::ok_or_err(unsafe {
144            self.0.lib.borrow().fmi.fmi2DeSerializeFMUstate(
145                self.0.instance,
146                raw_serialized_state,
147                serialized_state.len(),
148                pfmu2state,
149            )
150        })?;
151        Ok(FmuState(fmu2state, self.0))
152    }
153}
154
155/// Generates unique instance names for starting new FMU instances.
156struct InstanceNameFactory {
157    model_identifier: String,
158    /// This gets incremented every time we start a new instance of a simulation
159    /// on the dll. Instances must have unique names so we append this counter
160    /// to the instance name.
161    instance_counter: AtomicUsize,
162}
163
164impl Deref for FmuLibrary {
165    type Target = Fmu;
166
167    /// Borrow to the inner [`Fmu`] type.
168    fn deref(&self) -> &Self::Target {
169        &self.fmu
170    }
171}
172
173impl InstanceNameFactory {
174    fn new(model_identifier: String) -> Self {
175        Self {
176            model_identifier,
177            instance_counter: AtomicUsize::new(0),
178        }
179    }
180
181    fn next(&self) -> String {
182        let instance_counter = self.instance_counter.fetch_add(1, Ordering::Relaxed);
183        format!("{}_{}", self.model_identifier, instance_counter)
184    }
185}
186
187impl Fmu {
188    /// Unpack an FMU file to a tempdir and parse it's model description.
189    pub fn unpack(fmu_path: impl Into<std::path::PathBuf>) -> Result<Self, FmuUnpackError> {
190        let temp_dir = tempfile::Builder::new()
191            .prefix("fmi-runner")
192            .tempdir()
193            .map_err(FmuUnpackError::NoTempdir)?;
194
195        let fmu = Self::unpack_to(fmu_path, temp_dir.path())?;
196
197        Ok(Self {
198            temp_dir: Some(temp_dir),
199            unpacked_dir: fmu.unpacked_dir,
200            model_description: fmu.model_description,
201        })
202    }
203
204    /// Unpack an FMU file to a given target dir and parse it's model description.
205    pub fn unpack_to(
206        fmu_path: impl Into<std::path::PathBuf>,
207        target_dir: impl Into<std::path::PathBuf>,
208    ) -> Result<Self, FmuUnpackError> {
209        let fmu_path = fs::canonicalize(fmu_path.into()).map_err(FmuUnpackError::InvalidFile)?;
210        let target_dir = target_dir.into();
211
212        let zipfile = std::fs::File::open(fmu_path).map_err(FmuUnpackError::InvalidFile)?;
213        let mut archive = zip::ZipArchive::new(zipfile).map_err(|e| match e {
214            ZipError::Io(e) => FmuUnpackError::InvalidFile(e),
215            e => FmuUnpackError::InvalidArchive(e),
216        })?;
217        archive.extract(&target_dir).map_err(|e| match e {
218            ZipError::Io(e) => FmuUnpackError::InvalidOutputDir(e),
219            e => FmuUnpackError::InvalidArchive(e),
220        })?;
221
222        let model_description = FmiModelDescription::new(&target_dir.join("modelDescription.xml"))?;
223
224        Ok(Self {
225            temp_dir: None,
226            unpacked_dir: target_dir,
227            model_description,
228        })
229    }
230
231    /// Load the FMU dynamic library.
232    pub fn load(self, simulation_type: fmi2Type) -> Result<FmuLibrary, FmuLoadError> {
233        self.load_with_handler(simulation_type, |_| {})
234    }
235
236    /// Load the FMU dynamic library, but pass in a handler to load custom symbols
237    /// from the dynamic library before it's passed to the runner.
238    ///
239    /// This is useful for loading custom symbols and functions from the FMU library
240    /// that are not part of the FMI standard.
241    ///
242    /// # Example
243    /// ```
244    /// # use fmu_runner::Fmu;
245    /// # use std::path::Path;
246    /// # use fmu_runner::fmi2Type;
247    /// let mut register_handler: Option<force_injector::RegisterHandlerFn> = None;
248    /// let fmu = Fmu::unpack(Path::new("./tests/fmu/planar_ball.fmu"))
249    ///     .unwrap()
250    ///     .load_with_handler(fmi2Type::fmi2CoSimulation, |lib| {
251    ///         register_handler = unsafe { lib.get(b"register_handler\0") }
252    ///             .map(|sym| *sym)
253    ///             .ok();
254    ///     })
255    ///     .unwrap();
256    /// ```
257    pub fn load_with_handler<F>(
258        self,
259        simulation_type: fmi2Type,
260        handler: F,
261    ) -> Result<FmuLibrary, FmuLoadError>
262    where
263        F: FnOnce(&::libloading::Library),
264    {
265        let (os_type, lib_type) = match env::consts::OS {
266            "macos" => ("darwin", "dylib"),
267            "linux" => ("linux", "so"),
268            "windows" => ("win", "dll"),
269            _ => ("unknown", "so"),
270        };
271
272        let arch_type = match std::env::consts::ARCH {
273            "x86" => "32",
274            "x86_64" => "64",
275            // "arm" => "32",
276            "aarch64" => "64",
277            _ => "unknown",
278        };
279
280        let model_identifier = match simulation_type {
281            fmi2Type::fmi2ModelExchange => self
282                .model_description
283                .model_exchange
284                .as_ref()
285                .ok_or(FmuLoadError::NoModelExchangeModel)?
286                .model_identifier
287                .clone(),
288            fmi2Type::fmi2CoSimulation => self
289                .model_description
290                .co_simulation
291                .as_ref()
292                .ok_or(FmuLoadError::NoCoSimulationModel)?
293                .model_identifier
294                .clone(),
295        };
296
297        // construct the library folder string
298        let lib_str = os_type.to_owned() + arch_type;
299
300        // construct the full library path
301        let mut lib_path = self
302            .unpacked_dir
303            .join("binaries")
304            .join(lib_str)
305            .join(&model_identifier);
306        lib_path.set_extension(lib_type);
307
308        // Load the library
309        let library = unsafe { ::libloading::Library::new(lib_path)? };
310
311        // Let the user map their own symbols in the library
312        handler(&library);
313
314        // Map our signals in the library
315        let fmi = unsafe { Fmi2Dll::from_library(library) }?;
316
317        Ok(FmuLibrary {
318            fmi,
319            simulation_type,
320            fmu: self,
321            instance_name_factory: InstanceNameFactory::new(model_identifier),
322        })
323    }
324
325    pub fn variables(&self) -> &HashMap<String, ScalarVariable> {
326        &self.model_description.model_variables.scalar_variable
327    }
328}
329
330unsafe impl<C: Borrow<FmuLibrary>> Send for FmuInstance<C> {}
331
332impl<C: Borrow<FmuLibrary>> FmuInstance<C> {
333    /// Call `fmi2Instantiate()` on the FMU library to start a new simulation instance.
334    pub fn instantiate(lib: C, logging_on: bool) -> Result<Self, FmuError> {
335        let fmu_guid = &lib.borrow().model_description.guid;
336
337        let callbacks = Box::<fmi2CallbackFunctions>::new(fmi2CallbackFunctions {
338            logger: Some(libfmi::logger::callback_logger_handler),
339            allocateMemory: Some(libc::calloc),
340            freeMemory: Some(libc::free),
341            stepFinished: None,
342            componentEnvironment: std::ptr::null_mut::<std::os::raw::c_void>(),
343        });
344
345        let fmu_guid = CString::new(fmu_guid.as_bytes()).expect("Error building fmu_guid CString");
346
347        let resource_location = "file://".to_owned()
348            + lib
349                .borrow()
350                .unpacked_dir
351                .join("resources")
352                .to_str()
353                .unwrap();
354        let resource_location =
355            CString::new(resource_location).expect("Error building resource_location CString");
356
357        let visible = false as fmi2Boolean;
358        let logging_on = logging_on as fmi2Boolean;
359
360        // Generate a unique instance name to support multiple simulations at once.
361        let instance_name = CString::new(lib.borrow().instance_name_factory.next())
362            .expect("Error building instance_name CString");
363
364        let instance = unsafe {
365            lib.borrow().fmi.fmi2Instantiate(
366                instance_name.as_ptr(),
367                lib.borrow().simulation_type,
368                fmu_guid.as_ptr(),
369                resource_location.as_ptr(),
370                &*callbacks,
371                visible,
372                logging_on,
373            )
374        };
375
376        if instance.is_null() {
377            return Err(FmuError::FmuInstantiateFailed);
378        }
379
380        Ok(Self {
381            lib,
382            instance,
383            callbacks,
384        })
385    }
386
387    pub fn get_set_state_capability(&self) -> Option<FmuGetSetStateCapability<C>> {
388        if let Some(description) = self.lib.borrow().model_description.co_simulation.as_ref() {
389            if description.can_get_and_set_fmustate {
390                Some(FmuGetSetStateCapability(self))
391            } else {
392                None
393            }
394        } else if let Some(description) =
395            self.lib.borrow().model_description.model_exchange.as_ref()
396        {
397            if description.can_get_and_set_fmustate {
398                Some(FmuGetSetStateCapability(self))
399            } else {
400                None
401            }
402        } else {
403            None
404        }
405    }
406
407    pub fn serialize_state_capability(&self) -> Option<FmuSerializeStateCapability<C>> {
408        if let Some(description) = self.lib.borrow().model_description.co_simulation.as_ref() {
409            if description.can_serialize_fmustate {
410                Some(FmuSerializeStateCapability(self))
411            } else {
412                None
413            }
414        } else if let Some(description) =
415            self.lib.borrow().model_description.model_exchange.as_ref()
416        {
417            if description.can_serialize_fmustate {
418                Some(FmuSerializeStateCapability(self))
419            } else {
420                None
421            }
422        } else {
423            None
424        }
425    }
426
427    pub fn get_types_platform(&self) -> &str {
428        let types_platform =
429            unsafe { std::ffi::CStr::from_ptr(self.lib.borrow().fmi.fmi2GetTypesPlatform()) }
430                .to_str()
431                .unwrap();
432        types_platform
433    }
434
435    pub fn set_debug_logging(
436        &self,
437        logging_on: bool,
438        log_categories: &[&str],
439    ) -> Result<(), FmuError> {
440        let category_cstr = log_categories
441            .iter()
442            .map(|c| CString::new(*c).unwrap())
443            .collect::<Vec<_>>();
444
445        let category_ptrs: Vec<_> = category_cstr.iter().map(|c| c.as_ptr()).collect();
446
447        Self::ok_or_err(unsafe {
448            self.lib.borrow().fmi.fmi2SetDebugLogging(
449                self.instance,
450                logging_on as fmi2Boolean,
451                category_ptrs.len(),
452                category_ptrs.as_ptr(),
453            )
454        })
455    }
456
457    pub fn setup_experiment(
458        &self,
459        start_time: f64,
460        stop_time: Option<f64>,
461        tolerance: Option<f64>,
462    ) -> Result<(), FmuError> {
463        Self::ok_or_err(unsafe {
464            self.lib.borrow().fmi.fmi2SetupExperiment(
465                self.instance,
466                tolerance.is_some() as fmi2Boolean,
467                tolerance.unwrap_or(0.0),
468                start_time,
469                stop_time.is_some() as fmi2Boolean,
470                stop_time.unwrap_or(0.0),
471            )
472        })
473    }
474
475    pub fn enter_initialization_mode(&self) -> Result<(), FmuError> {
476        Self::ok_or_err(unsafe {
477            self.lib
478                .borrow()
479                .fmi
480                .fmi2EnterInitializationMode(self.instance)
481        })
482    }
483
484    pub fn exit_initialization_mode(&self) -> Result<(), FmuError> {
485        Self::ok_or_err(unsafe {
486            self.lib
487                .borrow()
488                .fmi
489                .fmi2ExitInitializationMode(self.instance)
490        })
491    }
492
493    pub fn get_reals<'fmu>(
494        &'fmu self,
495        signals: &[&'fmu ScalarVariable],
496    ) -> Result<HashMap<&ScalarVariable, fmi2Real>, FmuError> {
497        self.get(signals, Fmi2Dll::fmi2GetReal)
498    }
499
500    pub fn get_integers<'fmu>(
501        &'fmu self,
502        signals: &[&'fmu ScalarVariable],
503    ) -> Result<HashMap<&ScalarVariable, fmi2Integer>, FmuError> {
504        self.get(signals, Fmi2Dll::fmi2GetInteger)
505    }
506
507    pub fn get_booleans<'fmu>(
508        &'fmu self,
509        signals: &[&'fmu ScalarVariable],
510    ) -> Result<HashMap<&ScalarVariable, fmi2Integer>, FmuError> {
511        self.get(signals, Fmi2Dll::fmi2GetBoolean)
512    }
513
514    pub fn set_reals(
515        &self,
516        value_map: &HashMap<&ScalarVariable, fmi2Real>,
517    ) -> Result<(), FmuError> {
518        self.set(value_map, Fmi2Dll::fmi2SetReal)
519    }
520
521    pub fn set_integers(
522        &self,
523        value_map: &HashMap<&ScalarVariable, fmi2Integer>,
524    ) -> Result<(), FmuError> {
525        self.set(value_map, Fmi2Dll::fmi2SetInteger)
526    }
527
528    pub fn set_booleans(
529        &self,
530        value_map: &HashMap<&ScalarVariable, fmi2Integer>,
531    ) -> Result<(), FmuError> {
532        self.set(value_map, Fmi2Dll::fmi2SetBoolean)
533    }
534
535    pub fn do_step(
536        &self,
537        current_communication_point: fmi2Real,
538        communication_step_size: fmi2Real,
539        no_set_fmustate_prior_to_current_point: bool,
540    ) -> Result<(), FmuError> {
541        Self::ok_or_err(unsafe {
542            self.lib.borrow().fmi.fmi2DoStep(
543                self.instance,
544                current_communication_point,
545                communication_step_size,
546                no_set_fmustate_prior_to_current_point as fmi2Boolean,
547            )
548        })
549    }
550
551    fn get<'fmu, T>(
552        &'fmu self,
553        signals: &[&'fmu ScalarVariable],
554        func: unsafe fn(
555            &Fmi2Dll,
556            fmi2Component,
557            *const fmi2ValueReference,
558            usize,
559            *mut T,
560        ) -> fmi2Status,
561    ) -> Result<HashMap<&'fmu ScalarVariable, T>, FmuError> {
562        let mut values = Vec::<T>::with_capacity(signals.len());
563        match unsafe {
564            values.set_len(signals.len());
565            func(
566                &self.lib.borrow().fmi,
567                self.instance,
568                signals
569                    .iter()
570                    .map(|s| s.value_reference)
571                    .collect::<Vec<_>>()
572                    .as_ptr(),
573                signals.len(),
574                values.as_mut_ptr(),
575            )
576        } {
577            fmi2Status::fmi2OK => Ok(zip(signals.to_owned(), values).collect()),
578            status => Err(FmuError::BadFunctionCall(status)),
579        }
580    }
581
582    fn set<T: Copy>(
583        &self,
584        value_map: &HashMap<&ScalarVariable, T>,
585        func: unsafe fn(
586            &Fmi2Dll,
587            fmi2Component,
588            *const fmi2ValueReference,
589            usize,
590            *const T,
591        ) -> fmi2Status,
592    ) -> Result<(), FmuError> {
593        let len = value_map.len();
594        let mut vrs = Vec::<fmi2ValueReference>::with_capacity(len);
595        let mut values = Vec::<T>::with_capacity(len);
596
597        for (signal, value) in value_map.iter() {
598            vrs.push(signal.value_reference);
599            values.push(*value);
600        }
601
602        Self::ok_or_err(unsafe {
603            func(
604                &self.lib.borrow().fmi,
605                self.instance,
606                vrs.as_ptr(),
607                len,
608                values.as_ptr(),
609            )
610        })
611    }
612
613    fn ok_or_err(status: fmi2Status) -> Result<(), FmuError> {
614        match status {
615            fmi2Status::fmi2OK => Ok(()),
616            status => Err(FmuError::BadFunctionCall(status)),
617        }
618    }
619}
620
621impl<C: Borrow<FmuLibrary>> Drop for FmuInstance<C> {
622    fn drop(&mut self) {
623        unsafe { self.lib.borrow().fmi.fmi2FreeInstance(self.instance) };
624    }
625}
626
627pub fn outputs_to_string<T: Display>(outputs: &HashMap<&ScalarVariable, T>) -> String {
628    let mut s = String::new();
629
630    for signal in outputs.keys().sorted_by_key(|s| &s.name) {
631        s.push_str(&format!("{}: {:.3} | ", signal.name, outputs[signal]));
632    }
633
634    s
635}
636
637#[derive(Error, Debug)]
638pub enum FmuUnpackError {
639    #[error("Failed to create tempdir")]
640    NoTempdir(#[source] io::Error),
641    #[error("Invalid FMU path")]
642    InvalidFile(#[source] io::Error),
643    #[error("Invalid FMU unzip output directory")]
644    InvalidOutputDir(#[source] io::Error),
645    #[error("Invalid FMU archive")]
646    InvalidArchive(#[from] ZipError),
647    #[error("Invalid FMU model description XML")]
648    InvalidModelDescription(#[from] quick_xml::DeError),
649}
650
651#[derive(Error, Debug)]
652pub enum FmuLoadError {
653    #[error("FMU does not contain CoSimulation model")]
654    NoCoSimulationModel,
655    #[error("FMU does not contain ModelExchange model")]
656    NoModelExchangeModel,
657    #[error("Error loading FMU dynamic library")]
658    DLOpen(#[from] libloading::Error),
659}
660
661#[derive(Error, Debug)]
662pub enum FmuError {
663    #[error("FMU bad function call: {0:?}")]
664    BadFunctionCall(fmi2Status),
665    // #[error("FMU load error: {0}")]
666    // LoadError(#[from] FmuLoadError),
667    #[error("fmi2Instantiate() call failed")]
668    FmuInstantiateFailed,
669}
670
671// test module
672#[cfg(test)]
673mod tests {
674    use super::*;
675
676    fn print_err(err: impl std::error::Error) {
677        eprintln!("Display:\n{}", err);
678        eprintln!("Debug:\n{:?}", err);
679    }
680
681    #[test]
682    fn test_invalid_file() {
683        let res = Fmu::unpack("dasf:?-()");
684        assert!(matches!(res, Err(FmuUnpackError::InvalidFile { .. })));
685        print_err(res.unwrap_err());
686    }
687
688    #[test]
689    fn test_invalid_output_dir() {
690        let res = Fmu::unpack_to("./tests/fmu/free_fall.fmu", "/z.(),.dasda/dasd");
691        assert!(matches!(res, Err(FmuUnpackError::InvalidOutputDir { .. })));
692        print_err(res.unwrap_err());
693    }
694}