Getting started

To start using sofirpy, import the package:

import sofirpy

The following 3 examples demonstrate how to export a Modelica model as a FMU, simulate a FMU and a controller and how to initialize a project.

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_direcotry = dir_path

export_open_modelica_model(model_path, model_name, dir_path)

Exporting a Dymola model and importing parameters

from pathlib import Path
from sofirpy import export_dymola_model

dir_path = Path(__file__).parent
model_path = dir_path.parent / "DC_Motor.mo"
output_direcotry = dir_path
dymola_exe_path = r"C:\Program Files\Dymola 2018 FD01\bin64\Dymola.exe"
model_name = "DC_Motor"
# To import paramater define the parameters and their values in a dictionary as
# follows:

parameters = {"damper.d": 0.1, "damper.useHeatPort": False}

# Note: The values can also be strings, but then they must correspond to the
# Modelica syntax:
# >>> parameters = {"damper.d": "0.1", "damper.useHeatPort": "false"}
    
export_dymola_model(
    dymola_exe_path, model_path, model_name, output_direcotry, parameters=parameters, keep_mos=False
)

Simulating a FMU and a Controller

from pathlib import Path
from sofirpy import simulate
from discrete_pid import PID # custom implemented pid controller 

fmu_path = Path(__file__).parent / "DC_Motor.fmu"

fmu_info = [
    {
        "name": "DC_Motor",
        "path": str(fmu_path),
        "connections": [
            {
                "parameter name": "u",
                "connect to system": "pid",
                "connect to external parameter": "u",
            }
        ],
    }
]


control_infos = [
    {
        "name": "pid",
        "connections": [
            {
                "parameter name": "speed",
                "connect to system": "DC_Motor",
                "connect to external parameter": "y",
            }
        ],
    }
]
pid = PID(1e-3, 3, 20, 0.1, set_point=100, u_max=100, u_min=0)
control_class = {"pid": pid}
parameters_to_log = {"DC_Motor": ["y", "MotorTorque.tau"], "pid": ["u"]}

results, units = simulate(
    stop_time=10,
    step_size=1e-3,
    fmu_infos=fmu_info,
    model_infos=control_infos,
    model_classes=control_class,
    parameters_to_log=parameters_to_log,
    get_units=True,
)

The custom implemented pid controller is shown below.

from sofirpy import SimulationEntity


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

    def __init__(
        self, step_size, K_p=1, K_i=0, K_d=0, set_point=0, u_max=1000, u_min=-1000
    ):

        self.T_a = step_size
        self.K_p = K_p
        self.K_i = K_i
        self.K_d = K_d
        self.set_point = set_point
        self.inputs = {"speed": 0}
        self.outputs = {"u": 0}
        self.d_0 = K_p * (
            1 + (self.T_a * self.K_i / self.K_p) + self.K_d / (self.K_p * self.T_a)
        )
        self.d_1 = K_p * (-1 - 2 * self.K_d / (self.K_p * self.T_a))
        self.d_2 = K_p * self.K_d / (self.K_p * self.T_a)
        self.error = [0, 0, 0]
        self.u_max = u_max
        self.u_min = u_min

    def compute_error(self):

        self.error[2] = self.error[1]
        self.error[1] = self.error[0]
        self.error[0] = self.set_point - self.inputs["speed"]

    def set_input(self, input_name, input_value):  # mandatory methode

        self.inputs[input_name] = input_value

    def do_step(self, time):  # mandatory methode

        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 get_parameter_value(self, output_name):  # mandatory methode
        return self.outputs[output_name]

Initializing a project and storing data

from pathlib import Path
from sofirpy import Project

# Initialize a project by giving it the path to a project directory and the
# path to a hdf5. If the given pathts do not exist, they will be created.

project_dir = Path(__file__).parent
hdf5_name = "example_project"
hdf5_path = Path(project_dir) / f"{hdf5_name}.hdf5"
project = Project(hdf5_path, project_dir)

# actions can be taken only on the hdf5, only on the project directory and
# simultaneously on both the hdf5 and the project directory.

# storing data in the hdf5
data = [
    [15, 8, 1, 24, 17],
    [16, 14, 7, 5, 23],
    [22, 20, 13, 6, 4],
    [3, 21, 19, 12, 10],
    [9, 2, 25, 18, 11],
]
data_name = "magic"
data_attributes = {"created on": "01.01.01"}

# specify in which folder the matrix should be stored, if the data should be
# stored at the top level, folder_name must be set to None.

folder_name = "magic_matrices"
project.hdf5.store_data(
    data_name, data, group_path=folder_name, attributes=data_attributes
)

# reading data
data, attr = project.hdf5.read_data(data_name, folder_name, get_attributes=True)

# create a folder in the project directory
folder_name = "magic_matrices"
project.project_dir.create_folder(folder_name)

# create a folder in the project directory and the hdf5 simultaneously
folder_name = "magic_matrices/examples"
project.create_folder(folder_name)

# delete a folder in the project directory and the hdf5 simultaneously
project.delete_folder(folder_name)

Additional examples

Additional examples can be found here.