diff --git a/cadet/cadet.py b/cadet/cadet.py index 5a63636..e814198 100644 --- a/cadet/cadet.py +++ b/cadet/cadet.py @@ -518,7 +518,7 @@ def run_load( def run_simulation( self, - timeout: Optional[int] = None, + timeout: Optional[float] = None, clear: bool = True ) -> ReturnInformation: """ @@ -526,7 +526,7 @@ def run_simulation( Parameters ---------- - timeout : Optional[int] + timeout : Optional[float] Maximum time allowed for the simulation to run, in seconds. clear : bool If True, clear the simulation results from the current runner instance. @@ -551,14 +551,14 @@ def run_simulation( def run( self, - timeout: Optional[int] = None, + timeout: Optional[float] = None, ) -> ReturnInformation: """ Run the CADET simulation. Parameters ---------- - timeout : Optional[int] + timeout : Optional[float] Maximum time allowed for the simulation to run, in seconds. Returns diff --git a/cadet/cadet_dll.py b/cadet/cadet_dll.py index 162e87f..5ae50b1 100644 --- a/cadet/cadet_dll.py +++ b/cadet/cadet_dll.py @@ -2,8 +2,9 @@ import io import os from pathlib import Path -from typing import Any, Optional +from typing import Any, Optional, Union +from packaging.version import Version import addict import numpy @@ -26,7 +27,7 @@ _CDT_DATA_NOT_STORED = -3 -class CADETAPIV010000_DATA: +class CADET_API_V1_SIGNATURES: """ Definition of CADET-C-API v1.0 function signatures and type mappings. @@ -42,65 +43,68 @@ class CADETAPIV010000_DATA: # API function signatures # Note, order is important, it has to match the cdtAPIv010000 struct of the C-API - signatures = {} - - signatures['getFileFormat'] = ('return', 'fileFormat') - - signatures['createDriver'] = ('drv',) - signatures['deleteDriver'] = (None, 'drv') - signatures['runSimulation'] = ('return', 'drv', 'parameterProvider') - - signatures['getNumUnitOp'] = ('return', 'drv', 'nUnits') - signatures['getNumParTypes'] = ('return', 'drv', 'unitOpId', 'nParTypes') - signatures['getNumSensitivities'] = ('return', 'drv', 'nSens') - - signatures['getSolutionInlet'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nPort', 'nComp') - signatures['getSolutionOutlet'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nPort', 'nComp') - signatures['getSolutionBulk'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nComp', 'keepAxialSingletonDimension') - signatures['getSolutionParticle'] = ('return', 'drv', 'unitOpId', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nComp', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') - signatures['getSolutionSolid'] = ('return', 'drv', 'unitOpId', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nBound', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') - signatures['getSolutionFlux'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParTypes', 'nComp', 'keepAxialSingletonDimension') - signatures['getSolutionVolume'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime') - - signatures['getSolutionDerivativeInlet'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nPort', 'nComp') - signatures['getSolutionDerivativeOutlet'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nPort', 'nComp') - signatures['getSolutionDerivativeBulk'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nComp', 'keepAxialSingletonDimension') - signatures['getSolutionDerivativeParticle'] = ('return', 'drv', 'unitOpId', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nComp', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') - signatures['getSolutionDerivativeSolid'] = ('return', 'drv', 'unitOpId', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nBound', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') - signatures['getSolutionDerivativeFlux'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParTypes', 'nComp', 'keepAxialSingletonDimension') - signatures['getSolutionDerivativeVolume'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime') - - signatures['getSensitivityInlet'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nPort', 'nComp') - signatures['getSensitivityOutlet'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nPort', 'nComp') - signatures['getSensitivityBulk'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nComp', 'keepAxialSingletonDimension') - signatures['getSensitivityParticle'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nComp', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') - signatures['getSensitivitySolid'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nBound', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') - signatures['getSensitivityFlux'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParTypes', 'nComp', 'keepAxialSingletonDimension') - signatures['getSensitivityVolume'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime') - - signatures['getSensitivityDerivativeInlet'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nPort', 'nComp') - signatures['getSensitivityDerivativeOutlet'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nPort', 'nComp') - signatures['getSensitivityDerivativeBulk'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nComp', 'keepAxialSingletonDimension') - signatures['getSensitivityDerivativeParticle'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nComp', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') - signatures['getSensitivityDerivativeSolid'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nBound', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') - signatures['getSensitivityDerivativeFlux'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParTypes', 'nComp', 'keepAxialSingletonDimension') - signatures['getSensitivityDerivativeVolume'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime') - - signatures['getLastState'] = ('return', 'drv', 'state', 'nStates') - signatures['getLastStateTimeDerivative'] = ('return', 'drv', 'state', 'nStates') - signatures['getLastUnitState'] = ('return', 'drv', 'unitOpId', 'state', 'nStates') - signatures['getLastUnitStateTimeDerivative'] = ('return', 'drv', 'unitOpId', 'state', 'nStates') - signatures['getLastSensitivityState'] = ('return', 'drv', 'sensIdx', 'state', 'nStates') - signatures['getLastSensitivityStateTimeDerivative'] = ('return', 'drv', 'sensIdx', 'state', 'nStates') - signatures['getLastSensitivityUnitState'] = ('return', 'drv', 'sensIdx', 'unitOpId', 'state', 'nStates') - signatures['getLastSensitivityUnitStateTimeDerivative'] = ('return', 'drv', 'sensIdx', 'unitOpId', 'state', 'nStates') - - signatures['getPrimaryCoordinates'] = ('return', 'drv', 'unitOpId', 'coords', 'nCoords') - signatures['getSecondaryCoordinates'] = ('return', 'drv', 'unitOpId', 'coords', 'nCoords') - signatures['getParticleCoordinates'] = ('return', 'drv', 'unitOpId', 'parType', 'coords', 'nCoords') - signatures['getSolutionTimes'] = ('return', 'drv', 'time', 'nTime') - - signatures['getTimeSim'] = ('return', 'drv', 'timeSim') + signatures_1_0_0 = {} + + signatures_1_0_0['getFileFormat'] = ('return', 'fileFormat') + + signatures_1_0_0['createDriver'] = ('drv',) + signatures_1_0_0['deleteDriver'] = (None, 'drv') + signatures_1_0_0['runSimulation'] = ('return', 'drv', 'parameterProvider') + + signatures_1_0_0['getNumUnitOp'] = ('return', 'drv', 'nUnits') + signatures_1_0_0['getNumParTypes'] = ('return', 'drv', 'unitOpId', 'nParTypes') + signatures_1_0_0['getNumSensitivities'] = ('return', 'drv', 'nSens') + + signatures_1_0_0['getSolutionInlet'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nPort', 'nComp') + signatures_1_0_0['getSolutionOutlet'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nPort', 'nComp') + signatures_1_0_0['getSolutionBulk'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nComp', 'keepAxialSingletonDimension') + signatures_1_0_0['getSolutionParticle'] = ('return', 'drv', 'unitOpId', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nComp', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') + signatures_1_0_0['getSolutionSolid'] = ('return', 'drv', 'unitOpId', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nBound', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') + signatures_1_0_0['getSolutionFlux'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParTypes', 'nComp', 'keepAxialSingletonDimension') + signatures_1_0_0['getSolutionVolume'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime') + + signatures_1_0_0['getSolutionDerivativeInlet'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nPort', 'nComp') + signatures_1_0_0['getSolutionDerivativeOutlet'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nPort', 'nComp') + signatures_1_0_0['getSolutionDerivativeBulk'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nComp', 'keepAxialSingletonDimension') + signatures_1_0_0['getSolutionDerivativeParticle'] = ('return', 'drv', 'unitOpId', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nComp', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') + signatures_1_0_0['getSolutionDerivativeSolid'] = ('return', 'drv', 'unitOpId', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nBound', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') + signatures_1_0_0['getSolutionDerivativeFlux'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParTypes', 'nComp', 'keepAxialSingletonDimension') + signatures_1_0_0['getSolutionDerivativeVolume'] = ('return', 'drv', 'unitOpId', 'time', 'data', 'nTime') + + signatures_1_0_0['getSensitivityInlet'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nPort', 'nComp') + signatures_1_0_0['getSensitivityOutlet'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nPort', 'nComp') + signatures_1_0_0['getSensitivityBulk'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nComp', 'keepAxialSingletonDimension') + signatures_1_0_0['getSensitivityParticle'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nComp', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') + signatures_1_0_0['getSensitivitySolid'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nBound', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') + signatures_1_0_0['getSensitivityFlux'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParTypes', 'nComp', 'keepAxialSingletonDimension') + signatures_1_0_0['getSensitivityVolume'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime') + + signatures_1_0_0['getSensitivityDerivativeInlet'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nPort', 'nComp') + signatures_1_0_0['getSensitivityDerivativeOutlet'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nPort', 'nComp') + signatures_1_0_0['getSensitivityDerivativeBulk'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nComp', 'keepAxialSingletonDimension') + signatures_1_0_0['getSensitivityDerivativeParticle'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nComp', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') + signatures_1_0_0['getSensitivityDerivativeSolid'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'parType', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParShells', 'nBound', 'keepAxialSingletonDimension', 'keepParticleSingletonDimension') + signatures_1_0_0['getSensitivityDerivativeFlux'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime', 'nAxialCells', 'nRadialCells', 'nParTypes', 'nComp', 'keepAxialSingletonDimension') + signatures_1_0_0['getSensitivityDerivativeVolume'] = ('return', 'drv', 'unitOpId', 'sensIdx', 'time', 'data', 'nTime') + + signatures_1_0_0['getLastState'] = ('return', 'drv', 'state', 'nStates') + signatures_1_0_0['getLastStateTimeDerivative'] = ('return', 'drv', 'state', 'nStates') + signatures_1_0_0['getLastUnitState'] = ('return', 'drv', 'unitOpId', 'state', 'nStates') + signatures_1_0_0['getLastUnitStateTimeDerivative'] = ('return', 'drv', 'unitOpId', 'state', 'nStates') + signatures_1_0_0['getLastSensitivityState'] = ('return', 'drv', 'sensIdx', 'state', 'nStates') + signatures_1_0_0['getLastSensitivityStateTimeDerivative'] = ('return', 'drv', 'sensIdx', 'state', 'nStates') + signatures_1_0_0['getLastSensitivityUnitState'] = ('return', 'drv', 'sensIdx', 'unitOpId', 'state', 'nStates') + signatures_1_0_0['getLastSensitivityUnitStateTimeDerivative'] = ('return', 'drv', 'sensIdx', 'unitOpId', 'state', 'nStates') + + signatures_1_0_0['getPrimaryCoordinates'] = ('return', 'drv', 'unitOpId', 'coords', 'nCoords') + signatures_1_0_0['getSecondaryCoordinates'] = ('return', 'drv', 'unitOpId', 'coords', 'nCoords') + signatures_1_0_0['getParticleCoordinates'] = ('return', 'drv', 'unitOpId', 'parType', 'coords', 'nCoords') + signatures_1_0_0['getSolutionTimes'] = ('return', 'drv', 'time', 'nTime') + + signatures_1_0_0['getTimeSim'] = ('return', 'drv', 'timeSim') + + signatures_1_1_0a1 = {} + signatures_1_1_0a1['timeout'] = ('return', 'drv', 'timeout') # Mappings for common ctypes parameters lookup_prototype = { @@ -131,6 +135,7 @@ class CADETAPIV010000_DATA: 'keepAxialSingletonDimension': point_bool, 'keepParticleSingletonDimension': point_bool, 'timeSim': point_double, + 'timeout': point_double, } lookup_output_argument_type = { @@ -154,31 +159,48 @@ class CADETAPIV010000_DATA: 'keepAxialSingletonDimension': ctypes.c_bool, 'keepParticleSingletonDimension': ctypes.c_bool, 'timeSim': ctypes.c_double, + 'timeout': ctypes.c_double, } +_VERSION_SIGNATURES: dict[Version, dict] = {} +_VERSION_SIGNATURES[Version("1.0.0")] = dict(CADET_API_V1_SIGNATURES.signatures_1_0_0) -def _setup_api() -> list[tuple[str, ctypes.CFUNCTYPE]]: - """ - Set up the API function prototypes for CADETAPIV010000. +_sigs_1_1_0a1 = dict(CADET_API_V1_SIGNATURES.signatures_1_0_0) +_sigs_1_1_0a1.update(CADET_API_V1_SIGNATURES.signatures_1_1_0a1) +_VERSION_SIGNATURES[Version("1.1.0a1")] = _sigs_1_1_0a1 - Returns - ------- - list of tuple - List of function names and corresponding ctypes function prototypes. +def _get_api_signatures(api: Any) -> dict[str, tuple[str, ...]]: + return _VERSION_SIGNATURES[api._version] + +def _setup_api(version: Version) -> list[tuple[str, ctypes.CFUNCTYPE]]: + """ + Set up the API function prototypes for a given CADET API signature table. """ - _fields_ = [] - for key, value in CADETAPIV010000_DATA.signatures.items(): - args = tuple(CADETAPIV010000_DATA.lookup_prototype[key] for key in value) - _fields_.append((key, ctypes.CFUNCTYPE(*args))) + + signatures = _VERSION_SIGNATURES[version] + fields = [] + + for name, value in signatures.items(): + args = tuple(CADET_API_V1_SIGNATURES.lookup_prototype[arg_name] for arg_name in value) + fields.append((name, ctypes.CFUNCTYPE(*args))) + + return fields - return _fields_ +class CADETAPI_V1_0_0(ctypes.Structure): + """Mimic cdtAPIv1.0.0 struct of CADET C-API in ctypes.""" + _version = Version("1.0.0") + _fields_ = _setup_api(_version) -class CADETAPIV010000(ctypes.Structure): - """Mimic cdtAPIv010000 struct of CADET C-API in ctypes.""" - _fields_ = _setup_api() +CADETAPIV010000 = CADETAPI_V1_0_0 +class CADETAPI_V1_1_0a1(ctypes.Structure): + """Mimic cdtAPIv1.1.0a.1 struct of CADET C-API in ctypes.""" + _version = Version("1.1.0a1") + _fields_ = _setup_api(_version) + + class SimulationResult: """ Handles reading results from a CADET simulation. @@ -192,7 +214,10 @@ class SimulationResult: """ - def __init__(self, api: CADETAPIV010000, driver: CadetDriver) -> None: + def __init__( + self, + api: Union[CADETAPI_V1_0_0, CADETAPI_V1_1_0a1], driver: CadetDriver + ) -> None: self._api = api self._driver = driver @@ -228,21 +253,29 @@ def _load_data( call_args = [] call_outputs = {} + signatures = _get_api_signatures(self._api) + # Construct API call function arguments - for key in CADETAPIV010000_DATA.signatures[get_solution_str]: + for key in signatures[get_solution_str]: if key == 'return': # Skip, this is the actual return value of the API function continue elif key == 'drv': call_args.append(self._driver) - elif key == 'unitOpId' and unitOpId is not None: + elif key == 'unitOpId': + if unitOpId is None: + raise ValueError(f"{get_solution_str} requires unitOpId") call_args.append(unitOpId) elif key == 'sensIdx': + if sensIdx is None: + raise ValueError(f"{get_solution_str} requires sensIdx") call_args.append(sensIdx) elif key == 'parType': + if parType is None: + raise ValueError(f"{get_solution_str} requires parType") call_args.append(parType) else: - _obj = CADETAPIV010000_DATA.lookup_output_argument_type[key]() + _obj = CADET_API_V1_SIGNATURES.lookup_output_argument_type[key]() call_outputs[key] = _obj call_args.append(ctypes.byref(_obj)) @@ -1672,26 +1705,50 @@ def _initialize_dll(self): # Query API try: - cdtGetLatestCAPIVersion = self._lib.cdtGetLatestCAPIVersion# + cdtGetLatestCAPIVersion = self._lib.cdtGetLatestCAPIVersion except AttributeError: raise ValueError( "CADET-Python does not support CADET-CAPI at all." ) cdtGetLatestCAPIVersion.restype = ctypes.c_char_p - self._cadet_capi_version = cdtGetLatestCAPIVersion().decode('utf-8') - - # Check which C-API is provided by CADET (given the current install path) - if self._cadet_capi_version == "1.0.0": - cdtGetAPIv010000 = self._lib.cdtGetAPIv010000 - cdtGetAPIv010000.argtypes = [ctypes.POINTER(CADETAPIV010000)] - cdtGetAPIv010000.restype = c_cadet_result - self._api = CADETAPIV010000() - cdtGetAPIv010000(ctypes.byref(self._api)) - else: - raise ValueError( - "CADET-Python does not support CADET-CAPI version " + self._cadet_capi_version = Version(cdtGetLatestCAPIVersion().decode('utf-8')) + + # Use the latest supported C-API if applicable, i.e. + # - unsupported major version -> error + # - minor/patch versions later than the last supported version fall back to the last supported version + if self._cadet_capi_version < Version("1.0.0") or self._cadet_capi_version >= Version("2.0.0"): + + raise TypeError( + "This version of CADET-Python does not support CADET-CAPI version " f"({self._cadet_capi_version})." ) + + elif self._cadet_capi_version >= Version("1.1.0a1"): + cdtGetAPIv1_1_0a1 = self._lib.cdtGetAPIv1_1_0a1 + cdtGetAPIv1_1_0a1.argtypes = [ctypes.POINTER(CADETAPI_V1_1_0a1)] + cdtGetAPIv1_1_0a1.restype = c_cadet_result + self._api = CADETAPI_V1_1_0a1() + cdtGetAPIv1_1_0a1(ctypes.byref(self._api)) + + elif self._cadet_capi_version == Version("1.0.0"): + + # Support of old CAPI version semantic + if Version(self._cadet_version) < Version("6.0.0a3"): + cdtGetAPIv1_0_0 = self._lib.cdtGetAPIv010000 + else: + cdtGetAPIv1_0_0 = self._lib.cdtGetAPIv1_0_0 + + cdtGetAPIv1_0_0.argtypes = [ctypes.POINTER(CADETAPI_V1_0_0)] + cdtGetAPIv1_0_0.restype = c_cadet_result + self._api = CADETAPI_V1_0_0() + cdtGetAPIv1_0_0(ctypes.byref(self._api)) + + else: # this case must not happen and if it happens, the above logic is incomplete. + + raise TypeError( + "This version of CADET-Python does not support CADET-CAPI version " + f"({self._cadet_capi_version}). Check CADET-Python implementation of CAPI support logic." + ) self._driver = self._api.createDriver() self.res: Optional[SimulationResult] = None @@ -1766,7 +1823,7 @@ def log_handler(file, func, line, level, level_name, message): def run( self, simulation: Optional["Cadet"] = None, - timeout: Optional[int] = None, + timeout: Optional[float] = None, ) -> ReturnInformation: """ Run a CADET simulation using the DLL interface. @@ -1783,6 +1840,12 @@ def run( RuntimeError If the simulation process returns a non-zero exit code. """ + if timeout is not None: + if(self._cadet_capi_version < Version("1.1.0a1")): + raise TypeError( + "timeout is not support CADET-CAPI version: " + f"({self._cadet_capi_version})." + ) pp = cadet_dll_parameterprovider.PARAMETERPROVIDER(simulation) log_buffer = self.setup_log_buffer() @@ -1975,11 +2038,6 @@ def load_state(self, sim: "Cadet") -> None: soldot_last_unit = self.res.last_state_ydot_unit(unit) solution[unit_index]['last_state_ydot'] = soldot_last_unit - @staticmethod - def _get_index_string(prefix: str, index: int) -> str: - """Get a formatted string index (e.g., ('unit', 0) -> 'unit_000').""" - return f'{prefix}_{index:03d}' - def _checks_if_write_is_true(func): """Decorator to check if unit operation solution should be written out.""" def wrapper(self, sim, unitOpId, solution_str, *args, **kwargs): diff --git a/cadet/runner.py b/cadet/runner.py index 602e681..ec0afa6 100644 --- a/cadet/runner.py +++ b/cadet/runner.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from pathlib import Path from typing import Optional +from packaging.version import Version @dataclass @@ -38,7 +39,7 @@ class CadetRunnerBase(ABC): def run( self, simulation: "Cadet", - timeout: Optional[int] = None, + timeout: Optional[float] = None, ) -> ReturnInformation: """ Run a CADET simulation. @@ -126,7 +127,7 @@ def __init__(self, cadet_path: str | os.PathLike) -> None: def run( self, simulation: "Cadet", - timeout: Optional[int] = None, + timeout: Optional[float] = None, ) -> ReturnInformation: """ Run a CADET simulation using the CLI executable. @@ -135,7 +136,7 @@ def run( ---------- simulation : Cadet Not used in this runner. - timeout : Optional[int] + timeout : Optional[float] Maximum time allowed for the simulation to run, in seconds. Raises diff --git a/tests/test_capi_versioning.py b/tests/test_capi_versioning.py new file mode 100644 index 0000000..d1f3663 --- /dev/null +++ b/tests/test_capi_versioning.py @@ -0,0 +1,148 @@ +""" +Unit tests for CADET-C-API versioning support in cadet_dll.py. + +Tests the following aspects of CAPI versioning: +1. Support for CAPI version 1.0.0 +2. Support for CAPI version 1.1.0a1 and its timeout function +3. Error handling for unsupported versions +4. Old CAPI version semantic (cdtGetAPIv010000 vs cdtGetAPIv1_0_0) +""" + +import pytest +from unittest.mock import MagicMock, patch +from pathlib import Path +from packaging.version import Version + +from cadet.cadet_dll import ( + CADETAPI_V1_0_0, + CADETAPI_V1_1_0a1, + _setup_api, + CadetDLLRunner, +) + + +# Tests for ctypes Structure Definitions + +def test_v1_0_0_structure_version(): + """Test that CADETAPI_V1_0_0 has correct version.""" + assert CADETAPI_V1_0_0._version == Version("1.0.0") + + +def test_v1_1_0a1_structure_version(): + """Test that CADETAPI_V1_1_0a1 has correct version.""" + assert CADETAPI_V1_1_0a1._version == Version("1.1.0a1") + + +def test_v1_0_0_v1_0_0a1_fields(): + """Test that v1.0.0 fields are a subset of v1.1.0a1 fields.""" + v1_0_0_fields = {field[0] for field in CADETAPI_V1_0_0._fields_} + v1_1_0a1_fields = {field[0] for field in CADETAPI_V1_1_0a1._fields_} + + assert 'timeout' in v1_1_0a1_fields + assert 'timeout' not in v1_0_0_fields + + assert v1_0_0_fields.issubset(v1_1_0a1_fields) + + +# Tests for _setup_api Function + +def test_setup_api_v1_0_0(): + """Test _setup_api correctly sets up v1.0.0 API fields.""" + fields = _setup_api(Version("1.0.0")) + field_names = [field[0] for field in fields] + + assert 'createDriver' in field_names + assert 'deleteDriver' in field_names + assert 'runSimulation' in field_names + assert 'timeout' not in field_names + + +def test_setup_api_v1_1_0a1(): + """Test _setup_api correctly sets up v1.1.0a1 API fields.""" + fields = _setup_api(Version("1.1.0a1")) + field_names = [field[0] for field in fields] + + assert 'createDriver' in field_names + assert 'deleteDriver' in field_names + assert 'runSimulation' in field_names + assert 'timeout' in field_names + + +def test_setup_api_fields_are_cfunctype(): + """Test that all setup API fields are CFUNCTYPE.""" + + fields = _setup_api(Version("1.0.0")) + + for field_name, field_type in fields: + assert callable(field_type) + + +# Tests for _initialize_dll Method and Version Handling + +@patch('cadet.cadet_dll.ctypes.cdll.LoadLibrary') +def test_unsupported_version_error_below_1_0_0(mock_load): + """Test that versions below 1.0.0 raise TypeError.""" + mock_lib = MagicMock() + mock_load.return_value = mock_lib + + mock_lib.cdtGetLibraryVersion.return_value = b"5.0.0" + mock_lib.cdtGetLibraryCommitHash.return_value = b"abc123" + mock_lib.cdtGetLibraryBranchRefspec.return_value = b"main" + mock_lib.cdtGetLibraryBuildType.return_value = b"Release" + mock_lib.cdtGetLatestCAPIVersion.return_value = b"0.9.0" + + runner = CadetDLLRunner.__new__(CadetDLLRunner) + runner._cadet_path = Path("fake_path.so") + + with pytest.raises(TypeError) as exc_info: + runner._initialize_dll() + + assert "does not support CADET-CAPI version" in str(exc_info.value) + assert "0.9.0" in str(exc_info.value) + + +@patch('cadet.cadet_dll.ctypes.cdll.LoadLibrary') +def test_unsupported_version_error_above_2_0_0(mock_load): + """Test that versions 2.0.0 and above raise TypeError.""" + mock_lib = MagicMock() + mock_load.return_value = mock_lib + + mock_lib.cdtGetLibraryVersion.return_value = b"5.0.0" + mock_lib.cdtGetLibraryCommitHash.return_value = b"abc123" + mock_lib.cdtGetLibraryBranchRefspec.return_value = b"main" + mock_lib.cdtGetLibraryBuildType.return_value = b"Release" + mock_lib.cdtGetLatestCAPIVersion.return_value = b"2.0.0" + + runner = CadetDLLRunner.__new__(CadetDLLRunner) + runner._cadet_path = Path("fake_path.so") + + with pytest.raises(TypeError) as exc_info: + runner._initialize_dll() + + assert "does not support CADET-CAPI version" in str(exc_info.value) + assert "2.0.0" in str(exc_info.value) + + +@patch('cadet.cadet_dll.ctypes.cdll.LoadLibrary') +def test_no_capi_function_error(mock_load): + """Test that missing cdtGetLatestCAPIVersion raises ValueError.""" + mock_lib = MagicMock() + mock_load.return_value = mock_lib + + mock_lib.cdtGetLibraryVersion.return_value = b"5.0.0" + mock_lib.cdtGetLibraryCommitHash.return_value = b"abc123" + mock_lib.cdtGetLibraryBranchRefspec.return_value = b"main" + mock_lib.cdtGetLibraryBuildType.return_value = b"Release" + del mock_lib.cdtGetLatestCAPIVersion + + runner = CadetDLLRunner.__new__(CadetDLLRunner) + runner._cadet_path = Path("fake_path.so") + + with pytest.raises(ValueError) as exc_info: + runner._initialize_dll() + + assert "does not support CADET-CAPI" in str(exc_info.value) + + +if __name__ == '__main__': + pytest.main([__file__])