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

import os
import sys
from pathlib import Path

from sofirpy import plot_results, simulate

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

from discrete_pid import PID  # custom implemented pid controller

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

connections_config = {
    "DC_Motor": [
        {
            "parameter_name": "u",
            "connect_to_system": "pid",
            "connect_to_external_parameter": "u",
        }
    ],
    "pid": [
        {
            "parameter_name": "speed",
            "connect_to_system": "DC_Motor",
            "connect_to_external_parameter": "y",
        }
    ],
}

fmu_paths = {"DC_Motor": str(fmu_path)}


model_classes = {"pid": PID}

parameters_to_log = {
    "DC_Motor": ["y", "MotorTorque.tau", "inertia.J", "dC_PermanentMagnet.Jr"],
    "pid": ["u"],
}

start_values = {
    "DC_Motor": {"inertia.J": 2, "damper.phi_rel.start": (1, "deg")},
    "pid": {
        "step_size": 1e-3,
        "K_p": 3,
        "K_i": 20,
        "K_d": 0.1,
        "set_point": 100,
        "u_max": 100,
        "u_min": 0,
    },
}

results, units = simulate(
    stop_time=10,
    step_size=1e-3,
    fmu_paths=fmu_paths,
    model_classes=model_classes,
    connections_config=connections_config,
    start_values=start_values,
    parameters_to_log=parameters_to_log,
    logging_step_size=1e-3,
    get_units=True,
)

ax, fig = plot_results(
    results,
    "time",
    "DC_Motor.y",
    x_label="time in s",
    y_label="speed in rad/s",
    title="Speed over Time",
)

The custom implemented pid controller is shown below.

from __future__ import annotations

from sofirpy import SimulationEntity
from sofirpy.common import StartValues


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

    def __init__(self) -> 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}

    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, _):  # 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, start_values
    ) -> None:  # start values passed to simulation function are passed to this method
        self._apply_start_values(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: StartValues) -> 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)