Skip to content

Commit 260075f

Browse files
Implement model exchange (#26)
* Implement model exchange * remove cout * add methods to base class * update example * update mixin classess * fix class name search * update tests * Remove unused interface enum * update * fix rebase * remvove dockerfiles * Test FMU with combined interfaces * update example docs * add vanderpol example * update docs * update supported doc * fix log callback * check logging callback before logging * remove git ignore changes * enforce requirement to have time variable
1 parent a137ca9 commit 260075f

File tree

18 files changed

+754
-107
lines changed

18 files changed

+754
-107
lines changed

README.md

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# PythonFMU3
22

3-
> A lightweight framework that enables the packaging of Python 3 code as co-simulation FMUs (following FMI version 3.0).
3+
> A lightweight framework that enables the packaging of Python 3 code as FMUs (following FMI version 3.0).
44
55
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
66
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/StephenSmith25/PythonFMU3/issues)
@@ -15,14 +15,16 @@ This project is a fork of the original PythonFMU repository available at https:/
1515
### Support:
1616

1717
Please take a look at the examples to see the supported features.
18+
We currently support both the Model exchange and Cosimulation Interfaces.
1819

19-
### Future
20+
For both interfaces event mode is not supported.
2021

21-
In no particular order, we plan to add support for:
22+
We currently do not support the following methods (beyond a default, trivial implementation)
2223

23-
- Support more variable types from FMI3
24-
- Improve array support
25-
- Add event mode
24+
- fmi3UpdateDiscreteStates
25+
- fmi3GetNumberOfEventindicators
26+
- fmi3GetNominalsOfContinuousStates
27+
- fmi3EvaluateDiscreteStates
2628

2729
### How do I build an FMU from python code?
2830

@@ -96,7 +98,7 @@ optional arguments:
9698
Requirements or environment file.
9799
```
98100

99-
### Example:
101+
### Cosimulation Example:
100102

101103
#### Write the script
102104

@@ -142,3 +144,54 @@ pythonfmu3 build -f pythonslave.py myproject
142144
In this example a python class named `PythonSlave` that extends `Fmi3Slave` is declared in a file named `pythonslave.py`,
143145
where `myproject` is an optional folder containing additional project files required by the python script.
144146
Project folders such as this will be recursively copied into the FMU. Multiple project files/folders may be added.
147+
148+
149+
### Model Exchange Example
150+
151+
To create a model exchange FMU you must inherit from `Fmi3SlaveBase` and one of (or both) mixin classes `ModelExchange` or `CoSimulation`.
152+
153+
#### Write the script
154+
155+
```python
156+
157+
from pythonfmu3 import Fmi3Causality, Fmi3SlaveBase, Float64, ModelExchange
158+
159+
160+
class PythonSlaveMX(Fmi3SlaveBase, ModelExchange):
161+
162+
def __init__(self, **kwargs):
163+
super().__init__(**kwargs)
164+
165+
self.author = "John Doe"
166+
self.description = "A simple description"
167+
168+
self.time = 0.0
169+
self.mu = 1.0
170+
self.x0 = 2
171+
self.x1 = 0
172+
self.derx0 = 0.0
173+
self.derx1 = 0.0
174+
175+
self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous))
176+
self.register_variable(Float64("x0", causality=Fmi3Causality.output, start=2, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact))
177+
self.register_variable(Float64("x1", causality=Fmi3Causality.output, start=0, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact))
178+
self.register_variable(Float64("derx0", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1))
179+
self.register_variable(Float64("derx1", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=2))
180+
self.register_variable(Float64("mu", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed))
181+
182+
183+
def get_continuous_state_derivatives(self) -> List[float]:
184+
self.derx0 = self.x1
185+
self.derx1 = self.mu * ((1 - self.x0**2) * self.x1) - self.x0
186+
return [self.derx0, self.derx1]
187+
188+
189+
```
190+
191+
Note: There is a hard requirement that we have a `self.time` member in the derived class.
192+
193+
#### Create the FMU
194+
195+
```
196+
pythonfmu3 build -f pythonslaveMX.py
197+
```

examples/dahlquist_me.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from pythonfmu3 import Fmi3Causality, ModelExchange, Fmi3Variability, Fmi3SlaveBase, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult
2+
3+
from typing import List
4+
5+
class Dahlquist(Fmi3SlaveBase, ModelExchange):
6+
7+
def __init__(self, **kwargs):
8+
super().__init__(**kwargs)
9+
10+
self.author = "Stephen Smith"
11+
self.description = "Dahlquist's test problem for model exchange FMUs"
12+
13+
self.time = 0.0
14+
self.k = 1.0
15+
self.x = 1.0
16+
self.derx = 0.0
17+
18+
self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous))
19+
self.register_variable(Float64("x", causality=Fmi3Causality.output, start=1, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact))
20+
self.register_variable(Float64("derx", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1))
21+
self.register_variable(Float64("k", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed))
22+
23+
24+
def get_continuous_state_derivatives(self) -> List[float]:
25+
self.derx = -self.k * self.x
26+
return [self.derx]
27+

examples/dahlquist_me_cs.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from pythonfmu3 import Fmi3Causality, CoSimulation, ModelExchange, Fmi3Variability, Fmi3SlaveBase, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult
2+
3+
from typing import List
4+
5+
class Dahlquist(Fmi3SlaveBase, ModelExchange, CoSimulation):
6+
7+
def __init__(self, **kwargs):
8+
super().__init__(**kwargs)
9+
10+
self.author = "Stephen Smith"
11+
self.description = "Dahlquist's test problem"
12+
13+
self.time = 0.0
14+
self.k = 1.0
15+
self.x = 1.0
16+
self.derx = 0.0
17+
18+
self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous))
19+
self.register_variable(Float64("x", causality=Fmi3Causality.output, start=1, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact))
20+
self.register_variable(Float64("derx", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1))
21+
self.register_variable(Float64("k", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed))
22+
23+
24+
def get_continuous_state_derivatives(self) -> List[float]:
25+
self.derx = -self.k * self.x
26+
return [self.derx]
27+
28+
def do_step(self, current_time: float, step_size: float) -> Fmi3StepResult:
29+
self.derx = -self.k * self.x
30+
self.x += self.derx * step_size
31+
return True

examples/runner.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
from fmpy.util import plot_result # import the plot function
33
import numpy as np
44
# fmu = '../LinearTransform.fmu'
5-
input_values = {'m': 3, 'n': 3, 'A': np.array([1, 2, 3, 9, 11, 14, 12, 4, 8]), 'scalar': 10.0}
6-
fmu = 'LinearTransformVariable.fmu'
5+
input_values = {}
6+
fmu = 'dahlquist.fmu'
77
# input_values = {'n': 4, 'm': 4}
88
# input_values = {}
99
dump(fmu) # get information
1010

11-
result = simulate_fmu(fmu, start_time=0, stop_time=1e-4, step_size=1e-3, fmi_call_logger=lambda s: print('[FMI] ' + s), start_values=input_values, debug_logging=True)
11+
result = simulate_fmu(fmu, start_time=0, stop_time=1, step_size=1e-3, fmi_call_logger=lambda s: print('[FMI] ' + s), debug_logging=True)
1212

1313

1414
plot_result(result) # plot two variables

examples/vanderpol_me.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from pythonfmu3 import Fmi3Causality, ModelExchange, Fmi3Variability, Fmi3SlaveBase, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult
2+
3+
from typing import List
4+
5+
class VanDerPol(Fmi3SlaveBase, ModelExchange):
6+
7+
def __init__(self, **kwargs):
8+
super().__init__(**kwargs)
9+
10+
self.author = "Stephen Smith"
11+
self.description = "Van Der Pol oscillator problem for model exchange FMUs"
12+
13+
self.time = 0.0
14+
self.mu = 1.0
15+
self.x0 = 2
16+
self.x1 = 0
17+
self.derx0 = 0.0
18+
self.derx1 = 0.0
19+
20+
self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous))
21+
self.register_variable(Float64("x0", causality=Fmi3Causality.output, start=2, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact))
22+
self.register_variable(Float64("x1", causality=Fmi3Causality.output, start=0, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact))
23+
self.register_variable(Float64("derx0", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1))
24+
self.register_variable(Float64("derx1", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=2))
25+
self.register_variable(Float64("mu", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed))
26+
27+
28+
def get_continuous_state_derivatives(self) -> List[float]:
29+
self.derx0 = self.x1
30+
self.derx1 = self.mu * ((1 - self.x0**2) * self.x1) - self.x0
31+
return [self.derx0, self.derx1]
32+

pythonfmu3/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from ._version import __version__
22
from .builder import FmuBuilder
3+
from .cosimulation import CoSimulation
4+
from .modelexchange import ModelExchange
35
from .enums import Fmi3Causality, Fmi3Initial, Fmi3Status, Fmi3Variability
4-
from .fmi3slave import Fmi3Slave, Fmi3StepResult
6+
from .fmi3slave import Fmi3Slave, Fmi3SlaveBase, Fmi3StepResult
57
from .variables import Boolean, Enumeration, Int32, Int64, UInt64, Float64, String, Dimension
68
from .default_experiment import DefaultExperiment
79
from .variable_types import Float64Type, EnumerationType

pythonfmu3/builder.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from xml.dom.minidom import parseString
1414
from xml.etree.ElementTree import Element, SubElement, tostring
1515
from .osutil import get_lib_extension, get_platform
16-
from .fmi3slave import FMI3_MODEL_OPTIONS, Fmi3Slave
16+
from .fmi3slave import FMI3_MODEL_OPTIONS, Fmi3Slave, Fmi3SlaveBase
1717

1818
FilePath = Union[str, Path]
1919
HERE = Path(__file__).parent
@@ -24,7 +24,7 @@
2424
def get_class_name(file_name: Path) -> str:
2525
with open(str(file_name), 'r') as file:
2626
data = file.read()
27-
return re.search(r'class (\w+)\(\s*Fmi3Slave\s*\)\s*:', data).group(1)
27+
return re.search(r'class (\w+)\(([^)]*\bFmi3Slave(?:Base)?\b[^)]*)\)\s*:', data).group(1)
2828

2929

3030
def get_model_description(filepath: Path, module_name: str) -> Tuple[str, Element]:
@@ -50,9 +50,9 @@ def get_model_description(filepath: Path, module_name: str) -> Tuple[str, Elemen
5050
finally:
5151
sys.path.remove(str(filepath.parent)) # remove inserted temporary path
5252

53-
if not isinstance(instance, Fmi3Slave):
53+
if not isinstance(instance, Fmi3SlaveBase):
5454
raise TypeError(
55-
f"The provided class '{class_name}' does not inherit from {Fmi3Slave.__qualname__}"
55+
f"The provided class '{class_name}' does not inherit from {Fmi3SlaveBase.__qualname__}"
5656
)
5757
# Produce the xml
5858
return instance.modelName, instance.to_xml()
@@ -138,9 +138,16 @@ def build_FMU(
138138

139139
type_node = xml.find("CoSimulation")
140140
option_names = [opt.name for opt in FMI3_MODEL_OPTIONS]
141-
for option, value in options.items():
142-
if option in option_names:
143-
type_node.set(option, str(value).lower())
141+
if type_node:
142+
for option, value in options.items():
143+
if option in option_names:
144+
type_node.set(option, str(value).lower())
145+
146+
type_node = xml.find("ModelExchange")
147+
if type_node:
148+
for option, value in options.items():
149+
if option in option_names:
150+
type_node.set(option, str(value).lower())
144151

145152
with zipfile.ZipFile(dest_file, "w") as zip_fmu:
146153

pythonfmu3/cosimulation.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from abc import ABC, abstractmethod
2+
3+
# co-simulation mixin
4+
class CoSimulation(ABC):
5+
6+
@abstractmethod
7+
def do_step(self, current_time: float, step_size: float):
8+
pass

pythonfmu3/fmi3slave.py

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
from .logmsg import LogMsg
1313
from .default_experiment import DefaultExperiment
14+
from .cosimulation import CoSimulation
15+
from .modelexchange import ModelExchange
1416
from ._version import __version__ as VERSION
1517
from .enums import Fmi3Type, Fmi3Status, Fmi3Causality, Fmi3Initial, Fmi3Variability
1618
from .variables import Boolean, Enumeration, Int32, Int64, UInt64, Float64, ModelVariable, String
@@ -33,7 +35,8 @@ class Fmi3StepResult(NamedTuple):
3335
terminateSimulation: bool = False
3436
earlyReturn: bool = False
3537

36-
class Fmi3Slave(ABC):
38+
39+
class Fmi3SlaveBase(object):
3740
"""Abstract facade class to execute Python through FMI standard."""
3841

3942
# Dictionary of (category, description) entries
@@ -104,8 +107,18 @@ def to_xml(self, model_options: Dict[str, str] = dict()) -> Element:
104107
options[option.name] = str(value).lower()
105108
options["modelIdentifier"] = self.modelName
106109
options["canNotUseMemoryManagementFunctions"] = "true"
107-
108-
SubElement(root, "CoSimulation", attrib=options)
110+
111+
options_me = dict()
112+
options_me["canGetAndSetFMUState"] = "true"
113+
options_me["modelIdentifier"] = self.modelName
114+
options_me["needsCompletedIntegratorStep"] = "false"
115+
116+
# check if we have cosim mixin or model exchange mixin
117+
if isinstance(self, ModelExchange):
118+
SubElement(root, "ModelExchange", attrib=options_me)
119+
120+
if isinstance(self, CoSimulation):
121+
SubElement(root, "CoSimulation", attrib=options)
109122

110123
if self.units:
111124
unit_defs = SubElement(root, "UnitDefinitions")
@@ -239,7 +252,6 @@ def enter_initialization_mode(self):
239252
def exit_initialization_mode(self):
240253
pass
241254

242-
@abstractmethod
243255
def do_step(self, current_time: float, step_size: float) -> Fmi3StepResult:
244256
pass
245257

@@ -404,6 +416,50 @@ def _set_fmu_state(self, state: Dict[str, Any]):
404416
if v.setter is not None:
405417
v.setter(value)
406418

419+
def set_continuous_states(self, values: List[float]):
420+
offset = 0
421+
continuous_state_derivatives = list(
422+
filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values())
423+
)
424+
425+
vrs = [v.derivative for v in continuous_state_derivatives]
426+
427+
for vr in vrs:
428+
var = self.vars[vr]
429+
size = var.size(self.vars)
430+
if size > 1:
431+
var.setter(values[offset:offset+size])
432+
else:
433+
var.setter(values[offset])
434+
offset += size
435+
436+
def get_continuous_states(self) -> List[float]:
437+
offset = 0
438+
continuous_state_derivatives = list(
439+
filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values())
440+
)
441+
442+
vrs = [v.derivative for v in continuous_state_derivatives]
443+
444+
refs = list()
445+
for vr in vrs:
446+
var = self.vars[vr]
447+
if len(var.dimensions) == 0:
448+
refs.append(float(var.getter()))
449+
else:
450+
refs.extend(var.getter())
451+
452+
return refs
453+
454+
def get_number_of_continuous_states(self) -> int:
455+
continuous_state_derivatives = list(
456+
filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values())
457+
)
458+
return len(continuous_state_derivatives)
459+
460+
def set_time(self, time: float):
461+
self.time = time
462+
407463
@staticmethod
408464
def _fmu_state_to_bytes(state: Dict[str, Any]) -> bytes:
409465
return json.dumps(state).encode("utf-8")
@@ -436,3 +492,6 @@ def log(
436492
category = "logAll"
437493
log_msg = LogMsg(status, category, msg, debug)
438494
self.log_queue.append(log_msg)
495+
496+
class Fmi3Slave(Fmi3SlaveBase, CoSimulation):
497+
pass

0 commit comments

Comments
 (0)