# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_Simulation.ipynb.

# %% auto 0
__all__ = ['logger', 'ch', 'formatter', 'convert_value_to_osp_type', 'clean_header', 'SimulationConfigurationError',
           'VariableType', 'SignalType', 'Causality', 'VariableEndpoint', 'is_osp_variable_group',
           'get_variables_from_osp_variable_group', 'Component', 'InitialValues', 'SimulationOutput', 'SignalEndpoint',
           'Function', 'SimulationConfiguration']

# %% ../nbs/03_Simulation.ipynb 4
import glob
import logging
import os
import shutil
import uuid
from dataclasses import dataclass, field
from enum import Enum
from logging import Logger
from sys import platform
from typing import Union, List, Type, Optional, Any

from pyOSPParser.logging_configuration import (
    OspLoggingConfiguration, OspSimulatorForLogging
)
from pyOSPParser.model_description import (
    OspTorqueType,
    OspGenericType,
    OspForceType,
    OspVoltageType,
    OspHydraulicPowerPortType,
    OspPressureType,
    OspLinearVelocityType,
    OspAngularVelocityType,
    OspCurrentType,
    OspVolumeFlowRateType,
    OspLinearDisplacementType,
    OspAngularDisplacementType,
    OspChargeType,
    OspVolumeType,
    OspLinearMechanicalPortType,
    OspAngularMechanicalPortType,
    OspElectromagneticPortType,
    OspHydraulicPortType,
    OspLinearMechanicalQuasiPortType,
    OspAngularMechanicalQuasiPortType,
    OspElectromagneticQuasiPortType,
    OspHydraulicQuasiPortType,
    OspLinearMechanicalPowerPortType,
    OspAngularMechanicalPowerPortType,
    OspElectromagneticPowerPortType, OspVariableType
)
from pyOSPParser.scenario import OSPScenario, OSPEvent
from pyOSPParser.system_configuration import (
    OspSystemStructure,
    OspSimulator,
    OspVariableEndpoint,
    OspVariableConnection,
    OspVariableGroupConnection,
    OspInitialValue,
    OspSignalEndpoint,
    OspSignalConnection,
    OspSignalGroupConnection,
    FunctionType,
    OspLinearTransformationFunction,
    OspSumFunction,
    OspVectorSumFunction,
    OspReal,
    OspInteger,
    OspString,
    OspBoolean
)
import pyOSPParser.system_configuration as osp_parser_sys

from .fmu import FMU
from .fmu_proxy import DistributedSimulationProxyServer
from pycosim.osp_command_line import (
    run_cosimulation, LoggingLevel, SimulationResult, deploy_files_for_cosimulation
)

# %% ../nbs/03_Simulation.ipynb 6
# Define logger
logger: Logger = logging.getLogger('__name__')
logger.setLevel(logging.INFO)

ch = logging.StreamHandler()
ch.setLevel(logging.INFO)

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)

logger.addHandler(ch)


def convert_value_to_osp_type(
        value: Union[float, int, bool, str],
        type_var: osp_parser_sys.VariableType
) -> Union[OspReal, OspInteger, OspString, OspBoolean]:
    """Convert a generic python variable type to OspVariable type used in the initial values

    Args:
        value: Value of the variable
        type_var(Optional): Specify a type of the variable if one wants to force or make sure
            that the type is defined as intended.
    """
    if type_var == osp_parser_sys.VariableType.Real:
        if not isinstance(value, float) and not isinstance(value, int):
            raise TypeError(f'Expected a float or int, got {type(value)}')
        return OspReal(value=float(value))
    if type_var == osp_parser_sys.VariableType.Boolean:
        if not isinstance(value, bool):
            raise TypeError(f'Expected a bool, got {type(value)}')
        return OspBoolean(value=bool(value))
    if type_var == osp_parser_sys.VariableType.Integer:
        if not isinstance(value, int) and not isinstance(value, float):
            raise TypeError(f'Expected an int or float, got {type(value)}')
        return OspInteger(value=int(value))
    if type_var == osp_parser_sys.VariableType.String:
        if not isinstance(value, str):
            raise TypeError(f'Expected a str, got {type(value)}')
        return OspString(value=str(value))
    raise ValueError(f'Value {value} is not a valid type')


def clean_header(header: str):
    """Clean up the header for the output files of cosim."""
    if '[' in header:
        return header[0:header.rindex('[') - 1]
    return header


class SimulationConfigurationError(Exception):
    """Exception for simulation configuration error"""


# %% ../nbs/03_Simulation.ipynb 8
class VariableType(Enum):
    """Variable type"""
    VARIABLE = "Variable"
    VARIABLE_GROUP = "Variable Group"


class SignalType(Enum):
    """Signal type"""
    SIGNAL = "Signal"
    SIGNAL_GROUP = "Signal Group"


class Causality(Enum):
    """Causality used for variable connection"""
    INPUT = "input"
    OUTPUT = "output"
    INDEFINITE = "indefinite"


@dataclass
class VariableEndpoint:
    """Variable endpoint class"""
    name: str
    variable_type: VariableType
    causality: Causality
    connected: bool = False

    def get_osp_variable_endpoint(self, component_name: str):
        """Get the OSP variable endpoint"""
        return OspVariableEndpoint(
            simulator=component_name,
            name=self.name
        )


def is_osp_variable_group(osp_variable_group: Any):
    """Checks if the variable group is an OSP variable group"""
    try:
        return osp_variable_group.__class__.__name__ in [
            'OspGenericType', 'OspForceType', 'OspTorqueType', 'OspVoltageType',
            'OspPressureType', 'OspLinearVelocityType', 'OspAngularVelocityType', 'OspCurrentType',
            'OspVolumeFlowRateType', 'OspLinearDisplacementType', 'OspAngularDisplacementType',
            'OspChargeType', 'OspVolumeType', 'OspLinearMechanicalPortType',
            'OspAngularMechanicalPortType', 'OspElectromagneticPortType',
            'OspHydraulicPortType', 'OspLinearMechanicalQuasiPortType',
            'OspAngularMechanicalQuasiPortType', 'OspElectromagneticQuasiPortType',
            'OspHydraulicQuasiPortType', 'OspLinearMechanicalPowerPortType',
            'OspAngularMechanicalPowerPortType', 'OspElectromagneticPowerPortType',
            'OspHydraulicPowerPortType'
        ]
    except AttributeError:
        return False

def get_variables_from_osp_variable_group(osp_variable_group: Union[
    OspGenericType, OspForceType, OspTorqueType, OspVoltageType,
    OspPressureType, OspLinearVelocityType, OspAngularVelocityType, OspCurrentType,
    OspVolumeFlowRateType, OspLinearDisplacementType, OspAngularDisplacementType,
    OspChargeType, OspVolumeType, OspLinearMechanicalPortType,
    OspAngularMechanicalPortType, OspElectromagneticPortType,
    OspHydraulicPortType, OspLinearMechanicalQuasiPortType,
    OspAngularMechanicalQuasiPortType, OspElectromagneticQuasiPortType,
    OspHydraulicQuasiPortType, OspLinearMechanicalPowerPortType,
    OspAngularMechanicalPowerPortType, OspElectromagneticPowerPortType,
    OspHydraulicPowerPortType
], number_loops: int = 0) -> List[str]:
    """Get variables from an OSP variable group"""
    if number_loops > 5:
        print(osp_variable_group.__dict__)
        raise TypeError("Too many loops have run.")
    variables = []
    for _, value in osp_variable_group.__dict__.items():
        if isinstance(value, list):
            if isinstance(value[0], OspVariableType):
                variables.extend([var.ref for var in value])
        elif is_osp_variable_group(value):
            variables.extend(get_variables_from_osp_variable_group(value))
    return variables


@dataclass
class Component:
    """Component used in SimulationConfiguration"""
    name: str
    fmu: Optional[FMU] = None
    distributed_simulation_setting: DistributedSimulationProxyServer = None
    variable_end_points: List[VariableEndpoint] = field(default_factory=list)
    _inputs_used_as_variable_end_points: List[str] = field(default_factory=list)

    @property
    def runs_on_network(self) -> bool:
        """Returns True if the component runs on a network"""
        return self.distributed_simulation_setting is not None

    def get_unconnected_variable_endpoint_with_variable(
        self, variable_name: str
    ) -> VariableEndpoint:
        """Returns a variable endpoint with the given variable name that is not connected"""
        try:
            variable_endpoint = self.get_variable_endpoint(variable_name)
        except ValueError:
            variable_endpoint = self._get_variable_endpoint_from_variable_group(
                variable_name
            )
        if variable_endpoint.connected:
            raise ValueError(f'Variable {variable_name} is already used as an '
                             f'input and it is used in another connection.')
        return variable_endpoint

    def add_variable_endpoint(self, variable_name: str) -> VariableEndpoint:
        """Add a variable endpoint. If the variable is already added, it will return the existing
        variable endpoint if it is not connected. Otherwise, it will raise an error."""
        variables_for_variable_group = []
        # Check if the causality of the endpoint is valid
        variable_type = VariableType.VARIABLE
        if variable_name in self.fmu.get_input_names():
            causality = Causality.INPUT
        elif variable_name in self.fmu.get_output_names():
            causality = Causality.OUTPUT
        elif variable_name in self.fmu.get_variable_group_names():
            causality = Causality.INDEFINITE
            variable_type = VariableType.VARIABLE_GROUP
            osp_variable_group = next(filter(
                lambda var_group: var_group.name == variable_name,
                self.fmu.get_variable_groups()
            ))
            variables_for_variable_group = get_variables_from_osp_variable_group(osp_variable_group)
        else:
            raise ValueError(f'Variable {variable_name} is not defined in the FMU.')
        # Check if there exists a variable endpoint with the same name and input causality
        if causality == Causality.INPUT:
            if variable_name in self._inputs_used_as_variable_end_points:
                raise ValueError(f'Variable {variable_name} is already used as an input.')
            self._inputs_used_as_variable_end_points.append(variable_name)
        # Check if the input used in the variable group is not already used as an input
        if causality == Causality.INDEFINITE:
            for sub_variable in variables_for_variable_group:
                if sub_variable in self.fmu.get_input_names():
                    if sub_variable in self._inputs_used_as_variable_end_points:
                        raise ValueError(f'Variable {variable_name} is already used as an input.')
                    self._inputs_used_as_variable_end_points.append(sub_variable)
        # Add the variable endpoint
        variable_endpoint = VariableEndpoint(
            name=variable_name,
            variable_type=variable_type,
            causality=causality
        )
        self.variable_end_points.append(variable_endpoint)
        return variable_endpoint

    def delete_variable_endpoint(self, variable_name: str) -> VariableEndpoint:
        """Delete variable endpoint"""
        variable_endpoint_to_delete = self.get_variable_endpoint(variable_name)
        if variable_endpoint_to_delete.variable_type == VariableType.VARIABLE_GROUP:
            osp_var_group = next(filter(
                lambda var_group: var_group.name == variable_name,
                self.fmu.get_variable_groups()
            ))
            variables_to_delete = get_variables_from_osp_variable_group(osp_var_group)
            for variable in variables_to_delete:
                if variable in self._inputs_used_as_variable_end_points:
                    self._inputs_used_as_variable_end_points.remove(variable)
        else:
            if variable_name in self._inputs_used_as_variable_end_points:
                self._inputs_used_as_variable_end_points.remove(variable_name)
        self.variable_end_points.remove(variable_endpoint_to_delete)
        return variable_endpoint_to_delete

    def get_variable_endpoint(self, variable_name: str) -> VariableEndpoint:
        """Get variable endpoint"""
        try:
            return next(filter(
                lambda var_endpoint: var_endpoint.name == variable_name,
                self.variable_end_points
            ))
        except StopIteration as exc:
            raise ValueError(f'Variable {variable_name} is not defined in the component.') from exc

    def get_variable_endpoint_not_connected(self, variable_name: str) -> VariableEndpoint:
        """Get variable endpoint not connected"""
        try:
            return next(filter(
                lambda var_endpoint: var_endpoint.name == variable_name and not var_endpoint.connected,
                self.variable_end_points
            ))
        except StopIteration as exc:
            raise ValueError(f'There is no unconnected variable endpoint with '
                             f'the name ({variable_name})in the component.') from exc

    def _get_variable_endpoint_from_variable_group(self, variable_name: str) -> VariableEndpoint:
        """Get variable endpoint from variable group"""
        variable_endpoints_as_variable_group = list(filter(
            lambda var_endpoint: var_endpoint.variable_type == VariableType.VARIABLE_GROUP,
            self.variable_end_points
        ))
        for variable_endpoint in variable_endpoints_as_variable_group:
            osp_variable_group = next(filter(
                lambda var_group: var_group.name == variable_endpoint.name,
                self.fmu.get_variable_groups()
            ))
            variables_for_variable_group = get_variables_from_osp_variable_group(osp_variable_group)
            if variable_name in variables_for_variable_group:
                return variable_endpoint
        raise ValueError(f'Variable {variable_name} cannot be found in the variable groups')


@dataclass
class InitialValues:
    """InitialValue used in SimulationConfiguration"""
    component: str
    variable: str
    value: Union[float, int, bool, str]
    type_var: osp_parser_sys.VariableType


@dataclass
class SimulationOutput(SimulationResult):
    """Return type of run_simulation"""
    output_file_path: str = None


@dataclass
class SignalEndpoint:
    """SignalEndpoint used in SimulationConfiguration"""
    signal_name: str
    signal_type: SignalType
    causality: Causality
    connected: bool = False

    def get_osp_signal_endpoint(self, function_name: str) -> OspSignalEndpoint:
        """Get OSP SignalEndpoint"""
        return OspSignalEndpoint(
            function=function_name,
            name=self.signal_name,
        )


@dataclass
class Function:
    """Function used in SimulationConfiguration"""
    type: FunctionType
    name: str
    factor: float = None
    offset: float = None
    inputCount: int = None
    dimension: int = None
    signal_endpoints: List[SignalEndpoint] = field(default_factory=list)
    _inputs_used_as_signal_endpoints: List[str] = field(default_factory=list)

    def add_signal_endpoint(self, signal_name: str, signal_type: SignalType ,causality: Causality) -> SignalEndpoint:
        """Add a variable endpoint"""
        if causality == Causality.INPUT:
            if signal_name in self._inputs_used_as_signal_endpoints:
                raise ValueError(f'Signal {signal_name} is already used as an input.')
            self._inputs_used_as_signal_endpoints.append(signal_name)
        if signal_type == SignalType.SIGNAL_GROUP:
            if self.inputCount < 2:
                raise ValueError(
                    f'Function {self.name} should have inputCount > 2 to add signal group.'
                )
        signal_endpoint = SignalEndpoint(
            signal_name=signal_name,
            signal_type=signal_type,
            causality=causality
        )
        self.signal_endpoints.append(signal_endpoint)
        return signal_endpoint

    def delete_signal_endpoint(self, signal_name: str) -> SignalEndpoint:
        """Delete variable endpoint"""
        signal_endpoint_to_remove = self.get_signal_endpoint(signal_name)
        try:
            self._inputs_used_as_signal_endpoints.remove(signal_name)
        except ValueError:
            pass
        self.signal_endpoints.remove(signal_endpoint_to_remove)
        return signal_endpoint_to_remove

    def get_signal_endpoint(self, signal_name: str) -> SignalEndpoint:
        """Get variable endpoint"""
        try:
            return next(filter(
                lambda sig_endpoint: sig_endpoint.signal_name == signal_name,
                self.signal_endpoints
            ))
        except StopIteration as exc:
            raise ValueError(f'Variable {signal_name} is not defined in the component.') from exc

    def get_signal_endpoint_not_connected(self, signal_name: str) -> SignalEndpoint:
        """Get variable endpoint not connected"""
        try:
            return next(filter(
                lambda sig_endpoint: sig_endpoint.signal_name == signal_name
                and not sig_endpoint.connected,
                self.signal_endpoints
            ))
        except StopIteration as exc:
            raise ValueError(f'There is no unconnected signal endpoint with '
                             f'the name ({signal_name})in the component.') from exc


class SimulationConfiguration:
    """Class for running simulation"""
    _scenario: OSPScenario = None
    _logging_config: OspLoggingConfiguration = None
    _current_sim_path: str = None

    def __init__(
            self,
            path_to_system: str = None,
            system_structure: Union[str, OspSystemStructure] = None,
            path_to_fmu: str = "",
            components: List[Component] = None,
            initial_values: List[InitialValues] = None,
            scenario: OSPScenario = None,
            logging_config: OspLoggingConfiguration = None,
    ):
        """Constructor for SimulationConfiguration class
        If one wants to create the system structure from scratch, one can use the
        OspSystemStructure class. Otherwise, one can use the path to the xml file.

        Args:
            system_structure(optional): A source for the system structure,
                either string content of the XML file or path to the file or
                an instance of OspSystemStructure.
                Must be given together with the path_to_fmu argument..
            path_to_fmu(optional): A path to the FMUs for the given system structure.
                Only relevant when system_structure is given.
                If not given, the FMU path is taken from the 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 path_to_system is not None:
            # First check if the system structure is found in the path
            system_structure = os.path.join(path_to_system, 'OspSystemStructure.xml')
            if not os.path.isfile(system_structure):
                raise FileNotFoundError(f'File {system_structure} does not exist.')
            # Then check if the FMU is found in the path
            path_to_fmu = glob.glob(os.path.join(path_to_system, "**/*.fmu"), recursive=True)
            if len(path_to_fmu) == 0:
                raise FileNotFoundError(f'No FMU found in {path_to_system}.')
            path_to_fmu = os.path.dirname(path_to_fmu[0])
        if system_structure is not None:
            self.components = []
            self.initial_values = []
            self.functions = []
            self._load_system_from_file(system_structure, path_to_fmu)
        else:
            self.system_structure = OspSystemStructure()
            self.components = []
            self.initial_values = []
            self.functions = []
            if components:
                for comp in components:
                    assert isinstance(comp, Component)
                self.components = components
            if initial_values:
                for init_value in initial_values:
                    assert isinstance(init_value, 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):
        """scenario"""
        return self._scenario

    @scenario.setter
    def scenario(self, value):
        assert isinstance(value, OSPScenario)
        self._scenario = value

    @property
    def logging_config(self):
        """logging configuration"""
        return self._logging_config

    @logging_config.setter
    def logging_config(self, value):
        assert isinstance(value, OspLoggingConfiguration)
        self._logging_config = value

    @property
    def current_simulation_path(self):
        """get current simulation path"""
        return self._current_sim_path

    @staticmethod
    def prepare_temp_dir_for_simulation() -> str:
        """create a temporatry directory for the simulation"""
        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):
        """Get relative path of fmus from the system structure file"""
        if path_to_deploy.endswith(os.sep):
            path_to_deploy = path_to_deploy[:path_to_deploy.rfind(os.sep)]
        if path_to_sys_struct.endswith(os.sep):
            path_to_sys_struct = path_to_sys_struct[:path_to_sys_struct.rfind(os.sep)]
        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}/'
            return ''

        rel_path = path_to_sys_struct[len(path_to_deploy):]
        depth = rel_path.count(os.sep)
        return '../' * depth

    def _load_system_from_file(self, system_structure: str, path_to_fmu: str):
        """Import system structure from file"""
        self.system_structure = OspSystemStructure(xml_source=system_structure)
        # Add components
        for simulator in self.system_structure.Simulators:
            if simulator.fmu_rel_path == "proxy-fmy://":
                raise TypeError(
                    "OspSystemStructure is outdated for describing the proxy server. "
                    "Please read the documentation for the new format. "
                    "(https://open-simulation-platform.github.io/libcosim/distributed) "
                )
            if simulator.fmu_rel_path == "proxyfmu://":
                proxy_server = DistributedSimulationProxyServer(
                    source_text=simulator.source
                )
                if proxy_server.endpoint.is_local_host:
                    file_path = os.path.join(path_to_fmu, proxy_server.file_path_fmu)
                else:
                    file_path = proxy_server.file_path_fmu
                fmu = FMU(
                    fmu_file=file_path,
                    runs_on_proxy_server=True,
                    network_endpoint=proxy_server.endpoint,
                )
            else:
                fmu = FMU(fmu_file=os.path.join(path_to_fmu, simulator.source))
            self.components.append(Component(
                name=simulator.name,
                fmu=fmu
            ))
            if simulator.InitialValues:
                self.initial_values.extend([InitialValues(
                    component=simulator.name,
                    variable=initial_value.variable,
                    value=initial_value.value.value,
                    type_var=initial_value.type_var
                ) for initial_value in simulator.InitialValues])

        # Add functions
        if self.system_structure.Functions is not None:
            if self.system_structure.Functions.LinearTransformation is not None:
                for linear_transformation in self.system_structure.Functions.LinearTransformation:
                    self.functions.append(Function(
                        name=linear_transformation.name,
                        type=FunctionType.LINEAR_TRANSFORMATION,
                        factor=linear_transformation.factor,
                        offset=linear_transformation.offset
                    ))
            if self.system_structure.Functions.Sum is not None:
                for sum_function in self.system_structure.Functions.Sum:
                    self.functions.append(Function(
                        name=sum_function.name,
                        type=FunctionType.Sum,
                        inputCount=sum_function.inputCount
                    ))
            if self.system_structure.Functions.VectorSum is not None:
                for vector_sum in self.system_structure.Functions.VectorSum:
                    self.functions.append(Function(
                        name=vector_sum.name,
                        type=FunctionType.VectorSum,
                        inputCount=vector_sum.inputCount,
                        dimension=vector_sum.dimension
                    ))

        # Add variable endpoints
        if self.system_structure.Connections is not None:
            if self.system_structure.Connections.SignalConnection is not None:
                for connection in self.system_structure.Connections.SignalConnection:
                    component = self.get_component_by_name(connection.Variable.simulator)
                    var_endpoint = component.add_variable_endpoint(
                        variable_name=connection.Variable.name
                    )
                    signal_causality = Causality.OUTPUT
                    if var_endpoint.causality == Causality.OUTPUT:
                        signal_causality = Causality.INPUT
                    function = self.get_function_by_name(connection.Signal.function)
                    function.add_signal_endpoint(
                        signal_name=connection.Signal.name,
                        signal_type=SignalType.SIGNAL,
                        causality=signal_causality,
                    )
            if self.system_structure.Connections.SignalGroupConnection is not None:
                for connection in self.system_structure.Connections.SignalGroupConnection:
                    component = self.get_component_by_name(connection.VariableGroup.simulator)
                    var_endpoint = component.add_variable_endpoint(
                        variable_name=connection.VariableGroup.name
                    )
                    signal_causality = Causality.OUTPUT
                    if var_endpoint.causality == Causality.OUTPUT:
                        signal_causality = Causality.INPUT
                    function = self.get_function_by_name(connection.SignalGroup.function)
                    function.add_signal_endpoint(
                        signal_name=connection.SignalGroup.name,
                        signal_type=SignalType.SIGNAL_GROUP,
                        causality=signal_causality,
                    )
            if self.system_structure.Connections.VariableConnection is not None:
                for connection in self.system_structure.Connections.VariableConnection:
                    for variable in connection.Variable:
                        component = self.get_component_by_name(variable.simulator)
                        component.add_variable_endpoint(variable_name=variable.name)
            if self.system_structure.Connections.VariableGroupConnection is not None:
                for connection in self.system_structure.Connections.VariableGroupConnection:
                    for variable_group in connection.VariableGroup:
                        component = self.get_component_by_name(variable_group.simulator)
                        component.add_variable_endpoint(variable_name=variable_group.name)

        if len(self.initial_values) == 0:
            # noinspection PyTypeChecker
            self.initial_values = None

    def import_system(
        self, system_config,
        add_logging: bool = False,
        add_scenario: bool = False
    ):
        """Import system structure from SimulationConfiguration"""
        # Add components
        for component in system_config.components:
            osp_component = system_config.system_structure.get_component_by_name(component.name)
            self.add_component(
                name=component.name,
                fmu=component.fmu,
                stepSize=osp_component.stepSize,
            )
        # Add functions
        for function in system_config.functions:
            osp_function = self.system_structure.get_function_by_name(function.name)
            if isinstance(osp_function, OspLinearTransformationFunction):
                self.add_function(
                    function_name=function.name,
                    function_type=FunctionType.LinearTransformation,
                    factor=osp_function.factor,
                    offset=osp_function.offset,
                )
            if isinstance(osp_function, OspSumFunction):
                self.add_function(
                    function_name=function.name,
                    function_type=FunctionType.Sum,
                    inputCount=osp_function.inputCount,
                )
            if isinstance(osp_function, OspVectorSumFunction):
                self.add_function(
                    function_name=function.name,
                    function_type=FunctionType.VectorSum,
                    inputCount=osp_function.inputCount,
                    dimension=osp_function.dimension,
                )
        # Add initial values
        for initial_value in system_config.initial_values:
            self.add_update_initial_value(
                component_name=initial_value.component,
                variable=initial_value.variable,
                value=initial_value.value,
            )
        # Add connections
        if system_config.system_structure.Connections.SignalConnection is not None:
            for connection in system_config.system_structure.Connections.SignalConnection:
                osp_variable = connection.Variable
                component = system_config.get_component_by_name(osp_variable.simulator)
                variable_endpoint = component.get_variable_endpoint(osp_variable.name)
                if variable_endpoint.causality == Causality.OUTPUT:
                    source = osp_variable
                    target = connection.Signal
                else:
                    source = connection.Signal
                    target = osp_variable
                self.add_connection(source=source, target=target, group=False)
        if system_config.system_structure.Connections.VariableConnection is not None:
            for connection in system_config.system_structure.Connections.VariableConnection:
                osp_variable1 = connection.Variable[0]
                osp_variable2 = connection.Variable[1]
                component1 = system_config.get_component_by_name(osp_variable1.simulator)
                variable_endpoint1 = component1.get_variable_endpoint(osp_variable1.name)
                if variable_endpoint1.causality == Causality.OUTPUT:
                    source = osp_variable1
                    target = osp_variable2
                else:
                    source = osp_variable2
                    target = osp_variable1
                self.add_connection(source=source, target=target, group=False)
        if system_config.system_structure.Connections.VariableGroupConnection is not None:
            for connection in system_config.system_structure.Connections.VariableGroupConnection:
                osp_variable1 = connection.VariableGroup[0]
                osp_variable2 = connection.VariableGroup[1]
                component1 = system_config.get_component_by_name(osp_variable1.simulator)
                variable_endpoint1 = component1.get_variable_endpoint(osp_variable1.name)
                if variable_endpoint1.causality == Causality.OUTPUT:
                    source = osp_variable1
                    target = osp_variable2
                else:
                    source = osp_variable2
                    target = osp_variable1
                self.add_connection(source=source, target=target, group=True)
        if system_config.system_structure.Connections.SignalGroupConnection is not None:
            for connection in system_config.system_structure.Connections.SignalGroupConnection:
                osp_variable = connection.VariableGroup
                osp_signal = connection.SignalGroup
                component = system_config.get_component_by_name(osp_variable.simulator)
                variable_endpoint = component.get_variable_endpoint(osp_variable.name)
                if variable_endpoint.causality == Causality.OUTPUT:
                    source = osp_variable
                    target = osp_signal
                else:
                    source = osp_signal
                    target = osp_variable
                self.add_connection(source=source, target=target, group=True)
        # Add logging
        if add_logging:
            for each_simulator in system_config.logging_config.simulators:
                for each_variable in each_simulator.variables:
                    self.add_logging_variable(
                        component_name=each_simulator.name,
                        variable_name=each_variable.name,
                        decimation_factor=each_simulator.decimation_factor,
                    )
        # Add scenario
        if add_scenario:
            for each_event in system_config.scenario.events:
                self.add_event(
                    time=each_event.time,
                    component=each_event.model,
                    variable=each_event.variable,
                    value=each_event.value,
                    action=each_event.action,
                )

    def import_system_from_file(self, system_structure: str, path_to_fmu: str):
        """Import system structure from system_structure.xml"""
        system_config_to_import = SimulationConfiguration(
            system_structure=system_structure, path_to_fmu=path_to_fmu
        )
        self.import_system(system_config_to_import)

    @property
    def fmus(self) -> List[FMU]:
        """Return list of FMUs"""
        return list(set(component.fmu for component in self.components))

    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
        if not os.path.isdir(path_to_deploy):
            os.makedirs(path_to_deploy)

        # Set relative path for fmus
        path_to_system_structure = os.path.join(path_to_deploy, rel_path_to_system_structure)
        fmu_rel_path = self.get_fmu_rel_path(
            path_to_deploy=path_to_deploy, path_to_sys_struct=path_to_system_structure
        )
        for component in self.system_structure.Simulators:
            if component.fmu_rel_path != "proxyfmu://":
                component.fmu_rel_path = fmu_rel_path
            else:
                if component.source.startswith("localhost") or component.source.startswith(
                        "127.0.0.1"):
                    address_query, file_name = component.source.split("=")
                    component.source = f"{address_query}={fmu_rel_path}{file_name}"

        return deploy_files_for_cosimulation(
            path_to_deploy=path_to_deploy,
            fmus=self.fmus,
            system_structure=self.system_structure,
            rel_path_to_system_structure=rel_path_to_system_structure,
            logging_config=self.logging_config,
            scenario=self.scenario,
        )

    def run_simulation(
            self,
            duration: float,
            rel_path_to_sys_struct: str = '',
            time_out_s: int = 60,
            logging_level: LoggingLevel = LoggingLevel.warning
    ):
        """Runs a simulation"""
        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,
        )
        sim_result = run_cosimulation(
            path_to_system_structure=path_to_sys_struct,
            output_file_path=path_to_sys_struct,
            scenario_name=self.scenario.name if self.scenario is not None else None,
            duration=duration,
            logging_level=logging_level,
            logging_stream=True,
            time_out_s=time_out_s,
        )

        return SimulationOutput(
            **sim_result.__dict__,
            output_file_path=path_to_sys_struct
        )

    def get_component_names(self) -> List[str]:
        """Get component names"""
        return [component.name for component in self.components]

    def add_component(
            self,
            name: str,
            fmu: Optional[FMU] = None,
            stepSize: float = None,
            rel_path_to_fmu: str = '',
    ) -> Component:
        """Add a component to the system structure

        Args:
            name: Name of the component
            fmu(Optional): The model for the component given as FMU instance.
                If it is a network fmu from a remote server, no need to be provided
            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 self.get_component_names():
            # 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()

            # Get source string depending on if the fmu is a network fmu or not
            source = fmu.source
            if fmu.is_remote_network_fmu:
                source = source.replace(
                    os.path.basename(fmu.fmu_file),
                    os.path.join(rel_path_to_fmu, os.path.basename(fmu.fmu_file))
                )
            if fmu.runs_on_proxy_server:
                rel_path_to_fmu = "proxyfmu://"

            # Add the component to the system structure
            self.system_structure.add_simulator(OspSimulator(
                name=name,
                source=os.path.basename(source),
                stepSize=stepSize,
                fmu_rel_path=rel_path_to_fmu
            ))
            return component

        raise TypeError('The name duplicates with the existing components.')

    def delete_component(self, component_name: str) -> bool:
        """Delete a component in the system"""
        if component_name not in self.get_component_names():
            raise TypeError('No component is found with ')
        # Delete from its attributes
        component = self.get_component_by_name(component_name)
        self.components.pop(self.components.index(component))

        # Delete from the system structure attribute
        self.system_structure.delete_simulator(component_name)

        return True

    def change_component_name(self, old_name: str, new_name: str) -> bool:
        """Change the name of a component"""
        if old_name not in self.get_component_names():
            raise TypeError('No component is found with ')
        if new_name in self.get_component_names():
            raise TypeError('The new name duplicates with the existing components.')
        # Change the name of the component
        component = self.get_component_by_name(old_name)
        component.name = new_name

        # Change the name of the component in the system structure
        osp_simulator = self.system_structure.get_component_by_name(old_name)
        osp_simulator.name = new_name

        # Change the name of the component in the scenario
        if self.scenario is not None:
            for event in self.scenario.events:
                if event.model == old_name:
                    event.model = new_name

        # Change the name of the component in the logging configuration
        if self.logging_config is not None:
            for simulator in self.logging_config.simulators:
                if simulator.name == old_name:
                    simulator.name = new_name

        # Change the name of the component in the initial values
        if self.initial_values is not None:
            for initial_value in self.initial_values:
                if initial_value.component == old_name:
                    initial_value.component = new_name

        # Change the name of the component in the existing connections
        if self.system_structure.Connections is not None:
            if self.system_structure.Connections.SignalConnection is not None:
                for connection in self.system_structure.Connections.SignalConnection:
                    if connection.Variable.simulator == old_name:
                        connection.Variable.simulator = new_name
            if self.system_structure.Connections.SignalGroupConnection is not None:
                for connection in self.system_structure.Connections.SignalGroupConnection:
                    if connection.VariableGroup.simulator == old_name:
                        connection.VariableGroup.simulator = new_name
            if self.system_structure.Connections.VariableConnection is not None:
                for connection in self.system_structure.Connections.VariableConnection:
                    for variable in connection.Variable:
                        if variable.simulator == old_name:
                            variable.simulator = new_name
            if self.system_structure.Connections.VariableGroupConnection is not None:
                for connection in self.system_structure.Connections.VariableGroupConnection:
                    for variable_group in connection.VariableGroup:
                        if variable_group.simulator == old_name:
                            variable_group.simulator = new_name

        return True

    def get_variable_endpoints_of_component_for_variable_connection(
        self,
        component_name: str,
        causality: Causality = None
    ) -> List[OspVariableEndpoint]:
        """Returns variable endpoints used for variable connections

        Args:
            component_name
            causality(Optional): Indicates if the endpoints are input or output.
        """
        try:
            endpoints = self.system_structure.get_all_endpoints_for_component(component_name)
        except TypeError:
            return []
        if causality == Causality.INPUT:
            target_endpoint = []
            for endpoint in endpoints:
                component = self.get_component_by_name(endpoint.simulator)
                if endpoint.name in component.fmu.get_input_names():
                    target_endpoint.append(endpoint)
            return target_endpoint
        if causality == Causality.OUTPUT:
            source_endpoint = []
            for endpoint in endpoints:
                component = self.get_component_by_name(endpoint.simulator)
                if endpoint.name in component.fmu.get_output_names():
                    source_endpoint.append(endpoint)
            return source_endpoint
        return endpoints

    def get_connection_for_variable_endpoint(
            self,
            component_name: str,
            variable_endpoint: VariableEndpoint
    ) -> Union[
        OspVariableConnection,
        OspVariableGroupConnection,
        OspSignalConnection,
        OspSignalGroupConnection
    ]:
        """Returns a connection for a variable endpoint"""
        osp_variable_endpoint = variable_endpoint.get_osp_variable_endpoint(component_name)
        osp_variable_endpoint_dict_xml = osp_variable_endpoint.to_dict_xml()
        try:
            if variable_endpoint.variable_type == VariableType.VARIABLE:
                return next(filter(
                    lambda connection: osp_variable_endpoint_dict_xml
                    in [osp_var.to_dict_xml() for osp_var in connection.Variable],
                    self.system_structure.Connections.VariableConnection
                ))
            if variable_endpoint.variable_type == VariableType.VARIABLE_GROUP:
                return next(filter(
                    lambda connection: osp_variable_endpoint_dict_xml
                    in [osp_var.to_dict_xml() for osp_var in connection.VariableGroup],
                    self.system_structure.Connections.VariableGroupConnection
                ))
        except StopIteration:
            try:
                if variable_endpoint.variable_type == VariableType.VARIABLE:
                    return next(filter(
                        lambda connection: osp_variable_endpoint_dict_xml ==
                        connection.Variable.to_dict_xml(),
                        self.system_structure.Connections.SignalConnection
                    ))
                if variable_endpoint.variable_type == VariableType.VARIABLE_GROUP:
                    return next(filter(
                        lambda connection: osp_variable_endpoint_dict_xml ==
                        connection.VariableGroup.to_dict_xml(),
                        self.system_structure.Connections.SignalGroupConnection
                    ))
            except StopIteration as exc:
                raise TypeError(f'No connection is found for the variable endpoint '
                                f'({osp_variable_endpoint_dict_xml})') from exc

    def get_connection_for_signal_endpoint(
            self,
            function_name: str,
            signal_endpoint: SignalEndpoint
    ) -> Union[OspSignalConnection, OspSignalGroupConnection]:
        """Returns a connection for a variable endpoint"""
        osp_signal_endpoint = signal_endpoint.get_osp_signal_endpoint(function_name)
        osp_signal_endpoint_dict_xml = osp_signal_endpoint.to_dict_xml()
        try:
            if signal_endpoint.signal_type == SignalType.SIGNAL:
                return next(filter(
                    lambda connection: osp_signal_endpoint_dict_xml
                    == connection.Signal.to_dict_xml(),
                    self.system_structure.Connections.SignalConnection
                ))
            if signal_endpoint.signal_type == SignalType.SIGNAL_GROUP:
                return next(filter(
                    lambda connection: osp_signal_endpoint_dict_xml
                    == connection.SignalGroup.to_dict_xml(),
                    self.system_structure.Connections.SignalGroupConnection
                ))
        except StopIteration as exc:
            raise TypeError(f'No connection is found for the signal endpoint '
                            f'({osp_signal_endpoint_dict_xml})') from exc

    def add_variable_endpoint(self, component_name: str, variable_name: str) -> VariableEndpoint:
        """Add a variable endpoint to the system structure"""
        # find the component
        component = self.get_component_by_name(component_name)
        return component.add_variable_endpoint(variable_name)

    def delete_variable_endpoint(self, component_name: str, variable_name: str) -> VariableEndpoint:
        """Delete a variable endpoint from the system structure and all connections"""
        # find the component
        component = self.get_component_by_name(component_name)
        var_endpoint_to_delete = component.get_variable_endpoint(variable_name)
        osp_var_endpoint_to_delete = var_endpoint_to_delete.get_osp_variable_endpoint(
            component_name
        )
        # Check if the variable endpoint is connected to another variable endpoint. If so, delete
        # the connection and disconnect the other endpoint.
        if var_endpoint_to_delete.connected:
            connection = self.get_connection_for_variable_endpoint(
                component_name=component_name,
                variable_endpoint=var_endpoint_to_delete
            )
            if var_endpoint_to_delete.variable_type == VariableType.VARIABLE:
                if isinstance(connection, OspVariableConnection):
                    osp_endpoint_connected = next(filter(
                        lambda var_endpoint: var_endpoint.to_dict_xml()
                        != osp_var_endpoint_to_delete.to_dict_xml(),
                        connection.Variable
                    ))
                elif isinstance(connection, OspSignalConnection):
                    osp_endpoint_connected = connection.Signal
                else:
                    raise TypeError(f'Unknown connection type: {type(connection)}')
            elif var_endpoint_to_delete.variable_type == VariableType.VARIABLE_GROUP:
                if isinstance(connection, OspVariableGroupConnection):
                    osp_endpoint_connected = next(filter(
                        lambda var_endpoint: var_endpoint.to_dict_xml()
                        != osp_var_endpoint_to_delete.to_dict_xml(),
                        connection.VariableGroup
                    ))
                elif isinstance(connection, OspSignalGroupConnection):
                    osp_endpoint_connected = connection.SignalGroup
                else:
                    raise TypeError(f'Unknown connection type: {type(connection)}')
            else:
                raise TypeError(f'Unknown variable type: {var_endpoint_to_delete.variable_type}')
            self.delete_connection(
                endpoint1=osp_var_endpoint_to_delete, endpoint2=osp_endpoint_connected
            )
        return component.delete_variable_endpoint(variable_name)

    def delete_signal_endpoint(self, function_name: str, signal_name: str) -> SignalEndpoint:
        """Delete a function endpoint from the system structure and all connections"""
        # find the component
        function = self.get_function_by_name(function_name)
        sig_endpoint_to_delete = function.get_signal_endpoint(signal_name)
        osp_var_endpoint_to_delete = sig_endpoint_to_delete.get_osp_signal_endpoint(function_name)
        # check if the signal is connected. If yes, delete the connection and disconnect the other
        # endpoint
        if sig_endpoint_to_delete.connected:
            connection = self.get_connection_for_signal_endpoint(
                function_name=function_name,
                signal_endpoint=sig_endpoint_to_delete
            )
            if sig_endpoint_to_delete.signal_type == SignalType.SIGNAL:
                osp_endpoint_connected = connection.Variable
            elif sig_endpoint_to_delete.signal_type == SignalType.SIGNAL_GROUP:
                osp_endpoint_connected = connection.VariableGroup
            else:
                raise TypeError(f'Unknown signal type: {sig_endpoint_to_delete.signal_type}')
            component = self.get_component_by_name(osp_endpoint_connected.simulator)
            component.get_variable_endpoint(osp_endpoint_connected.name).connected = False
            self.delete_connection(
                endpoint1=osp_var_endpoint_to_delete, endpoint2=osp_endpoint_connected
            )
        return function.delete_signal_endpoint(signal_name)

    def add_signal_endpoint(
        self,
        function_name: str,
        signal_name: str,
        signal_type: SignalType,
        causality: Causality
    ) -> SignalEndpoint:
        """Add a signal endpoint to the function"""
        # find the function
        function = self.get_function_by_name(function_name)
        # Delete the signal endpoint in the function
        return function.add_signal_endpoint(
            signal_name=signal_name,
            signal_type=signal_type,
            causality=causality
        )

    def add_connection(
            self,
            source: Union[OspVariableEndpoint, OspSignalEndpoint],
            target: Union[OspVariableEndpoint, OspSignalEndpoint],
            group: bool
    ) -> Union[
        OspVariableConnection,
        OspSignalConnection,
        OspVariableGroupConnection,
        OspSignalGroupConnection
    ]:
        """Add a connection to the system for variable input/output

        type of connection       | source             | target              | group
        variable connection      | OspVariableEndpoint | OspVariableEndpoint | False
        variable group connection| OspVariableEndpoint | OspVariableEndpoint | True
        singal connection        | OspVariableEndpoint | OspSignalEndpoint   | False
        signal connection        | OspSingalEndpoint   | OspVariableEndpoint | False
        singal group connection  | OspVariableEndpoint | OspSignalEndpoint   | True
        signal group connection  | OspSingalEndpoint   | OspVariableEndpoint | True
        """
        # Find the component and add the variable/signal endpoint for the source
        if isinstance(source, OspVariableEndpoint):
            source_component_function = self.get_component_by_name(source.simulator)
            try:
                source_endpoint = self.add_variable_endpoint(
                    component_name=source.simulator,
                    variable_name=source.name
                )
            except ValueError:
                try:
                    source_endpoint = \
                        source_component_function.get_variable_endpoint_not_connected(source.name)
                except ValueError as exc:
                    raise ValueError(
                        f'The source variable {source.name} is already connected.'
                    ) from exc
        elif isinstance(source, OspSignalEndpoint):
            source_component_function = self.get_function_by_name(source.function)
            try:
                source_endpoint = self.add_signal_endpoint(
                    function_name=source.function,
                    signal_type=SignalType.SIGNAL if not group else  SignalType.SIGNAL_GROUP,
                    signal_name=source.name,
                    causality=Causality.OUTPUT
                )
            except ValueError:
                try:
                    source_endpoint = \
                        source_component_function.get_signal_endpoint_not_connected(source.name)
                except ValueError as exc:
                    raise ValueError(
                        f'The source signal {source.name} is already connected.'
                    ) from exc
        else:
            raise TypeError(
                'Source endpoint should be either OspVariableEndpoint or OspSignalEndpoint.'
            )
        if source_endpoint.causality not in [Causality.OUTPUT, Causality.INDEFINITE]:
            raise TypeError(
                'The source endpoint is not an output endpoint. '
                'The source endpoint should be an output endpoint.'
            )
        source_endpoint.connected = True
        # Find the component and add the variable/signal endpoint for the target
        if isinstance(target, OspVariableEndpoint):
            # First try to add the endpoint. If it already exists, it will return the existing one.
            target_component_function = self.get_component_by_name(target.simulator)
            try:
                target_endpoint = self.add_variable_endpoint(
                    component_name=target.simulator,
                    variable_name=target.name
                )
            except ValueError:
                try:
                    target_endpoint = \
                        target_component_function.get_variable_endpoint_not_connected(target.name)
                except ValueError as exc:
                    raise ValueError(
                        f'The target endpoint {target.name} is already connected.'
                    ) from exc
        elif isinstance(target, OspSignalEndpoint):
            target_component_function = self.get_function_by_name(target.function)
            try:
                target_endpoint = self.add_signal_endpoint(
                    function_name=target.function,
                    signal_type=SignalType.SIGNAL if not group else  SignalType.SIGNAL_GROUP,
                    signal_name=target.name,
                    causality=Causality.INPUT
                )
            except ValueError:
                try:
                    target_endpoint = \
                        target_component_function.get_signal_endpoint_not_connected(target.name)
                except ValueError as exc:
                    raise ValueError(
                        f'The target endpoint {target.name} is already connected.'
                    ) from exc
        else:
            raise TypeError(
                'Target endpoint should be either OspVariableEndpoint or OspSignalEndpoint.'
            )
        if target_endpoint.causality not in [Causality.INPUT, Causality.INDEFINITE]:
            raise TypeError('The target endpoint should have an input causality')
        target_endpoint.connected = True
        connection = self.system_structure.add_connection(source=source, target=target, group=group)
        return connection

    def delete_connection(
            self,
            endpoint1: Union[OspVariableEndpoint, OspSignalEndpoint],
            endpoint2: Union[OspVariableEndpoint, OspSignalEndpoint]
    ):
        """Deletes a connection having the given endpoints"""
        # Find the component and change the connection status of the VariableEndpoint
        if isinstance(endpoint1, OspVariableEndpoint):
            component_function1 = self.get_component_by_name(endpoint1.simulator)
            variable_endpoint1 = component_function1.get_variable_endpoint(endpoint1.name)
            variable_endpoint1.connected = False
        elif isinstance(endpoint1, OspSignalEndpoint):
            component_function1 = self.get_function_by_name(endpoint1.function)
            signal_endpoint1 = component_function1.get_signal_endpoint(endpoint1.name)
            signal_endpoint1.connected = False
        if isinstance(endpoint2, OspVariableEndpoint):
            component_function2 = self.get_component_by_name(endpoint2.simulator)
            variable_endpoint2 = component_function2.get_variable_endpoint(endpoint2.name)
            variable_endpoint2.connected = False
        elif isinstance(endpoint2, OspSignalEndpoint):
            component_function2 = self.get_function_by_name(endpoint2.function)
            signal_endpoint2 = component_function2.get_signal_endpoint(endpoint2.name)
            signal_endpoint2.connected = False
        return self.system_structure.delete_connection(
            endpoint1=endpoint1,
            endpoint2=endpoint2
        )

    def add_update_initial_value(
            self,
            component_name: str,
            variable: str,
            value: Union[float, int, bool, str],
            type_value: Union[Type[float], Type[int], Type[bool], Type[str]] = None
    ) -> InitialValues:
        """Add or update initial value. Returns True if successful

        Args:
            component_name: Name of the component
            variable: Name of the variable
            value: Value
            type_value(optional): type of the value if one wants to make sure to have a
                correct type for the value
        """

        # Check if the initial value is valid
        component = self.get_component_by_name(component_name)
        if variable in component.fmu.get_parameter_names():
            osp_variable = next(filter(
                lambda x: x.get("name") == variable,
                component.fmu.parameters
            ))
        elif variable in component.fmu.get_input_names():
            osp_variable = next(filter(
                lambda x: x.get("name") == variable,
                component.fmu.inputs
            ))
        else:
            raise TypeError(
                f'The variable {variable} is not found in the inputs or parameters of the model.'
            )
        type_var = osp_parser_sys.VariableType[osp_variable.get('type')]

        # Search for an initial value that already exists. Otherwise, create a new instance
        try:
            init_value = self.get_initial_value_by_variable(component_name, variable)
            self.initial_values.pop(self.initial_values.index(init_value))
        except TypeError:
            pass

        init_value = InitialValues(
            component=component_name,
            variable=variable,
            value=value,
            type_var=type_var
        )

        self.initial_values.append(init_value)
        value_osp_type = convert_value_to_osp_type(value=value, type_var=type_var)
        self.system_structure.add_update_initial_value(
            component_name=component_name,
            init_value=OspInitialValue(variable=variable, value=value_osp_type)
        )

        return init_value

    def delete_initial_value(self, component: str, variable: str):
        """Deletes the initial value. Returns True if successful."""
        init_value = self.get_initial_value_by_variable(
            component=component,
            variable=variable
        )
        init_value = self.initial_values.pop(self.initial_values.index(init_value))
        if self.system_structure.delete_initial_value(component_name=component, variable=variable):
            return True

        self.initial_values.append(init_value)
        raise TypeError('The initial value could not be added.')

    def get_component_by_name(self, name) -> Component:
        """Returns a Component instnace from its attributes"""
        try:
            return next(component for component in self.components if component.name == name)
        except StopIteration:
            raise TypeError(f'No component is found with the given name: {name}')

    def get_initial_value_by_variable(self, component: str, variable: str) -> InitialValues:
        """Returns an InitialValues instance from its attributes"""
        try:
            return next(
                init_value for init_value in self.initial_values
                if init_value.component == component and init_value.variable == variable
            )
        except StopIteration:
            raise TypeError(f'No initial value is found with the given variable: {variable}')

    def add_function(self, function_name: str, function_type: FunctionType, **kwargs) \
            -> Function:
        """Add a function

        'factor', 'offset' arguments are required for FunctionType.LinearTransformation
        'inputCount' is required for FunctionType.Sum
        'inputCount', 'dimension' are required for FunctionType.VectorSumFunction

        Args:
            function_name: Name of the function
            function_type: Either of FunctionType.LinearTransformation, FunctionType.Sum or
                FunctionType.VectorSum
            factor (float): factor for linear transformation f(x) = factor * x + offset
            offset (float): offset for linear transformation f(x) = factor * x + offset
            inputCount (int): number of inputs for sum or vector sum
            dimension (int): Dimension of a vector for vector sum

        Returns:
            OspLinearTransformationFunction, OspSumFunction, OspVectorSumFunction

        Exceptions:
            TypeError if correct arguments are not given for a function type
        """
        if function_type == FunctionType.LinearTransformation:
            factor = kwargs.get('factor', None)
            if factor is None:
                raise TypeError('"factor" argument should be provided for a linear '
                                'transformation function')
            offset = kwargs.get('offset', None)
            if offset is None:
                raise TypeError('"offset" argument should be provided for a linear '
                                'transformation function')
            function = Function(
                name=function_name, type=function_type, factor=factor, offset=offset
            )
            self.system_structure.add_function(
                function_name=function_name,
                function_type=function_type,
                factor=factor,
                offset=offset
            )
        elif function_type == FunctionType.Sum:
            inputCount = kwargs.get('inputCount', None)
            if inputCount is None:
                raise TypeError('"inputCount" argument should be provided for a sum function')
            function = Function(
                name=function_name, type=function_type, inputCount=inputCount
            )
            self.system_structure.add_function(
                function_name=function_name, function_type=function_type, inputCount=inputCount
            )
        elif function_type == FunctionType.VectorSum:
            inputCount = kwargs.get('inputCount', None)
            if inputCount is None:
                raise TypeError('"inputCount" argument should be provided for a sum function')
            dimension = kwargs.get('dimension', None)
            if dimension is None:
                raise TypeError('"dimension" argument should be provided for a sum function')
            function = Function(
                name=function_name, type=function_type, inputCount=inputCount, dimension=dimension
            )
            self.system_structure.add_function(
                function_name=function_name,
                function_type=function_type,
                inputCount=inputCount,
                dimension=dimension
            )
        else:
            raise TypeError(f'Function type ({function_type}) is not supported')

        self.functions.append(function)
        return function

    def get_function_by_name(self, function_name: str) -> Function:
        """Returns a Function instance from its attributes"""
        try:
            return next(function for function in self.functions if function.name == function_name)
        except StopIteration as exc:
            raise TypeError(f'No function is found with the given name: {function_name}') from exc

    def add_logging_variable(
            self, component_name: str,
            variable_name: str,
            decimation_factor: int = 1
    ):
        """Add a variable to log during a simulation

        Args:
            component_name: Name of the simulator
            variable_name: Name of the variable
            decimation_factor: Sampling rate of the
                simulation results. For example, decimationFactor=1 means the
                results of every simulation step of the simulator are logged.
                And decimationFactor=10 means every 10th of the simulation
                results are logged.
        """
        # Check if the component name is found in the system
        if component_name not in self.get_component_names():
            raise TypeError('No component is found with the name. '
                            f'It should be either of {self.get_component_names()}')
        # Check if the variable is found in the model
        comp = self.get_component_by_name(component_name)
        if variable_name not in [
                *(comp.fmu.get_input_names()),
                *(comp.fmu.get_output_names()),
                *(comp.fmu.get_parameter_names()),
                *(comp.fmu.get_other_variable_names())
        ]:
            raise TypeError('No variable or parameter is found with the name.')
        if self.logging_config is None:
            self.logging_config = OspLoggingConfiguration()
        try:
            if self.logging_config.simulators is None:
                self.logging_config.simulators = []
            logging_for_component: OspSimulatorForLogging = next(
                logging_component for logging_component in self.logging_config.simulators
                if logging_component.name == component_name
            )
            logging_for_component.add_variable(variable_name)
        except StopIteration:
            self.logging_config.simulators.append(OspSimulatorForLogging(
                name=component_name,
                decimation_factor=decimation_factor
            ))
            logging_for_component = next(
                logging_component for logging_component in self.logging_config.simulators
                if logging_component.name == component_name
            )
            logging_for_component.add_variable(variable_name)
        return True

    def set_decimation_factor(self, component_name: str, decimation_factor: int) -> bool:
        """Set decimal factor for a component logging"""
        return self.logging_config.set_decimation_factor(component_name, decimation_factor)

    def set_scenario(self, name: str, end: float, description: str = None):
        """Sets a scenario"""
        self.scenario = OSPScenario(name=name, end=end, description=description)

    def set_scenario_from_json(self, source: str):
        """Sets a scenario from the json

        Args:
            source: json string or path to the file
        """
        if os.path.isfile(source):
            with open(source, 'rt') as file:
                source = file.read()
        self.scenario = OSPScenario(name='', end=0)
        self.scenario.from_json(source)

    def add_event(self, time: float, component: str, variable: str, action: int, value: float):
        """Add an event

        Args:
            time: Time when the event is triggered
            component: Name of the component for the event to apply
            variable: Name of the variable for the event to apply
            action: Type of action. Recommended to use OSPEvent.OVERRIDE,
                OSPEvent.BIAS, OSPEvent.RESET
            value: Value for the change
        """
        if not isinstance(self.scenario, OSPScenario):
            raise TypeError('No scenario has been set up. Use set_scenario or '
                            'se_scenario_from_json to set up a scenario')
        if component not in self.get_component_names():
            raise TypeError(f'No component is found with the name {component}')
        fmu = self.get_component_by_name(component).fmu
        if variable not in [*(fmu.get_input_names()), *(fmu.get_parameter_names())]:
            raise TypeError(f'No input or parameter is found with the name {variable}')
        return self.scenario.add_event(OSPEvent(
            time=time,
            model=component,
            variable=variable,
            action=action,
            value=value
        ))

    def update_event(
            self,
            time: float,
            component: str,
            variable: str,
            action: int = None,
            value: float = None
    ):
        """Update an event

        One should provide time, model(component name) and variable to find the event to update.
        One can provide either action or value or both.
        """
        if not isinstance(self.scenario, OSPScenario):
            raise TypeError('No scenario has been set up. Use set_scenario or '
                            'se_scenario_from_json to set up a scenario')
        return self.scenario.update_event(
            time=time, component=component, variable=variable, action=action, value=value
        )

    def delete_events(self, time: float = None, component: str = None, variable: str = None):
        """Delete events

         If no argument is provided, it deletes all events. Givent the arguments, events
         that match the argument values are found and deleted.
         """
        if not isinstance(self.scenario, OSPScenario):
            raise TypeError('No scenario has been set up. Use set_scenario or '
                            'se_scenario_from_json to set up a scenario')
        return self.scenario.delete_events(time=time, component=component, variable=variable)

    def set_base_step_size(self, step_size: float) -> float:
        """Sets a base step size for master algorithm in co-simulation.

        Returns the step size set.
        """
        self.system_structure.BaseStepSize = float(step_size)
        return float(step_size)

