CATIA-Systems / FMPy

Simulate Functional Mockup Units (FMUs) in Python
Other
429 stars 118 forks source link

Potential Problem Loading Directory FMU using relative path #633

Closed clagms closed 8 months ago

clagms commented 8 months ago

I am getting an error loading an FMU from a directory using a relative path.

Here's the code on my side:


from fmpy import read_model_description, dump
from fmpy.fmi2 import FMU2Slave

LOGGING = True
def logger(msg):
    if LOGGING:
        print(msg)

def getVars(model_description):
    vrs = {}
    for variable in model_description.modelVariables:
        vrs[variable.name] = variable
    return vrs

def load(fmuPath, model_description, instanceName):
    fmu = FMU2Slave(guid=model_description.guid,
                    unzipDirectory=fmuPath,
                    modelIdentifier=model_description.coSimulation.modelIdentifier,
                    instanceName=instanceName, 
                    fmiCallLogger=logger)
    return fmu

fmucontroller_path = "model/"
fmucontroller_description = read_model_description(fmucontroller_path)
fmucontroller_vars = getVars(fmucontroller_description)
fmucontroller = load(fmucontroller_path, fmucontroller_description, "fmucontroller")   # Gives error
fmucontroller

I think the problem is that fmucontroller_path is a relative path, and I think the issue is that the following fmpy code assumes that the path for the FMU is an absolute one:

class _FMU(object):
    """ Base class for all FMUs """

    def __init__(self, guid, modelIdentifier, unzipDirectory, instanceName=None, libraryPath=None, fmiCallLogger=None, requireFunctions=True):
        """
        Parameters:
            guid             the GUI from the modelDescription.xml
            modelIdentifier  the model identifier from the modelDescription.xml
            unzipDirectory   folder where the FMU has been extracted
            instanceName     the name of the FMU instance
            libraryPath      path to the shared library
            fmiCallLogger    logger callback that takes a message as input
            requireFunctions assert required FMI functions in the shared library
        """

        self.guid = guid
        self.modelIdentifier = modelIdentifier
        self.unzipDirectory = unzipDirectory
        self.instanceName = instanceName if instanceName is not None else self.modelIdentifier
        self.fmiCallLogger = fmiCallLogger
        self.requireFunctions = requireFunctions

        # remember the current working directory
        work_dir = os.getcwd()

        if libraryPath is None:
            library_dir = os.path.join(unzipDirectory, 'binaries', platform)
            libraryPath = str(os.path.join(library_dir, self.modelIdentifier + sharedLibraryExtension))
        else:
            library_dir = os.path.dirname(libraryPath)

        # check if shared library exists
        if not os.path.isfile(libraryPath):
            raise Exception("Cannot find shared library %s." % libraryPath)

        # change to the library directory as some DLLs expect this to resolve dependencies
        os.chdir(library_dir)

        # load the shared library
        try:
            self.dll = cdll.LoadLibrary(libraryPath)
        except Exception as e:
            raise Exception("Failed to load shared library %s. %s" % (libraryPath, e))

The problem occurs when self.dll = cdll.LoadLibrary(libraryPath) gets called with a relative path (model/binaries/win64/unifmu.dll) but the current working directory is model/binaries/win64. This makes the loader look for a library model/binaries/win64/unifmu.dll inside model/binaries/win64/ which naturally leads to the following exception:

FileNotFoundError                         Traceback (most recent call last)
File [c:\Python\Python312\Lib\site-packages\fmpy\fmi1.py:164](file:///C:/Python/Python312/Lib/site-packages/fmpy/fmi1.py:164), in _FMU.__init__(self, guid, modelIdentifier, unzipDirectory, instanceName, libraryPath, fmiCallLogger, requireFunctions)
    [163](file:///C:/Python/Python312/Lib/site-packages/fmpy/fmi1.py:163)     print(libraryPath)
--> [164](file:///C:/Python/Python312/Lib/site-packages/fmpy/fmi1.py:164)     self.dll = cdll.LoadLibrary(libraryPath)
    [165](file:///C:/Python/Python312/Lib/site-packages/fmpy/fmi1.py:165) except Exception as e:

File [c:\Python\Python312\Lib\ctypes\__init__.py:460](file:///C:/Python/Python312/Lib/ctypes/__init__.py:460), in LibraryLoader.LoadLibrary(self, name)
    [459](file:///C:/Python/Python312/Lib/ctypes/__init__.py:459) def LoadLibrary(self, name):
--> [460](file:///C:/Python/Python312/Lib/ctypes/__init__.py:460)     return self._dlltype(name)

File [c:\Python\Python312\Lib\ctypes\__init__.py:379](file:///C:/Python/Python312/Lib/ctypes/__init__.py:379), in CDLL.__init__(self, name, mode, handle, use_errno, use_last_error, winmode)
    [378](file:///C:/Python/Python312/Lib/ctypes/__init__.py:378) if handle is None:
--> [379](file:///C:/Python/Python312/Lib/ctypes/__init__.py:379)     self._handle = _dlopen(self._name, mode)
    [380](file:///C:/Python/Python312/Lib/ctypes/__init__.py:380) else:

FileNotFoundError: Could not find module 'c:\work\gitlab\project_digit-bench-dlte\demonstrators\tb_lorc_halt_6dof\model_tlu\fmi2_tlu_force\tight_coupling_python\model\binaries\win64\model\binaries\win64\unifmu.dll' (or one of its dependencies). Try using the full path with constructor syntax.

I think the solution is to check if the path is relative, and then adjust the path of the loaded DLL accordingly.

Thanks for all the great work @t-sommer !

clagms commented 8 months ago

Also, a minor request, but it would be nice if the working directory could be restored in case there's an exception loading the DLL. Currently the code leaves the working directory in an inconsistent state, which breaks all code upstream 😋