Skip to content

Linearize Example🔗

Below is the code for the Custom Function linearize.py.

The actual file is a part of the Linearize Example introduced in Project Specific Custom Functions and which parts are referenced Creating Custom Functions.

The actual file can also be found in the installation folder: modelon_impact_installation_folder/custom_function_examples.

import os
import numbers
import tempfile

import pandas
import numpy as np
import scipy


def signature():
    """
    The signature function specifying how the Custom Function is presented to the user
    in Modelon Impact.
    :return: A dictionary on the format:
                "version" - The version number of the Custom Function
                "name" - The name of the Custom Function to appear in Modelon Impact
                "description" - A description of what the Custom Function does.
                "parameters" - A list of parameters to be set by the user via the
                Modelon Impact simulation browser (optional).
                Each parameter is specified by a dictionary on the format (all
                optional):
                   "name" - the name of the parameter to appear in Modelon Impact
                   "type" - The type of the parameter: String, Number, Boolean or
                   Enumeration
                   "description" - A description of the parameter
                   "defaultValue" - The default value of the parameter
    """
    return {
        "version": "0.0.1",
        "name": "Linearize",
        "description": "Linearize the model and compute its state space representation"
        "(matrices A, B, C and D).",
        "parameters": [
            {
                "name": "t_linearize",
                "type": "Number",
                "description": "Time (in seconds) at which to perform linearization."
                "To linearize at initialization,"
                "set t=0.",
                "defaultValue": 1,
            },
            {
                "name": "print_to_log",
                "type": "Boolean",
                "description": "Linearized model statistics are printed in the log, "
                "if this option is set to True",
                "defaultValue": True,
            },
        ],
    }


def run(get_fmu, environment, parametrization, upload_custom_artifact, t_linearize, print_to_log):
    """
    The run function, defining the operation or computation of the Custom Function.
    :param get_fmu: A function returning an FMU object for the model the custom
    function is applied on, with applied
    non-structural parameters as set in Modelon Impact.
    :param environment: A dictionary specifying environment variables:
                          "result_folder_path" - The path to the folder where the
                          result is to be saved
                          "result_file_name" - The name of the file where the
                          results are to be saved
                          "workspace_id" - The workspace ID
                          "case_id" - The case ID
                          "experiment_id" - The experiment ID
                          "log_file_name" - The name of the log file
    :param upload_custom_artifact: Function for uploading a custom artifact to the
    storage. Takes arguments:
                          "artifact_id": The artifact ID.
                          "local_file_path": Path to the local custom artifact to upload
            Returns the route for downloading the artifact.
    :param parametrization: The parametrization of the model as set in Modelon Impact
    experiment mode.
    :param t_linearize: Time (in seconds) at which to perform linearization.
    :param print_to_log: Toggle weather the linearized model statistics should be shown
                          in the simulation log.
    """

    # In this case, the linearization is packaged into a separate function. This
    # enables to use it outside of Modelon Impact and thereby also makes
    # it convenient to test.
    model = get_fmu()
    for key, value in parametrization.items():
        model.set(key, value)
    return linearize(model, environment, upload_custom_artifact, t_linearize, print_to_log)


def linearize(model, environment, upload_custom_artifact, t_linearize, print_to_log):
    """
    Compute the ABCD state space representation for a model and write the result to
    a .csv-file. Also save the result as a .mat-file and upload it as a custom artifact.
    The .mat-file can be used to load the result into MATLAB.
    :param model: An FMU object for the model to linearize.
    :param environment: A dictionary specifying environment variables:
                          "result_folder_path" - The path to the folder where the
                          result is to be saved
                          "result_file_name" - The name of the file where the results
                          are to be saved
                          "workspace_id" - The workspace ID
                          "case_id" - The case ID
                          "experiment_id" - The experiment ID
                          "log_file_name" - The name of the log file
    :param upload_custom_artifact: Function for uploading a custom artifact to the
    storage. Takes arguments:
                          "artifact_id": The artifact ID.
                          "local_file_path": Path to the local custom artifact to upload
            Returns the route for downloading the artifact.
    :param t_linearize: The time to simulate the model before linearizing.
    :param print_to_log: Toggle weather the linearized model statistics should be shown
                          in the simulation log
    """

    # Start by type checking the parameter, in case an invalid entry is given by
    # the user
    if not isinstance(t_linearize, numbers.Number) or t_linearize < 0:
        raise ValueError("The parameter t_linearize needs to be a non-negative number.")
    if t_linearize == 0:
        model.initialize()
    else:
        model.simulate(final_time=t_linearize)

    # Retrieve the state space representation of the linearized model
    result = model.get_state_space_representation(use_structure_info=False)

    ss = {matrix_name: result[i] for i, matrix_name in enumerate(["A", "B", "C", "D"])}

    # Pretty print the matrices to the simulation log
    if print_to_log:
        for matrix_name, result in ss.items():
            print('\n' + matrix_name + '= [')
            matrix_shape = result.shape
            if not (matrix_shape[0] == 0 or matrix_shape[1] == 0):
                max_len = max(len(str(e)) for row in result for e in row)
                for row in result:
                    print(
                        "\t".join(
                            ['{:<{max_len}}'.format(e, max_len=max_len) for e in row]
                        )
                    )
            print(']')

    # Scalarize the state space matrices
    scalarized_ss = {
        "{}[{},{}]".format(matrix_name, index[0], index[1]): [x]
        for matrix_name, matrix in ss.items()
        for index, x in np.ndenumerate(matrix)
    }

    # Write the matrices to a csv file in the prescribed path
    csv_file_path = os.path.join(
        environment["result_folder_path"], environment["result_file_name"]
    )
    df = pandas.DataFrame(data=scalarized_ss)
    df.to_csv(csv_file_path, index=False)

    # Add variable names
    state_names = list(model.get_states_list().keys())
    input_names = list(model.get_input_list().keys())
    output_names = list(model.get_output_list().keys())

    ss['state_names'] = state_names
    ss['input_names'] = input_names
    ss['output_names'] = output_names

    # Add operating point
    operating_point_time = t_linearize
    operating_point_states = [x[0] for x in model.get(state_names)]
    operating_point_derivatives = list(model.get_derivatives())
    operating_point_inputs = [x[0] for x in model.get(input_names)]
    operating_point_outputs = [x[0] for x in model.get(output_names)]

    ss['operating_point_time'] = operating_point_time
    ss['operating_point_states'] = np.array(operating_point_states)
    ss['operating_point_derivatives'] = np.array(operating_point_derivatives)
    ss['operating_point_inputs'] = np.array(operating_point_inputs)
    ss['operating_point_outputs'] = np.array(operating_point_outputs)

    # Pretty print the linearization statistics to the simulation log
    if print_to_log:
        if state_names:
            print('\n' + "# At operating point {}s :".format(str(t_linearize)) + '\n')
            print(f'state_names = {state_names}\n')
            print(f'operating_point_states = {operating_point_states}\n')
            print(f'operating_point_derivatives = {operating_point_derivatives}\n')
        if input_names:
            print(f'input_names = {input_names}\n')
            print(f'operating_point_inputs = {operating_point_inputs}\n')
        if output_names:
            print(f'output_names = {output_names}\n')
            print(f'operating_point_outputs = {operating_point_outputs}\n')

    # Write result to a .mat file that can be imported in MATLAB
    # First write the result to a temporary directory
    temp_dir = tempfile.mkdtemp()
    temp_mat_file = os.path.join(temp_dir, "result.mat")
    scipy.io.savemat(temp_mat_file, ss)

    # Now upload the result to the server as a custom artifact
    artifact_id = "ABCD"
    artifact_route = upload_custom_artifact(artifact_id, temp_mat_file)

    # Finally print the route where the artifact can be accessed
    print('Stored artifact with ID: {}'.format(artifact_id))
    print('')
    print('Artifact can be downloaded from @artifact[here]({})'.format(artifact_route))