Custom Function🔗
A custom function is an integrated Python interface to Modelon Impact. With a custom function, the user defines a function that does a computation or another operation on an FMU or a model, calls it from the simulation browser and returns the result to the result browser.
What can custom function do?🔗
- Expand Modelon Impacts abilities by implementing a custom analysis or calculation
- Get access to an analysis type that is not accessible from the Modelon Impact UI such as dynamic optimization using the OCT Python API.
- Automatically create an artifact such as a data file or an HTML report.
Create and install a custom function🔗
Custom functions can either be developed and installed as part of projects, released libraries or globally.
Project Specific Custom Functions🔗
Installation as a part of a library or projects makes the custom functions available in the Workspaces that include that project or library. This is the recommended way to develop and install custom functions.
To get started with your own development, try to check out the open source example from Linearize Example . Use Workspace Management to bring the LinearizeExample project into your workspace.
The function LinerazeExample.linearize
is now available under the custom analysis in the experiment tab in the Details panel to the right.
It can also be picked under the Run button.
To create a custom function in your own project:
-
Open the project for editing in VSCode.
-
Create subdirectories
Resources/CustomFunctions
(note that names are case sensitive). -
Double check that
.impact/project.json
file in your project contains a reference to this directory. This should be similar to:{ "relpath": "Resources/CustomFunctions", "contentType": "CUSTOM_FUNCTIONS", "defaultDisabled": false, "id": "5fc1a0703df74195ae8bc2c52af432ba" },
-
Create a Python file to develop the custom function code in the
Resources/CustomFunctions
subdirectory. -
Implement the custom function interface, see Custom function API.
The new function will become available in the workspaces that use your project after user interface reloads.
Global Custom Functions🔗
Globally installed custom functions are available in all the workspaces. They are installed by uploading
Python files into /home/jovyan/impact/custom_functions
directory either via JupyterLab or VSCode user interface.
Globally installed custom functions do not have a project prefix in the name and are not distributed together with
Modelon Impact Workspaces.
- Create a Python file in your preferred coding browser such as Visual Studio Code.
- Implement the custom function interface, see Custom function API.
- Add the Python file to
C:/Users/<user_name>/impact/custom_functions
.
The function is now available under the custom analysis in the experiment tab in the Details panel to the right.
Run a custom function:🔗
-
Select Experiment mode.
-
Go to the Experiment section of the Details browser. Select Custom.
-
Select your custom function in the drop-down list. If a custom function is not loaded, check the server log file available in
/home/jovyan/impact/logs
folder. The log file is also useful for any other custom function-related server logs. -
Reload (press F5) the website if you've just added or modified the custom function.
-
Hover over the Simulation button in the Model canvas and select the custom function wanted.
-
Simulate using the custom function by pressing the Simulation button on the Model Canvas.
-
Browse the results in the Results section in the Details browser (available if the custom function writes a result file).
The log files in
/home/jovyan/impact/logs
are useful for any custom function-related issues.
The log files in
C:/Users/<user_name>/impact/logs
are useful for any custom function-related issues.
Custom function API🔗
Three (two required and one optional) functions need to be implemented to create a custom function:
- signature()
- configuration() (Optional)
- run()
signature() - specifies the name of the custom function and parameters that the user can set. It returns a dictionary with keys:
Required parameters
name
- specifies the name of the custom function to appear in Modelon Impact.
Optional parameters
parameters
- list of parameters to be set by the user via the simulation browser. Each parameter is specified by a dictionary in the format:name
- the name of the parameter to appear in Modelon Impacttype
- the type of the parameter: String, Number, Boolean or Enumerationvalues
- A list specifying values to choose the parameter from (applicable only for parameters of type Enumeration).description
- description of the parameter that appears when hovering over itdefaultValue
- default value of the parameter
version
- the version number of the custom functiondescription
- description of what the custom function doescan_initialize_from
- Indicates whether to enable initializing from previous results for this custom function. Defaults to False if not entered.
An example of the signature() function, as implemented in the example linearize.py is shown below.
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,
},
],
}
configuration() - defines the configuration of the custom function.
Optional configurations
result_type
- The format of the result file. Possible values are "mat" and "csv". Default: "csv".requires_compilation
- A boolean to skip compilation of the model when running the custom function. Default: True.
An example of the configuration() function is shown below.
def configuration():
return {
"result_type": "mat",
'requires_compilation': True
}
run() - defines the computation of the custom function.
Optional parameters
get_fmu
- is a function returning an FMU object for the model on which the custom function is applied.environment
- dictionary specifying environment variables:result_folder_path
– path to the folder where the result is to be savedresult_file_name
– the name of the file where the results are to be savedworkspace_id
- The workspace IDcase_id
- The case IDexperiment_id
- The experiment IDlog_file_name
- The name of the log file
upload_custom_artifact
- Function for uploading a custom artifact to the storage. It 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.
parametrization
- A dictionary containing the parameter values set in Modelon Impact experiment mode.initialize_from
- A dictionary-like object containing a previous result to use for initialization.options
- A dictionary containing the simulation-specific options set by the user. The options dictionary contains the following keys - simulationOptions, solverOptions and simulationLogLevel.
Thereafter follows one argument for each of the parameters of the custom function. In the example linearize.py, this means there is a third and fourth argument t_linearize
and print_to_log
.
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)
Interact with Modelon Impact🔗
A custom function can interact with Modelon Impact in three ways:
- Return result
- Write to log
- Upload an artifact
Custom function returns result🔗
Instructions to return the result to Modelon Impact:
- In the run() function, write the result to a csv-file. See Result format for information on the format.
- Save the csv-file in the path specified by the
environment["result_folder_path"]
. The name of the file should be set equal to the value of theenvironment["result_file_name"]
.
If the result is stored as a dictionary with variable names as keys and result lists as values, it can conveniently be printed to file using the Python package pandas:
# Write the result 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=my_result_dictionary)
df.to_csv(csv_file_path, index=False)
Note: Result from a dynamic simulation in pyfmi comes in arrays rather than lists, so it needs to be converted.
# Converting pyfmi result format, from arrays to lists
return_res=dict((name,res[name].tolist())) for name in res.keys())
Result format🔗
The result file format is CSV (comma-separated values).
- First row - names of the variables.
- Following rows - values of the variable for a specific time. For parameters and constants, which do not change over time, the same value is repeated in each row.
Example of a csv result file is seen below. k
is a parameter, x
is a variable, and a
is an array.
time,k,x,a[1],a[2]
0 ,2,1, 3 ,4
1 ,2,3, 4 ,5
Custom function writes to log🔗
Writing to the log is done by adding print statements in the run() function.
print('Print to log')
Custom function uploads an artifact🔗
A custom artifact is a file that is created in the run() function and is uploaded to a workspace.
Follow the instructions below to upload a custom artifact:
- Create the artifact in a temporary folder
- Upload the artifact using
upload_custom_artifact()
.upload_custom_artifact()
is a parameter of therun()
function.
Example of creating and uploading a .mat-file is seen below.
# 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)
Access and download a custom artifact🔗
You can access and download a custom artifact directly by clicking on the link available in the log.
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 above.
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))