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)