Getting started#

The following 3 examples demonstrate how to export a Modelica model as a FMU, co-simulate a FMU and a controller and how to use the rdm features.

Exporting a Modelica Model#

SOFIRpy allows to export a OpenModelica and Dymola model as a FMU.

Exporting a OpenModelica model

from pathlib import Path

from sofirpy import export_open_modelica_model

dir_path = Path(__file__).parent.parent
model_path = dir_path / "DC_Motor.mo"
model_name = "DC_Motor"
output_directory = dir_path

fmu_path = export_open_modelica_model(model_path, model_name, dir_path)

Exporting a Dymola model

from pathlib import Path

from sofirpy import export_dymola_model

dir_path = Path(__file__).parent
model_path = dir_path.parent / "DC_Motor.mo"
output_directory = dir_path
dymola_exe_path = r"C:\Program Files\Dymola 2018 FD01\bin64\Dymola.exe"
model_name = "DC_Motor"
fmu_path = export_dymola_model(
    dymola_exe_path=dymola_exe_path,
    model_path=model_path,
    model_name=model_name,
    fmu_name="DC_Motor",
    output_directory=output_directory,
)

Simulating a FMU and a Controller#

The custom implemented pid controller is shown below.

from __future__ import annotations

from typing import Any

import sofirpy.common as co
from sofirpy import SimulationEntity


class PID(SimulationEntity):
    """Simple implementation of a discrete pid controller"""

    def __init__(self, init_config: co.InitConfig) -> None:
        self.parameters = {
            "sampling_rate": 1e-3,
            "K_p": 1,
            "K_i": 0,
            "K_d": 0,
            "set_point": 0,
            "u_max": 1000,
            "u_min": -1000,
        }
        self.inputs = {"speed": 0}
        self.outputs = {"u": 0}
        self.units = {"u": "V"}
        self.dtypes = {"u": float}
        self.initialize(init_config)

    def _compute_error(self) -> None:
        self.error[2] = self.error[1]
        self.error[1] = self.error[0]
        self.error[0] = self.parameters["set_point"] - self.inputs["speed"]

    def do_step(self, time: float, time_step: float):  # mandatory method
        self._compute_error()
        u = (
            self.outputs["u"]
            + self.d_0 * self.error[0]
            + self.d_1 * self.error[1]
            + self.d_2 * self.error[2]
        )
        if u > self.u_max or u < self.u_min:
            if u > self.u_max:
                u = self.u_max
            if u < self.u_min:
                u = self.u_min

        self.outputs["u"] = u

    def set_parameter(
        self, parameter_name, parameter_value
    ) -> None:  # mandatory method
        self.inputs[parameter_name] = parameter_value

    def get_parameter_value(self, output_name):  # mandatory method
        return self.outputs[output_name]

    def initialize(self, init_config: dict[str, Any]) -> None:
        self._apply_start_values(init_config["start_values"])
        K_p = self.parameters["K_p"]
        K_i = self.parameters["K_i"]
        K_d = self.parameters["K_d"]
        T_a = self.parameters["sampling_rate"]
        self.d_0 = K_p * (1 + (T_a * K_i / K_p) + K_d / (K_p * T_a))
        self.d_1 = K_p * (-1 - 2 * K_d / (K_p * T_a))
        self.d_2 = K_p * K_d / (K_p * T_a)
        self.error = [0, 0, 0]
        self.u_max = self.parameters["u_max"]
        self.u_min = self.parameters["u_min"]

    def _apply_start_values(self, start_values: dict[str, Any]) -> None:
        for name, value in start_values.items():
            self.parameters[name] = value

    def get_unit(self, parameter_name: str) -> str | None:
        return self.units.get(parameter_name)

    def get_dtype_of_parameter(self, parameter_name: str) -> type:
        return self.dtypes.get(parameter_name, float)

RDM Features#

This example demonstrates how to create a simulation run from a config file, store the run inside a HDF5 file and read the run again from the HDF5 file.

import os
import sys
from pathlib import Path

from sofirpy import Run

sys.path.append(os.path.join(os.path.dirname(__file__), "../"))

from discrete_pid import PID

# Simulating and storing a run to a hdf5
run_name = "Run_1"

config_path = Path(__file__).parent / "example_config.json"

model_classes = {"pid": PID}

if sys.platform == "win32":
    fmu_path = Path(__file__).parent.parent / "DC_Motor.fmu"
elif sys.platform == "darwin":
    fmu_path = Path(__file__).parent.parent / "DC_Motor_mac.fmu"
elif sys.platform == "linux":
    fmu_path = Path(__file__).parent.parent / "DC_Motor_linux.fmu"

fmu_paths = {"DC_Motor": fmu_path}

run = Run.from_config_file(run_name, config_path, fmu_paths, model_classes)

run.simulate()

hdf5_path = Path(__file__).parent / "run_example.hdf5"

run.to_hdf5(hdf5_path)

# Loading the run from the hdf5
run_loaded = Run.from_hdf5(run_name, hdf5_path)