diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 816d8fc3..cbe23036 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -21,8 +21,10 @@ import numpy as np from OMPython.OMCSession import ( + ModelExecutionData, + ModelExecutionException, + OMCSessionException, - OMCSessionRunData, OMCSession, OMCSessionLocal, OMCPath, @@ -34,7 +36,7 @@ class ModelicaSystemError(Exception): """ - Exception used in ModelicaSystem and ModelicaSystemCmd classes. + Exception used in ModelicaSystem classes. """ @@ -89,7 +91,7 @@ def __getitem__(self, index: int): return {0: self.A, 1: self.B, 2: self.C, 3: self.D}[index] -class ModelicaSystemCmd: +class ModelExecutionCmd: """ All information about a compiled model executable. This should include data about all structured parameters, i.e. parameters which need a recompilation of the model. All non-structured parameters can be easily changed without @@ -98,16 +100,22 @@ class ModelicaSystemCmd: def __init__( self, - session: OMCSession, - runpath: OMCPath, - modelname: Optional[str] = None, + runpath: os.PathLike, + cmd_prefix: list[str], + cmd_local: bool = False, + cmd_windows: bool = False, + timeout: float = 10.0, + model_name: Optional[str] = None, ) -> None: - if modelname is None: - raise ModelicaSystemError("Missing model name!") + if model_name is None: + raise ModelExecutionException("Missing model name!") - self._session = session - self._runpath = runpath - self._model_name = modelname + self._cmd_local = cmd_local + self._cmd_windows = cmd_windows + self._cmd_prefix = cmd_prefix + self._runpath = pathlib.PurePosixPath(runpath) + self._model_name = model_name + self._timeout = timeout # dictionaries of command line arguments for the model executable self._args: dict[str, str | None] = {} @@ -152,26 +160,26 @@ def override2str( elif isinstance(orval, numbers.Number): val_str = str(orval) else: - raise ModelicaSystemError(f"Invalid value for override key {orkey}: {type(orval)}") + raise ModelExecutionException(f"Invalid value for override key {orkey}: {type(orval)}") return f"{orkey}={val_str}" if not isinstance(key, str): - raise ModelicaSystemError(f"Invalid argument key: {repr(key)} (type: {type(key)})") + raise ModelExecutionException(f"Invalid argument key: {repr(key)} (type: {type(key)})") key = key.strip() if isinstance(val, dict): if key != 'override': - raise ModelicaSystemError("Dictionary input only possible for key 'override'!") + raise ModelExecutionException("Dictionary input only possible for key 'override'!") for okey, oval in val.items(): if not isinstance(okey, str): - raise ModelicaSystemError("Invalid key for argument 'override': " - f"{repr(okey)} (type: {type(okey)})") + raise ModelExecutionException("Invalid key for argument 'override': " + f"{repr(okey)} (type: {type(okey)})") if not isinstance(oval, (str, bool, numbers.Number, type(None))): - raise ModelicaSystemError(f"Invalid input for 'override'.{repr(okey)}: " - f"{repr(oval)} (type: {type(oval)})") + raise ModelExecutionException(f"Invalid input for 'override'.{repr(okey)}: " + f"{repr(oval)} (type: {type(oval)})") if okey in self._arg_override: if oval is None: @@ -193,7 +201,7 @@ def override2str( elif isinstance(val, numbers.Number): argval = str(val) else: - raise ModelicaSystemError(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})") + raise ModelExecutionException(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})") if key in self._args: logger.warning(f"Override model executable argument: {repr(key)} = {repr(argval)} " @@ -233,7 +241,7 @@ def get_cmd_args(self) -> list[str]: return cmdl - def definition(self) -> OMCSessionRunData: + def definition(self) -> ModelExecutionData: """ Define all needed data to run the model executable. The data is stored in an OMCSessionRunData object. """ @@ -242,18 +250,50 @@ def definition(self) -> OMCSessionRunData: if not isinstance(result_file, str): result_file = (self._runpath / f"{self._model_name}.mat").as_posix() - omc_run_data = OMCSessionRunData( - cmd_path=self._runpath.as_posix(), + # as this is the local implementation, pathlib.Path can be used + cmd_path = self._runpath + + cmd_library_path = None + if self._cmd_local and self._cmd_windows: + cmd_library_path = "" + + # set the process environment from the generated .bat file in windows which should have all the dependencies + # for this pathlib.PurePosixPath() must be converted to a pathlib.Path() object, i.e. WindowsPath + path_bat = pathlib.Path(cmd_path) / f"{self._model_name}.bat" + if not path_bat.is_file(): + raise ModelExecutionException("Batch file (*.bat) does not exist " + str(path_bat)) + + content = path_bat.read_text(encoding='utf-8') + for line in content.splitlines(): + match = re.match(pattern=r"^SET PATH=([^%]*)", string=line, flags=re.IGNORECASE) + if match: + cmd_library_path = match.group(1).strip(';') # Remove any trailing semicolons + my_env = os.environ.copy() + my_env["PATH"] = cmd_library_path + os.pathsep + my_env["PATH"] + + cmd_model_executable = cmd_path / f"{self._model_name}.exe" + else: + # for Linux the paths to the needed libraries should be included in the executable (using rpath) + cmd_model_executable = cmd_path / self._model_name + + # define local(!) working directory + cmd_cwd_local = None + if self._cmd_local: + cmd_cwd_local = cmd_path.as_posix() + + omc_run_data = ModelExecutionData( + cmd_path=cmd_path.as_posix(), cmd_model_name=self._model_name, cmd_args=self.get_cmd_args(), - cmd_result_path=result_file, - ) - - omc_run_data_updated = self._session.omc_run_data_update( - omc_run_data=omc_run_data, + cmd_result_file=result_file, + cmd_prefix=self._cmd_prefix, + cmd_library_path=cmd_library_path, + cmd_model_executable=cmd_model_executable.as_posix(), + cmd_cwd_local=cmd_cwd_local, + cmd_timeout=self._timeout, ) - return omc_run_data_updated + return omc_run_data @staticmethod def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | numbers.Number]]: @@ -262,17 +302,19 @@ def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | n The return data can be used as input for self.args_set(). """ - warnings.warn(message="The argument 'simflags' is depreciated and will be removed in future versions; " - "please use 'simargs' instead", - category=DeprecationWarning, - stacklevel=2) + warnings.warn( + message="The argument 'simflags' is depreciated and will be removed in future versions; " + "please use 'simargs' instead", + category=DeprecationWarning, + stacklevel=2, + ) simargs: dict[str, Optional[str | dict[str, Any] | numbers.Number]] = {} args = [s for s in simflags.split(' ') if s] for arg in args: if arg[0] != '-': - raise ModelicaSystemError(f"Invalid simulation flag: {arg}") + raise ModelExecutionException(f"Invalid simulation flag: {arg}") arg = arg[1:] parts = arg.split('=') if len(parts) == 1: @@ -284,12 +326,12 @@ def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | n for item in override.split(','): kv = item.split('=') if not 0 < len(kv) < 3: - raise ModelicaSystemError(f"Invalid value for '-override': {override}") + raise ModelExecutionException(f"Invalid value for '-override': {override}") if kv[0]: try: override_dict[kv[0]] = kv[1] except (KeyError, IndexError) as ex: - raise ModelicaSystemError(f"Invalid value for '-override': {override}") from ex + raise ModelExecutionException(f"Invalid value for '-override': {override}") from ex simargs[parts[0]] = override_dict @@ -461,6 +503,15 @@ def get_session(self) -> OMCSession: """ return self._session + def get_model_name(self) -> str: + """ + Return the defined model name. + """ + if not isinstance(self._model_name, str): + raise ModelicaSystemError("No model name defined!") + + return self._model_name + def set_command_line_options(self, command_line_option: str): """ Set the provided command line option via OMC setCommandLineOptions(). @@ -540,15 +591,17 @@ def buildModel(self, variableFilter: Optional[str] = None): logger.debug("OM model build result: %s", build_model_result) # check if the executable exists ... - om_cmd = ModelicaSystemCmd( - session=self._session, + om_cmd = ModelExecutionCmd( runpath=self.getWorkDirectory(), - modelname=self._model_name, + cmd_local=self._session.model_execution_local, + cmd_windows=self._session.model_execution_windows, + cmd_prefix=self._session.model_execution_prefix(cwd=self.getWorkDirectory()), + model_name=self._model_name, ) # ... by running it - output help for command help om_cmd.arg_set(key="help", val="help") cmd_definition = om_cmd.definition() - returncode = self._session.run_model_executable(cmd_run_data=cmd_definition) + returncode = cmd_definition.run() if returncode != 0: raise ModelicaSystemError("Model executable not working!") @@ -1153,7 +1206,7 @@ def _parse_om_version(version: str) -> tuple[int, int, int]: def _process_override_data( self, - om_cmd: ModelicaSystemCmd, + om_cmd: ModelExecutionCmd, override_file: OMCPath, override_var: dict[str, str], override_sim: dict[str, str], @@ -1189,7 +1242,7 @@ def simulate_cmd( result_file: OMCPath, simflags: Optional[str] = None, simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, - ) -> ModelicaSystemCmd: + ) -> ModelExecutionCmd: """ This method prepares the simulates model according to the simulation options. It returns an instance of ModelicaSystemCmd which can be used to run the simulation. @@ -1211,10 +1264,12 @@ def simulate_cmd( An instance if ModelicaSystemCmd to run the requested simulation. """ - om_cmd = ModelicaSystemCmd( - session=self._session, + om_cmd = ModelExecutionCmd( runpath=self.getWorkDirectory(), - modelname=self._model_name, + cmd_local=self._session.model_execution_local, + cmd_windows=self._session.model_execution_windows, + cmd_prefix=self._session.model_execution_prefix(cwd=self.getWorkDirectory()), + model_name=self._model_name, ) # always define the result file to use @@ -1303,7 +1358,7 @@ def simulate( self._result_file.unlink() # ... run simulation ... cmd_definition = om_cmd.definition() - returncode = self._session.run_model_executable(cmd_run_data=cmd_definition) + returncode = cmd_definition.run() # and check returncode *AND* resultfile if returncode != 0 and self._result_file.is_file(): # check for an empty (=> 0B) result file which indicates a crash of the model executable @@ -1906,10 +1961,12 @@ def linearize( "use ModelicaSystem() to build the model first" ) - om_cmd = ModelicaSystemCmd( - session=self._session, + om_cmd = ModelExecutionCmd( runpath=self.getWorkDirectory(), - modelname=self._model_name, + cmd_local=self._session.model_execution_local, + cmd_windows=self._session.model_execution_windows, + cmd_prefix=self._session.model_execution_prefix(cwd=self.getWorkDirectory()), + model_name=self._model_name, ) self._process_override_data( @@ -1949,7 +2006,7 @@ def linearize( linear_file.unlink(missing_ok=True) cmd_definition = om_cmd.definition() - returncode = self._session.run_model_executable(cmd_run_data=cmd_definition) + returncode = cmd_definition.run() if returncode != 0: raise ModelicaSystemError(f"Linearize failed with return code: {returncode}") if not linear_file.is_file(): @@ -2051,9 +2108,13 @@ def run_doe(): resdir = mypath / 'DoE' resdir.mkdir(exist_ok=True) - doe_mod = OMPython.ModelicaSystemDoE( + mod = OMPython.ModelicaSystem() + mod.model( model_name="M", model_file=model.as_posix(), + ) + doe_mod = OMPython.ModelicaSystemDoE( + mod=mod, parameters=param, resultpath=resdir, simargs={"override": {'stopTime': 1.0}}, @@ -2080,15 +2141,8 @@ def run_doe(): def __init__( self, - # data to be used for ModelicaSystem - model_file: Optional[str | os.PathLike] = None, - model_name: Optional[str] = None, - libraries: Optional[list[str | tuple[str, str]]] = None, - command_line_options: Optional[list[str]] = None, - variable_filter: Optional[str] = None, - work_directory: Optional[str | os.PathLike] = None, - omhome: Optional[str] = None, - session: Optional[OMCSession] = None, + # ModelicaSystem definition to use + mod: ModelicaSystem, # simulation specific input # TODO: add more settings (simulation options, input options, ...) simargs: Optional[dict[str, Optional[str | dict[str, str] | numbers.Number]]] = None, @@ -2101,30 +2155,18 @@ def __init__( ModelicaSystem.simulate(). Additionally, the path to store the result files is needed (= resultpath) as well as a list of parameters to vary for the Doe (= parameters). All possible combinations are considered. """ - if model_name is None: - raise ModelicaSystemError("No model name provided!") + if not isinstance(mod, ModelicaSystem): + raise ModelicaSystemError("Missing definition of ModelicaSystem!") - self._mod = ModelicaSystem( - command_line_options=command_line_options, - work_directory=work_directory, - omhome=omhome, - session=session, - ) - self._mod.model( - model_file=model_file, - model_name=model_name, - libraries=libraries, - variable_filter=variable_filter, - ) - - self._model_name = model_name + self._mod = mod + self._model_name = mod.get_model_name() self._simargs = simargs if resultpath is None: self._resultpath = self.get_session().omcpath_tempdir() else: - self._resultpath = self.get_session().omcpath(resultpath) + self._resultpath = self.get_session().omcpath(resultpath).resolve() if not self._resultpath.is_dir(): raise ModelicaSystemError("Argument resultpath must be set to a valid path within the environment used " f"for the OpenModelica session: {resultpath}!") @@ -2135,7 +2177,7 @@ def __init__( self._parameters = {} self._doe_def: Optional[dict[str, dict[str, Any]]] = None - self._doe_cmd: Optional[dict[str, OMCSessionRunData]] = None + self._doe_cmd: Optional[dict[str, ModelExecutionData]] = None def get_session(self) -> OMCSession: """ @@ -2254,7 +2296,7 @@ def get_doe_definition(self) -> Optional[dict[str, dict[str, Any]]]: """ return self._doe_def - def get_doe_command(self) -> Optional[dict[str, OMCSessionRunData]]: + def get_doe_command(self) -> Optional[dict[str, ModelExecutionData]]: """ Get the definitions of simulations commands to run for this DoE. """ @@ -2300,13 +2342,13 @@ def worker(worker_id, task_queue): if cmd_definition is None: raise ModelicaSystemError("Missing simulation definition!") - resultfile = cmd_definition.cmd_result_path + resultfile = cmd_definition.cmd_result_file resultpath = self.get_session().omcpath(resultfile) logger.info(f"[Worker {worker_id}] Performing task: {resultpath.name}") try: - returncode = self.get_session().run_model_executable(cmd_run_data=cmd_definition) + returncode = cmd_definition.run() logger.info(f"[Worker {worker_id}] Simulation {resultpath.name} " f"finished with return code: {returncode}") except ModelicaSystemError as ex: diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index c0e5499b..b95f36c1 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -50,7 +50,7 @@ def poll(self): return None if self.process.is_running() else True def kill(self): - return os.kill(self.pid, signal.SIGKILL) + return os.kill(pid=self.pid, signal=signal.SIGKILL) def wait(self, timeout): try: @@ -451,31 +451,38 @@ class OMCPathCompatibilityWindows(pathlib.WindowsPath, OMCPathCompatibility): OMCPath = OMCPathReal +class ModelExecutionException(Exception): + """ + Exception which is raised by ModelException* classes. + """ + + @dataclasses.dataclass -class OMCSessionRunData: +class ModelExecutionData: """ Data class to store the command line data for running a model executable in the OMC environment. All data should be defined for the environment, where OMC is running (local, docker or WSL) To use this as a definition of an OMC simulation run, it has to be processed within - OMCProcess*.omc_run_data_update(). This defines the attribute cmd_model_executable. + OMCProcess*.self_update(). This defines the attribute cmd_model_executable. """ # cmd_path is the expected working directory cmd_path: str cmd_model_name: str + # command prefix data (as list of strings); needed for docker or WSL + cmd_prefix: list[str] + # cmd_model_executable is build out of cmd_path and cmd_model_name; this is mainly needed on Windows (add *.exe) + cmd_model_executable: str # command line arguments for the model executable cmd_args: list[str] # result file with the simulation output - cmd_result_path: str + cmd_result_file: str + # command timeout + cmd_timeout: float - # command prefix data (as list of strings); needed for docker or WSL - cmd_prefix: Optional[list[str]] = None - # cmd_model_executable is build out of cmd_path and cmd_model_name; this is mainly needed on Windows (add *.exe) - cmd_model_executable: Optional[str] = None # additional library search path; this is mainly needed if OMCProcessLocal is run on Windows cmd_library_path: Optional[str] = None - # working directory to be used on the *local* system cmd_cwd_local: Optional[str] = None @@ -484,14 +491,49 @@ def get_cmd(self) -> list[str]: Get the command line to run the model executable in the environment defined by the OMCProcess definition. """ - if self.cmd_model_executable is None: - raise OMCSessionException("No model file defined for the model executable!") - - cmdl = [] if self.cmd_prefix is None else self.cmd_prefix - cmdl += [self.cmd_model_executable] + self.cmd_args + cmdl = self.cmd_prefix + cmdl += [self.cmd_model_executable] + cmdl += self.cmd_args return cmdl + def run(self) -> int: + """ + Run the model execution defined in this class. + """ + + my_env = os.environ.copy() + if isinstance(self.cmd_library_path, str): + my_env["PATH"] = self.cmd_library_path + os.pathsep + my_env["PATH"] + + cmdl = self.get_cmd() + + logger.debug("Run OM command %s in %s", repr(cmdl), self.cmd_path) + try: + cmdres = subprocess.run( + cmdl, + capture_output=True, + text=True, + env=my_env, + cwd=self.cmd_cwd_local, + timeout=self.cmd_timeout, + check=True, + ) + stdout = cmdres.stdout.strip() + stderr = cmdres.stderr.strip() + returncode = cmdres.returncode + + logger.debug("OM output for command %s:\n%s", repr(cmdl), stdout) + + if stderr: + raise ModelExecutionException(f"Error running model executable {repr(cmdl)}: {stderr}") + except subprocess.TimeoutExpired as ex: + raise ModelExecutionException(f"Timeout running model executable {repr(cmdl)}: {ex}") from ex + except subprocess.CalledProcessError as ex: + raise ModelExecutionException(f"Error running model executable {repr(cmdl)}: {ex}") from ex + + return returncode + class OMCSessionZMQ: """ @@ -541,21 +583,6 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: """ return self.omc_process.omcpath_tempdir(tempdir_base=tempdir_base) - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: - """ - Modify data based on the selected OMCProcess implementation. - - Needs to be implemented in the subclasses. - """ - return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data) - - def run_model_executable(self, cmd_run_data: OMCSessionRunData) -> int: - """ - Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to - keep instances of over classes around. - """ - return self.omc_process.run_model_executable(cmd_run_data=cmd_run_data) - def execute(self, command: str): return self.omc_process.execute(command=command) @@ -634,6 +661,10 @@ def __init__( Initialisation for OMCSession """ + # some helper data + self.model_execution_windows = platform.system() == "Windows" + self.model_execution_local = False + # store variables self._timeout = timeout # generate a random string for this instance of OMC @@ -772,6 +803,13 @@ def set_workdir(self, workdir: OMCPath) -> None: exp = f'cd("{workdir.as_posix()}")' self.sendExpression(exp) + def model_execution_prefix(self, cwd: Optional[OMCPath] = None) -> list[str]: + """ + Helper function which returns a command prefix needed for docker and WSL. It defaults to an empty list. + """ + + return [] + def omcpath(self, *path) -> OMCPath: """ Create an OMCPath object based on the given path segments and the current OMCSession* class. @@ -790,7 +828,6 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: Get a temporary directory using OMC. It is our own implementation as non-local usage relies on OMC to run all filesystem related access. """ - names = [str(uuid.uuid4()) for _ in range(100)] if tempdir_base is None: # fallback solution for Python < 3.12; a modified pathlib.Path object is used as OMCPath replacement @@ -800,6 +837,12 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: tempdir_str = self.sendExpression(expr="getTempDirectoryPath()") tempdir_base = self.omcpath(tempdir_str) + return self._tempdir(tempdir_base=tempdir_base) + + @staticmethod + def _tempdir(tempdir_base: OMCPath) -> OMCPath: + names = [str(uuid.uuid4()) for _ in range(100)] + tempdir: Optional[OMCPath] = None for name in names: # create a unique temporary directory name @@ -816,48 +859,13 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: return tempdir - def run_model_executable(self, cmd_run_data: OMCSessionRunData) -> int: - """ - Run the command defined in cmd_run_data. - """ - - my_env = os.environ.copy() - if isinstance(cmd_run_data.cmd_library_path, str): - my_env["PATH"] = cmd_run_data.cmd_library_path + os.pathsep + my_env["PATH"] - - cmdl = cmd_run_data.get_cmd() - - logger.debug("Run OM command %s in %s", repr(cmdl), cmd_run_data.cmd_path) - try: - cmdres = subprocess.run( - cmdl, - capture_output=True, - text=True, - env=my_env, - cwd=cmd_run_data.cmd_cwd_local, - timeout=self._timeout, - check=True, - ) - stdout = cmdres.stdout.strip() - stderr = cmdres.stderr.strip() - returncode = cmdres.returncode - - logger.debug("OM output for command %s:\n%s", repr(cmdl), stdout) - - if stderr: - raise OMCSessionException(f"Error running model executable {repr(cmdl)}: {stderr}") - except subprocess.TimeoutExpired as ex: - raise OMCSessionException(f"Timeout running model executable {repr(cmdl)}") from ex - except subprocess.CalledProcessError as ex: - raise OMCSessionException(f"Error running model executable {repr(cmdl)}") from ex - - return returncode - def execute(self, command: str): - warnings.warn(message="This function is depreciated and will be removed in future versions; " - "please use sendExpression() instead", - category=DeprecationWarning, - stacklevel=2) + warnings.warn( + message="This function is depreciated and will be removed in future versions; " + "please use sendExpression() instead", + category=DeprecationWarning, + stacklevel=2, + ) return self.sendExpression(command, parsed=False) @@ -1029,18 +1037,6 @@ def _get_portfile_path(self) -> Optional[pathlib.Path]: return portfile_path - @abc.abstractmethod - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: - """ - Update the OMCSessionRunData object based on the selected OMCSession implementation. - - The main point is the definition of OMCSessionRunData.cmd_model_executable which contains the specific command - to run depending on the selected system. - - Needs to be implemented in the subclasses. - """ - raise NotImplementedError("This method must be implemented in subclasses!") - class OMCSessionPort(OMCSession): """ @@ -1054,28 +1050,6 @@ def __init__( super().__init__() self._omc_port = omc_port - @staticmethod - def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: - """ - Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to - keep instances of over classes around. - """ - raise OMCSessionException("OMCSessionPort does not support run_model_executable()!") - - def get_log(self) -> str: - """ - Get the log file content of the OMC session. - """ - log = f"No log available if OMC session is defined by port ({self.__class__.__name__})" - - return log - - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: - """ - Update the OMCSessionRunData object based on the selected OMCSession implementation. - """ - raise OMCSessionException(f"({self.__class__.__name__}) does not support omc_run_data_update()!") - class OMCSessionLocal(OMCSession): """ @@ -1090,6 +1064,8 @@ def __init__( super().__init__(timeout=timeout) + self.model_execution_local = True + # where to find OpenModelica self._omhome = self._omc_home_get(omhome=omhome) # start up omc executable, which is waiting for the ZMQ connection @@ -1155,48 +1131,6 @@ def _omc_port_get(self) -> str: return port - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: - """ - Update the OMCSessionRunData object based on the selected OMCSession implementation. - """ - # create a copy of the data - omc_run_data_copy = dataclasses.replace(omc_run_data) - - # as this is the local implementation, pathlib.Path can be used - cmd_path = pathlib.Path(omc_run_data_copy.cmd_path) - - if platform.system() == "Windows": - path_dll = "" - - # set the process environment from the generated .bat file in windows which should have all the dependencies - path_bat = cmd_path / f"{omc_run_data.cmd_model_name}.bat" - if not path_bat.is_file(): - raise OMCSessionException("Batch file (*.bat) does not exist " + str(path_bat)) - - content = path_bat.read_text(encoding='utf-8') - for line in content.splitlines(): - match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE) - if match: - path_dll = match.group(1).strip(';') # Remove any trailing semicolons - my_env = os.environ.copy() - my_env["PATH"] = path_dll + os.pathsep + my_env["PATH"] - - omc_run_data_copy.cmd_library_path = path_dll - - cmd_model_executable = cmd_path / f"{omc_run_data_copy.cmd_model_name}.exe" - else: - # for Linux the paths to the needed libraries should be included in the executable (using rpath) - cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name - - if not cmd_model_executable.is_file(): - raise OMCSessionException(f"Application file path not found: {cmd_model_executable}") - omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix() - - # define local(!) working directory - omc_run_data_copy.cmd_cwd_local = omc_run_data.cmd_path - - return omc_run_data_copy - class OMCSessionDockerHelper(OMCSession): """ @@ -1309,27 +1243,21 @@ def get_docker_container_id(self) -> str: return self._docker_container_id - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + def model_execution_prefix(self, cwd: Optional[OMCPath] = None) -> list[str]: """ - Update the OMCSessionRunData object based on the selected OMCSession implementation. + Helper function which returns a command prefix needed for docker and WSL. It defaults to an empty list. """ - omc_run_data_copy = dataclasses.replace(omc_run_data) - - omc_run_data_copy.cmd_prefix = ( - [ - "docker", "exec", - "--user", str(self._getuid()), - "--workdir", omc_run_data_copy.cmd_path, - ] - + self._docker_extra_args - + [self._docker_container_id] - ) - - cmd_path = pathlib.PurePosixPath(omc_run_data_copy.cmd_path) - cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name - omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix() + docker_cmd = [ + "docker", "exec", + "--user", str(self._getuid()), + ] + if isinstance(cwd, OMCPath): + docker_cmd += ["--workdir", cwd.as_posix()] + docker_cmd += self._docker_extra_args + if isinstance(self._docker_container_id, str): + docker_cmd += [self._docker_container_id] - return omc_run_data_copy + return docker_cmd class OMCSessionDocker(OMCSessionDockerHelper): @@ -1592,15 +1520,18 @@ def __init__( # connect to the running omc instance using ZMQ self._omc_port = self._omc_port_get() - def _wsl_cmd(self, wsl_cwd: Optional[str] = None) -> list[str]: + def model_execution_prefix(self, cwd: Optional[OMCPath] = None) -> list[str]: + """ + Helper function which returns a command prefix needed for docker and WSL. It defaults to an empty list. + """ # get wsl base command wsl_cmd = ['wsl'] if isinstance(self._wsl_distribution, str): wsl_cmd += ['--distribution', self._wsl_distribution] if isinstance(self._wsl_user, str): wsl_cmd += ['--user', self._wsl_user] - if isinstance(wsl_cwd, str): - wsl_cmd += ['--cd', wsl_cwd] + if isinstance(cwd, OMCPath): + wsl_cmd += ['--cd', cwd.as_posix()] wsl_cmd += ['--'] return wsl_cmd @@ -1608,7 +1539,7 @@ def _wsl_cmd(self, wsl_cwd: Optional[str] = None) -> list[str]: def _omc_process_get(self) -> subprocess.Popen: my_env = os.environ.copy() - omc_command = self._wsl_cmd() + [ + omc_command = self.model_execution_prefix() + [ self._wsl_omc, "--locale=C", "--interactive=zmq", @@ -1630,7 +1561,7 @@ def _omc_port_get(self) -> str: omc_portfile_path = self._get_portfile_path() if omc_portfile_path is not None: output = subprocess.check_output( - args=self._wsl_cmd() + ["cat", omc_portfile_path.as_posix()], + args=self.model_execution_prefix() + ["cat", omc_portfile_path.as_posix()], stderr=subprocess.DEVNULL, ) port = output.decode().strip() @@ -1647,17 +1578,3 @@ def _omc_port_get(self) -> str: f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}") return port - - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: - """ - Update the OMCSessionRunData object based on the selected OMCSession implementation. - """ - omc_run_data_copy = dataclasses.replace(omc_run_data) - - omc_run_data_copy.cmd_prefix = self._wsl_cmd(wsl_cwd=omc_run_data.cmd_path) - - cmd_path = pathlib.PurePosixPath(omc_run_data_copy.cmd_path) - cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name - omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix() - - return omc_run_data_copy diff --git a/OMPython/__init__.py b/OMPython/__init__.py index 59a0ad10..7c199ef3 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -1,38 +1,49 @@ # -*- coding: utf-8 -*- """ OMPython is a Python interface to OpenModelica. -To get started, create an OMCSessionZMQ object: -from OMPython import OMCSessionZMQ -omc = OMCSessionZMQ() +To get started on a local OMC server, create an OMCSessionLocal object: + +``` +import OMPython +omc = OMPython.OMCSessionLocal() omc.sendExpression("command") +``` + """ from OMPython.ModelicaSystem import ( LinearizationResult, ModelicaSystem, - ModelicaSystemCmd, + ModelExecutionCmd, ModelicaSystemDoE, ModelicaSystemError, ) from OMPython.OMCSession import ( OMCPath, OMCSession, + + ModelExecutionData, + ModelExecutionException, + OMCSessionCmd, - OMCSessionException, - OMCSessionRunData, - OMCSessionZMQ, - OMCSessionPort, - OMCSessionLocal, OMCSessionDocker, OMCSessionDockerContainer, + OMCSessionException, + OMCSessionLocal, + OMCSessionPort, OMCSessionWSL, + OMCSessionZMQ, ) # global names imported if import 'from OMPython import *' is used __all__ = [ 'LinearizationResult', + + 'ModelExecutionData', + 'ModelExecutionException', + 'ModelicaSystem', - 'ModelicaSystemCmd', + 'ModelExecutionCmd', 'ModelicaSystemDoE', 'ModelicaSystemError', @@ -40,12 +51,11 @@ 'OMCSession', 'OMCSessionCmd', + 'OMCSessionDocker', + 'OMCSessionDockerContainer', 'OMCSessionException', - 'OMCSessionRunData', - 'OMCSessionZMQ', 'OMCSessionPort', 'OMCSessionLocal', - 'OMCSessionDocker', - 'OMCSessionDockerContainer', 'OMCSessionWSL', + 'OMCSessionZMQ', ] diff --git a/tests/test_ModelicaSystemCmd.py b/tests/test_ModelicaSystemCmd.py index 2480aad9..6fa2658f 100644 --- a/tests/test_ModelicaSystemCmd.py +++ b/tests/test_ModelicaSystemCmd.py @@ -23,11 +23,15 @@ def mscmd_firstorder(model_firstorder): model_file=model_firstorder, model_name="M", ) - mscmd = OMPython.ModelicaSystemCmd( - session=mod.get_session(), + + mscmd = OMPython.ModelExecutionCmd( runpath=mod.getWorkDirectory(), - modelname=mod._model_name, + cmd_local=mod.get_session().model_execution_local, + cmd_windows=mod.get_session().model_execution_windows, + cmd_prefix=mod.get_session().model_execution_prefix(cwd=mod.getWorkDirectory()), + model_name=mod._model_name, ) + return mscmd diff --git a/tests/test_ModelicaSystemDoE.py b/tests/test_ModelicaSystemDoE.py index 0e8d6caa..6b2b0993 100644 --- a/tests/test_ModelicaSystemDoE.py +++ b/tests/test_ModelicaSystemDoE.py @@ -55,12 +55,17 @@ def test_ModelicaSystemDoE_local(tmp_path, model_doe, param_doe): tmpdir = tmp_path / 'DoE' tmpdir.mkdir(exist_ok=True) - doe_mod = OMPython.ModelicaSystemDoE( + mod = OMPython.ModelicaSystem() + mod.model( model_file=model_doe, model_name="M", + ) + + doe_mod = OMPython.ModelicaSystemDoE( + mod=mod, parameters=param_doe, resultpath=tmpdir, - simargs={"override": {'stopTime': 1.0}}, + simargs={"override": {'stopTime': '1.0'}}, ) _run_ModelicaSystemDoe(doe_mod=doe_mod) @@ -72,12 +77,18 @@ def test_ModelicaSystemDoE_docker(tmp_path, model_doe, param_doe): omcs = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") assert omcs.sendExpression("getVersion()") == "OpenModelica 1.25.0" - doe_mod = OMPython.ModelicaSystemDoE( + mod = OMPython.ModelicaSystem( + session=omcs, + ) + mod.model( model_file=model_doe, model_name="M", + ) + + doe_mod = OMPython.ModelicaSystemDoE( + mod=mod, parameters=param_doe, - session=omcs, - simargs={"override": {'stopTime': 1.0}}, + simargs={"override": {'stopTime': '1.0'}}, ) _run_ModelicaSystemDoe(doe_mod=doe_mod) @@ -86,15 +97,21 @@ def test_ModelicaSystemDoE_docker(tmp_path, model_doe, param_doe): @pytest.mark.skip(reason="Not able to run WSL on github") @skip_python_older_312 def test_ModelicaSystemDoE_WSL(tmp_path, model_doe, param_doe): - tmpdir = tmp_path / 'DoE' - tmpdir.mkdir(exist_ok=True) + omcs = OMPython.OMCSessionWSL() + assert omcs.sendExpression("getVersion()") == "OpenModelica 1.25.0" - doe_mod = OMPython.ModelicaSystemDoE( + mod = OMPython.ModelicaSystem( + session=omcs, + ) + mod.model( model_file=model_doe, model_name="M", + ) + + doe_mod = OMPython.ModelicaSystemDoE( + mod=mod, parameters=param_doe, - resultpath=tmpdir, - simargs={"override": {'stopTime': 1.0}}, + simargs={"override": {'stopTime': '1.0'}}, ) _run_ModelicaSystemDoe(doe_mod=doe_mod)