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 typing import Optional
from sofirpy import SimulationEntity
from sofirpy.common import StartValues
class PID(SimulationEntity):
"""Simple implementation of a discrete pid controller"""
def __init__(self):
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"}
def compute_error(self):
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) -> Optional[None]:
return self.units.get(parameter_name)
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)