fmu-manipulation-toolbox 1.7.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fmu_manipulation_toolbox/__init__.py +1 -0
- fmu_manipulation_toolbox/__main__.py +25 -0
- fmu_manipulation_toolbox/__version__.py +1 -0
- fmu_manipulation_toolbox/checker.py +61 -0
- fmu_manipulation_toolbox/cli.py +216 -0
- fmu_manipulation_toolbox/fmu_container.py +784 -0
- fmu_manipulation_toolbox/fmu_operations.py +489 -0
- fmu_manipulation_toolbox/gui.py +493 -0
- fmu_manipulation_toolbox/help.py +87 -0
- fmu_manipulation_toolbox/resources/checkbox-checked-disabled.png +0 -0
- fmu_manipulation_toolbox/resources/checkbox-checked-hover.png +0 -0
- fmu_manipulation_toolbox/resources/checkbox-checked.png +0 -0
- fmu_manipulation_toolbox/resources/checkbox-unchecked-disabled.png +0 -0
- fmu_manipulation_toolbox/resources/checkbox-unchecked-hover.png +0 -0
- fmu_manipulation_toolbox/resources/checkbox-unchecked.png +0 -0
- fmu_manipulation_toolbox/resources/drop_fmu.png +0 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Annotation.xsd +58 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2AttributeGroups.xsd +78 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ModelDescription.xsd +345 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ScalarVariable.xsd +218 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Type.xsd +89 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Unit.xsd +116 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2VariableDependency.xsd +92 -0
- fmu_manipulation_toolbox/resources/fmu.png +0 -0
- fmu_manipulation_toolbox/resources/fmu_manipulation_toolbox.png +0 -0
- fmu_manipulation_toolbox/resources/help.png +0 -0
- fmu_manipulation_toolbox/resources/icon.png +0 -0
- fmu_manipulation_toolbox/resources/license.txt +34 -0
- fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
- fmu_manipulation_toolbox/resources/linux32/server_sm +0 -0
- fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
- fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
- fmu_manipulation_toolbox/resources/linux64/server_sm +0 -0
- fmu_manipulation_toolbox/resources/model.png +0 -0
- fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
- fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
- fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
- fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
- fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
- fmu_manipulation_toolbox/version.py +9 -0
- fmu_manipulation_toolbox-1.7.5.dist-info/LICENSE.txt +22 -0
- fmu_manipulation_toolbox-1.7.5.dist-info/METADATA +20 -0
- fmu_manipulation_toolbox-1.7.5.dist-info/RECORD +46 -0
- fmu_manipulation_toolbox-1.7.5.dist-info/WHEEL +5 -0
- fmu_manipulation_toolbox-1.7.5.dist-info/entry_points.txt +3 -0
- fmu_manipulation_toolbox-1.7.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import uuid
|
|
6
|
+
import zipfile
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import *
|
|
10
|
+
|
|
11
|
+
from .fmu_operations import FMU, OperationAbstract, FMUException
|
|
12
|
+
from .version import __version__ as tool_version
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("fmu_manipulation_toolbox")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FMUPort:
|
|
18
|
+
def __init__(self, attrs: Dict[str, str]):
|
|
19
|
+
self.name = attrs["name"]
|
|
20
|
+
self.vr = int(attrs["valueReference"])
|
|
21
|
+
self.causality = attrs.get("causality", "local")
|
|
22
|
+
self.attrs = attrs.copy()
|
|
23
|
+
self.attrs.pop("name")
|
|
24
|
+
self.attrs.pop("valueReference")
|
|
25
|
+
if "causality" in self.attrs:
|
|
26
|
+
self.attrs.pop("causality")
|
|
27
|
+
self.type_name = None
|
|
28
|
+
self.child = None
|
|
29
|
+
|
|
30
|
+
def set_port_type(self, type_name: str, attrs: Dict[str, str]):
|
|
31
|
+
self.type_name = type_name
|
|
32
|
+
self.child = attrs.copy()
|
|
33
|
+
try:
|
|
34
|
+
self.child.pop("unit") # Unit are not supported
|
|
35
|
+
except KeyError:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def xml(self, vr: int, name=None, causality=None, start=None):
|
|
39
|
+
|
|
40
|
+
if self.child is None:
|
|
41
|
+
raise FMUException(f"FMUPort has no child. Bug?")
|
|
42
|
+
|
|
43
|
+
child_str = f"<{self.type_name}"
|
|
44
|
+
if self.child:
|
|
45
|
+
if start is not None and 'start' in self.child:
|
|
46
|
+
self.child['start'] = start
|
|
47
|
+
child_str += " " + " ".join([f'{key}="{value}"' for (key, value) in self.child.items()]) + "/>"
|
|
48
|
+
else:
|
|
49
|
+
child_str += "/>"
|
|
50
|
+
|
|
51
|
+
if name is None:
|
|
52
|
+
name = self.name
|
|
53
|
+
if causality is None:
|
|
54
|
+
causality = self.causality
|
|
55
|
+
|
|
56
|
+
variability = "continuous" if self.type_name == "Real" else "discrete"
|
|
57
|
+
|
|
58
|
+
scalar_attrs = {
|
|
59
|
+
"name": name,
|
|
60
|
+
"valueReference": vr,
|
|
61
|
+
"causality": causality,
|
|
62
|
+
"variability": variability,
|
|
63
|
+
}
|
|
64
|
+
scalar_attrs.update(self.attrs)
|
|
65
|
+
|
|
66
|
+
scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in scalar_attrs.items()])
|
|
67
|
+
|
|
68
|
+
return f'<ScalarVariable {scalar_attrs_str}>{child_str}</ScalarVariable>'
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class EmbeddedFMU(OperationAbstract):
|
|
72
|
+
capability_list = ("needsExecutionTool",
|
|
73
|
+
"canHandleVariableCommunicationStepSize",
|
|
74
|
+
"canBeInstantiatedOnlyOncePerProcess")
|
|
75
|
+
|
|
76
|
+
def __init__(self, filename):
|
|
77
|
+
self.fmu = FMU(filename)
|
|
78
|
+
self.name = Path(filename).name
|
|
79
|
+
|
|
80
|
+
self.fmi_version = None
|
|
81
|
+
self.step_size = None
|
|
82
|
+
self.model_identifier = None
|
|
83
|
+
self.guid = None
|
|
84
|
+
self.ports: Dict[str, FMUPort] = {}
|
|
85
|
+
|
|
86
|
+
self.capabilities: Dict[str, str] = {}
|
|
87
|
+
self.current_port = None # used during apply_operation()
|
|
88
|
+
|
|
89
|
+
self.fmu.apply_operation(self) # Should be the last command in constructor!
|
|
90
|
+
|
|
91
|
+
def fmi_attrs(self, attrs):
|
|
92
|
+
self.guid = attrs['guid']
|
|
93
|
+
self.fmi_version = attrs['fmiVersion']
|
|
94
|
+
|
|
95
|
+
def scalar_attrs(self, attrs) -> int:
|
|
96
|
+
self.current_port = FMUPort(attrs)
|
|
97
|
+
self.ports[self.current_port.name] = self.current_port
|
|
98
|
+
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
def cosimulation_attrs(self, attrs: Dict[str, str]):
|
|
102
|
+
self.model_identifier = attrs['modelIdentifier']
|
|
103
|
+
for capability in self.capability_list:
|
|
104
|
+
self.capabilities[capability] = attrs.get(capability, "false")
|
|
105
|
+
|
|
106
|
+
def experiment_attrs(self, attrs):
|
|
107
|
+
self.step_size = float(attrs['stepSize'])
|
|
108
|
+
|
|
109
|
+
def scalar_type(self, type_name, attrs):
|
|
110
|
+
self.current_port.set_port_type(type_name, attrs)
|
|
111
|
+
|
|
112
|
+
def __repr__(self):
|
|
113
|
+
return f"FMU '{self.name}' ({len(self.ports)} variables)"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class FMUContainerError(Exception):
|
|
117
|
+
def __init__(self, reason: str):
|
|
118
|
+
self.reason = reason
|
|
119
|
+
|
|
120
|
+
def __repr__(self):
|
|
121
|
+
return f"{self.reason}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ContainerPort:
|
|
125
|
+
def __init__(self, fmu: EmbeddedFMU, port_name: str):
|
|
126
|
+
self.fmu = fmu
|
|
127
|
+
try:
|
|
128
|
+
self.port = fmu.ports[port_name]
|
|
129
|
+
except KeyError:
|
|
130
|
+
raise FMUContainerError(f"Port '{fmu.name}/{port_name}' does not exist")
|
|
131
|
+
self.vr = None
|
|
132
|
+
|
|
133
|
+
def __repr__(self):
|
|
134
|
+
return f"Port {self.fmu.name}/{self.port.name}"
|
|
135
|
+
|
|
136
|
+
def __hash__(self):
|
|
137
|
+
return hash(f"{self.fmu.name}/{self.port.name}")
|
|
138
|
+
|
|
139
|
+
def __eq__(self, other):
|
|
140
|
+
return str(self) == str(other)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Local:
|
|
144
|
+
def __init__(self, cport_from: ContainerPort):
|
|
145
|
+
self.name = cport_from.fmu.name[:-4] + "." + cport_from.port.name # strip .fmu suffix
|
|
146
|
+
self.cport_from = cport_from
|
|
147
|
+
self.cport_to_list: List[ContainerPort] = []
|
|
148
|
+
self.vr = None
|
|
149
|
+
|
|
150
|
+
if not cport_from.port.causality == "output":
|
|
151
|
+
raise FMUContainerError(f"{cport_from} is {cport_from.port.causality} instead of OUTPUT")
|
|
152
|
+
|
|
153
|
+
def add_target(self, cport_to: ContainerPort):
|
|
154
|
+
if not cport_to.port.causality == "input":
|
|
155
|
+
raise FMUContainerError(f"{cport_to} is {cport_to.port.causality} instead of INPUT")
|
|
156
|
+
|
|
157
|
+
if cport_to.port.type_name == self.cport_from.port.type_name:
|
|
158
|
+
self.cport_to_list.append(cport_to)
|
|
159
|
+
else:
|
|
160
|
+
raise FMUContainerError(f"failed to connect {self.cport_from} to {cport_to} due to type.")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ValueReferenceTable:
|
|
164
|
+
def __init__(self):
|
|
165
|
+
self.vr_table: Dict[str, int] = {
|
|
166
|
+
"Real": 0,
|
|
167
|
+
"Integer": 0,
|
|
168
|
+
"Boolean": 0,
|
|
169
|
+
"String": 0,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def get_vr(self, cport: ContainerPort) -> int:
|
|
173
|
+
return self.add_vr(cport.port.type_name)
|
|
174
|
+
|
|
175
|
+
def add_vr(self, type_name:str) -> int:
|
|
176
|
+
vr = self.vr_table[type_name]
|
|
177
|
+
self.vr_table[type_name] += 1
|
|
178
|
+
return vr
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class FMUContainer:
|
|
182
|
+
def __init__(self, identifier: str, fmu_directory: Union[str, Path]):
|
|
183
|
+
self.fmu_directory = Path(fmu_directory)
|
|
184
|
+
self.identifier = identifier
|
|
185
|
+
if not self.fmu_directory.is_dir():
|
|
186
|
+
raise FMUContainerError(f"{self.fmu_directory} is not a valid directory")
|
|
187
|
+
self.involved_fmu: Dict[str, EmbeddedFMU] = {}
|
|
188
|
+
self.execution_order: List[EmbeddedFMU] = []
|
|
189
|
+
|
|
190
|
+
self.description_pathname = None # Will be set up by FMUContainerSpecReader
|
|
191
|
+
self.period = None # Will be set up by FMUContainerSpecReader
|
|
192
|
+
|
|
193
|
+
# Rules
|
|
194
|
+
self.inputs: Dict[str, ContainerPort] = {}
|
|
195
|
+
self.outputs: Dict[str, ContainerPort] = {}
|
|
196
|
+
self.locals: Dict[ContainerPort, Local] = {}
|
|
197
|
+
|
|
198
|
+
self.rules: Dict[ContainerPort, str] = {}
|
|
199
|
+
self.start_values: Dict[ContainerPort, str] = {}
|
|
200
|
+
|
|
201
|
+
def get_fmu(self, fmu_filename: str) -> EmbeddedFMU:
|
|
202
|
+
if fmu_filename in self.involved_fmu:
|
|
203
|
+
return self.involved_fmu[fmu_filename]
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
fmu = EmbeddedFMU(self.fmu_directory / fmu_filename)
|
|
207
|
+
self.involved_fmu[fmu_filename] = fmu
|
|
208
|
+
self.execution_order.append(fmu)
|
|
209
|
+
if not fmu.fmi_version == "2.0":
|
|
210
|
+
raise FMUException("Only FMI-2.0 is supported by FMUContainer")
|
|
211
|
+
logger.debug(f"Adding FMU #{len(self.execution_order)}: {fmu}")
|
|
212
|
+
except Exception as e:
|
|
213
|
+
raise FMUException(f"Cannot load '{fmu_filename}': {e}")
|
|
214
|
+
|
|
215
|
+
return fmu
|
|
216
|
+
|
|
217
|
+
def mark_ruled(self, cport: ContainerPort, rule: str):
|
|
218
|
+
if cport in self.rules:
|
|
219
|
+
previous_rule = self.rules[cport]
|
|
220
|
+
if rule not in ("OUTPUT", "LINK") and previous_rule not in ("OUTPUT", "LINK"):
|
|
221
|
+
raise FMUContainerError(f"try to {rule} port {cport} which is already {previous_rule}")
|
|
222
|
+
|
|
223
|
+
self.rules[cport] = rule
|
|
224
|
+
|
|
225
|
+
def add_input(self, container_port_name: str, to_fmu_filename: str, to_port_name: str):
|
|
226
|
+
if not container_port_name:
|
|
227
|
+
container_port_name = to_port_name
|
|
228
|
+
cport_to = ContainerPort(self.get_fmu(to_fmu_filename), to_port_name)
|
|
229
|
+
if not cport_to.port.causality == "input": # check causality
|
|
230
|
+
raise FMUException(f"{cport_to} is {cport_to.port.causality} instead of INPUT.")
|
|
231
|
+
|
|
232
|
+
logger.debug(f"INPUT: {to_fmu_filename}:{to_port_name}")
|
|
233
|
+
self.mark_ruled(cport_to, 'INPUT')
|
|
234
|
+
self.inputs[container_port_name] = cport_to
|
|
235
|
+
|
|
236
|
+
def add_output(self, from_fmu_filename: str, from_port_name: str, container_port_name: str):
|
|
237
|
+
if not container_port_name: # empty is allowed
|
|
238
|
+
container_port_name = from_port_name
|
|
239
|
+
|
|
240
|
+
cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
|
|
241
|
+
if cport_from.port.causality not in ("output", "local"): # check causality
|
|
242
|
+
raise FMUException(f"{cport_from} is {cport_from.port.causality} instead of OUTPUT or LOCAL")
|
|
243
|
+
|
|
244
|
+
logger.debug(f"OUTPUT: {from_fmu_filename}:{from_port_name}")
|
|
245
|
+
self.mark_ruled(cport_from, 'OUTPUT')
|
|
246
|
+
self.outputs[container_port_name] = cport_from
|
|
247
|
+
|
|
248
|
+
def drop_port(self, from_fmu_filename: str, from_port_name: str):
|
|
249
|
+
cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
|
|
250
|
+
if not cport_from.port.causality == "output": # check causality
|
|
251
|
+
raise FMUException(f"{cport_from}: trying to DROP {cport_from.port.causality}")
|
|
252
|
+
|
|
253
|
+
logger.debug(f"DROP: {from_fmu_filename}:{from_port_name}")
|
|
254
|
+
self.mark_ruled(cport_from, 'DROP')
|
|
255
|
+
|
|
256
|
+
def add_link(self, from_fmu_filename: str, from_port_name: str, to_fmu_filename: str, to_port_name: str):
|
|
257
|
+
cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
|
|
258
|
+
try:
|
|
259
|
+
local = self.locals[cport_from]
|
|
260
|
+
except KeyError:
|
|
261
|
+
local = Local(cport_from)
|
|
262
|
+
|
|
263
|
+
cport_to = ContainerPort(self.get_fmu(to_fmu_filename), to_port_name)
|
|
264
|
+
local.add_target(cport_to) # Causality is check in the add() function
|
|
265
|
+
|
|
266
|
+
self.mark_ruled(cport_from, 'LINK')
|
|
267
|
+
self.mark_ruled(cport_to, 'LINK')
|
|
268
|
+
self.locals[cport_from] = local
|
|
269
|
+
|
|
270
|
+
def add_start_value(self, fmu_filename: str, port_name: str, value: str):
|
|
271
|
+
cport = ContainerPort(self.get_fmu(fmu_filename), port_name)
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
if cport.port.type_name == 'Real':
|
|
275
|
+
value = float(value)
|
|
276
|
+
elif cport.port.type_name == 'Integer':
|
|
277
|
+
value = int(value)
|
|
278
|
+
elif cport.port.type_name == 'Boolean':
|
|
279
|
+
value = int(bool(value))
|
|
280
|
+
else:
|
|
281
|
+
value = value
|
|
282
|
+
except ValueError:
|
|
283
|
+
raise FMUContainerError(f"Start value is not conforming to '{cport.port.type_name}' format.")
|
|
284
|
+
|
|
285
|
+
self.start_values[cport] = value
|
|
286
|
+
|
|
287
|
+
def find_input(self, port_to_connect: FMUPort) -> Union[ContainerPort, None]:
|
|
288
|
+
for fmu in self.execution_order:
|
|
289
|
+
for port in fmu.ports.values():
|
|
290
|
+
if (port.causality == 'input' and port.name == port_to_connect.name
|
|
291
|
+
and port.type_name == port_to_connect.type_name):
|
|
292
|
+
return ContainerPort(fmu, port.name)
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
def add_implicit_rule(self, auto_input: bool = True, auto_output: bool = True, auto_link: bool = True):
|
|
296
|
+
# Auto Link outputs
|
|
297
|
+
for fmu in self.execution_order:
|
|
298
|
+
for port_name in fmu.ports:
|
|
299
|
+
cport = ContainerPort(fmu, port_name)
|
|
300
|
+
if cport not in self.rules:
|
|
301
|
+
if cport.port.causality == 'output':
|
|
302
|
+
candidate_cport = self.find_input(cport.port)
|
|
303
|
+
if auto_link and candidate_cport:
|
|
304
|
+
local = Local(cport)
|
|
305
|
+
local.add_target(candidate_cport)
|
|
306
|
+
logger.info(f"AUTO LINK: {cport} -> {candidate_cport}")
|
|
307
|
+
self.mark_ruled(cport, 'LINK')
|
|
308
|
+
self.mark_ruled(candidate_cport, 'LINK')
|
|
309
|
+
self.locals[cport] = local
|
|
310
|
+
else:
|
|
311
|
+
if auto_output:
|
|
312
|
+
self.mark_ruled(cport, 'OUTPUT')
|
|
313
|
+
self.outputs[port_name] = cport
|
|
314
|
+
logger.info(f"AUTO OUTPUT: Expose {cport}")
|
|
315
|
+
|
|
316
|
+
if auto_input:
|
|
317
|
+
# Auto link inputs
|
|
318
|
+
for fmu in self.execution_order:
|
|
319
|
+
for port_name in fmu.ports:
|
|
320
|
+
cport = ContainerPort(fmu, port_name)
|
|
321
|
+
if cport not in self.rules:
|
|
322
|
+
if cport.port.causality == 'input':
|
|
323
|
+
self.mark_ruled(cport, 'INPUT')
|
|
324
|
+
self.inputs[port_name] = cport
|
|
325
|
+
logger.info(f"AUTO INPUT: Expose {cport}")
|
|
326
|
+
|
|
327
|
+
def minimum_step_size(self) -> float:
|
|
328
|
+
step_size = None
|
|
329
|
+
for fmu in self.execution_order:
|
|
330
|
+
if step_size:
|
|
331
|
+
if fmu.step_size and fmu.step_size < step_size:
|
|
332
|
+
step_size = fmu.step_size
|
|
333
|
+
else:
|
|
334
|
+
step_size = fmu.step_size
|
|
335
|
+
|
|
336
|
+
if not step_size:
|
|
337
|
+
step_size = 0.1
|
|
338
|
+
logger.warning(f"Defaulting to step_size={step_size}")
|
|
339
|
+
|
|
340
|
+
return step_size
|
|
341
|
+
|
|
342
|
+
def sanity_check(self, step_size: Union[float, None]):
|
|
343
|
+
nb_error = 0
|
|
344
|
+
for fmu in self.execution_order:
|
|
345
|
+
ts_ratio = step_size / fmu.step_size
|
|
346
|
+
if ts_ratio < 1.0:
|
|
347
|
+
logger.error(f"Container step_size={step_size}s is lower than FMU '{fmu.name}' "
|
|
348
|
+
f"step_size={fmu.step_size}s")
|
|
349
|
+
if ts_ratio != int(ts_ratio):
|
|
350
|
+
logger.error(f"Container step_size={step_size}s should divisible by FMU '{fmu.name}' "
|
|
351
|
+
f"step_size={fmu.step_size}s")
|
|
352
|
+
for port_name in fmu.ports:
|
|
353
|
+
cport = ContainerPort(fmu, port_name)
|
|
354
|
+
if cport not in self.rules:
|
|
355
|
+
if cport.port.causality == 'input':
|
|
356
|
+
logger.error(f"{cport} is not connected")
|
|
357
|
+
nb_error += 1
|
|
358
|
+
if cport.port.causality == 'output':
|
|
359
|
+
logger.warning(f"{cport} is not connected")
|
|
360
|
+
|
|
361
|
+
if nb_error:
|
|
362
|
+
raise FMUContainerError(f"Some ports are not connected.")
|
|
363
|
+
|
|
364
|
+
def make_fmu(self, fmu_filename: Union[str, Path], step_size: Union[float, None] = None, debug=False, mt=False,
|
|
365
|
+
profiling=False):
|
|
366
|
+
if isinstance(fmu_filename, str):
|
|
367
|
+
fmu_filename = Path(fmu_filename)
|
|
368
|
+
|
|
369
|
+
if step_size is None:
|
|
370
|
+
logger.info(f"step_size will be deduced from the embedded FMU's")
|
|
371
|
+
step_size = self.minimum_step_size()
|
|
372
|
+
self.sanity_check(step_size)
|
|
373
|
+
|
|
374
|
+
logger.info(f"Building FMU '{fmu_filename}', step_size={step_size}")
|
|
375
|
+
|
|
376
|
+
base_directory = self.fmu_directory / fmu_filename.with_suffix('')
|
|
377
|
+
resources_directory = self.make_fmu_skeleton(base_directory)
|
|
378
|
+
with open(base_directory / "modelDescription.xml", "wt") as xml_file:
|
|
379
|
+
self.make_fmu_xml(xml_file, step_size, profiling)
|
|
380
|
+
with open(resources_directory / "container.txt", "wt") as txt_file:
|
|
381
|
+
self.make_fmu_txt(txt_file, step_size, mt, profiling)
|
|
382
|
+
|
|
383
|
+
self.make_fmu_package(base_directory, fmu_filename)
|
|
384
|
+
if not debug:
|
|
385
|
+
self.make_fmu_cleanup(base_directory)
|
|
386
|
+
|
|
387
|
+
def make_fmu_xml(self, xml_file, step_size: float, profiling: bool):
|
|
388
|
+
vr_table = ValueReferenceTable()
|
|
389
|
+
|
|
390
|
+
timestamp = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
391
|
+
guid = str(uuid.uuid4())
|
|
392
|
+
embedded_fmu = ", ".join([fmu_name for fmu_name in self.involved_fmu])
|
|
393
|
+
try:
|
|
394
|
+
author = os.getlogin()
|
|
395
|
+
except OSError:
|
|
396
|
+
author = "Unspecified"
|
|
397
|
+
|
|
398
|
+
capabilities = {}
|
|
399
|
+
for capability in EmbeddedFMU.capability_list:
|
|
400
|
+
capabilities[capability] = "false"
|
|
401
|
+
for fmu in self.involved_fmu.values():
|
|
402
|
+
if fmu.capabilities[capability] == "true":
|
|
403
|
+
capabilities[capability] = "true"
|
|
404
|
+
|
|
405
|
+
xml_file.write(f"""<?xml version="1.0" encoding="ISO-8859-1"?>
|
|
406
|
+
<fmiModelDescription
|
|
407
|
+
fmiVersion="2.0"
|
|
408
|
+
modelName="{self.identifier}"
|
|
409
|
+
generationTool="FMUContainer-{tool_version}"
|
|
410
|
+
generationDateAndTime="{timestamp}"
|
|
411
|
+
guid="{guid}"
|
|
412
|
+
description="FMUContainer with {embedded_fmu}"
|
|
413
|
+
author="{author}"
|
|
414
|
+
license="Proprietary"
|
|
415
|
+
copyright="© Renault S.A.S"
|
|
416
|
+
variableNamingConvention="structured">
|
|
417
|
+
|
|
418
|
+
<CoSimulation
|
|
419
|
+
modelIdentifier="{self.identifier}"
|
|
420
|
+
canHandleVariableCommunicationStepSize="{capabilities['canHandleVariableCommunicationStepSize']}"
|
|
421
|
+
canBeInstantiatedOnlyOncePerProcess="{capabilities['canBeInstantiatedOnlyOncePerProcess']}"
|
|
422
|
+
canNotUseMemoryManagementFunctions="true"
|
|
423
|
+
canGetAndSetFMUstate="false"
|
|
424
|
+
canSerializeFMUstate="false"
|
|
425
|
+
providesDirectionalDerivative="false"
|
|
426
|
+
needsExecutionTool="{capabilities['needsExecutionTool']}">
|
|
427
|
+
</CoSimulation>
|
|
428
|
+
|
|
429
|
+
<LogCategories>
|
|
430
|
+
<Category name="fmucontainer"/>
|
|
431
|
+
</LogCategories>
|
|
432
|
+
|
|
433
|
+
<DefaultExperiment stepSize="{step_size}"/>
|
|
434
|
+
|
|
435
|
+
<ModelVariables>
|
|
436
|
+
""")
|
|
437
|
+
if profiling:
|
|
438
|
+
for fmu in self.execution_order:
|
|
439
|
+
vr = vr_table.add_vr("Real")
|
|
440
|
+
name = f"container.{fmu.model_identifier}.rt_ratio"
|
|
441
|
+
print(f'<ScalarVariable valueReference="{vr}" name="{name}" causality="local"><Real /></ScalarVariable>', file=xml_file)
|
|
442
|
+
|
|
443
|
+
# Local variable should be first to ensure to attribute them the lowest VR.
|
|
444
|
+
for local in self.locals.values():
|
|
445
|
+
vr = vr_table.get_vr(local.cport_from)
|
|
446
|
+
print(f' {local.cport_from.port.xml(vr, name=local.name, causality="local")}', file=xml_file)
|
|
447
|
+
local.vr = vr
|
|
448
|
+
|
|
449
|
+
for input_port_name, cport in self.inputs.items():
|
|
450
|
+
vr = vr_table.get_vr(cport)
|
|
451
|
+
start = self.start_values.get(cport, None)
|
|
452
|
+
print(f" {cport.port.xml(vr, name=input_port_name, start=start)}", file=xml_file)
|
|
453
|
+
cport.vr = vr
|
|
454
|
+
|
|
455
|
+
for output_port_name, cport in self.outputs.items():
|
|
456
|
+
vr = vr_table.get_vr(cport)
|
|
457
|
+
print(f" {cport.port.xml(vr, name=output_port_name)}", file=xml_file)
|
|
458
|
+
cport.vr = vr
|
|
459
|
+
|
|
460
|
+
xml_file.write(""" </ModelVariables>
|
|
461
|
+
|
|
462
|
+
<ModelStructure>
|
|
463
|
+
<Outputs>
|
|
464
|
+
""")
|
|
465
|
+
|
|
466
|
+
index_offset = len(self.locals) + len(self.inputs) + 1
|
|
467
|
+
for i, _ in enumerate(self.outputs.keys()):
|
|
468
|
+
print(f' <Unknown index="{index_offset+i}"/>', file=xml_file)
|
|
469
|
+
xml_file.write(""" </Outputs>
|
|
470
|
+
<InitialUnknowns>
|
|
471
|
+
""")
|
|
472
|
+
for i, _ in enumerate(self.outputs.keys()):
|
|
473
|
+
print(f' <Unknown index="{index_offset+i}"/>', file=xml_file)
|
|
474
|
+
xml_file.write(""" </InitialUnknowns>
|
|
475
|
+
</ModelStructure>
|
|
476
|
+
|
|
477
|
+
</fmiModelDescription>
|
|
478
|
+
""")
|
|
479
|
+
|
|
480
|
+
def make_fmu_txt(self, txt_file, step_size: float, mt: bool, profiling: bool):
|
|
481
|
+
if mt:
|
|
482
|
+
print("# Use MT\n1", file=txt_file)
|
|
483
|
+
else:
|
|
484
|
+
print("# Don't use MT\n0", file=txt_file)
|
|
485
|
+
|
|
486
|
+
if profiling:
|
|
487
|
+
print("# Profiling ENABLED\n1", file=txt_file)
|
|
488
|
+
else:
|
|
489
|
+
print("# Profiling DISABLED\n0", file=txt_file)
|
|
490
|
+
|
|
491
|
+
print(f"# Internal time step in seconds", file=txt_file)
|
|
492
|
+
print(f"{step_size}", file=txt_file)
|
|
493
|
+
print(f"# NB of embedded FMU's", file=txt_file)
|
|
494
|
+
print(f"{len(self.involved_fmu)}", file=txt_file)
|
|
495
|
+
fmu_rank: Dict[str, int] = {}
|
|
496
|
+
for i, fmu in enumerate(self.execution_order):
|
|
497
|
+
print(f"{fmu.name}", file=txt_file)
|
|
498
|
+
print(f"{fmu.model_identifier}", file=txt_file)
|
|
499
|
+
print(f"{fmu.guid}", file=txt_file)
|
|
500
|
+
fmu_rank[fmu.name] = i
|
|
501
|
+
|
|
502
|
+
# Prepare data structure
|
|
503
|
+
type_names_list = ("Real", "Integer", "Boolean", "String") # Ordered list
|
|
504
|
+
inputs_per_type: Dict[str, List[ContainerPort]] = {} # Container's INPUT
|
|
505
|
+
outputs_per_type: Dict[str, List[ContainerPort]] = {} # Container's OUTPUT
|
|
506
|
+
|
|
507
|
+
inputs_fmu_per_type: Dict[str, Dict[str, Dict[ContainerPort, int]]] = {} # [type][fmu]
|
|
508
|
+
start_values_fmu_per_type = {}
|
|
509
|
+
outputs_fmu_per_type = {}
|
|
510
|
+
locals_per_type: Dict[str, List[Local]] = {}
|
|
511
|
+
|
|
512
|
+
for type_name in type_names_list:
|
|
513
|
+
inputs_per_type[type_name] = []
|
|
514
|
+
outputs_per_type[type_name] = []
|
|
515
|
+
locals_per_type[type_name] = []
|
|
516
|
+
|
|
517
|
+
inputs_fmu_per_type[type_name] = {}
|
|
518
|
+
start_values_fmu_per_type[type_name] = {}
|
|
519
|
+
outputs_fmu_per_type[type_name] = {}
|
|
520
|
+
|
|
521
|
+
for fmu in self.execution_order:
|
|
522
|
+
inputs_fmu_per_type[type_name][fmu.name] = {}
|
|
523
|
+
start_values_fmu_per_type[type_name][fmu.name] = {}
|
|
524
|
+
outputs_fmu_per_type[type_name][fmu.name] = {}
|
|
525
|
+
|
|
526
|
+
# Fill data structure
|
|
527
|
+
# Inputs
|
|
528
|
+
for input_port_name, cport in self.inputs.items():
|
|
529
|
+
inputs_per_type[cport.port.type_name].append(cport)
|
|
530
|
+
for cport, value in self.start_values.items():
|
|
531
|
+
start_values_fmu_per_type[cport.port.type_name][cport.fmu.name][cport] = value
|
|
532
|
+
# Outputs
|
|
533
|
+
for output_port_name, cport in self.outputs.items():
|
|
534
|
+
outputs_per_type[cport.port.type_name].append(cport)
|
|
535
|
+
# Locals
|
|
536
|
+
for local in self.locals.values():
|
|
537
|
+
vr = local.vr
|
|
538
|
+
locals_per_type[local.cport_from.port.type_name].append(local)
|
|
539
|
+
outputs_fmu_per_type[local.cport_from.port.type_name][local.cport_from.fmu.name][local.cport_from] = vr
|
|
540
|
+
for cport_to in local.cport_to_list:
|
|
541
|
+
inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = vr
|
|
542
|
+
|
|
543
|
+
print(f"# NB local variables Real, Integer, Boolean, String", file=txt_file)
|
|
544
|
+
for type_name in type_names_list:
|
|
545
|
+
nb = len(locals_per_type[type_name])
|
|
546
|
+
if profiling and type_name == "Real":
|
|
547
|
+
nb += len(self.execution_order)
|
|
548
|
+
print(f"{nb} ", file=txt_file, end='')
|
|
549
|
+
print("", file=txt_file)
|
|
550
|
+
|
|
551
|
+
print("# CONTAINER I/O: <VR> <FMU_INDEX> <FMU_VR>", file=txt_file)
|
|
552
|
+
for type_name in type_names_list:
|
|
553
|
+
print(f"# {type_name}", file=txt_file)
|
|
554
|
+
nb = len(inputs_per_type[type_name])+len(outputs_per_type[type_name])+len(locals_per_type[type_name])
|
|
555
|
+
if profiling and type_name == "Real":
|
|
556
|
+
nb += len(self.execution_order)
|
|
557
|
+
print(nb, file=txt_file)
|
|
558
|
+
for profiling_port,_ in enumerate(self.execution_order):
|
|
559
|
+
print(f"{profiling_port} -2 {profiling_port}", file=txt_file)
|
|
560
|
+
else:
|
|
561
|
+
print(nb, file=txt_file)
|
|
562
|
+
for cport in inputs_per_type[type_name]:
|
|
563
|
+
print(f"{cport.vr} {fmu_rank[cport.fmu.name]} {cport.port.vr}", file=txt_file)
|
|
564
|
+
for cport in outputs_per_type[type_name]:
|
|
565
|
+
print(f"{cport.vr} {fmu_rank[cport.fmu.name]} {cport.port.vr}", file=txt_file)
|
|
566
|
+
for local in locals_per_type[type_name]:
|
|
567
|
+
print(f"{local.vr} -1 {local.vr}", file=txt_file)
|
|
568
|
+
|
|
569
|
+
# LINKS
|
|
570
|
+
for fmu in self.execution_order:
|
|
571
|
+
for type_name in type_names_list:
|
|
572
|
+
print(f"# Inputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
|
|
573
|
+
print(len(inputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
574
|
+
for cport, vr in inputs_fmu_per_type[type_name][fmu.name].items():
|
|
575
|
+
print(f"{vr} {cport.port.vr}", file=txt_file)
|
|
576
|
+
|
|
577
|
+
for type_name in type_names_list:
|
|
578
|
+
print(f"# Start values of {fmu.name} - {type_name}: <FMU_VR> <VALUE>", file=txt_file)
|
|
579
|
+
print(len(start_values_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
580
|
+
for cport, value in start_values_fmu_per_type[type_name][fmu.name].items():
|
|
581
|
+
print(f"{cport.port.vr} {value}", file=txt_file)
|
|
582
|
+
|
|
583
|
+
for type_name in type_names_list:
|
|
584
|
+
print(f"# Outputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
|
|
585
|
+
print(len(outputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
586
|
+
for cport, vr in outputs_fmu_per_type[type_name][fmu.name].items():
|
|
587
|
+
print(f"{vr} {cport.port.vr}", file=txt_file)
|
|
588
|
+
|
|
589
|
+
def make_fmu_skeleton(self, base_directory: Path) -> Path:
|
|
590
|
+
logger.debug(f"Initialize directory '{base_directory}'")
|
|
591
|
+
|
|
592
|
+
origin = Path(__file__).parent / "resources"
|
|
593
|
+
resources_directory = base_directory / "resources"
|
|
594
|
+
documentation_directory = base_directory / "documentation"
|
|
595
|
+
binaries_directory = base_directory / "binaries"
|
|
596
|
+
|
|
597
|
+
base_directory.mkdir(exist_ok=True)
|
|
598
|
+
resources_directory.mkdir(exist_ok=True)
|
|
599
|
+
binaries_directory.mkdir(exist_ok=True)
|
|
600
|
+
documentation_directory.mkdir(exist_ok=True)
|
|
601
|
+
|
|
602
|
+
if self.description_pathname:
|
|
603
|
+
shutil.copy(self.description_pathname, documentation_directory)
|
|
604
|
+
|
|
605
|
+
shutil.copy(origin / "model.png", base_directory)
|
|
606
|
+
for bitness in ('win32', 'win64'):
|
|
607
|
+
library_filename = origin / bitness / "container.dll"
|
|
608
|
+
if library_filename.is_file():
|
|
609
|
+
binary_directory = binaries_directory / bitness
|
|
610
|
+
binary_directory.mkdir(exist_ok=True)
|
|
611
|
+
shutil.copy(library_filename, binary_directory / f"{self.identifier}.dll")
|
|
612
|
+
|
|
613
|
+
for fmu in self.involved_fmu.values():
|
|
614
|
+
shutil.copytree(fmu.fmu.tmp_directory, resources_directory / fmu.name, dirs_exist_ok=True)
|
|
615
|
+
|
|
616
|
+
return resources_directory
|
|
617
|
+
|
|
618
|
+
def make_fmu_package(self, base_directory: Path, fmu_filename: Path):
|
|
619
|
+
logger.debug(f"Zipping directory '{base_directory}' => '{fmu_filename}'")
|
|
620
|
+
with zipfile.ZipFile(self.fmu_directory / fmu_filename, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
|
621
|
+
for root, dirs, files in os.walk(base_directory):
|
|
622
|
+
for file in files:
|
|
623
|
+
zip_file.write(os.path.join(root, file),
|
|
624
|
+
os.path.relpath(os.path.join(root, file), base_directory))
|
|
625
|
+
logger.info(f"'{fmu_filename}' is available.")
|
|
626
|
+
|
|
627
|
+
@staticmethod
|
|
628
|
+
def make_fmu_cleanup(base_directory: Path):
|
|
629
|
+
logger.debug(f"Delete directory '{base_directory}'")
|
|
630
|
+
shutil.rmtree(base_directory)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
class FMUContainerSpecReader:
|
|
634
|
+
def __init__(self, fmu_directory: Union[Path, str]):
|
|
635
|
+
self.fmu_directory = Path(fmu_directory)
|
|
636
|
+
|
|
637
|
+
def read(self, description_filename: Union[str, Path]) -> FMUContainer:
|
|
638
|
+
if isinstance(description_filename, str):
|
|
639
|
+
description_filename = Path(description_filename)
|
|
640
|
+
|
|
641
|
+
if description_filename.suffix == ".csv":
|
|
642
|
+
return self.read_csv(description_filename)
|
|
643
|
+
else:
|
|
644
|
+
logger.critical(f"Unable to read from '{description_filename}': format unsupported.")
|
|
645
|
+
|
|
646
|
+
def read_csv(self, description_filename: Path) -> FMUContainer:
|
|
647
|
+
container = FMUContainer(description_filename.stem, self.fmu_directory)
|
|
648
|
+
container.description_pathname = self.fmu_directory / description_filename
|
|
649
|
+
logger.info(f"Building FMU Container from '{container.description_pathname}'")
|
|
650
|
+
|
|
651
|
+
with open(container.description_pathname) as file:
|
|
652
|
+
reader = csv.reader(file, delimiter=';')
|
|
653
|
+
self.check_headers(reader)
|
|
654
|
+
for i, row in enumerate(reader):
|
|
655
|
+
if not row or row[0][0] == '#': # skip blank line of comment
|
|
656
|
+
continue
|
|
657
|
+
|
|
658
|
+
try:
|
|
659
|
+
rule, from_fmu_filename, from_port_name, to_fmu_filename, to_port_name = row
|
|
660
|
+
except ValueError:
|
|
661
|
+
logger.error(f"Line #{i+2}: expecting 5 columns. Line skipped.")
|
|
662
|
+
continue
|
|
663
|
+
|
|
664
|
+
rule = rule.upper()
|
|
665
|
+
if rule in ("LINK", "INPUT", "OUTPUT", "DROP", "FMU", "START"):
|
|
666
|
+
try:
|
|
667
|
+
self._read_csv_rule(container, rule,
|
|
668
|
+
from_fmu_filename, from_port_name,
|
|
669
|
+
to_fmu_filename, to_port_name)
|
|
670
|
+
except FMUContainerError as e:
|
|
671
|
+
logger.error(f"Line #{i+2}: {e}. Line skipped.")
|
|
672
|
+
continue
|
|
673
|
+
except FMUException as e:
|
|
674
|
+
logger.critical(f"Line #{i + 2}: {e}.")
|
|
675
|
+
raise
|
|
676
|
+
else:
|
|
677
|
+
logger.error(f"Line #{i+2}: unexpected rule '{rule}'. Line skipped.")
|
|
678
|
+
|
|
679
|
+
return container
|
|
680
|
+
|
|
681
|
+
@staticmethod
|
|
682
|
+
def _read_csv_rule(container: FMUContainer, rule: str, from_fmu_filename: str, from_port_name: str,
|
|
683
|
+
to_fmu_filename: str, to_port_name: str):
|
|
684
|
+
if rule == "FMU":
|
|
685
|
+
if not from_fmu_filename:
|
|
686
|
+
raise FMUException("Missing FMU information.")
|
|
687
|
+
container.get_fmu(from_fmu_filename)
|
|
688
|
+
|
|
689
|
+
elif rule == "INPUT":
|
|
690
|
+
if not to_fmu_filename or not to_port_name:
|
|
691
|
+
raise FMUException("Missing INPUT ports information.")
|
|
692
|
+
container.add_input(from_port_name, to_fmu_filename, to_port_name)
|
|
693
|
+
|
|
694
|
+
elif rule == "OUTPUT":
|
|
695
|
+
if not from_fmu_filename or not from_port_name:
|
|
696
|
+
raise FMUException("Missing OUTPUT ports information.")
|
|
697
|
+
container.add_output(from_fmu_filename, from_port_name, to_port_name)
|
|
698
|
+
|
|
699
|
+
elif rule == "DROP":
|
|
700
|
+
if not from_fmu_filename or not from_port_name:
|
|
701
|
+
raise FMUException("Missing DROP ports information.")
|
|
702
|
+
container.drop_port(from_fmu_filename, from_port_name)
|
|
703
|
+
|
|
704
|
+
elif rule == "LINK":
|
|
705
|
+
container.add_link(from_fmu_filename, from_port_name, to_fmu_filename, to_port_name)
|
|
706
|
+
|
|
707
|
+
elif rule == "START":
|
|
708
|
+
if not from_fmu_filename or not from_port_name or not to_fmu_filename:
|
|
709
|
+
raise FMUException("Missing START ports information.")
|
|
710
|
+
|
|
711
|
+
container.add_start_value(from_fmu_filename, from_port_name, to_fmu_filename)
|
|
712
|
+
# no else: check on rule is already done in read_description()
|
|
713
|
+
|
|
714
|
+
@staticmethod
|
|
715
|
+
def check_headers(reader):
|
|
716
|
+
headers = next(reader)
|
|
717
|
+
if not headers == ["rule", "from_fmu", "from_port", "to_fmu", "to_port"]:
|
|
718
|
+
raise FMUContainerError("Header (1st line of the file) is not well formatted.")
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class FMUContainerSpecWriter:
|
|
722
|
+
def __init__(self, container: FMUContainer):
|
|
723
|
+
self.container = container
|
|
724
|
+
|
|
725
|
+
def write(self, description_filename: Union[str, Path]):
|
|
726
|
+
if description_filename.endswith(".csv"):
|
|
727
|
+
return self.write_csv(description_filename)
|
|
728
|
+
elif description_filename.endswith(".json"):
|
|
729
|
+
return self.write_json(description_filename)
|
|
730
|
+
else:
|
|
731
|
+
logger.critical(f"Unable to write to '{description_filename}': format unsupported.")
|
|
732
|
+
|
|
733
|
+
def write_csv(self, description_filename: Union[str, Path]):
|
|
734
|
+
with open(description_filename, "wt") as outfile:
|
|
735
|
+
print("rule;from_fmu;from_port;to_fmu;to_port", file=outfile)
|
|
736
|
+
for fmu in self.container.involved_fmu.keys():
|
|
737
|
+
print(f"FMU;{fmu};;;", file=outfile)
|
|
738
|
+
for cport in self.container.inputs.values():
|
|
739
|
+
print(f"INPUT;;;{cport.fmu.name};{cport.port.name}", file=outfile)
|
|
740
|
+
for cport in self.container.outputs.values():
|
|
741
|
+
print(f"OUTPUT;{cport.fmu.name};{cport.port.name};;", file=outfile)
|
|
742
|
+
for local in self.container.locals.values():
|
|
743
|
+
for target in local.cport_to_list:
|
|
744
|
+
print(f"LINK;{local.cport_from.fmu.name};{local.cport_from.port.name};"
|
|
745
|
+
f"{target.fmu.name};{target.port.name}",
|
|
746
|
+
file=outfile)
|
|
747
|
+
for cport, value in self.container.start_values.items():
|
|
748
|
+
print(f"START;{cport.fmu.name};{cport.port.name};{value};", file=outfile)
|
|
749
|
+
|
|
750
|
+
def write_json(self, description_filename: Union[str, Path]):
|
|
751
|
+
with open(description_filename, "wt") as outfile:
|
|
752
|
+
print("{", file=outfile)
|
|
753
|
+
|
|
754
|
+
print(f' "fmu": [', file=outfile)
|
|
755
|
+
fmus = [f' "{fmu}"' for fmu in self.container.involved_fmu.keys()]
|
|
756
|
+
print(",\n".join(fmus), file=outfile)
|
|
757
|
+
print(f' ],', file=outfile)
|
|
758
|
+
|
|
759
|
+
print(f' "input": [', file=outfile)
|
|
760
|
+
inputs = [f' [{cport.fmu.name}, {cport.port.name}, {container_name}]'
|
|
761
|
+
for container_name, cport in self.container.inputs.items()]
|
|
762
|
+
print(",\n".join(inputs), file=outfile)
|
|
763
|
+
print(f' ],', file=outfile)
|
|
764
|
+
|
|
765
|
+
print(f' "output": [', file=outfile)
|
|
766
|
+
outputs = [f' ["{cport.fmu.name}", "{cport.port.name}", "{container_name}"]'
|
|
767
|
+
for container_name, cport in self.container.outputs.items()]
|
|
768
|
+
print(",\n".join(outputs), file=outfile)
|
|
769
|
+
print(f' ],', file=outfile)
|
|
770
|
+
|
|
771
|
+
print(f' "link": [', file=outfile)
|
|
772
|
+
links = [f' ["{local.cport_from.fmu.name}", "{local.cport_from.port.name}", '
|
|
773
|
+
f'"{target.fmu.name}", "{target.port.name}"]'
|
|
774
|
+
for local in self.container.locals.values()
|
|
775
|
+
for target in local.cport_to_list]
|
|
776
|
+
print(",\n".join(links), file=outfile)
|
|
777
|
+
print(f' ],', file=outfile)
|
|
778
|
+
print(f' "start": [', file=outfile)
|
|
779
|
+
start = [f' ["{cport.fmu.name}", "{cport.port.name}, "{value}"]'
|
|
780
|
+
for cport, value in self.container.start_values.items()]
|
|
781
|
+
print(f' ],', file=outfile)
|
|
782
|
+
|
|
783
|
+
#print(f' "period": {self.container.})
|
|
784
|
+
print("}", file=outfile)
|