Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
* Fixed a result handling issue for `dynamic_diagnostics = True` and `["<solver>_options"]["clock_step"] = False`.
* Fixed an issue for the `Master` algorithm where connection values could be initialized incorrectly, when FMUs
were initialized separately and using `step_size_downsampling_factor`.
* Enabled use of `pathlib.Path` objects for:
* Paths when loading FMUs
* `log_file_name` when loading FMUs
* `pyfmi.common.log.parse_xml_log` & `extract_xml_log`, including `fmu.extract_xml_log()`.
* `result_file_name` as simulation option and manual use of `ResultReader` classes

--- PyFMI-2.20.1 ---
* Resolved issue where caching in result handling was too persistent and could prevent automatic garbage collection.
Expand Down
18 changes: 10 additions & 8 deletions src/common/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from shutil import disk_usage
import abc
import warnings
from pathlib import Path

import numpy as np
import scipy
Expand Down Expand Up @@ -388,7 +389,7 @@ def __init__(self, filename, delimiter=";"):
Default: ";""
"""

if isinstance(filename, str):
if isinstance(filename, (str, Path)):
try:
fid = codecs.open(filename,'r','utf-8')
except FileNotFoundError as e:
Expand Down Expand Up @@ -997,7 +998,8 @@ def __init__(self,fname):
Name of file or stream object which the result is written to.
If fname is a stream, it needs to support 'readline' and 'seek'.
"""
if isinstance(fname, str):
if isinstance(fname, (str, Path)):
fname = os.path.abspath(fname)
try:
fid = codecs.open(fname,'r','utf-8')
except FileNotFoundError as e:
Expand Down Expand Up @@ -1302,8 +1304,8 @@ def __init__(self, fname, delayed_trajectory_loading = True, allow_file_updates=
Default: False
"""

if isinstance(fname, str):
self._fname = fname
if isinstance(fname, (str, Path)):
self._fname = os.path.abspath(fname)
self._is_stream = False
elif hasattr(fname, "name") and os.path.isfile(fname.name):
self._fname = fname.name
Expand Down Expand Up @@ -2134,7 +2136,7 @@ def simulation_start(self):
cont_alias_bool.append(-1 if var.alias == fmi.FMI_NEGATED_ALIAS else 1)

# Open file
if isinstance(self.file_name, str):
if isinstance(self.file_name, (str, Path)):
f = codecs.open(self.file_name,'w','utf-8')
self.file_open = True
else:
Expand Down Expand Up @@ -2254,7 +2256,7 @@ def file_name(self):
@cached_property
def _is_stream(self):
file = self.file_name
if isinstance(file, str):
if isinstance(file, (str, Path)):
return False
else:
if not (hasattr(file, 'write') and hasattr(file, 'seek')):
Expand Down Expand Up @@ -2899,7 +2901,7 @@ def simulation_start(self, diagnostics_params={}, diagnostics_vars={}):

# Open file
file_name = self.file_name
if isinstance(self.file_name, str):
if isinstance(self.file_name, (str, Path)):
self._file = open(file_name,'wb')
else:
if not (hasattr(self.file_name, 'write') and hasattr(self.file_name, 'seek') and (hasattr(self.file_name, 'tell'))):
Expand Down Expand Up @@ -3177,7 +3179,7 @@ def __init__(self, fname):
which the result is written to.
"""

if isinstance(fname, str):
if isinstance(fname, (str, Path)):
self._fname = fname
elif hasattr(fname, "name") and os.path.isfile(fname.name):
self._fname = fname.name
Expand Down
5 changes: 3 additions & 2 deletions src/common/log/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from distutils.util import strtobool
from pyfmi.common.log.tree import Node, Comment
from pyfmi.exceptions import FMUException
from pathlib import Path

## Leaf parser ##

Expand Down Expand Up @@ -217,12 +218,12 @@ def extract_xml_log(dest, log, modulename = 'Model', max_size = None):
Default: None
"""
# if it is a string, we assume we write to a file (since the file doesn't exist yet)
dest_is_file = isinstance(dest, str)
dest_is_file = isinstance(dest, (str, Path))
if not dest_is_file:
if not hasattr(dest, 'write'):
raise FMUException("If input argument 'dest' is a stream it needs to support the attribute 'write'.")

if isinstance(log, str):
if isinstance(log, (str, Path)):
with open(log, 'r') as sourcefile:
if dest_is_file:
with open(dest, 'w') as destfile:
Expand Down
14 changes: 8 additions & 6 deletions src/pyfmi/fmi.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ For profiling:
"""
import os
cimport cython
from pathlib import Path
from typing import Union

cimport pyfmi.fmil_import as FMIL

Expand Down Expand Up @@ -177,15 +179,15 @@ from pyfmi.fmi3 import (
cdef void importlogger_load_fmu(FMIL.jm_callbacks* c, FMIL.jm_string module, FMIL.jm_log_level_enu_t log_level, FMIL.jm_string message):
(<list>c.context).append("FMIL: module = %s, log level = %d: %s"%(module, log_level, message))

cpdef load_fmu(fmu, log_file_name = "", kind = 'auto',
cpdef load_fmu(fmu: Union[str, Path], log_file_name = None, kind = 'auto',
log_level = FMI_DEFAULT_LOG_LEVEL, allow_unzipped_fmu = False):
"""
Helper method for creating a model instance.

Parameters::

fmu --
Name of the fmu as a string.
Path to the fmu.

log_file_name --
Filename for file used to save log messages.
Expand All @@ -194,7 +196,7 @@ cpdef load_fmu(fmu, log_file_name = "", kind = 'auto',
for asyncio-streams, then this needs to be implemented on the user-side, there is no additional methods invoked
on the stream instance after 'write' has been invoked on the PyFMI side.
The stream must also be open and writable during the entire time.
Default: "" (Generates automatically)
Default: None = Generates automatically as <model_identifier>_log.txt

kind --
String indicating the kind of model to create. This is only
Expand Down Expand Up @@ -266,9 +268,9 @@ cpdef load_fmu(fmu, log_file_name = "", kind = 'auto',
context = FMIL.fmi_import_allocate_context(&callbacks)

# Get the FMI version of the provided model
fmu_temp_dir = pyfmi_util.encode(fmu) if allow_unzipped_fmu else pyfmi_util.encode(create_temp_dir())
fmu_full_path = pyfmi_util.encode(fmu_full_path)
version = FMI_BASE.import_and_get_version(context, fmu_full_path, fmu_temp_dir, allow_unzipped_fmu)
fmu_temp_dir = pyfmi_util.encode(fmu_full_path) if allow_unzipped_fmu else pyfmi_util.encode(create_temp_dir())
fmu_full_path_encoded = pyfmi_util.encode(fmu_full_path)
version = FMI_BASE.import_and_get_version(context, fmu_full_path_encoded, fmu_temp_dir, allow_unzipped_fmu)

# Check the version & parse XML
if version == FMIL.fmi_version_1_enu:
Expand Down
25 changes: 16 additions & 9 deletions src/pyfmi/fmi1.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import os
import logging
cimport cython
from pathlib import Path
from typing import Union

import numpy as np
cimport numpy as np
Expand Down Expand Up @@ -230,15 +232,15 @@ cdef class FMUModelBase(FMI_BASE.ModelBase):
"""
An FMI Model loaded from a DLL.
"""
def __init__(self, fmu, log_file_name="", log_level=FMI_DEFAULT_LOG_LEVEL,
def __init__(self, fmu: Union[str, Path], log_file_name=None, log_level=FMI_DEFAULT_LOG_LEVEL,
_unzipped_dir=None, _connect_dll=True, allow_unzipped_fmu = False):
"""
Constructor of the model.

Parameters::

fmu --
Name of the fmu as a string.
Path to the FMU.

log_file_name --
Filename for file used to save logmessages.
Expand All @@ -247,7 +249,7 @@ cdef class FMUModelBase(FMI_BASE.ModelBase):
for asyncio-streams, then this needs to be implemented on the user-side, there is no additional methods invoked
on the stream instance after 'write' has been invoked on the PyFMI side.
The stream must also be open and writable during the entire time.
Default: "" (Generates automatically)
Default: None = Generates automatically as <model_identifier>_log.txt

log_level --
Determines the logging output. Can be set between 0
Expand Down Expand Up @@ -297,7 +299,8 @@ cdef class FMUModelBase(FMI_BASE.ModelBase):
self._setup_log_state(log_level)
self._loaded_with_log_level = log_level

self._fmu_full_path = os.path.abspath(fmu)
fmu = os.path.abspath(fmu)
self._fmu_full_path = fmu
if _unzipped_dir:
fmu_temp_dir = pyfmi_util.encode(_unzipped_dir)
elif self._allow_unzipped_fmu:
Expand Down Expand Up @@ -389,7 +392,10 @@ cdef class FMUModelBase(FMI_BASE.ModelBase):
self._nContinuousStates = FMIL1.fmi1_import_get_number_of_continuous_states(self._fmu)
self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size)

if not isinstance(log_file_name, str):
if log_file_name is None:
log_file_name = self._get_default_log_file_name()

if not isinstance(log_file_name, (str, Path)):
self._set_log_stream(log_file_name)
for i in range(len(self._log)):
try:
Expand All @@ -400,7 +406,8 @@ cdef class FMUModelBase(FMI_BASE.ModelBase):
else:
logging.warning("Unable to log to stream.")
else:
fmu_log_name = pyfmi_util.encode((self._modelId + "_log.txt") if log_file_name=="" else log_file_name)
log_file_name = str(log_file_name) # convert e.g. pathlib.Path objects
fmu_log_name = pyfmi_util.encode(log_file_name)
self._fmu_log_name = <char*>FMIL.malloc((FMIL.strlen(fmu_log_name)+1)*sizeof(char))
FMIL.strcpy(self._fmu_log_name, fmu_log_name)

Expand Down Expand Up @@ -1713,7 +1720,7 @@ cdef class FMUModelCS1(FMUModelBase):
#First step only support fmi1_fmu_kind_enu_cs_standalone
#stepFinished not supported

def __init__(self, fmu, log_file_name="", log_level=FMI_DEFAULT_LOG_LEVEL,
def __init__(self, fmu: Union[str, Path], log_file_name=None, log_level=FMI_DEFAULT_LOG_LEVEL,
_unzipped_dir=None, _connect_dll=True, allow_unzipped_fmu = False):
#Call super
FMUModelBase.__init__(self,fmu,log_file_name, log_level, _unzipped_dir, _connect_dll, allow_unzipped_fmu)
Expand Down Expand Up @@ -2255,7 +2262,7 @@ cdef class FMUModelME1(FMUModelBase):
An FMI Model loaded from a DLL.
"""

def __init__(self, fmu, log_file_name="", log_level=FMI_DEFAULT_LOG_LEVEL,
def __init__(self, fmu: Union[str, Path], log_file_name=None, log_level=FMI_DEFAULT_LOG_LEVEL,
_unzipped_dir=None, _connect_dll=True, allow_unzipped_fmu = False):
#Call super
FMUModelBase.__init__(self,fmu,log_file_name, log_level, _unzipped_dir, _connect_dll, allow_unzipped_fmu)
Expand Down Expand Up @@ -2980,7 +2987,7 @@ cdef class FMUModelME1(FMUModelBase):
self._instantiated_fmu = 0

cdef object _load_fmi1_fmu(
fmu,
fmu: Union[str, Path],
object log_file_name,
str kind,
int log_level,
Expand Down
32 changes: 19 additions & 13 deletions src/pyfmi/fmi2.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import os
import logging
cimport cython
from pathlib import Path
from typing import Union

import numpy as np
cimport numpy as np
Expand Down Expand Up @@ -460,15 +462,15 @@ cdef class FMUModelBase2(FMI_BASE.ModelBase):
"""
FMI Model loaded from a dll.
"""
def __init__(self, fmu, log_file_name="", log_level=FMI_DEFAULT_LOG_LEVEL,
def __init__(self, fmu: Union[str, Path], log_file_name=None, log_level=FMI_DEFAULT_LOG_LEVEL,
_unzipped_dir=None, _connect_dll=True, allow_unzipped_fmu = False):
"""
Constructor of the model.

Parameters::

fmu --
Name of the fmu as a string.
Path to the FMU.

log_file_name --
Filename for file used to save logmessages.
Expand All @@ -477,7 +479,7 @@ cdef class FMUModelBase2(FMI_BASE.ModelBase):
for asyncio-streams, then this needs to be implemented on the user-side, there is no additional methods invoked
on the stream instance after 'write' has been invoked on the PyFMI side.
The stream must also be open and writable during the entire time.
Default: "" (Generates automatically)
Default: None = Generates automatically as <model_identifier>_log.txt

log_level --
Determines the logging output. Can be set between 0
Expand Down Expand Up @@ -558,7 +560,8 @@ cdef class FMUModelBase2(FMI_BASE.ModelBase):
self._setup_log_state(log_level)
self._loaded_with_log_level = log_level

self._fmu_full_path = pyfmi_util.encode(os.path.abspath(fmu))
fmu = os.path.abspath(fmu)
self._fmu_full_path = pyfmi_util.encode(fmu)
check_fmu_args(self._allow_unzipped_fmu, fmu, self._fmu_full_path)

# Create a struct for allocation
Expand Down Expand Up @@ -659,12 +662,15 @@ cdef class FMUModelBase2(FMI_BASE.ModelBase):
self._nContinuousStates = FMIL2.fmi2_import_get_number_of_continuous_states(self._fmu)
self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size)

if not isinstance(log_file_name, str):
if log_file_name is None:
log_file_name = self._get_default_log_file_name()
if not isinstance(log_file_name, (str, Path)):
self._set_log_stream(log_file_name)
for i in range(len(self._log)):
self._log_stream.write("FMIL: module = %s, log level = %d: %s\n"%(self._log[i][0], self._log[i][1], self._log[i][2]))
else:
fmu_log_name = pyfmi_util.encode((self._modelId + "_log.txt") if log_file_name=="" else log_file_name)
log_file_name = str(log_file_name) # convert e.g. pathlib.Path objects
fmu_log_name = pyfmi_util.encode(log_file_name)
self._fmu_log_name = <char*>FMIL.malloc((FMIL.strlen(fmu_log_name)+1)*sizeof(char))
FMIL.strcpy(self._fmu_log_name, fmu_log_name)

Expand Down Expand Up @@ -3564,15 +3570,15 @@ cdef class FMUModelCS2(FMUModelBase2):
"""
Co-simulation model loaded from a dll
"""
def __init__(self, fmu, log_file_name = "", log_level=FMI_DEFAULT_LOG_LEVEL,
def __init__(self, fmu: Union[str, Path], log_file_name = None, log_level=FMI_DEFAULT_LOG_LEVEL,
_unzipped_dir=None, _connect_dll=True, allow_unzipped_fmu = False):
"""
Constructor of the model.

Parameters::

fmu --
Name of the fmu as a string.
Path to the FMU.

log_file_name --
Filename for file used to save logmessages.
Expand All @@ -3581,7 +3587,7 @@ cdef class FMUModelCS2(FMUModelBase2):
for asyncio-streams, then this needs to be implemented on the user-side, there is no additional methods invoked
on the stream instance after 'write' has been invoked on the PyFMI side.
The stream must also be open and writable during the entire time.
Default: "" (Generates automatically)
Default: None = Generates automatically as <model_identifier>_log.txt

log_level --
Determines the logging output. Can be set between 0
Expand Down Expand Up @@ -4195,15 +4201,15 @@ cdef class FMUModelME2(FMUModelBase2):
Model-exchange model loaded from a dll
"""

def __init__(self, fmu, log_file_name = "", log_level=FMI_DEFAULT_LOG_LEVEL,
def __init__(self, fmu: Union[str, Path], log_file_name = None, log_level=FMI_DEFAULT_LOG_LEVEL,
_unzipped_dir=None, _connect_dll=True, allow_unzipped_fmu = False):
"""
Constructor of the model.

Parameters::

fmu --
Name of the fmu as a string.
Path to the FMU.

log_file_name --
Filename for file used to save logmessages.
Expand All @@ -4212,7 +4218,7 @@ cdef class FMUModelME2(FMUModelBase2):
for asyncio-streams, then this needs to be implemented on the user-side, there is no additional methods invoked
on the stream instance after 'write' has been invoked on the PyFMI side.
The stream must also be open and writable during the entire time.
Default: "" (Generates automatically)
Default: None = Generates automatically as <model_identifier>_log.txt

log_level --
Determines the logging output. Can be set between 0
Expand Down Expand Up @@ -5178,7 +5184,7 @@ cdef class WorkerClass2:
return ret

cdef object _load_fmi2_fmu(
fmu,
fmu: Union[str, Path],
object log_file_name,
str kind,
int log_level,
Expand Down
Loading
Loading