Skip to content

Commit

Permalink
First steps into importing FMI3 ME models
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter Meisrimel authored and Peter Meisrimel committed Feb 6, 2025
1 parent 6e52a62 commit f6b20b9
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 45 deletions.
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ def check_extensions():
ext_list += cythonize([os.path.join("src", "pyfmi", "fmi2.pyx")],
include_path = incl_path,
compiler_directives={'language_level' : "3str"})
ext_list += cythonize([os.path.join("src", "pyfmi", "fmi3.pyx")],
include_path = incl_path,
compiler_directives={'language_level' : "3str"})

#FMI UTIL
ext_list += cythonize([os.path.join("src", "pyfmi", "fmi_util.pyx")],
Expand Down
50 changes: 15 additions & 35 deletions src/pyfmi/fmi.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ import os
cimport cython

cimport pyfmi.fmil_import as FMIL
cimport pyfmi.fmil1_import as FMIL1
cimport pyfmi.fmil2_import as FMIL2

cimport pyfmi.fmi_base as FMI_BASE
cimport pyfmi.fmi1 as FMI1
cimport pyfmi.fmi2 as FMI2
cimport pyfmi.fmi3 as FMI3

from pyfmi.common.core import create_temp_dir

Expand Down Expand Up @@ -168,6 +167,12 @@ from pyfmi.fmi2 import (
FMI_OUTPUTS,
)

from pyfmi.fmi3 import (
# Classes
FMUModelBase3,
FMUModelME3,
)

# Callbacks
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))
Expand Down Expand Up @@ -264,34 +269,7 @@ cpdef load_fmu(fmu, log_file_name = "", kind = 'auto',
fmu_full_path = FMI_BASE.encode(fmu_full_path)
version = FMI_BASE.import_and_get_version(context, fmu_full_path, fmu_temp_dir, allow_unzipped_fmu)

# Check the version
if version == FMIL.fmi_version_unknown_enu:
# Delete context
last_error = FMIL.jm_get_last_error(&callbacks)
FMIL.fmi_import_free_context(context)
if not allow_unzipped_fmu:
FMIL.fmi_import_rmdir(&callbacks, fmu_temp_dir)
if callbacks.log_level >= FMIL.jm_log_level_error:
_handle_load_fmu_exception(fmu, log_data)
raise InvalidVersionException("The FMU could not be loaded. The FMU version could not be determined. "+FMI_BASE.decode(last_error))
else:
_handle_load_fmu_exception(fmu, log_data)
raise InvalidVersionException("The FMU could not be loaded. The FMU version could not be determined. Enable logging for possibly more information.")

if version > 2: # TODO: FMI3 remove this part?; simply use the final else in the if version = ... elif ... else ?
# Delete the context
last_error = FMIL.jm_get_last_error(&callbacks)
FMIL.fmi_import_free_context(context)
if not allow_unzipped_fmu:
FMIL.fmi_import_rmdir(&callbacks, fmu_temp_dir)
if callbacks.log_level >= FMIL.jm_log_level_error:
_handle_load_fmu_exception(fmu, log_data)
raise InvalidVersionException("The FMU could not be loaded. The FMU version is unsupported. "+FMI_BASE.decode(last_error))
else:
_handle_load_fmu_exception(fmu, log_data)
raise InvalidVersionException("The FMU could not be loaded. The FMU version is unsupported. Enable logging for possibly more information.")

# Parse the xml
# Check the version & parse XML
if version == FMIL.fmi_version_1_enu:
model = FMI1._load_fmi1_fmu(
fmu, log_file_name, kind, log_level, allow_unzipped_fmu,
Expand All @@ -302,19 +280,21 @@ cpdef load_fmu(fmu, log_file_name = "", kind = 'auto',
fmu, log_file_name, kind, log_level, allow_unzipped_fmu,
context, fmu_temp_dir, callbacks, log_data
)
elif version == FMIL.fmi_version_3_0_enu:
model = FMI3._load_fmi3_fmu(
fmu, log_file_name, kind, log_level, allow_unzipped_fmu,
context, fmu_temp_dir, callbacks, log_data
)
else:
# This else-statement ensures that the variables "context" and "version" are defined before proceeding

# Delete the context
last_error = FMIL.jm_get_last_error(&callbacks)
FMIL.fmi_import_free_context(context)
if not allow_unzipped_fmu:
FMIL.fmi_import_rmdir(&callbacks, fmu_temp_dir)
if callbacks.log_level >= FMIL.jm_log_level_error:
_handle_load_fmu_exception(fmu, log_data)
raise InvalidVersionException("The FMU could not be loaded. The FMU version is not found. "+FMI_BASE.decode(last_error))
raise InvalidVersionException("The FMU could not be loaded. The FMU version is unsupported. " + FMI_BASE.decode(last_error))
else:
_handle_load_fmu_exception(fmu, log_data)
raise InvalidVersionException("The FMU could not be loaded. The FMU version is not found. Enable logging for possibly more information.")
raise InvalidVersionException("The FMU could not be loaded. The FMU version is unsupported. Enable logging for possibly more information.")

return model
2 changes: 1 addition & 1 deletion src/pyfmi/fmi2.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

# Module containing the FMI interface Python wrappers.
# Module containing the FMI2 interface Python wrappers.

import numpy as np
cimport numpy as np
Expand Down
2 changes: 1 addition & 1 deletion src/pyfmi/fmi2.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -5273,7 +5273,7 @@ cdef object _load_fmi2_fmu(
list log_data
):
"""
The FMI1 part of fmi.pyx load_fmu.
The FMI2 part of fmi.pyx load_fmu.
"""
# TODO: Tons of duplicated code here for error handling
cdef FMIL.jm_string last_error
Expand Down
40 changes: 40 additions & 0 deletions src/pyfmi/fmi3.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (C) 2025 Modelon AB
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

# Module containing the FMI3 interface Python wrappers.

cimport pyfmi.fmil_import as FMIL
cimport pyfmi.fmil3_import as FMIL3
cimport pyfmi.fmi_base as FMI_BASE

cdef class FMUModelBase3(FMI_BASE.ModelBase):
pass

cdef class FMUModelME3(FMUModelBase3):
pass

cdef object _load_fmi3_fmu(
fmu,
object log_file_name,
str kind,
int log_level,
int allow_unzipped_fmu,
FMIL.fmi_import_context_t* context,
bytes fmu_temp_dir,
FMIL.jm_callbacks callbacks,
list log_data
)
50 changes: 50 additions & 0 deletions src/pyfmi/fmi3.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (C) 2025Modelon AB
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

# distutils: define_macros=NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION

cimport cython

cimport pyfmi.fmil_import as FMIL
cimport pyfmi.fmil3_import as FMIL3
cimport pyfmi.fmi_base as FMI_BASE

cdef class FMUModelBase3(FMI_BASE.ModelBase):
"""TODO"""
def __init__(self):
pass

cdef class FMUModelME3(FMUModelBase3):
"""TODO"""
def __init__(self):
pass

cdef object _load_fmi3_fmu(
fmu,
object log_file_name,
str kind,
int log_level,
int allow_unzipped_fmu,
FMIL.fmi_import_context_t* context,
bytes fmu_temp_dir,
FMIL.jm_callbacks callbacks,
list log_data
):
"""
The FMI3 part of fmi.pyx load_fmu.
"""
return FMUModelME3()
4 changes: 2 additions & 2 deletions src/pyfmi/fmi_coupled.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ cimport numpy as np
cimport pyfmi.fmil2_import as FMIL2
cimport pyfmi.fmi2 as FMI2
from pyfmi.fmi2 import FMI2_INPUT, FMI2_OUTPUT
from pyfmi.fmi_base import PyEventInfo
from pyfmi.fmi_util import enable_caching, Graph
from pyfmi.fmi_base import PyEventInfo, enable_caching
from pyfmi.fmi_util import Graph
from pyfmi.exceptions import FMUException, InvalidFMUException

from collections import OrderedDict
Expand Down
4 changes: 2 additions & 2 deletions src/pyfmi/fmil2_import.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ from fmil_import cimport (
)

cdef extern from 'fmilib.h':
#FMI VARIABLE TYPE DEFINITIONS
# FMI VARIABLE TYPE DEFINITIONS
ctypedef double fmi2_real_t
ctypedef int fmi2_boolean_t
ctypedef void* fmi2_component_t
Expand All @@ -40,7 +40,7 @@ cdef extern from 'fmilib.h':
ctypedef void* fmi2_component_environment_t
ctypedef size_t fmi2_value_reference_t

#STRUCTS
# STRUCTS
ctypedef enum fmi2_boolean_enu_t:
fmi2_true = 1
fmi2_false = 0
Expand Down
132 changes: 132 additions & 0 deletions src/pyfmi/fmil3_import.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-

# Copyright (C) 2025 Modelon AB
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

#==============================================
# C headers
#==============================================

# This file contains FMIL header content specific to FMI3
cimport pyfmi.fmil_import as FMIL
from libcpp cimport bool # TODO: Possible issue due to https://github.com/cython/cython/issues/5730 ??


# XXX: Copy more header content from FMI3 as it becomes necessary

cdef extern from 'fmilib.h':
# FMI VARIABLE TYPE DEFINITIONS
ctypedef bool fmi3_boolean_t
ctypedef void* fmi3_instance_environment_t
ctypedef const char* fmi3_string_t
ctypedef double fmi3_float64_t

# STATUS
ctypedef enum fmi3_status_t:
fmi3_status_ok = 0
fmi3_status_warning = 1
fmi3_status_discard = 2
fmi3_status_error = 3
fmi3_status_fatal = 4
cdef enum fmi3_fmu_kind_enu_t:
fmi3_fmu_kind_unknown = 1
fmi3_fmu_kind_me = 2
fmi3_fmu_kind_cs = 4
fmi3_fmu_kind_se = 8

# LOGGING
ctypedef void (*fmi3_log_message_callback_ft) (
fmi3_instance_environment_t instanceEnvironment,
fmi3_status_t status,
fmi3_string_t category,
fmi3_string_t messages
)
ctypedef int(*fmi3_xml_element_start_handle_ft)(void*, char*, void*, char*, char**)
ctypedef int(*fmi3_xml_element_data_handle_ft)(void*, char*, int)
ctypedef int(*fmi3_xml_element_end_handle_ft)(void*, char*)
cdef struct fmi3_xml_callbacks_t:
fmi3_xml_element_start_handle_ft startHandle
fmi3_xml_element_data_handle_ft dataHandle
fmi3_xml_element_end_handle_ft endHandle
void* context

cdef struct fmi3_import_t:
pass
# FMI SPECIFICATION METHODS (3.0)
# BASIC
FMIL.jm_status_enu_t fmi3_import_create_dllfmu(fmi3_import_t*, fmi3_fmu_kind_enu_t, const fmi3_instance_environment_t, const fmi3_log_message_callback_ft)
# TODO: CS, SE
# FMIL.jm_status_enu_t fmi3_import_instantiate_model_exchange(
# fmi3_import_t* fmu,
# fmi3_string_t instanceName,
# fmi3_string_t resourcePath,
# fmi3_boolean_t visible,
# fmi3_boolean_t loggingOn
# )
# TODO: This will change with next FMIL release
FMIL.jm_status_enu_t fmi3_import_instantiate_model_exchange(
fmi3_import_t* fmu,
fmi3_string_t instanceName,
fmi3_string_t resourcePath,
fmi3_boolean_t visible,
fmi3_boolean_t loggingOn,
fmi3_instance_environment_t instanceEnvironment,
fmi3_log_message_callback_ft logMessage
)
void fmi3_import_free_instance(fmi3_import_t* fmu)
FMIL.jm_status_enu_t fmi3_import_terminate(fmi3_import_t*)
# modes
FMIL.jm_status_enu_t fmi3_import_enter_initialization_mode(
fmi3_import_t* fmu,
fmi3_boolean_t toleranceDefined,
fmi3_float64_t tolerance,
fmi3_float64_t startTime,
fmi3_boolean_t stopTimeDefined,
fmi3_float64_t stopTime
);
FMIL.jm_status_enu_t fmi3_import_exit_initialization_mode(fmi3_import_t* fmu)
FMIL.jm_status_enu_t fmi3_import_enter_continuous_time_mode(fmi3_import_t* fmu)

# misc

# setting

# getting

# save states

# FMI HELPER METHODS (3.0)
const char* fmi3_import_get_model_name(fmi3_import_t*)
fmi3_fmu_kind_enu_t fmi3_import_get_fmu_kind(fmi3_import_t*)

# FMI XML METHODS

### Model information

# CONVERTER METHODS

# INTEGER

# OTHER HELPER METHODS

# Does NOT invoke CAPI calls
# TODO: Categorize these a bit better
void fmi3_import_free(fmi3_import_t*)
char* fmi3_fmu_kind_to_string(fmi3_fmu_kind_enu_t)
const char* fmi3_import_get_model_identifier_CS(fmi3_import_t*)
const char* fmi3_import_get_last_error(fmi3_import_t*)
void fmi3_log_forwarding(fmi3_instance_environment_t, fmi3_status_t, fmi3_string_t, fmi3_string_t)
fmi3_import_t* fmi3_import_parse_xml(FMIL.fmi_import_context_t*, char*, fmi3_xml_callbacks_t*)
const char* fmi3_import_get_model_identifier_ME(fmi3_import_t*)
void fmi3_import_destroy_dllfmu(fmi3_import_t*)
3 changes: 2 additions & 1 deletion src/pyfmi/fmil_import.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ cdef extern from 'fmilib.h':
fmi_version_unknown_enu = 0
fmi_version_1_enu = 1
fmi_version_2_0_enu = 2
fmi_version_unsupported_enu = 3
fmi_version_3_0_enu = 3
fmi_version_unsupported_enu = 4

ctypedef int(*jm_compare_ft)(void *, void *)
ctypedef jm_voidp(*jm_malloc_f)(size_t)
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def setup_reference_fmus():
Note that this requires an internet connection to work.
"""

def download_url(url, save_file_to, chunk_size=1024):
def download_url(url, save_file_to):
""" Download file from URL to 'save_file_to' in chunks. """
try:
with urllib.request.urlopen(url) as file_to_download:
Expand Down Expand Up @@ -52,4 +52,4 @@ def download_url(url, save_file_to, chunk_size=1024):
if fobj.filename.startswith('3.0') and fobj.filename.endswith('.fmu'):
zf.extract(fobj, zip_unzip_to)
with open(md5_file, 'w') as f:
f.write(md5)
f.write(md5)
Loading

0 comments on commit f6b20b9

Please sign in to comment.