use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use fmu_runner::{fmi2Type, model_description::ScalarVariable, Fmu, FmuInstance, FmuLibrary};
use pictorus_block_data::BlockData as OldBlockData;
use pictorus_traits::{Context, ProcessBlock};
use std::collections::HashMap;
pub struct FmuBlock<const N_IN: usize, const N_OUT: usize> {
pub data: Vec<OldBlockData>,
fm_cs: Option<FmuInstance<FmuLibrary>>,
}
impl<const N_IN: usize, const N_OUT: usize> Default for FmuBlock<N_IN, N_OUT> {
fn default() -> Self {
Self {
data: vec![OldBlockData::from_scalar(0.0); N_OUT],
fm_cs: None,
}
}
}
impl<const N_IN: usize, const N_OUT: usize> FmuBlock<N_IN, N_OUT> {
fn run_time_step(
&mut self,
params: &Parameters,
context: &dyn Context,
inputs: &[f64; N_IN],
) -> [f64; N_OUT] {
let fmu = self.fm_cs.get_or_insert_with(|| {
Self::build_fmu(params).expect("Failed to load and instantiate FMU")
});
let signals = fmu.lib.variables();
let mapped_inputs: HashMap<&ScalarVariable, f64> = params
.input_signals
.iter()
.enumerate()
.map(|(i, name)| {
let signal = signals.get(name).expect("Signal not found in FMU");
let input = inputs
.get(i)
.expect("Size mismatch between provided inputs and expected inputs");
(signal, *input)
})
.collect();
fmu.set_reals(&mapped_inputs)
.expect("Failed to set FMU inputs");
if let Some(curr_timestep) = context.timestep() {
let step_start_time = context.time() - curr_timestep;
fmu.do_step(
step_start_time.as_secs_f64(),
curr_timestep.as_secs_f64(),
false,
)
.expect("Failed to do FMU step");
}
let mut output_data = [0.0; N_OUT];
if N_OUT == 0 {
return output_data;
}
let desired_outputs = params
.output_signals
.iter()
.map(|name| signals.get(name).expect("Signal not found in FMU"))
.collect::<Vec<_>>();
let model_outputs = fmu
.get_reals(&desired_outputs)
.expect("Failed to get FMU outputs");
for (signal, output_value) in desired_outputs
.iter()
.map(|s| model_outputs.get(s).expect("Failed to get FMU output"))
.zip(output_data.iter_mut())
{
*output_value = *signal;
}
output_data
}
fn build_fmu(params: &Parameters) -> Result<FmuInstance<FmuLibrary>, FmuErrors> {
let fmu = Fmu::unpack(¶ms.fmu_path)?.load(fmi2Type::fmi2CoSimulation)?;
let fmu_cs = FmuInstance::instantiate(fmu, false)?;
let signals = fmu_cs.lib.variables();
fmu_cs.setup_experiment(0.0, None, None)?;
fmu_cs.enter_initialization_mode()?;
let param_values = params
.fmu_params
.iter()
.map(|(k, v)| (&signals[k], *v))
.collect::<HashMap<_, _>>();
fmu_cs.set_reals(¶m_values)?;
fmu_cs.exit_initialization_mode()?;
Ok(fmu_cs)
}
}
#[derive(Debug)]
#[allow(dead_code)]
enum FmuErrors {
Fmu(fmu_runner::FmuError),
FmuLoad(fmu_runner::FmuLoadError),
FmuUnpack(fmu_runner::FmuUnpackError),
}
impl From<fmu_runner::FmuError> for FmuErrors {
fn from(err: fmu_runner::FmuError) -> Self {
FmuErrors::Fmu(err)
}
}
impl From<fmu_runner::FmuLoadError> for FmuErrors {
fn from(err: fmu_runner::FmuLoadError) -> Self {
FmuErrors::FmuLoad(err)
}
}
impl From<fmu_runner::FmuUnpackError> for FmuErrors {
fn from(err: fmu_runner::FmuUnpackError) -> Self {
FmuErrors::FmuUnpack(err)
}
}
macro_rules! impl_process_block {
($n_in:tt, $n_out:tt) => {
impl ProcessBlock for FmuBlock<$n_in, $n_out> {
type Parameters = Parameters;
type Inputs = impl_process_block!(@tuple_type, f64, $n_in);
type Output = impl_process_block!(@tuple_type, f64, $n_out);
#[allow(unused_variables)]
fn process<'b>(
&'b mut self,
parameters: &Self::Parameters,
context: &dyn Context,
inputs: pictorus_traits::PassBy<'_, Self::Inputs>,
) -> pictorus_traits::PassBy<'b, Self::Output> {
let output: [f64; $n_out] = self.run_time_step(
parameters,
context,
impl_process_block!(@input_spec, $n_in, inputs),
);
self.data.clear();
self.data = output
.iter()
.map(|&x| OldBlockData::from_scalar(x))
.collect();
impl_process_block!(@output_spec, $n_out, output)
}
}
};
(@input_spec, 0, $name:expr) => {&[]};
(@input_spec, 1, $name:expr) => {&[$name]};
(@input_spec, $n:expr, $name:expr) => {&$name.try_into().expect("This is a known size")};
(@output_spec, 0, $name:expr) => {()};
(@output_spec, 1, $name:expr) => {$name[0]};
(@output_spec, $n:expr, $name:expr) => {
$name.try_into().expect("This is a known size")
};
(@tuple_type, $type:ty, 0) => {()};
(@tuple_type, $type:ty, 1) => {$type};
(@tuple_type, $type:ty, 2) => {($type, $type)};
(@tuple_type, $type:ty, 3) => {($type, $type, $type)};
(@tuple_type, $type:ty, 4) => {($type, $type, $type, $type)};
(@tuple_type, $type:ty, 5) => {($type, $type, $type, $type, $type)};
(@tuple_type, $type:ty, 6) => {($type, $type, $type, $type, $type, $type)};
(@tuple_type, $type:ty, 7) => {($type, $type, $type, $type, $type, $type, $type)};
(@tuple_type, $type:ty, 8) => {($type, $type, $type, $type, $type, $type, $type, $type)};
}
seq_macro::seq!(N in 0..=8 {
seq_macro::seq!(M in 0..=8 {
impl_process_block!(N, M);
});
});
pub struct Parameters {
pub fmu_path: String,
pub fmu_params: HashMap<String, f64>,
pub input_signals: Vec<String>,
pub output_signals: Vec<String>,
}
impl Parameters {
pub fn new(
fmu_path: &str,
fmu_params: &HashMap<&'static str, f64>,
input_signals: Vec<String>,
output_signals: Vec<String>,
) -> Self {
Self {
fmu_path: fmu_path.to_string(),
fmu_params: fmu_params
.iter()
.map(|(k, v)| (k.to_string(), *v))
.collect(),
input_signals,
output_signals,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_impls_associated_types() {
let _: <FmuBlock<0, 0> as pictorus_traits::ProcessBlock>::Inputs = ();
let _: <FmuBlock<0, 0> as pictorus_traits::ProcessBlock>::Output = ();
let _: <FmuBlock<1, 0> as pictorus_traits::ProcessBlock>::Inputs = 0.0;
let _: <FmuBlock<1, 0> as pictorus_traits::ProcessBlock>::Output = ();
let _: <FmuBlock<1, 1> as pictorus_traits::ProcessBlock>::Inputs = 0.0;
let _: <FmuBlock<1, 1> as pictorus_traits::ProcessBlock>::Output = 0.0;
let _: <FmuBlock<2, 0> as pictorus_traits::ProcessBlock>::Inputs = (0.0, 1.0);
let _: <FmuBlock<2, 0> as pictorus_traits::ProcessBlock>::Output = ();
let _: <FmuBlock<2, 1> as pictorus_traits::ProcessBlock>::Inputs = (0.0, 1.0);
let _: <FmuBlock<2, 1> as pictorus_traits::ProcessBlock>::Output = 0.0;
let _: <FmuBlock<2, 2> as pictorus_traits::ProcessBlock>::Inputs = (0.0, 1.0);
let _: <FmuBlock<2, 2> as pictorus_traits::ProcessBlock>::Output = (0.0, 1.0);
let _: <FmuBlock<2, 3> as pictorus_traits::ProcessBlock>::Inputs = (0.0, 1.0);
let _: <FmuBlock<2, 3> as pictorus_traits::ProcessBlock>::Output = (0.0, 1.0, 2.0);
let _inputs: <FmuBlock<3, 0> as pictorus_traits::ProcessBlock>::Inputs = (0.0, 1.0, 2.0);
let _output: <FmuBlock<3, 0> as pictorus_traits::ProcessBlock>::Output = ();
let _inputs: <FmuBlock<2, 3> as pictorus_traits::ProcessBlock>::Inputs = (1.0, 2.0);
let _output: <FmuBlock<2, 3> as pictorus_traits::ProcessBlock>::Output = (3.0, 4.0, 5.0);
let _: <FmuBlock<3, 4> as pictorus_traits::ProcessBlock>::Inputs = (0.0, 1.0, 2.0);
let _: <FmuBlock<3, 4> as pictorus_traits::ProcessBlock>::Output = (0.0, 1.0, 2.0, 3.0);
let _: <FmuBlock<8, 8> as pictorus_traits::ProcessBlock>::Inputs =
(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0);
let _: <FmuBlock<8, 8> as pictorus_traits::ProcessBlock>::Output =
(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0);
let _: <FmuBlock<7, 2> as pictorus_traits::ProcessBlock>::Inputs =
(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
let _: <FmuBlock<7, 2> as pictorus_traits::ProcessBlock>::Output = (0.0, 1.0);
}
}