diff --git a/.gitignore b/.gitignore
index b6e4761..33d6439 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,9 @@ __pycache__/
*.py[cod]
*$py.class
+# IDEA related
+.idea
+
# C extensions
*.so
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..ce7273e
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include pycosim/cosim/*.*
\ No newline at end of file
diff --git a/README.md b/README.md
index a28c466..0e4a583 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,141 @@
-# pyCOSIM
-pyCOSIM is a Python package that provides an user friendly interface to the COSIM-CLI from Open Simulation Platform.
+# pycosim
+
+pycosim is a package for running co-simulation using cosim-cli from [Open Simulation Project](https://open-simulation-platform.github.io/). It provides an user friendly interface for creating the simulation setups such as logging configuration, scenario, interfaces between models and initial values.
+
+## Features
+- Importing an FMU, getting information of the model description and running a single FMU simulation,
+- Importing a system configuration, configuring output logging and scenario, running co-simulation and retrieving the results,
+
+## Getting Started
+### Prerequisite
+- Operation system: Windows 7 or later
+- Python > 3.6
+### Installation
+The package can be installed by `pip`:
+```bash
+> pip install pyCOSIM --extra-index-url=https://test.pypi.org/simple/
+```
+### Basic Usage
+The package provides a SimulationConfiguration class that manages the system configuration and the settings for the simulation, deploys necessary files and run simulations.
+To use the class, import the class as follows:
+```python
+from pycosim.simulation import SimulationConfiguration
+```
+A target system can be built up bottom-up from scratch or imported using a system structure file that is usually named, `OspSystemStruct.xml`.
+Importing file can be done as follows:
+```python
+import os
+from pycosim.simulation import SimulationConfiguration
+
+path_or_str_content_of_system_structure_file = os.path.join('base', 'system') # Path where the systm structure file if found
+path_to_dir_for_fmu = 'base' # Path where the fmus are found
+
+sim_config = SimulationConfiguration(
+ system_structure=path_or_str_content_of_system_structure_file,
+ path_to_fmu=path_to_dir_for_fmu,
+)
+
+
+```
+Note that the path to the directories that contain all the relevant FMUs should be provided together with the source for the system structure file.
+When the system is configured, you can run the simulation for a given simulation time with default settings:
+```python
+from pycosim.simulation import SimulationConfiguration
+
+sim_config = SimulationConfiguration(
+ system_structure='system_path',
+ path_to_fmu='base_path',
+)
+
+output = sim_config.run_simulation(duration=10.0)
+result_comp1 = output.result.get('comp1') # Get result DataFrame for the component, naemd 'comp1'
+log = output.log # Logging during the simulation
+path_to_output_files = output.output_file_path #Path for the ouput files
+```
+Default setting for the simulation is:
+- No scenario
+- No logging configuration (All variables will be logged at each time step.)
+- The system structure and output files are saved in the same directory as the temporary one where FMUs are deployed.
+- Only warning from simulation setting up and progress messages are logged.
+
+The `run_simulation` method returns NamedTuple instance of output. It has three members:
+- result: The result of the simulation given in a dictionary instance. The dictionary has key of names of the components in the system and DataFrame as value for each key that contains all the numerical outputs.
+- log: Logged message during setting up and running simulation
+- output_file_path: Path to the temporary directory that contains fmus, settings and output csv files.
+
+### Scenario configuration
+A scenario is a collection of events that override / bias / reset a variable of components in the target system. A scenario can be created as follows:
+```python
+# Creating a scenario instance
+import os
+from pyOSPParser.scenario import OSPScenario, OSPEvent
+from pycosim.simulation import SimulationConfiguration
+
+sim_config = SimulationConfiguration(
+ system_structure='system_path',
+ path_to_fmu='base_path',
+)
+
+simulation_end_time = 10
+scenario = OSPScenario(name='test_scenario', end=simulation_end_time)
+
+# Adding an event to the scenario
+scenario.add_event(OSPEvent(
+ time=5, # Time when the event happens
+ model='component', # Name_of_the_component
+ variable='variable1', # name_of_the_variable,
+ action=OSPEvent.OVERRIDE, # Type of actions among OVERRIDE, BIAS, RESET
+ value=19.4 # Value (only for OVERRIDE and BIAS)
+))
+
+sim_config.scenario = scenario
+```
+Finally, the scenario instance can be assigned to the system configuration.
+
+### Logging configuration
+A logging configuration specifies which variables will be logged as output of the simulation. A logging configuration can be
+defined using OspLoggingConfiguration class:
+```python
+from pyOSPParser.logging_configuration import OspVariableForLogging, OspSimulatorForLogging, OspLoggingConfiguration
+from pycosim.simulation import SimulationConfiguration
+
+sim_config = SimulationConfiguration(
+ system_structure='system',
+ path_to_fmu='base',
+)
+# Create a variable object for logging
+variable_name = 'variable1'
+variable = OspVariableForLogging(name=variable_name)
+
+# Create a logging configuration of a component
+name_of_component = 'component1'
+logging_config_comp = OspSimulatorForLogging(
+ name=name_of_component,
+ decimation_factor=1,
+ variables=[variable]
+)
+
+# Create a logging configuration instance for the system
+logging_config = OspLoggingConfiguration(simulators=[logging_config_comp])
+
+sim_config.logging_config = logging_config
+```
+
+### Logging level setting
+You can set the logging level for the messages during setting up and running a simulation. You can do that
+by passing the `LoggingLevel` member when running the simulation. If not specified, it will be 'warning' by default.
+```python
+ from pycosim.simulation import SimulationConfiguration, LoggingLevel
+
+ sim_config = SimulationConfiguration(
+ system_structure='system',
+ path_to_fmu='base',
+)
+
+sim_config.run_simulation(duration=10.0, logging_level=LoggingLevel.info)
+```
+
+## License
+Copyright Kevin Koosup Yum, SINTEF Ocean and others 2020
+
+Distributed under the terms of the Apache license 2.0, pycosim is free and open source software.
\ No newline at end of file
diff --git a/pycosim/__init__.py b/pycosim/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pycosim/cosim/boost_atomic.dll b/pycosim/cosim/boost_atomic.dll
new file mode 100644
index 0000000..7f1216c
Binary files /dev/null and b/pycosim/cosim/boost_atomic.dll differ
diff --git a/pycosim/cosim/boost_chrono.dll b/pycosim/cosim/boost_chrono.dll
new file mode 100644
index 0000000..2aa5163
Binary files /dev/null and b/pycosim/cosim/boost_chrono.dll differ
diff --git a/pycosim/cosim/boost_context.dll b/pycosim/cosim/boost_context.dll
new file mode 100644
index 0000000..4d5e8d9
Binary files /dev/null and b/pycosim/cosim/boost_context.dll differ
diff --git a/pycosim/cosim/boost_date_time.dll b/pycosim/cosim/boost_date_time.dll
new file mode 100644
index 0000000..2b7a9cf
Binary files /dev/null and b/pycosim/cosim/boost_date_time.dll differ
diff --git a/pycosim/cosim/boost_fiber.dll b/pycosim/cosim/boost_fiber.dll
new file mode 100644
index 0000000..0e62e6f
Binary files /dev/null and b/pycosim/cosim/boost_fiber.dll differ
diff --git a/pycosim/cosim/boost_filesystem.dll b/pycosim/cosim/boost_filesystem.dll
new file mode 100644
index 0000000..7e48725
Binary files /dev/null and b/pycosim/cosim/boost_filesystem.dll differ
diff --git a/pycosim/cosim/boost_log.dll b/pycosim/cosim/boost_log.dll
new file mode 100644
index 0000000..dfade98
Binary files /dev/null and b/pycosim/cosim/boost_log.dll differ
diff --git a/pycosim/cosim/boost_log_setup.dll b/pycosim/cosim/boost_log_setup.dll
new file mode 100644
index 0000000..91dcfe3
Binary files /dev/null and b/pycosim/cosim/boost_log_setup.dll differ
diff --git a/pycosim/cosim/boost_program_options.dll b/pycosim/cosim/boost_program_options.dll
new file mode 100644
index 0000000..4a11db9
Binary files /dev/null and b/pycosim/cosim/boost_program_options.dll differ
diff --git a/pycosim/cosim/boost_regex.dll b/pycosim/cosim/boost_regex.dll
new file mode 100644
index 0000000..1b5f146
Binary files /dev/null and b/pycosim/cosim/boost_regex.dll differ
diff --git a/pycosim/cosim/boost_system.dll b/pycosim/cosim/boost_system.dll
new file mode 100644
index 0000000..d67cd1d
Binary files /dev/null and b/pycosim/cosim/boost_system.dll differ
diff --git a/pycosim/cosim/boost_thread.dll b/pycosim/cosim/boost_thread.dll
new file mode 100644
index 0000000..58c795b
Binary files /dev/null and b/pycosim/cosim/boost_thread.dll differ
diff --git a/pycosim/cosim/concrt140.dll b/pycosim/cosim/concrt140.dll
new file mode 100644
index 0000000..1065145
Binary files /dev/null and b/pycosim/cosim/concrt140.dll differ
diff --git a/pycosim/cosim/cosim.dll b/pycosim/cosim/cosim.dll
new file mode 100644
index 0000000..b95fff8
Binary files /dev/null and b/pycosim/cosim/cosim.dll differ
diff --git a/pycosim/cosim/cosim.exe b/pycosim/cosim/cosim.exe
new file mode 100644
index 0000000..d447e30
Binary files /dev/null and b/pycosim/cosim/cosim.exe differ
diff --git a/pycosim/cosim/msvcp140.dll b/pycosim/cosim/msvcp140.dll
new file mode 100644
index 0000000..98313d4
Binary files /dev/null and b/pycosim/cosim/msvcp140.dll differ
diff --git a/pycosim/cosim/msvcp140_1.dll b/pycosim/cosim/msvcp140_1.dll
new file mode 100644
index 0000000..c0253df
Binary files /dev/null and b/pycosim/cosim/msvcp140_1.dll differ
diff --git a/pycosim/cosim/msvcp140_2.dll b/pycosim/cosim/msvcp140_2.dll
new file mode 100644
index 0000000..93f00f5
Binary files /dev/null and b/pycosim/cosim/msvcp140_2.dll differ
diff --git a/pycosim/cosim/vcruntime140.dll b/pycosim/cosim/vcruntime140.dll
new file mode 100644
index 0000000..34a0e72
Binary files /dev/null and b/pycosim/cosim/vcruntime140.dll differ
diff --git a/pycosim/osp_command_line_interface.py b/pycosim/osp_command_line_interface.py
new file mode 100644
index 0000000..c12ecab
--- /dev/null
+++ b/pycosim/osp_command_line_interface.py
@@ -0,0 +1,369 @@
+""" Handles FMU and runs co-simulations using cosim-cli
+
+The module contains different functions for handling FMU or running co-simulations using cosim-cli
+(https://github.com/open-simulation-platform/cosim-cli)
+
+...
+Attributes:
+ PATH_TO_COSIM(str)
+
+Functions:
+ get_model_description():
+
+Classes:
+ ModelVariables(NamedTuple): Representation of model variables
+"""
+import datetime as dt
+import io
+import logging
+import os
+from enum import Enum
+from subprocess import Popen, PIPE
+from sys import platform
+from typing import NamedTuple, List, Dict, Union, Tuple
+
+import pandas
+import yaml
+
+from pyOSPParser.scenario import OSPScenario
+from pyOSPParser.logging_configuration import OspLoggingConfiguration
+
+COSIM_FILE_NAME = 'cosim' if platform.startswith("linux") else 'cosim.exe'
+
+PATH_TO_COSIM = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ 'cosim', COSIM_FILE_NAME
+)
+
+
+class SimulationError(Exception):
+ pass
+
+
+class ModelVariables(NamedTuple):
+ """ Representation of model variables from FMU's model description
+
+ Attributes:
+ parameters (List[Dict[str,str]], optional)
+ inputs (List[Dict[str,str]], optional)
+ outputs (List[Dict[str,str]], optional)
+ others (List[Dict[str,str]], optional)
+ """
+ parameters: List[Dict[str, str]] = []
+ inputs: List[Dict[str, str]] = []
+ outputs: List[Dict[str, str]] = []
+ others: List[Dict[str, str]] = []
+
+ def get_parameters_names(self) -> List:
+ """ Returns a list of the parameter names """
+ return [variable['name'] for variable in self.parameters]
+
+ def get_input_names(self) -> List:
+ """ Returns a list of the parameter names """
+ return [variable['name'] for variable in self.inputs]
+
+ def get_output_names(self) -> List:
+ """ Returns a list of the output names """
+ return [variable['name'] for variable in self.outputs]
+
+ def get_other_variable_names(self) -> List:
+ """ Returns a list of the parameter names """
+ return [variable['name'] for variable in self.others]
+
+
+class FMUModelDescription(NamedTuple):
+ """ Model description summary
+
+ Model description summary used as a return type for get_model_description
+
+ Attributes:
+ name(str)
+ uuid(str)
+ model_variable (ModelVariables)
+ description (str, optional)
+ author (str, optional)
+ version (str, optional)
+ """
+ name: str
+ uuid: str
+ model_variable: ModelVariables
+ description: str = ''
+ author: str = ''
+ version: str = ''
+
+
+class LoggingLevel(Enum):
+ error = 40
+ warning = 30
+ info = 20
+ debug = 10
+
+
+def parse_model_variables(variables: List[Dict[str, str]]) -> ModelVariables:
+ """Parse the variables according to its category
+
+ Args:
+ variables (List[Dict[str, str]]): The variable from cosim-cli inspect
+
+ Returns:
+ ModelVariables: Variables sorted into parameter, input, output, others
+ """
+ parameters = []
+ inputs = []
+ outputs = []
+ others = []
+ for variable in variables:
+ if variable['variability'] == 'fixed' or variable['causality'] == 'parameter':
+ parameters.append(variable)
+ elif variable['causality'] == 'input':
+ inputs.append(variable)
+ elif variable['causality'] == 'output':
+ outputs.append(variable)
+ else:
+ others.append(variable)
+ return ModelVariables(
+ parameters=parameters,
+ inputs=inputs,
+ outputs=outputs,
+ others=others
+ )
+
+
+def get_model_description(file_path_fmu: str) -> FMUModelDescription:
+ """
+ Returns the model description including variables_sorted.
+ the parameters, inputs, outputs and other internal variables from the model
+ description file of the fmu using the cosim-cli.
+
+ Args:
+ file_path_fmu (str): Absolute file path of the FMU file.
+
+ Returns:
+ FMUModelDescription: NamedTuple that contains model description
+ """
+ mode = 'inspect'
+
+ assert os.path.isfile(PATH_TO_COSIM), 'The cosim CLI is not found: %s' % PATH_TO_COSIM
+ assert os.path.isfile(file_path_fmu), 'The fmu file is not found: %s' % file_path_fmu
+
+ #: Run the cosim to get the result in yaml format
+ result = ''
+ error = ''
+ try:
+ with Popen(args=[PATH_TO_COSIM, mode, file_path_fmu], shell=True, stdout=PIPE, stderr=PIPE) as proc:
+ result = proc.stdout.read()
+ error = proc.stderr.read()
+ except OSError as e:
+ raise OSError('%s, %s, %s', (result, error, e))
+
+ #: Parse yaml to dictionary
+ result = yaml.BaseLoader(result).get_data()
+
+ return FMUModelDescription(
+ name=result['name'],
+ uuid=result['uuid'],
+ model_variable=parse_model_variables(result['variables']),
+ description=result['description'],
+ author=result['author'],
+ version=result['version'],
+ )
+
+
+def run_cli(args):
+ try:
+ with Popen(args=args, shell=True, stdout=PIPE, stderr=PIPE) as proc:
+ log = proc.stdout.read()
+ error = proc.stderr.read()
+ except OSError as e:
+ raise OSError('%s, %s, %s', (log, error, e))
+
+ # Catch errors
+
+ return log, error.decode('utf-8')
+
+
+def run_single_fmu(
+ path_to_fmu: str,
+ initial_values: Dict[str, Union[float, bool]] = None,
+ output_file_path: str = None,
+ duration: float = None,
+ step_size: float = None,
+) -> Tuple[pandas.DataFrame, str]:
+ """Runs a single fmu simulation
+
+ Args:
+ path_to_fmu(str): file path to the target fmu
+ initial_values(Dict[str, Union[float, bool]], optional): dictionary of initial values
+ output_file_path(str, optional): file path for the output
+ duration(float, optional): duration of simulation in seconds
+ step_size(float, optional): duration
+ Return:
+ (tuple): tuple containing:
+ result(pandas.DataFrame) simulation result
+ log(str) simulation logging
+ """
+ delete_output = False
+ if initial_values is None:
+ initial_values = {}
+ if output_file_path is None:
+ # Delete output if the output file path is not given
+ output_file_path = 'model-output.csv'
+ delete_output = True
+ mode = "run-single"
+
+ assert os.path.isfile(PATH_TO_COSIM), 'The cosim CLI is not found: %s' % PATH_TO_COSIM
+ assert os.path.isfile(path_to_fmu), 'The fmu file is not found: %s' % path_to_fmu
+
+ # Create a list of initial values and set arguments for simulation
+ args = [PATH_TO_COSIM, mode, path_to_fmu]
+ args.extend('%s=%s' % (key, value) for key, value in initial_values.items())
+ args.append('--output-file=%s' % output_file_path)
+ if duration:
+ args.append('-d%f' % duration)
+ if step_size:
+ args.append('-s%f' % step_size)
+
+ #: Run the cosim to get the result in yaml format
+ log, error = run_cli(args)
+
+ # Parse the output
+ result = pandas.read_csv(output_file_path)
+ if delete_output:
+ os.remove(output_file_path)
+
+ return result, log.decode('utf-8')
+
+
+def deploy_output_config(output_config: OspLoggingConfiguration, path: str):
+ file_path = os.path.join(path, 'LogConfig.xml')
+
+ xml_text = output_config.to_xml_str()
+
+ with open(file_path, 'w+') as file:
+ file.write(xml_text)
+
+
+def deploy_scenario(scenario: OSPScenario, path: str):
+ file_path = os.path.join(path, scenario.get_file_name())
+
+ with open(file_path, 'w+') as file:
+ file.write(scenario.to_json())
+
+ return file_path
+
+
+def clean_header(header: str):
+ if '[' in header:
+ return header[0:header.rindex('[')-1]
+ else:
+ return header
+
+
+def run_cosimulation(
+ path_to_system_structure: str,
+ # initial_values=None,
+ logging_config: OspLoggingConfiguration = None,
+ output_file_path: str = None,
+ scenario: OSPScenario = None,
+ duration: float = None,
+ logging_level: LoggingLevel = LoggingLevel.warning,
+ logging_stream: bool = False
+) -> Tuple[Dict[str, pandas.DataFrame], str]:
+ """Runs a co-simulation
+
+ Args:
+ path_to_system_structure(str): The path to the system structure definition file/directory.
+ If this is a file with .xml extension, or a directory that contains a file named
+ OspSystemStructure.xml, it will be interpreted as a OSP system structure
+ definition.
+ logging_config(Dict[str, str], optional): dictionary of output configuration
+ output_file_path(str, optional): file path for the output
+ scenario(Dict[str, str], optional), dictionary of scenario
+ duration(float, optional): duration of simulation in seconds
+ logging_level(LoggingLevel, optional): Sets the detail/severity level of diagnostic output.
+ Valid arguments are 'error', 'warning', 'info', and 'debug'. Default is 'warning'.
+ logging_stream(bool, optional): logging will be returned as a string if True value is given.
+ Otherwise, logging will be only displayed.
+ Return:
+ (tuple): tuple containing:
+ result(Dict[str, pandas.DataFrame]) simulation result
+ log(str) simulation logging
+ """
+ # Set loggers
+ logger = logging.getLogger()
+ if logging_stream:
+ log_stream = io.StringIO()
+ log_handler = logging.StreamHandler(log_stream)
+ log_handler.setLevel(logging.INFO)
+ logger.addHandler(log_handler)
+ logger.setLevel(logging_level.value)
+
+ # Set simulation parameters
+ delete_output = False
+ mode = "run"
+
+ # Check if the cosim-cli exists and the system structure exists
+ assert os.path.isfile(PATH_TO_COSIM), 'The cosim CLI is not found: %s' % PATH_TO_COSIM
+ assert os.path.isdir(path_to_system_structure), \
+ 'The system structure directory is not found: %s' % path_to_system_structure
+ path_to_osp_sys_structure = os.path.join(path_to_system_structure, 'OspSystemStructure.xml')
+ assert os.path.isfile(path_to_osp_sys_structure), \
+ 'The system structure directory is not found: %s' % path_to_system_structure
+ args = [PATH_TO_COSIM, mode, path_to_system_structure]
+
+ if logging_config is not None:
+ logger.info('Deploying the logging configuration.')
+ deploy_output_config(logging_config, path_to_system_structure)
+ if output_file_path is None:
+ output_file_path = path_to_system_structure
+ delete_output = True
+ else:
+ assert os.path.isdir(output_file_path), \
+ "The directory for the output doesn't exist: %s." % output_file_path
+ logger.info('Output csv files will be saved in the following directory: %s.' % output_file_path)
+ args.append('--output-dir=%s' % output_file_path)
+ if scenario is not None:
+ logger.info('Deploying the scenario.')
+ scenario_file_path = deploy_scenario(scenario, path_to_system_structure)
+ args.append('--scenario=%s' % scenario_file_path)
+ if duration:
+ logger.info('Simulation will run until %f seconds.' % duration)
+ args.append('--duration=%s' % duration)
+ args.append('--log-level=%s' % logging_level.name)
+
+ # Run simulation
+ logger.info('Running simulation.')
+ log, error = run_cli(args)
+ logger.info(error)
+
+ # construct result from csvs that are created within last 30 seconds
+ output_files = [file_name for file_name in os.listdir(output_file_path) if file_name.endswith('csv')]
+ ago = dt.datetime.now() - dt.timedelta(seconds=30)
+ output_files = [
+ file_name for file_name in output_files
+ if dt.datetime.fromtimestamp(os.stat(os.path.join(output_file_path, file_name)).st_mtime) > ago
+ ]
+ result = {}
+ for file in output_files:
+ simulator_name = file
+ for _ in range(3):
+ simulator_name = simulator_name[:simulator_name.rfind('_')]
+ result[simulator_name] = pandas.read_csv(os.path.join(output_file_path, file))
+ new_column_name = list(map(clean_header, result[simulator_name].columns))
+ result[simulator_name].columns = new_column_name
+ if delete_output:
+ for file_name in output_files:
+ os.remove(os.path.join(output_file_path, file_name))
+
+ # Get logging data
+ if logging_stream:
+ # noinspection PyUnboundLocalVariable
+ logger.removeHandler(log_handler)
+ log_handler.flush()
+ # noinspection PyUnboundLocalVariable
+ log_stream.flush()
+ log = log_stream.getvalue()
+ else:
+ log = ''
+
+ return result, log
diff --git a/pycosim/simulation.py b/pycosim/simulation.py
new file mode 100644
index 0000000..f943cad
--- /dev/null
+++ b/pycosim/simulation.py
@@ -0,0 +1,363 @@
+import os
+import shutil
+import uuid
+from enum import Enum
+from sys import platform
+from typing import NamedTuple, Union, List, Dict, Tuple
+
+import pandas
+from pyOSPParser.logging_configuration import OspLoggingConfiguration
+from pyOSPParser.model_description import OspModelDescription, OspVariableGroupsType
+from pyOSPParser.scenario import OSPScenario
+from pyOSPParser.system_configuration import OspSystemStructure, OspSimulator
+
+from .osp_command_line_interface import get_model_description, run_cosimulation, LoggingLevel, run_single_fmu
+
+COSIM_FILE_NAME = 'cosim' if platform.startswith("linux") else 'cosim.exe'
+
+PATH_TO_COSIM = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ 'cosim', COSIM_FILE_NAME
+)
+
+
+class FMU:
+ """Class for managing FMU"""
+ osp_model_description: OspModelDescription = None
+
+ def __init__(self, fmu_file: str):
+ """Constructor for FMU"""
+ assert fmu_file.endswith('.fmu')
+ self.fmu_file = fmu_file
+ if os.path.isfile(fmu_file):
+ self.get_fmu_information_from_file()
+ # Check if there is OSP Model description file in the same directory
+ osp_model_description_file = os.path.join(
+ os.path.dirname(self.fmu_file),
+ '%s_OspModelDescription.xml' % self.name
+ )
+ if os.path.isfile(osp_model_description_file):
+ self.get_osp_model_description(osp_model_description_file)
+
+ else:
+ self.name = None
+ self.uuid = None
+ self.description = None
+ self.parameters = None
+ self.inputs = None
+ self.outputs = None
+ self.other_variables = None
+
+ def get_fmu_information_from_file(self):
+ model_description = get_model_description(self.fmu_file)
+ self.name = model_description.name
+ self.uuid = model_description.uuid
+ self.description = model_description.description
+ self.parameters = model_description.model_variable.parameters
+ self.inputs = model_description.model_variable.inputs
+ self.outputs = model_description.model_variable.outputs
+ self.other_variables = model_description.model_variable.others
+
+ def get_osp_model_description(self, xml_source: str):
+ """Import OSP Model Description file or string
+
+ Args:
+ xml_source: Path to the file or string content of the OSP model description
+ """
+ self.osp_model_description = OspModelDescription(xml_source=xml_source)
+
+ def get_endpoint_dict(self):
+ return {
+ 'input': self.inputs,
+ 'output': self.outputs,
+ 'variable_group': self.osp_model_description.to_dict().get('VariableGroups', None)
+ if self.osp_model_description is not None else None
+ }
+
+ def add_variable_group(self, var_group: OspVariableGroupsType):
+ if self.osp_model_description is None:
+ self.osp_model_description = OspModelDescription(VariableGroups=[var_group])
+ else:
+ self.osp_model_description.add_interface(var_group)
+
+ def run_simulation(
+ self,
+ initial_values: Dict[str, Union[float, bool]] = None,
+ output_file_path: str = None,
+ duration: float = None,
+ step_size: float = None,
+ ) -> Tuple[pandas.DataFrame, str]:
+ """Runs a single FMU simulation
+
+ Args:
+ initial_values(Dict[str, Union[float, bool]], optional): dictionary of initial values
+ output_file_path(str, optional): file path for the output including the file name
+ duration(float, optional): duration of simulation in seconds
+ step_size(float, optional): duration
+ Return:
+ (tuple): tuple containing:
+ result(pandas.DataFrame) simulation result
+ log(str) simulation logging
+ """
+ return run_single_fmu(
+ path_to_fmu=self.fmu_file,
+ initial_values=initial_values,
+ output_file_path=output_file_path,
+ duration=duration,
+ step_size=step_size
+ )
+
+
+class Component(NamedTuple):
+ name: str
+ fmu: FMU
+
+
+class Causality(Enum):
+ input = "input"
+ output = "output"
+ indefinite = "indefinite"
+
+
+class InitialValues(NamedTuple):
+ component: str
+ variable: str
+ value: Union[float, int, bool, str]
+
+
+class SimulationOutput(NamedTuple):
+ result: Dict[str, pandas.DataFrame]
+ log: str
+ output_file_path: str
+
+
+class SimulationConfiguration:
+ """Class for running simulation"""
+ components: List[Component] = []
+ initial_values: List[InitialValues] = []
+ system_structure: OspSystemStructure = None
+ _scenario: OSPScenario = None
+ _logging_config: OspLoggingConfiguration = None
+ _current_sim_path: str = None
+
+ # add_component(name: str, fmu: fmu)
+ # add_initial_value(comp_name: str, variable_name: str, value: float)
+ # get_initial_values()
+ # add_variable_interface(source: VariableInterface, target: VariableInterface)
+ # get_variable_interfaces()
+
+ def __init__(
+ self,
+ system_structure: str = None,
+ path_to_fmu: str = None,
+ components: List[Component] = None,
+ initial_values: List[InitialValues] = None,
+ scenario: OSPScenario = None,
+ logging_config: OspLoggingConfiguration = None,
+ ):
+ """Constructor for SimulationConfiguration class
+
+ Args:
+ system_structure(optional): A source for the system structure, either string content of the
+ XML file or path to the file. Must be given together with the path_to_fmu argument..
+ path_to_fmu(optional): A path to the FMUs for the given system structure.
+ components(optional): Components for the system given as a list of Component instance
+ initial_values(optional): Initial values for the simulation given as a list of InitialValues instance
+ scenario(optional): A scenario for the simulation given as a OSPScenario instance
+ logging_config(optional): A logging configuration for the output of the simulation given as a
+ OSPScenario instance
+ """
+ if system_structure:
+ assert path_to_fmu is not None, "The path to fmu should be given together with the system structure"
+ self.system_structure = OspSystemStructure(xml_source=system_structure)
+ self.components = []
+ self.initial_values = []
+ for Simulator in self.system_structure.Simulators:
+ self.components.append(Component(
+ name=Simulator.name,
+ fmu=FMU(os.path.join(path_to_fmu, Simulator.source))
+ ))
+ if Simulator.InitialValues:
+ self.initial_values.extend([InitialValues(
+ component=Simulator.name,
+ variable=initial_value.variable,
+ value=initial_value.value.value
+ ) for initial_value in Simulator.InitialValues])
+ if len(self.initial_values) == 0:
+ # noinspection PyTypeChecker
+ self.initial_values = None
+ else:
+ if components:
+ for comp in components:
+ assert type(comp) is Component
+ self.components = components
+ if initial_values:
+ for init_value in initial_values:
+ assert type(init_value) is InitialValues
+ self.initial_values = initial_values
+ if scenario:
+ self.scenario = scenario
+ if logging_config:
+ self.logging_config = logging_config
+
+ def __del__(self):
+ """Destructor for the class
+
+ Deletes the deployed directory and files for the simulation.
+ """
+ if self._current_sim_path:
+ if os.path.isdir(self._current_sim_path):
+ shutil.rmtree(self._current_sim_path)
+
+ @property
+ def scenario(self):
+ return self._scenario
+
+ @scenario.setter
+ def scenario(self, value):
+ assert type(value) is OSPScenario
+ self._scenario = value
+
+ @property
+ def logging_config(self):
+ return self._logging_config
+
+ @logging_config.setter
+ def logging_config(self, value):
+ assert type(value) is OspLoggingConfiguration
+ self._logging_config = value
+
+ @property
+ def current_simulation_path(self):
+ return self._current_sim_path
+
+ @staticmethod
+ def prepare_temp_dir_for_simulation() -> str:
+ base_dir_name = os.path.join('pycosim_tmp', f'sim_{uuid.uuid4().__str__()}')
+
+ if platform.startswith('win'):
+ path = os.path.join(os.environ.get('TEMP'), base_dir_name)
+ else:
+ path = os.path.join(os.environ.get('TMPDIR'), base_dir_name) if os.environ.get('TMPDIR') \
+ else os.path.join('/var', 'tmp', base_dir_name)
+ if not os.path.isdir(path):
+ os.makedirs(path)
+ return path
+
+ @staticmethod
+ def get_fmu_rel_path(path_to_deploy: str, path_to_sys_struct: str):
+ if len(path_to_deploy) > len(path_to_sys_struct):
+ rel_path = path_to_deploy[len(path_to_sys_struct):].replace(os.sep, "/")[1:]
+ if len(rel_path) > 0:
+ return f'{rel_path}/'
+ else:
+ return ''
+ else:
+ rel_path = path_to_sys_struct[len(path_to_deploy):]
+ depth = rel_path.count(os.sep)
+ return '../' * depth
+
+ def deploy_files_for_simulation(
+ self,
+ path_to_deploy: str,
+ rel_path_to_system_structure: str = '',
+ ) -> str:
+ """Deploy files for the simulation
+
+ Returns:
+ str: path to the system structure file
+ """
+ # Update the state for the current path
+ if self._current_sim_path:
+ if os.path.isdir(self._current_sim_path):
+ shutil.rmtree(self._current_sim_path)
+ self._current_sim_path = path_to_deploy
+
+ # Create a fmu list from the components
+ fmus = []
+ fmu_names = []
+ for comp in self.components:
+ if comp.fmu.name not in fmu_names:
+ fmus.append(comp.fmu)
+ fmu_names.append(comp.fmu.name)
+ for fmu in fmus:
+ destination_file = os.path.join(path_to_deploy, os.path.basename(fmu.fmu_file))
+ shutil.copyfile(fmu.fmu_file, destination_file)
+
+ # Check out with the path for the system structure file. If it doesn't exist
+ # create the directory.
+ path_to_sys_struct = os.path.join(path_to_deploy, rel_path_to_system_structure)
+ if not os.path.isdir(path_to_sys_struct):
+ os.mkdir(path_to_sys_struct)
+
+ # Create a system structure file
+ fmu_rel_path = self.get_fmu_rel_path(path_to_deploy, path_to_sys_struct)
+ for component in self.system_structure.Simulators:
+ component.fmu_rel_path = fmu_rel_path
+ with open(os.path.join(path_to_sys_struct, 'OspSystemStructure.xml'), 'wt') as file:
+ file.write(self.system_structure.to_xml_str())
+
+ return path_to_sys_struct
+
+ def run_simulation(
+ self,
+ duration: float,
+ rel_path_to_sys_struct: str = '',
+ logging_level: LoggingLevel = LoggingLevel.warning
+ ):
+ path = self.prepare_temp_dir_for_simulation()
+ path_to_sys_struct = self.deploy_files_for_simulation(
+ path_to_deploy=path,
+ rel_path_to_system_structure=rel_path_to_sys_struct,
+ )
+ result, log = run_cosimulation(
+ path_to_system_structure=path_to_sys_struct,
+ logging_config=self.logging_config,
+ output_file_path=path_to_sys_struct,
+ scenario=self._scenario,
+ duration=duration,
+ logging_level=logging_level,
+ logging_stream=True
+ )
+
+ return SimulationOutput(
+ result=result,
+ log=log,
+ output_file_path=path_to_sys_struct
+ )
+
+ def add_component(
+ self,
+ name: str,
+ fmu: FMU,
+ stepSize: float = None,
+ rel_path_to_fmu: str = ''
+ ) -> Component:
+ """Add a component to the system structure
+
+ Args:
+ name: Name of the component
+ fmu: The model for the component given as FMU instance
+ stepSize(optional): Step size for the simulator in seconds. If not given, its default value is used.
+ rel_path_to_fmu(optional): Relative path to fmu from a system structure file.
+ Return:
+ Component: the created component.
+ """
+ # Add component only in case the name is unique.
+ if name not in [component.name for component in self.components]:
+ # Create the instance and add it to the member
+ component = Component(name=name, fmu=fmu)
+ self.components.append(component)
+
+ # Update the system_structure instance. Create one if it has not been initialized.
+ if not self.system_structure:
+ self.system_structure = OspSystemStructure()
+ self.system_structure.add_simulator(OspSimulator(
+ name=name,
+ source=os.path.basename(fmu.fmu_file),
+ stepSize=stepSize,
+ fmu_rel_path=rel_path_to_fmu
+ ))
+ return component
+ else:
+ raise NameError('The name duplicates with the existing components.')
diff --git a/pycosim/xml_scheme/LoggingConfiguration.xsd b/pycosim/xml_scheme/LoggingConfiguration.xsd
new file mode 100644
index 0000000..10c3263
--- /dev/null
+++ b/pycosim/xml_scheme/LoggingConfiguration.xsd
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/requirements.txt b/requirements.txt
index f4ff351..c232a34 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,4 +3,4 @@ pytest
pandas
numpy
xmlschema
-pyOSPParser
\ No newline at end of file
+pyOSPParser
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..9ceeef5
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,26 @@
+from setuptools import setup
+
+setup(
+ name='pyCoSim',
+ version='0.1.0',
+ description='Python library running co-simulation using cosim-cli from Open Simulation Platform',
+ author='Kevin Koosup Yum',
+ author_email='kevinkoosup.yum@sintef.no',
+ license="Apache License 2.0",
+ classifiers=[
+ "Intended Audience :: Science/Research",
+ "Topic :: Scientific/Engineering",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: Python :: 3",
+ ],
+ packages=['pycosim'],
+ install_requires=[
+ 'pyyaml',
+ 'pyOSPParse',
+ 'pandas',
+ 'numpy',
+ 'xmlschema'
+ ],
+ include_package_data=True,
+ zip_safe=False
+)
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/fmus/ControlVolume.fmu b/test/fmus/ControlVolume.fmu
new file mode 100644
index 0000000..2e5dce8
Binary files /dev/null and b/test/fmus/ControlVolume.fmu differ
diff --git a/test/fmus/KnuckleBoomCrane.fmu b/test/fmus/KnuckleBoomCrane.fmu
new file mode 100644
index 0000000..fd1c474
Binary files /dev/null and b/test/fmus/KnuckleBoomCrane.fmu differ
diff --git a/test/fmus/OspSystemStructure.xml b/test/fmus/OspSystemStructure.xml
new file mode 100644
index 0000000..d2d5844
--- /dev/null
+++ b/test/fmus/OspSystemStructure.xml
@@ -0,0 +1,52 @@
+
+
+ 0.01
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fmus/OspSystemStructure_bond.xml b/test/fmus/OspSystemStructure_bond.xml
new file mode 100644
index 0000000..b79ad7f
--- /dev/null
+++ b/test/fmus/OspSystemStructure_bond.xml
@@ -0,0 +1,44 @@
+
+
+ 0.01
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fmus/PlotConfig.json b/test/fmus/PlotConfig.json
new file mode 100644
index 0000000..b53af0e
--- /dev/null
+++ b/test/fmus/PlotConfig.json
@@ -0,0 +1,22 @@
+{
+ "plots": [
+ {
+ "label": "Displacements",
+ "plotType": "trend",
+ "variables": [
+ {
+ "simulator": "chassis",
+ "variable": "zChassis"
+ },
+ {
+ "simulator": "ground",
+ "variable": "zGround"
+ },
+ {
+ "simulator": "wheel",
+ "variable": "zWheel"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/fmus/SystemStructure.ssd b/test/fmus/SystemStructure.ssd
new file mode 100644
index 0000000..1f6768e
--- /dev/null
+++ b/test/fmus/SystemStructure.ssd
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fmus/Test case Quarter Truck.docx b/test/fmus/Test case Quarter Truck.docx
new file mode 100644
index 0000000..2a723b5
Binary files /dev/null and b/test/fmus/Test case Quarter Truck.docx differ
diff --git a/test/fmus/chassis.fmu b/test/fmus/chassis.fmu
new file mode 100644
index 0000000..49e4945
Binary files /dev/null and b/test/fmus/chassis.fmu differ
diff --git a/test/fmus/chassis_OspModelDescription.xml b/test/fmus/chassis_OspModelDescription.xml
new file mode 100644
index 0000000..a871428
--- /dev/null
+++ b/test/fmus/chassis_OspModelDescription.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fmus/ground.fmu b/test/fmus/ground.fmu
new file mode 100644
index 0000000..207fc21
Binary files /dev/null and b/test/fmus/ground.fmu differ
diff --git a/test/fmus/ground_OspModelDescription.xml b/test/fmus/ground_OspModelDescription.xml
new file mode 100644
index 0000000..a871428
--- /dev/null
+++ b/test/fmus/ground_OspModelDescription.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fmus/wheel.fmu b/test/fmus/wheel.fmu
new file mode 100644
index 0000000..b88831c
Binary files /dev/null and b/test/fmus/wheel.fmu differ
diff --git a/test/fmus/wheel_OspModelDescription.xml b/test/fmus/wheel_OspModelDescription.xml
new file mode 100644
index 0000000..086c736
--- /dev/null
+++ b/test/fmus/wheel_OspModelDescription.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/sim_temp/LogConfig.xml b/test/sim_temp/LogConfig.xml
new file mode 100644
index 0000000..ef9ecd3
--- /dev/null
+++ b/test/sim_temp/LogConfig.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/test_cli.py b/test/test_cli.py
new file mode 100644
index 0000000..68bb09b
--- /dev/null
+++ b/test/test_cli.py
@@ -0,0 +1,161 @@
+import os
+import random
+
+import numpy as np
+import pandas
+from pyOSPParser.logging_configuration import OspLoggingConfiguration
+from pyOSPParser.scenario import OSPScenario, OSPEvent
+
+from pycosim.osp_command_line_interface import get_model_description, run_single_fmu, ModelVariables, run_cosimulation, \
+ LoggingLevel
+
+path_to_fmu = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ 'fmus',
+ 'ControlVolume.fmu'
+)
+
+path_to_system_structure = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ 'fmus'
+)
+
+
+def test_get_model_description():
+ model_description = get_model_description(path_to_fmu)
+ assert model_description.name == 'ControlVolume'
+ assert model_description.uuid != ''
+ assert type(model_description.model_variable) is ModelVariables
+ assert len(model_description.model_variable.parameters) > 0
+ assert len(model_description.model_variable.inputs) > 0
+ assert len(model_description.model_variable.outputs) > 0
+ assert len(model_description.model_variable.others) > 0
+ print('Parameters:')
+ for param in model_description.model_variable.get_parameters_names():
+ print('\t%s' % param)
+ print('Inputs:')
+ for param in model_description.model_variable.get_input_names():
+ print('\t%s' % param)
+ print('Outputs:')
+ for param in model_description.model_variable.get_output_names():
+ print('\t%s' % param)
+ print('Others:')
+ for param in model_description.model_variable.get_other_variable_names():
+ print('\t%s' % param)
+
+
+def test_run_single_fmu():
+ result, log = run_single_fmu(path_to_fmu)
+
+ # Check if the output file does not exist if the output_file_path is not given
+ assert not os.path.isfile('model-output.csv')
+ print('')
+ print(result)
+
+ # Check if the file exists if the output_file_path is given
+ output_file_path = 'output.csv'
+ run_single_fmu(path_to_fmu, output_file_path=output_file_path)
+ assert os.path.isfile(output_file_path)
+ os.remove(output_file_path)
+
+ # Check if the initial values are effective
+ initial_values = {
+ 'p_loss.T': 330,
+ 'p_in.dQ': 100,
+ }
+ result, _ = run_single_fmu(path_to_fmu, initial_values=initial_values)
+ # Collect the column names that matches the initial value specified
+ columns = [column for column in result.columns if any(list(map(lambda x: column.startswith(x), initial_values)))]
+ for column in columns:
+ for key in initial_values:
+ if column.startswith(key):
+ break
+ # Compare the initial value
+ # noinspection PyUnboundLocalVariable
+ comparison = result[column].values == initial_values[key]
+ assert all(comparison.tolist())
+
+ # Check if the duration arg is effective
+ # Duration is rounded to the second decimal place because the
+ # step size is 0.01 by default.
+ duration = np.round(random.random() * 10, 2)
+ result, _ = run_single_fmu(path_to_fmu, duration=duration)
+ assert result['Time'].values[-1] == duration
+
+ # Check if the step size arg is effective
+ step_size = 0.05
+ result, _ = run_single_fmu(path_to_fmu, step_size=step_size)
+ step_size_sim = np.diff(result['Time'].values)
+ assert np.any(step_size_sim == step_size)
+
+
+def test_run_cosimulation():
+ duration = random.randint(5, 10)
+ result, log = run_cosimulation(
+ path_to_system_structure=path_to_system_structure,
+ duration=duration,
+ logging_level=LoggingLevel.info,
+ logging_stream=True
+ )
+ for each in result:
+ assert type(result[each]) is pandas.DataFrame
+ assert result[each]['Time'].values[-1] == duration
+ assert type(log) is str
+ assert len(log) > 0
+
+ # Test with logging configuration and output directory
+ path_to_sim_temp = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'sim_temp')
+ path_to_logging_config = os.path.join(path_to_sim_temp, 'LogConfig.xml')
+ logging_config = OspLoggingConfiguration(xml_source=path_to_logging_config)
+ result, log = run_cosimulation(
+ path_to_system_structure=path_to_system_structure,
+ output_file_path=path_to_sim_temp,
+ logging_config=logging_config,
+ logging_level=LoggingLevel.info,
+ logging_stream=True
+ )
+ output_files = [file_name for file_name in os.listdir(path_to_sim_temp) if file_name.endswith('.csv')]
+ for file_name in output_files:
+ os.remove(os.path.join(path_to_sim_temp, file_name))
+ os.remove(os.path.join(path_to_system_structure, 'LogConfig.xml'))
+ assert len(output_files) == len(logging_config.simulators)
+ for simulator in logging_config.simulators:
+ assert len(result[simulator.name].columns) == len(simulator.variables) + 2
+ for variable in simulator.variables:
+ assert variable.name in result[simulator.name].columns
+
+ # Test with a scenario
+ duration = 50
+ model = 'chassis'
+ variable = 'C.mChassis'
+ scenario = OSPScenario(name='test scenario', end=50)
+ scenario.add_event(
+ OSPEvent(time=5, model=model, variable=variable, action=OSPEvent.OVERRIDE, value=500)
+ )
+ scenario.add_event(
+ OSPEvent(time=15, model=model, variable=variable, action=OSPEvent.OVERRIDE, value=600)
+ )
+ scenario.add_event(
+ OSPEvent(time=30, model=model, variable=variable, action=OSPEvent.OVERRIDE, value=700)
+ )
+ scenario.add_event(
+ OSPEvent(time=45, model=model, variable=variable, action=OSPEvent.OVERRIDE, value=800)
+ )
+ result, log = run_cosimulation(
+ path_to_system_structure=path_to_system_structure,
+ duration=duration,
+ scenario=scenario,
+ logging_level=LoggingLevel.info,
+ logging_stream=True
+ )
+ print(log)
+ os.remove(os.path.join(path_to_system_structure, scenario.get_file_name()))
+
+ time_array = result[model]['Time'].values
+ for i, event in enumerate(scenario.events):
+ if i < len(scenario.events) - 1:
+ next_event = scenario.events[i + 1]
+ index = np.bitwise_and(time_array > event.time, time_array <= next_event.time)
+ else:
+ index = time_array > event.time
+ assert np.all(result[model][variable].values[index] == event.value)
diff --git a/test/test_fmu_and_system.py b/test/test_fmu_and_system.py
new file mode 100644
index 0000000..d51db6b
--- /dev/null
+++ b/test/test_fmu_and_system.py
@@ -0,0 +1,187 @@
+import json
+import os
+import random
+import string
+from functools import reduce
+
+import pytest
+from pyOSPParser.logging_configuration import OspLoggingConfiguration, OspSimulatorForLogging, OspVariableForLogging
+from pyOSPParser.scenario import OSPScenario, OSPEvent
+from pyOSPParser.system_configuration import OspSystemStructure
+
+from pycosim.osp_command_line_interface import LoggingLevel
+from pycosim.simulation import FMU, SimulationConfiguration
+
+
+PATH_TO_FMU = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fmus', 'chassis.fmu')
+PATH_TO_FMU_DIR = os.path.dirname(PATH_TO_FMU)
+PATH_TO_SYSTEM_STRUCTURE_FILE = os.path.join(PATH_TO_FMU_DIR, 'OspSystemStructure.xml')
+PATH_TO_LOG_CONFIG = os.path.join(PATH_TO_FMU_DIR, 'LogConfig.xml')
+
+
+def get_random_string(length: int = 5):
+ return ''.join(random.sample(string.ascii_lowercase, k=length))
+
+
+def test_fmu():
+ fmu = FMU(PATH_TO_FMU)
+
+ # Check if the fmu has correct reference, name and uuid
+ assert os.path.isfile(fmu.fmu_file)
+ assert fmu.name is not None
+ assert fmu.uuid is not None
+ assert fmu.osp_model_description is not None
+
+ endpoints = fmu.get_endpoint_dict()
+ print(json.dumps(endpoints, indent=2))
+ assert endpoints.get('input') == fmu.inputs
+ assert endpoints.get('output') == fmu.outputs
+ assert endpoints.get('variable_group') == fmu.osp_model_description.to_dict().get('VariableGroups')
+
+ # Test running a single FMU simualtion
+ finish_time = 10.0
+ output_file_path = os.path.join(os.path.dirname(fmu.fmu_file), 'output.csv')
+ result, log = fmu.run_simulation(
+ initial_values={fmu.inputs[0].get('name'): random.random() * 10},
+ output_file_path=output_file_path,
+ duration=finish_time,
+ )
+ assert result['Time'].values[-1] == finish_time
+ assert os.path.isfile(output_file_path)
+ os.remove(output_file_path)
+
+
+def test_simulation_configuration_initialization():
+ with pytest.raises(AssertionError):
+ sim_config = SimulationConfiguration(
+ system_structure=PATH_TO_SYSTEM_STRUCTURE_FILE
+ )
+ sim_config = SimulationConfiguration(
+ system_structure=PATH_TO_SYSTEM_STRUCTURE_FILE,
+ path_to_fmu=PATH_TO_FMU_DIR
+ )
+ system_struct = OspSystemStructure(
+ xml_source=PATH_TO_SYSTEM_STRUCTURE_FILE
+ )
+ assert sim_config.system_structure.to_xml_str() == system_struct.to_xml_str()
+ assert len(sim_config.components) == len(sim_config.system_structure.Simulators)
+ num_initial_values = sum(map(
+ lambda x: len(x.InitialValues) if x.InitialValues else 0,
+ system_struct.Simulators
+ ))
+ assert len(sim_config.initial_values) == num_initial_values
+ with pytest.raises(AssertionError):
+ sim_config.scenario = ''
+ with pytest.raises(AssertionError):
+ sim_config.logging_config = ''
+
+
+def test_simulation_configuration_get_fmu_rel_path():
+ sim_config = SimulationConfiguration()
+ for _ in range(3):
+ num_depth = random.randint(0, 5)
+ path_to_deploy = 'abc'
+ path_to_sys_struct = path_to_deploy
+ for _ in range(num_depth):
+ path_to_sys_struct = os.path.join(path_to_sys_struct, get_random_string())
+ fmu_rel_path = sim_config.get_fmu_rel_path(path_to_deploy, path_to_sys_struct)
+ num_depth_calculated = fmu_rel_path.count('../')
+ assert num_depth == num_depth_calculated
+ fmu_rel_path = sim_config.get_fmu_rel_path(path_to_sys_struct, path_to_deploy)
+ if len(fmu_rel_path) == 0:
+ assert path_to_deploy == path_to_sys_struct
+ else:
+ assert path_to_sys_struct == os.path.join(
+ path_to_deploy,
+ reduce(os.path.join, fmu_rel_path.split('/')[:-1])
+ )
+
+
+def test_simulation_configuration_deployment():
+ sim_config = SimulationConfiguration(
+ system_structure=PATH_TO_SYSTEM_STRUCTURE_FILE,
+ path_to_fmu=PATH_TO_FMU_DIR
+ )
+ path_to_deploy = sim_config.prepare_temp_dir_for_simulation()
+ assert os.path.isdir(path_to_deploy)
+
+ # Test deploy_files_for_simulation method
+ path_to_system_structure = sim_config.deploy_files_for_simulation(
+ path_to_deploy=path_to_deploy,
+ rel_path_to_system_structure='system_structure'
+ )
+ assert os.path.isdir(path_to_system_structure)
+ assert os.path.join(path_to_deploy, 'system_structure') == path_to_system_structure
+ assert os.path.isfile(os.path.join(path_to_system_structure, 'OspSystemStructure.xml'))
+ assert all(
+ list(map(
+ lambda x: os.path.isfile(os.path.join(path_to_deploy, os.path.basename(x.fmu.fmu_file))),
+ sim_config.components
+ )))
+
+ # deploy again and see if the previous directory has been deleted.
+ path_to_deploy_again = sim_config.prepare_temp_dir_for_simulation()
+ sim_config.deploy_files_for_simulation(path_to_deploy_again)
+ assert path_to_deploy != path_to_deploy_again
+ assert not os.path.isdir(path_to_deploy)
+ assert os.path.isdir(path_to_deploy_again)
+
+
+def test_simulation_configuration_run():
+
+ simulation_end_time = 10 + random.random() * 90
+
+ sim_config = SimulationConfiguration(
+ system_structure=PATH_TO_SYSTEM_STRUCTURE_FILE,
+ path_to_fmu=PATH_TO_FMU_DIR,
+ )
+
+ scenario = OSPScenario(
+ name='test_scenario',
+ end=0.5*simulation_end_time
+ )
+
+ scenario.add_event(OSPEvent(
+ time=0.5 * scenario.end,
+ model=sim_config.components[0].name,
+ variable=random.choice(sim_config.components[0].fmu.parameters).get('name'),
+ action=OSPEvent.OVERRIDE,
+ value=random.random() * 10
+ ))
+ sim_config.scenario = scenario
+
+ assert type(scenario.events[0].variable) is str
+
+ sim_config.logging_config = OspLoggingConfiguration(
+ simulators=list(
+ map(
+ lambda comp: OspSimulatorForLogging(
+ name=comp.name,
+ variables=[
+ OspVariableForLogging(name=variable.get('name'))
+ for variable in random.choices(comp.fmu.outputs, k=4)
+ ]
+ ),
+ sim_config.components
+ )
+ )
+ )
+
+ output = sim_config.run_simulation(
+ duration=simulation_end_time,
+ rel_path_to_sys_struct="system_structure",
+ logging_level=LoggingLevel.info
+ )
+ assert os.path.isdir(output.output_file_path)
+ path_to_scenario_file = os.path.join(output.output_file_path, scenario.get_file_name())
+ assert os.path.isfile(path_to_scenario_file)
+ path_to_logging_config_file = os.path.join(output.output_file_path, 'LogConfig.xml')
+ assert os.path.isfile(path_to_logging_config_file)
+ output_files = [file_name for file_name in os.listdir(output.output_file_path) if file_name.endswith('.csv')]
+ assert len(output_files) == len(sim_config.logging_config.simulators)
+ assert len(output.result) == len(sim_config.logging_config.simulators)
+ assert simulation_end_time == pytest.approx(
+ output.result[sim_config.components[0].name]['Time'].values[-1], rel=1e-3
+ )
+
+ print(output.result)