diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1b7842e0..42dbfd35 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -21,7 +21,7 @@ jobs: - bash: | source activate myEnvironment - conda install --yes --quiet --name myEnvironment -c conda-forge python=$PYTHON_VERSION cmake dask lark-parser lxml matplotlib numpy pyqt pyqtgraph pytest-cov requests $PYTHON_LIBRARIES + conda install --yes --quiet --name myEnvironment -c conda-forge python=$PYTHON_VERSION cmake dask lark-parser lxml matplotlib numpy pyqt pyqtgraph pytest-cov requests scipy $PYTHON_LIBRARIES displayName: Install Anaconda packages - bash: | @@ -85,7 +85,7 @@ jobs: - bash: | source activate myEnvironment - conda install --yes --quiet --name myEnvironment -c conda-forge python=$PYTHON_VERSION dask lark-parser lxml matplotlib numpy pyqt pyqtgraph pytest-cov requests $PYTHON_LIBRARIES + conda install --yes --quiet --name myEnvironment -c conda-forge python=$PYTHON_VERSION dask lark-parser lxml matplotlib numpy pyqt pyqtgraph pytest-cov requests scipy $PYTHON_LIBRARIES displayName: Install Anaconda packages - bash: | @@ -143,7 +143,7 @@ jobs: - script: | call activate myEnvironment - conda install --yes --quiet --name myEnvironment -c conda-forge python=%PYTHON_VERSION% cmake dask lark-parser lxml matplotlib numpy pyqt pyqtgraph pytest-cov pywin32 requests + conda install --yes --quiet --name myEnvironment -c conda-forge python=%PYTHON_VERSION% cmake dask lark-parser lxml matplotlib numpy pyqt pyqtgraph pytest-cov pywin32 requests scipy displayName: Install Anaconda packages - script: | diff --git a/docs/changelog.md b/docs/changelog.md index 956fa837..22162f15 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,20 @@ +## v0.2.23 (2020-09-02) + +### Enhancements + +- Add getAdjointDerivative() and fix getDirectionalDerivative() +- Validate results for FMI 3 Reference FMUs +- Add getClock() and setClock() to _FMU3 +- Add FMU2Model.getNominalsOfContinuousStates() + +### Bug fixes + +- Fix logging for FMI 3 (#159) +- Read start value of String variables in FMI 3 +- Add missing fields to EventInfoReturnValue message (#160) +- Move enterEventMode() and newDiscreteStates() to _FMU3 +- Fix variabilities for variable type "Clock" + ## v0.2.22 (2020-08-01) - `FIXED' Forward fmi2NewDiscreteStates() in remoting client (#154) diff --git a/fmpy/__init__.py b/fmpy/__init__.py index 2146fc3e..34ad9346 100644 --- a/fmpy/__init__.py +++ b/fmpy/__init__.py @@ -5,7 +5,7 @@ from ctypes import * import _ctypes -__version__ = '0.2.22' +__version__ = '0.2.23' # determine the platform diff --git a/fmpy/fmi1.py b/fmpy/fmi1.py index 8594e3af..c80ab2ad 100644 --- a/fmpy/fmi1.py +++ b/fmpy/fmi1.py @@ -172,15 +172,24 @@ def _log_fmi_args(self, fname, argnames, argtypes, args, restype, res): a += hex(0 if v is None else v) elif t == POINTER(c_uint): # value references - a += '[' + ', '.join(map(str, v)) + ']' + if v is None: + a += 'NULL' + else: + a += '[' + ', '.join(map(str, v)) + ']' elif t == POINTER(c_double): if hasattr(v, '__len__'): # c_double_Array_N a += '[' + ', '.join(map(str, v)) + ']' else: - # double pointers are always flowed by the size of the array - arr = np.ctypeslib.as_array(v, (args[i+1],)) - a += '[' + ', '.join(map(str, arr)) + ']' + if len(args) > i + 1: + # double pointers are always flowed by the size of the array + arr = np.ctypeslib.as_array(v, (args[i + 1],)) + a += '[' + ', '.join(map(str, arr)) + ']' + else: + # except for fmi3DoStep + v_ = cast(v, POINTER(c_double)) + a += str(str(v_.contents.value)) + elif hasattr(v, '_obj'): # byref object if hasattr(v._obj, 'value'): diff --git a/fmpy/fmi2.py b/fmpy/fmi2.py index 80ecc11d..271407ef 100644 --- a/fmpy/fmi2.py +++ b/fmpy/fmi2.py @@ -413,6 +413,12 @@ def __init__(self, **kwargs): super(FMU2Model, self).__init__(**kwargs) + # Enter and exit the different modes + + self._fmi2Function('fmi2EnterEventMode', + ['component'], + [fmi2Component]) + self._fmi2Function('fmi2NewDiscreteStates', ['component', 'eventInfo'], [fmi2Component, POINTER(fmi2EventInfo)]) @@ -421,18 +427,22 @@ def __init__(self, **kwargs): ['component'], [fmi2Component]) - self._fmi2Function('fmi2EnterEventMode', - ['component'], - [fmi2Component]) + self._fmi2Function('fmi2CompletedIntegratorStep', + ['component', 'noSetFMUStatePriorToCurrentPoint', 'enterEventMode', 'terminateSimulation'], + [fmi2Component, fmi2Boolean, POINTER(fmi2Boolean), POINTER(fmi2Boolean)]) - self._fmi2Function('fmi2GetContinuousStates', - ['component', 'x', 'nx'], - [fmi2Component, POINTER(fmi2Real), c_size_t]) + # Providing independent variables and re-initialization of caching + + self._fmi2Function('fmi2SetTime', + ['component', 'time'], + [fmi2Component, fmi2Real]) self._fmi2Function('fmi2SetContinuousStates', ['component', 'x', 'nx'], [fmi2Component, POINTER(fmi2Real), c_size_t]) + # Evaluation of the model equations + self._fmi2Function('fmi2GetDerivatives', ['component', 'derivatives', 'nx'], [fmi2Component, POINTER(fmi2Real), c_size_t]) @@ -441,13 +451,13 @@ def __init__(self, **kwargs): ['component', 'eventIndicators', 'ni'], [fmi2Component, POINTER(fmi2Real), c_size_t]) - self._fmi2Function('fmi2SetTime', - ['component', 'time'], - [fmi2Component, fmi2Real]) + self._fmi2Function('fmi2GetContinuousStates', + ['component', 'x', 'nx'], + [fmi2Component, POINTER(fmi2Real), c_size_t]) - self._fmi2Function('fmi2CompletedIntegratorStep', - ['component', 'noSetFMUStatePriorToCurrentPoint', 'enterEventMode', 'terminateSimulation'], - [fmi2Component, fmi2Boolean, POINTER(fmi2Boolean), POINTER(fmi2Boolean)]) + self._fmi2Function('fmi2GetNominalsOfContinuousStates', + ['component', 'x_nominal', 'nx'], + [fmi2Component, POINTER(fmi2Real), c_size_t]) # Enter and exit the different modes @@ -495,8 +505,8 @@ def getEventIndicators(self, z, nz): def getContinuousStates(self, x, nx): return self.fmi2GetContinuousStates(self.component, x, nx) - def getNominalsOfContinuousStatesTYPE(self): - pass + def getNominalsOfContinuousStates(self, x_nominal, nx): + return self.fmi2GetNominalsOfContinuousStates(self.component, x_nominal, nx) class FMU2Slave(_FMU2): diff --git a/fmpy/fmi3.py b/fmpy/fmi3.py index 638a6b6e..a9a10cc2 100644 --- a/fmpy/fmi3.py +++ b/fmpy/fmi3.py @@ -226,6 +226,18 @@ def __init__(self, **kwargs): (c_size_t, 'nSensitivity') ]) + self._fmi3Function('fmi3GetAdjointDerivative', [ + (fmi3Instance, 'instance'), + (POINTER(fmi3ValueReference), 'unknowns'), + (c_size_t, 'nUnknowns'), + (POINTER(fmi3ValueReference), 'knowns'), + (c_size_t, 'nKnowns'), + (POINTER(fmi3Float64), 'seed'), + (c_size_t, 'nSeed'), + (POINTER(fmi3Float64), 'sensitivity'), + (c_size_t, 'nSensitivity') + ]) + # Entering and exiting the Configuration or Reconfiguration Mode self._fmi3Function('fmi3EnterConfigurationMode', [(fmi3Instance, 'instance')]) @@ -376,6 +388,42 @@ def enterInitializationMode(self, tolerance=None, startTime=0.0, stopTime=None): def exitInitializationMode(self): return self.fmi3ExitInitializationMode(self.component) + def enterEventMode(self, stepEvent=False, rootsFound=[], timeEvent=False): + + rootsFound = (fmi3Int32 * len(rootsFound))(*rootsFound) + + return self.fmi3EnterEventMode( + self.component, + fmi3True if stepEvent else fmi3False, + rootsFound, + len(rootsFound), + fmi3True if timeEvent else fmi3False, + ) + + def newDiscreteStates(self): + + newDiscreteStatesNeeded = fmi3Boolean() + terminateSimulation = fmi3Boolean() + nominalsOfContinuousStatesChanged = fmi3Boolean() + valuesOfContinuousStatesChanged = fmi3Boolean() + nextEventTimeDefined = fmi3Boolean() + nextEventTime = fmi3Float64() + + self.fmi3NewDiscreteStates(self.component, + byref(newDiscreteStatesNeeded), + byref(terminateSimulation), + byref(nominalsOfContinuousStatesChanged), + byref(valuesOfContinuousStatesChanged), + byref(nextEventTimeDefined), + byref(nextEventTime)) + + return (newDiscreteStatesNeeded.value != fmi3False, + terminateSimulation.value != fmi3False, + nominalsOfContinuousStatesChanged.value != fmi3False, + valuesOfContinuousStatesChanged.value != fmi3False, + nextEventTimeDefined.value != fmi3False, + nextEventTime.value) + def terminate(self): return self.fmi3Terminate(self.component) @@ -485,6 +533,14 @@ def getBinary(self, vr): self.fmi3GetBinary(self.component, vr, len(vr), size, value, len(value)) return list(value) + def getClock(self, vr, nValues=None): + if nValues is None: + nValues = len(vr) + vr = (fmi3ValueReference * len(vr))(*vr) + value = (fmi3Clock * nValues)() + self.fmi3GetClock(self.component, vr, len(vr), value, nValues) + return list(value) + def setFloat32(self, vr, values): vr = (fmi3ValueReference * len(vr))(*vr) values = (fmi3Float32 * len(values))(*values) @@ -552,6 +608,11 @@ def setBinary(self, vr, values): size = (c_size_t * len(vr))(*[len(v) for v in values]) self.fmi3SetBinary(self.component, vr, len(vr), size, values_, len(values)) + def setClock(self, vr, values, subactive=None): + vr = (fmi3ValueReference * len(vr))(*vr) + values = (fmi3Clock * len(values))(*values) + self.fmi3SetClock(self.component, vr, len(vr), values, subactive, len(values)) + # Getting and setting the internal FMU state def getFMUState(self): @@ -594,26 +655,47 @@ def deSerializeFMUState(self, serializedState, state): # Getting partial derivatives - def getDirectionalDerivative(self, vUnknown_ref, vKnown_ref, dvKnown): - """ Get partial derivatives + def getDirectionalDerivative(self, unknowns, knowns, seed, sensitivity): + """ Get the directional derivatives Parameters: - vUnknown_ref a list of value references of the unknowns - vKnown_ref a list of value references of the knowns - dvKnown a list of delta values (one per known) + unknowns list of value references of the unknowns + knowns list of value references of the knowns + seed list of delta values (one per known) Returns: - a list of the partial derivatives (one per unknown) + sensitivity list of the partial derivatives (one per unknown) """ - vUnknown_ref = (fmi3ValueReference * len(vUnknown_ref))(*vUnknown_ref) - vKnown_ref = (fmi3ValueReference * len(vKnown_ref))(*vKnown_ref) - dvKnown = (fmi3Float64 * len(dvKnown))(*dvKnown) - dvUnknown = (fmi3Float64 * len(vUnknown_ref))() + unknowns = (fmi3ValueReference * len(unknowns))(*unknowns) + knowns = (fmi3ValueReference * len(knowns))(*knowns) + seed = (fmi3Float64 * len(seed))(*seed) + sensitivity = (fmi3Float64 * len(sensitivity))() - self.fmi3GetDirectionalDerivative(self.component, vUnknown_ref, len(vUnknown_ref), vKnown_ref, len(vKnown_ref), dvKnown, dvUnknown) + self.fmi3GetDirectionalDerivative(self.component, unknowns, len(unknowns), knowns, len(knowns), seed, sensitivity) - return list(dvUnknown) + return list(sensitivity) + + def getAdjointDerivative(self, unknowns, knowns, seed, sensitivity): + """ Get adjoint derivatives + + Parameters: + unknowns list of value references of the unknowns + knowns list of value references of the knowns + seed list of delta values (one per known) + + Returns: + sensitivity list of the partial derivatives (one per unknown) + """ + + unknowns = (fmi3ValueReference * len(unknowns))(*unknowns) + knowns = (fmi3ValueReference * len(knowns))(*knowns) + seed = (fmi3Float64 * len(seed))(*seed) + sensitivity = (fmi3Float64 * len(sensitivity))() + + self.fmi3GetAdjointDerivative(self.component, unknowns, len(unknowns), knowns, len(knowns), seed, sensitivity) + + return list(sensitivity) class FMU3Model(_FMU3): @@ -708,42 +790,6 @@ def instantiate(self, visible=False, loggingOn=False): # Enter and exit the different modes - def enterEventMode(self, stepEvent=False, rootsFound=[], timeEvent=False): - - rootsFound = (fmi3Int32 * len(rootsFound))(*rootsFound) - - return self.fmi3EnterEventMode( - self.component, - fmi3True if stepEvent else fmi3False, - rootsFound, - len(rootsFound), - fmi3True if timeEvent else fmi3False, - ) - - def newDiscreteStates(self): - - newDiscreteStatesNeeded = fmi3Boolean() - terminateSimulation = fmi3Boolean() - nominalsOfContinuousStatesChanged = fmi3Boolean() - valuesOfContinuousStatesChanged = fmi3Boolean() - nextEventTimeDefined = fmi3Boolean() - nextEventTime = fmi3Float64() - - self.fmi3NewDiscreteStates(self.component, - byref(newDiscreteStatesNeeded), - byref(terminateSimulation), - byref(nominalsOfContinuousStatesChanged), - byref(valuesOfContinuousStatesChanged), - byref(nextEventTimeDefined), - byref(nextEventTime)) - - return (newDiscreteStatesNeeded.value != fmi3False, - terminateSimulation.value != fmi3False, - nominalsOfContinuousStatesChanged.value != fmi3False, - valuesOfContinuousStatesChanged.value != fmi3False, - nextEventTimeDefined.value != fmi3False, - nextEventTime.value) - def enterContinuousTimeMode(self): return self.fmi3EnterContinuousTimeMode(self.component) @@ -852,6 +898,9 @@ def instantiate(self, visible=False, loggingOn=False, eventModeRequired=False): if not self.component: raise Exception("Failed to instantiate FMU") + def enterStepMode(self): + return self.fmi3EnterStepMode(self.component) + # Simulating the slave def setInputDerivatives(self, vr, order, value): @@ -873,20 +922,3 @@ def doStep(self, currentCommunicationPoint, communicationStepSize, noSetFMUState lastSuccessfulTime = fmi3Float64() status = self.fmi3DoStep(self.component, currentCommunicationPoint, communicationStepSize, noSetFMUStatePriorToCurrentPoint, byref(terminate), byref(earlyReturn), byref(lastSuccessfulTime)) return status, terminate.value != fmi3False, earlyReturn.value != fmi3False, lastSuccessfulTime.value - - def cancelStep(self): - self.fmi3CancelStep(self.component) - - # Inquire slave status - - def getDoStepPendingStatus(self): - status = fmi3Status(fmi3OK) - message = fmi3String() - self.fmi3GetDoStepPendingStatus(self.component, byref(status), byref(message)) - return status, message - - def getDoStepDiscardedStatus(self): - terminate = fmi3Boolean(fmi3False) - lastSuccessfulTime = fmi3Float64(0) - self.fmi3GetDoStepDiscardedStatus(self.component, byref(terminate), byref(lastSuccessfulTime)) - return terminate, lastSuccessfulTime diff --git a/fmpy/model_description.py b/fmpy/model_description.py index 2cccdc0f..c14347c7 100644 --- a/fmpy/model_description.py +++ b/fmpy/model_description.py @@ -614,7 +614,7 @@ def read_model_description(filename, validate=True, validate_variable_names=Fals 'tunable': {'parameter': 'exact', 'calculatedParameter': 'calculated', 'structuralParameter': 'exact', 'local': 'calculated'}, 'discrete': {'input': None, 'output': 'calculated', 'local': 'calculated'}, 'continuous': {'input': None, 'output': 'calculated', 'local': 'calculated', 'independent': None}, - 'clock': {'inferred': None, 'triggered': None}, + 'clock': {'input': None, 'output': None}, } # model variables @@ -625,14 +625,13 @@ def read_model_description(filename, validate=True, validate_variable_names=Fals sv = ScalarVariable(name=variable.get('name'), valueReference=int(variable.get('valueReference'))) sv.description = variable.get('description') - sv.start = variable.get('start') sv.causality = variable.get('causality', default='local') sv.variability = variable.get('variability') sv.initial = variable.get('initial') sv.sourceline = variable.sourceline if fmiVersion in ['1.0', '2.0']: - # get the "value" element + # get the nested "value" element for child in variable.iterchildren(): if child.tag in {'Real', 'Integer', 'Boolean', 'String', 'Enumeration'}: value = child @@ -641,7 +640,12 @@ def read_model_description(filename, validate=True, validate_variable_names=Fals value = variable sv.type = value.tag - sv.start = value.get('start') + + if variable.tag == 'String': + # handle element of String variables in FMI 3 + sv.start = variable.find('Start').get('value') + else: + sv.start = value.get('start') type_map = { 'Real': float, diff --git a/remoting/remoting.h b/remoting/remoting.h index 4e83e166..ae4e052b 100644 --- a/remoting/remoting.h +++ b/remoting/remoting.h @@ -41,5 +41,5 @@ struct EventInfoReturnValue { int valuesOfContinuousStatesChanged; int nextEventTimeDefined; double nextEventTime; - MSGPACK_DEFINE_ARRAY(newDiscreteStatesNeeded, terminateSimulation, nominalsOfContinuousStatesChanged, valuesOfContinuousStatesChanged, nextEventTimeDefined, nextEventTime) + MSGPACK_DEFINE_ARRAY(status, logMessages, newDiscreteStatesNeeded, terminateSimulation, nominalsOfContinuousStatesChanged, valuesOfContinuousStatesChanged, nextEventTimeDefined, nextEventTime) }; diff --git a/setup.py b/setup.py index a88b357c..71cfc309 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ extras_require['complete'] = sorted(set(sum(extras_require.values(), []))) setup(name='FMPy', - version='0.2.22', + version='0.2.23', description="Simulate Functional Mock-up Units (FMUs) in Python", long_description=long_description, author="Torsten Sommer", diff --git a/tests/test_reference_fmus.py b/tests/test_reference_fmus.py index f0d94d71..09b420f0 100644 --- a/tests/test_reference_fmus.py +++ b/tests/test_reference_fmus.py @@ -1,5 +1,5 @@ import unittest -from fmpy.util import download_file +from fmpy.util import download_file, validate_result from fmpy import * @@ -8,30 +8,54 @@ class ReferenceFMUsTest(unittest.TestCase): def setUp(self): download_file(url='https://github.com/modelica/Reference-FMUs/releases/download/v0.0.3/Reference-FMUs-0.0.3.zip', checksum='f9b5c0199127d174e38583fc8733de4286dfd1da8236507007ab7b38f0e32796') - extract('Reference-FMUs-0.0.3.zip', 'Reference-FMUs') + extract('Reference-FMUs-0.0.3.zip', 'Reference-FMUs-dist') + + download_file(url='https://github.com/modelica/Reference-FMUs/archive/v0.0.3.zip', + checksum='ce58f006d3fcee52261ce2f7a3dad635161d4dcaaf0e093fdcd5ded7ee0df647') + extract('v0.0.3.zip', 'Reference-FMUs-repo') def test_fmi1_cs(self): for model_name in ['BouncingBall', 'Dahlquist', 'Resource', 'Stair', 'VanDerPol']: - filename = os.path.join('Reference-FMUs', '1.0', 'cs', model_name + '.fmu') + filename = os.path.join('Reference-FMUs-dist', '1.0', 'cs', model_name + '.fmu') result = simulate_fmu(filename) # plot_result(result) def test_fmi1_me(self): for model_name in ['BouncingBall', 'Dahlquist', 'Stair', 'VanDerPol']: - filename = os.path.join('Reference-FMUs', '1.0', 'me', model_name + '.fmu') + filename = os.path.join('Reference-FMUs-dist', '1.0', 'me', model_name + '.fmu') result = simulate_fmu(filename) # plot_result(result) def test_fmi2(self): for model_name in ['BouncingBall', 'Dahlquist', 'Feedthrough', 'Resource', 'Stair', 'VanDerPol']: - filename = os.path.join('Reference-FMUs', '2.0', model_name + '.fmu') + filename = os.path.join('Reference-FMUs-dist', '2.0', model_name + '.fmu') for fmi_type in ['ModelExchange', 'CoSimulation']: result = simulate_fmu(filename, fmi_type=fmi_type) # plot_result(result) def test_fmi3(self): + for model_name in ['BouncingBall', 'Dahlquist', 'Feedthrough', 'Resource', 'Stair', 'VanDerPol']: - filename = os.path.join('Reference-FMUs', '3.0', model_name + '.fmu') + + if model_name == 'Feedthrough': + start_values = { + 'real_fixed_param': 1, + 'string_param': "FMI is awesome!" + } + + in_csv = os.path.join('Reference-FMUs-repo', 'Reference-FMUs-0.0.3', model_name, model_name + '_in.csv') + input = read_csv(in_csv) if os.path.isfile(in_csv) else None + else: + start_values = {} + input = None + + filename = os.path.join('Reference-FMUs-dist', '3.0', model_name + '.fmu') + + ref_csv = os.path.join('Reference-FMUs-repo', 'Reference-FMUs-0.0.3', model_name, model_name + '_ref.csv') + reference = read_csv(ref_csv) + for fmi_type in ['ModelExchange', 'CoSimulation']: - result = simulate_fmu(filename, fmi_type=fmi_type) - # plot_result(result) + result = simulate_fmu(filename, fmi_type=fmi_type, start_values=start_values, input=input) + rel_out = validate_result(result, reference) + self.assertEqual(0, rel_out) + # plot_result(result, reference)