fmu-manipulation-toolbox 1.8.1__tar.gz → 1.8.2__tar.gz

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.
Files changed (65) hide show
  1. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/PKG-INFO +8 -3
  2. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/README.md +2 -1
  3. fmu_manipulation_toolbox-1.8.2/fmu_manipulation_toolbox/__version__.py +1 -0
  4. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/assembly.py +127 -41
  5. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/cli.py +14 -5
  6. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/fmu_container.py +62 -18
  7. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/fmu_operations.py +2 -3
  8. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/gui.py +330 -79
  9. fmu_manipulation_toolbox-1.8.2/fmu_manipulation_toolbox/gui_style.py +129 -0
  10. fmu_manipulation_toolbox-1.8.2/fmu_manipulation_toolbox/resources/container.png +0 -0
  11. fmu_manipulation_toolbox-1.8.2/fmu_manipulation_toolbox/resources/drop_fmu.png +0 -0
  12. fmu_manipulation_toolbox-1.8.2/fmu_manipulation_toolbox/resources/fmu.png +0 -0
  13. fmu_manipulation_toolbox-1.8.2/fmu_manipulation_toolbox/resources/fmu_manipulation_toolbox.png +0 -0
  14. fmu_manipulation_toolbox-1.8.2/fmu_manipulation_toolbox/resources/icon_fmu.png +0 -0
  15. fmu_manipulation_toolbox-1.8.2/fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
  16. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
  17. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
  18. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
  19. fmu_manipulation_toolbox-1.8.2/fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
  20. fmu_manipulation_toolbox-1.8.2/fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
  21. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox.egg-info/PKG-INFO +8 -3
  22. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox.egg-info/SOURCES.txt +3 -0
  23. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox.egg-info/requires.txt +1 -1
  24. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/setup.py +2 -1
  25. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/tests/test_suite.py +19 -0
  26. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/__version__.py +0 -1
  27. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/drop_fmu.png +0 -0
  28. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/fmu.png +0 -0
  29. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/fmu_manipulation_toolbox.png +0 -0
  30. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
  31. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
  32. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
  33. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/LICENSE.txt +0 -0
  34. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/__init__.py +0 -0
  35. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/__main__.py +0 -0
  36. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/checker.py +0 -0
  37. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/help.py +0 -0
  38. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/checkbox-checked-disabled.png +0 -0
  39. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/checkbox-checked-hover.png +0 -0
  40. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/checkbox-checked.png +0 -0
  41. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/checkbox-unchecked-disabled.png +0 -0
  42. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/checkbox-unchecked-hover.png +0 -0
  43. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/checkbox-unchecked.png +0 -0
  44. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Annotation.xsd +0 -0
  45. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2AttributeGroups.xsd +0 -0
  46. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ModelDescription.xsd +0 -0
  47. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ScalarVariable.xsd +0 -0
  48. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Type.xsd +0 -0
  49. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Unit.xsd +0 -0
  50. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2VariableDependency.xsd +0 -0
  51. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/help.png +0 -0
  52. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/icon-round.png +0 -0
  53. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/icon.png +0 -0
  54. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/license.txt +0 -0
  55. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
  56. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/linux32/server_sm +0 -0
  57. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
  58. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/linux64/server_sm +0 -0
  59. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/mask.png +0 -0
  60. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/resources/model.png +0 -0
  61. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox/version.py +0 -0
  62. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox.egg-info/dependency_links.txt +0 -0
  63. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox.egg-info/entry_points.txt +0 -0
  64. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/fmu_manipulation_toolbox.egg-info/top_level.txt +0 -0
  65. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2}/setup.cfg +0 -0
@@ -1,14 +1,19 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: fmu_manipulation_toolbox
3
- Version: 1.8.1
3
+ Version: 1.8.2
4
4
  Summary: FMU Manipulation Toobox is a python application which help to modify a Functional Mock-up Units (FMUs) without recompilation or to group them into FMU Containers
5
5
  Home-page: https://github.com/grouperenault/fmu_manipulation_toolbox/
6
6
  Author: Nicolas.LAURENT@Renault.com
7
7
  License-File: LICENSE.txt
8
- Requires-Dist: PyQt5>=5.15.10
8
+ Requires-Dist: PySide6>=6.8.0
9
9
  Requires-Dist: xmlschema>=3.3.1
10
10
  Requires-Dist: elementpath>=4.4.0
11
11
  Requires-Dist: colorama>=0.4.6
12
+ Dynamic: author
13
+ Dynamic: description
14
+ Dynamic: home-page
15
+ Dynamic: requires-dist
16
+ Dynamic: summary
12
17
 
13
18
  FMU Manipulation Toolbox is a python application which help to modify a Functional Mock-up Units (FMUs)
14
19
  without recompilation. It mainly modifies the `modelDescription.xml` file. It is highly customizable.
@@ -123,7 +123,7 @@ optional arguments:
123
123
 
124
124
  ```
125
125
  usage: fmucontainer [-h] [-fmu-directory FMU_DIRECTORY] -container filename.{csv|json|ssp},[:step_size] [-debug]
126
- [-no-auto-input] [-no-auto-output] [-no-auto-link] [-mt] [-profile] [-dump-json]
126
+ [-no-auto-input] [-no-auto-output] [-auto-parameter] [-no-auto-link] [-mt] [-profile] [-dump-json]
127
127
 
128
128
  Generate FMU from FMU's
129
129
 
@@ -136,6 +136,7 @@ optional arguments:
136
136
  -debug Add lot of useful log during the process. (default: False)
137
137
  -no-auto-input Create ONLY explicit input. (default: True)
138
138
  -no-auto-output Create ONLY explicit output. (default: True)
139
+ -auto-parameter Expose parameters of the embedded fmu's. (default: False)
139
140
  -no-auto-link Create ONLY explicit links. (default: True)
140
141
  -mt Enable Multi-Threaded mode for the generated container. (default: False)
141
142
  -profile Enable Profiling mode for the generated container. (default: False)
@@ -23,16 +23,22 @@ class Port:
23
23
  def __eq__(self, other):
24
24
  return str(self) == str(other)
25
25
 
26
+ def __str__(self):
27
+ return f"{self.fmu_name}/{self.port_name}"
28
+
26
29
 
27
30
  class Connection:
28
31
  def __init__(self, from_port: Port, to_port: Port):
29
32
  self.from_port = from_port
30
33
  self.to_port = to_port
31
34
 
35
+ def __str__(self):
36
+ return f"{self.from_port} -> {self.to_port}"
37
+
32
38
 
33
39
  class AssemblyNode:
34
40
  def __init__(self, name: Optional[str], step_size: float = None, mt=False, profiling=False,
35
- auto_link=True, auto_input=True, auto_output=True):
41
+ auto_link=True, auto_input=True, auto_output=True, auto_parameter=False, auto_local=False):
36
42
  self.name = name
37
43
  if step_size:
38
44
  try:
@@ -47,12 +53,14 @@ class AssemblyNode:
47
53
  self.auto_link = auto_link
48
54
  self.auto_input = auto_input
49
55
  self.auto_output = auto_output
56
+ self.auto_parameter = auto_parameter
57
+ self.auto_local = auto_local
50
58
 
51
59
  self.parent: Optional[AssemblyNode] = None
52
- self.children: List[AssemblyNode] = [] # sub-containers
53
- self.fmu_names_list: Set[str] = set() # FMUs contained at this level
54
- self.input_ports: Dict[Port, str] = {}
55
- self.output_ports: Dict[Port, str] = {}
60
+ self.children: Dict[str, AssemblyNode] = {} # sub-containers
61
+ self.fmu_names_list: List[str] = [] # FMUs contained at this level (ordered list)
62
+ self.input_ports: Dict[Port, str] = {} # value is input port name, key is the source
63
+ self.output_ports: Dict[Port, str] = {} # value is output port name, key is the origin
56
64
  self.start_values: Dict[Port, str] = {}
57
65
  self.drop_ports: List[Port] = []
58
66
  self.links: List[Connection] = []
@@ -64,13 +72,17 @@ class AssemblyNode:
64
72
  if sub_node.parent is not None:
65
73
  raise AssemblyError(f"Internal Error: AssemblyNode {sub_node.name} is already parented.")
66
74
 
67
- sub_node.parent = self
68
- self.fmu_names_list.add(sub_node.name)
69
- self.children.append(sub_node)
75
+ if sub_node.name in self.children:
76
+ raise AssemblyError(f"Internal Error: AssemblyNode {sub_node.name} is already child of {self.name}")
70
77
 
78
+ sub_node.parent = self
79
+ if sub_node.name not in self.fmu_names_list:
80
+ self.fmu_names_list.append(sub_node.name)
81
+ self.children[sub_node.name] = sub_node
71
82
 
72
83
  def add_fmu(self, fmu_name: str):
73
- self.fmu_names_list.add(fmu_name)
84
+ if fmu_name not in self.fmu_names_list:
85
+ self.fmu_names_list.append(fmu_name)
74
86
 
75
87
  def add_input(self, from_port_name: str, to_fmu_filename: str, to_port_name: str):
76
88
  self.input_ports[Port(to_fmu_filename, to_port_name)] = from_port_name
@@ -89,12 +101,13 @@ class AssemblyNode:
89
101
  self.start_values[Port(fmu_filename, port_name)] = value
90
102
 
91
103
  def make_fmu(self, fmu_directory: Path, debug=False, description_pathname=None):
92
- for node in self.children:
104
+ for node in self.children.values():
93
105
  node.make_fmu(fmu_directory, debug=debug)
94
106
 
95
- container = FMUContainer(self.name, fmu_directory, description_pathname=description_pathname)
107
+ identifier = str(Path(self.name).stem)
108
+ container = FMUContainer(identifier, fmu_directory, description_pathname=description_pathname)
96
109
 
97
- for fmu_name in sorted(self.fmu_names_list):
110
+ for fmu_name in self.fmu_names_list:
98
111
  container.get_fmu(fmu_name)
99
112
 
100
113
  for port, source in self.input_ports.items():
@@ -115,14 +128,78 @@ class AssemblyNode:
115
128
 
116
129
  container.add_implicit_rule(auto_input=self.auto_input,
117
130
  auto_output=self.auto_output,
118
- auto_link=self.auto_link)
131
+ auto_link=self.auto_link,
132
+ auto_parameter=self.auto_parameter,
133
+ auto_local=self.auto_local)
119
134
 
120
135
  container.make_fmu(self.name, self.step_size, mt=self.mt, profiling=self.profiling, debug=debug)
121
136
 
122
- for node in self.children:
137
+ for node in self.children.values():
123
138
  logger.info(f"Deleting transient FMU Container '{node.name}'")
124
139
  (fmu_directory / node.name).unlink()
125
140
 
141
+ def get_final_from(self, port: Port) -> Port:
142
+ if port in self.input_ports:
143
+ ancestor = Port(self.name, self.input_ports[port])
144
+ if self.parent:
145
+ return self.parent.get_final_from(ancestor) # input port
146
+ else:
147
+ return ancestor # TOPLEVEL input port
148
+ elif port.fmu_name in self.fmu_names_list:
149
+ if port.fmu_name in self.children:
150
+ child = self.children[port.fmu_name]
151
+ ancestors = [key for key, val in child.output_ports.items() if val == port.port_name]
152
+ if len(ancestors) == 1:
153
+ return child.get_final_from(ancestors[0]) # child output port
154
+ else:
155
+ return port # embedded FMU
156
+
157
+ raise AssemblyError(f"{self.name}: Port {port} is not connected upstream.")
158
+
159
+ def get_final_to(self, port: Port) -> Port:
160
+ if port in self.output_ports:
161
+ successor = Port(self.name, self.output_ports[port])
162
+ if self.parent:
163
+ return self.parent.get_final_to(successor) # Output port
164
+ else:
165
+ return successor # TOLEVEL output port
166
+ elif port.fmu_name in self.fmu_names_list:
167
+ if port.fmu_name in self.children:
168
+ child = self.children[port.fmu_name]
169
+ successors = [key for key, val in child.input_ports.items() if val == port.port_name]
170
+ if len(successors) == 1:
171
+ return child.get_final_to(successors[0]) # Child input port
172
+ else:
173
+ return port # embedded FMU
174
+
175
+ raise AssemblyError(f"Node {self.name}: Port {port} is not connected downstream.")
176
+
177
+ def get_fmu_connections(self, fmu_name: str) -> List[Connection]:
178
+ connections = []
179
+ if fmu_name not in self.fmu_names_list:
180
+ raise AssemblyError(f"Internal Error: FMU {fmu_name} is not embedded by {self.name}.")
181
+ for link in self.links:
182
+ if link.from_port.fmu_name == fmu_name:
183
+ connections.append(Connection(link.from_port, self.get_final_to(link.to_port)))
184
+ elif link.to_port.fmu_name == fmu_name:
185
+ connections.append(Connection(self.get_final_from(link.from_port), link.to_port))
186
+
187
+ for to_port, input_port_name in self.input_ports.items():
188
+ if to_port.fmu_name == fmu_name:
189
+ if self.parent:
190
+ connections.append(Connection(self.parent.get_final_from(Port(self.name, input_port_name)), to_port))
191
+ else:
192
+ connections.append(Connection(Port(self.name, input_port_name), to_port))
193
+
194
+ for from_port, output_port_name in self.output_ports.items():
195
+ if from_port.fmu_name == fmu_name:
196
+ if self.parent:
197
+ connections.append(Connection(from_port, self.parent.get_final_to(Port(self.name, output_port_name))))
198
+ else:
199
+ connections.append(Connection(from_port, Port(self.name, output_port_name))) ###HERE
200
+
201
+ return connections
202
+
126
203
 
127
204
  class AssemblyError(Exception):
128
205
  def __init__(self, reason: str):
@@ -134,13 +211,16 @@ class AssemblyError(Exception):
134
211
 
135
212
  class Assembly:
136
213
  def __init__(self, filename: str, step_size=None, auto_link=True, auto_input=True, debug=False,
137
- auto_output=True, mt=False, profiling=False, fmu_directory: Path = "."):
214
+ auto_output=True, mt=False, profiling=False, fmu_directory: Path = Path("."), auto_parameter=False,
215
+ auto_local=False):
138
216
  self.filename = Path(filename)
139
217
  self.default_auto_input = auto_input
140
218
  self.debug = debug
141
219
  self.default_auto_output = auto_output
142
220
  self.default_step_size = step_size
143
221
  self.default_auto_link = auto_link
222
+ self.default_auto_parameter = auto_parameter
223
+ self.default_auto_local = auto_local
144
224
  self.default_mt = mt
145
225
  self.default_profiling = profiling
146
226
  self.fmu_directory = fmu_directory
@@ -197,7 +277,8 @@ class Assembly:
197
277
  name = str(self.filename.with_suffix(".fmu"))
198
278
  self.root = AssemblyNode(name, step_size=self.default_step_size, auto_link=self.default_auto_link,
199
279
  mt=self.default_mt, profiling=self.default_profiling,
200
- auto_input=self.default_auto_input, auto_output=self.default_auto_output)
280
+ auto_input=self.default_auto_input, auto_output=self.default_auto_output,
281
+ auto_parameter=self.default_auto_parameter, auto_local=self.default_auto_local)
201
282
 
202
283
  with open(self.input_pathname) as file:
203
284
  reader = csv.reader(file, delimiter=';')
@@ -292,49 +373,53 @@ class Assembly:
292
373
  raise AssemblyError(f"Cannot read json: {e}")
293
374
  self.root = self._json_decode_node(data)
294
375
  if not self.root.name:
295
- self.root.name = str(self.filename.with_suffix(".fmu"))
376
+ self.root.name = str(self.filename.with_suffix(".fmu").name)
296
377
 
297
- def _json_decode_node(self, data:Dict) -> AssemblyNode:
378
+ def _json_decode_node(self, data: Dict) -> AssemblyNode:
298
379
  name = data.get("name", None) # 1
299
- step_size = data.get("step_size", self.default_step_size) # 7
380
+ mt = data.get("mt", self.default_mt) # 2
381
+ profiling = data.get("profiling", self.default_profiling) # 3
300
382
  auto_link = data.get("auto_link", self.default_auto_link) # 4
301
383
  auto_input = data.get("auto_input", self.default_auto_input) # 5
302
384
  auto_output = data.get("auto_output", self.default_auto_output) # 6
303
- mt = data.get("mt", self.default_mt) # 2
304
- profiling = data.get("profiling", self.default_profiling) # 3
385
+ auto_parameter = data.get("auto_parameter", self.default_auto_parameter) # 6b
386
+ auto_local = data.get("auto_local", self.default_auto_local) # 6c
387
+ step_size = data.get("step_size", self.default_step_size) # 7
305
388
 
306
389
  node = AssemblyNode(name, step_size=step_size, auto_link=auto_link, mt=mt, profiling=profiling,
307
- auto_input=auto_input, auto_output=auto_output)
390
+ auto_input=auto_input, auto_output=auto_output, auto_parameter=auto_parameter,
391
+ auto_local=auto_local)
308
392
 
309
393
  for key, value in data.items():
310
- if key in ('name', 'step_size', 'auto_link', 'auto_input', 'auto_output', 'mt', 'profiling'):
394
+ if key in ('name', 'step_size', 'auto_link', 'auto_input', 'auto_output', 'mt', 'profiling',
395
+ 'auto_parameter', 'auto_local'):
311
396
  continue # Already read
312
397
 
313
- elif key == "container": # 8
398
+ elif key == "container": # 8
314
399
  if not isinstance(value, list):
315
400
  raise AssemblyError("JSON: 'container' keyword should define a list.")
316
401
  for sub_data in value:
317
402
  node.add_sub_node(self._json_decode_node(sub_data))
318
403
 
319
- elif key == "fmu": # 9
404
+ elif key == "fmu": # 9
320
405
  if not isinstance(value, list):
321
406
  raise AssemblyError("JSON: 'fmu' keyword should define a list.")
322
407
  for fmu in value:
323
408
  node.add_fmu(fmu)
324
409
 
325
- elif key == "input": # 10
410
+ elif key == "input": # 10
326
411
  self._json_decode_keyword('input', value, node.add_input)
327
412
 
328
- elif key == "output": # 11
413
+ elif key == "output": # 11
329
414
  self._json_decode_keyword('output', value, node.add_output)
330
415
 
331
- elif key == "link": # 12
416
+ elif key == "link": # 12
332
417
  self._json_decode_keyword('link', value, node.add_link)
333
418
 
334
- elif key == "start": # 13
419
+ elif key == "start": # 13
335
420
  self._json_decode_keyword('start', value, node.add_start_value)
336
421
 
337
- elif key == "drop": #14
422
+ elif key == "drop": # 14
338
423
  self._json_decode_keyword('drop', value, node.add_drop_port)
339
424
 
340
425
  else:
@@ -354,7 +439,6 @@ class Assembly:
354
439
  except TypeError:
355
440
  raise AssemblyError(f"JSON: '{keyword}' value does not contain right number of fields: {line}.")
356
441
 
357
-
358
442
  def write_json(self, filename: Union[str, Path]):
359
443
  with open(self.fmu_directory / filename, "wt") as file:
360
444
  data = self._json_encode_node(self.root)
@@ -362,21 +446,23 @@ class Assembly:
362
446
 
363
447
  def _json_encode_node(self, node: AssemblyNode) -> Dict[str, Any]:
364
448
  json_node = dict()
365
- json_node["name"] = node.name # 1
366
- json_node["mt"] = node.mt # 2
367
- json_node["profiling"] = node.profiling # 3
368
- json_node["auto_link"] = node.auto_link # 4
369
- json_node["auto_input"] = node.auto_input # 5
370
- json_node["auto_output"] = node.auto_output # 6
449
+ json_node["name"] = node.name # 1
450
+ json_node["mt"] = node.mt # 2
451
+ json_node["profiling"] = node.profiling # 3
452
+ json_node["auto_link"] = node.auto_link # 4
453
+ json_node["auto_input"] = node.auto_input # 5
454
+ json_node["auto_output"] = node.auto_output # 6
455
+ json_node["auto_parameter"] = node.auto_parameter # 6b
456
+ json_node["auto_local"] = node.auto_local # 6c
371
457
 
372
458
  if node.step_size:
373
- json_node["step_size"] = node.step_size # 7
459
+ json_node["step_size"] = node.step_size # 7
374
460
 
375
461
  if node.children:
376
- json_node["container"] = [self._json_encode_node(child) for child in node.children] # 8
462
+ json_node["container"] = [self._json_encode_node(child) for child in node.children.values()] # 8
377
463
 
378
464
  if node.fmu_names_list:
379
- json_node["fmu"] = [f"{fmu_name}" for fmu_name in sorted(node.fmu_names_list)] #9
465
+ json_node["fmu"] = [f"{fmu_name}" for fmu_name in sorted(node.fmu_names_list)] # 9
380
466
 
381
467
  if node.input_ports:
382
468
  json_node["input"] = [[f"{source}", f"{port.fmu_name}", f"{port.port_name}"] # 10
@@ -396,7 +482,7 @@ class Assembly:
396
482
  for port, value in node.start_values.items()]
397
483
 
398
484
  if node.drop_ports:
399
- json_node["drop"] = [[f"{port.fmu_name}", f"{port.port_name}"] for port in node.drop_ports] # 14
485
+ json_node["drop"] = [[f"{port.fmu_name}", f"{port.port_name}"] for port in node.drop_ports] # 14
400
486
 
401
487
  return json_node
402
488
 
@@ -176,6 +176,12 @@ def fmucontainer():
176
176
  parser.add_argument("-no-auto-output", action="store_false", dest="auto_output", default=True,
177
177
  help="Create ONLY explicit output.")
178
178
 
179
+ parser.add_argument("-auto-parameter", action="store_true", dest="auto_parameter", default=False,
180
+ help="Expose parameters of the embedded fmu's.")
181
+
182
+ parser.add_argument("-auto-local", action="store_true", dest="auto_local", default=False,
183
+ help="Expose local variables of the embedded fmu's.")
184
+
179
185
  parser.add_argument("-no-auto-link", action="store_false", dest="auto_link", default=True,
180
186
  help="Create ONLY explicit links.")
181
187
 
@@ -198,16 +204,19 @@ def fmucontainer():
198
204
 
199
205
  for description in config.container_descriptions_list:
200
206
  try:
201
- filename, step_size = description.split(":")
202
- step_size = float(step_size)
207
+ tokens = description.split(":")
208
+ filename = ":".join(tokens[:-1])
209
+ step_size = float(tokens[-1])
203
210
  except ValueError:
204
211
  step_size = None
205
212
  filename = description
213
+
206
214
  try:
207
215
  assembly = Assembly(filename, step_size=step_size, auto_link=config.auto_link,
208
- auto_input=config.auto_input, auto_output=config.auto_output, mt=config.mt,
209
- profiling=config.profiling, fmu_directory=fmu_directory, debug=config.debug)
210
-
216
+ auto_input=config.auto_input, auto_output=config.auto_output,
217
+ auto_local=config.auto_local, mt=config.mt,
218
+ profiling=config.profiling, fmu_directory=fmu_directory, debug=config.debug,
219
+ auto_parameter=config.auto_parameter)
211
220
  except FileNotFoundError as e:
212
221
  logger.fatal(f"Cannot read file: {e}")
213
222
  continue
@@ -74,9 +74,12 @@ class EmbeddedFMU(OperationAbstract):
74
74
  def __init__(self, filename):
75
75
  self.fmu = FMU(filename)
76
76
  self.name = Path(filename).name
77
+ self.id = Path(filename).stem
77
78
 
78
79
  self.fmi_version = None
79
80
  self.step_size = None
81
+ self.start_time = None
82
+ self.stop_time = None
80
83
  self.model_identifier = None
81
84
  self.guid = None
82
85
  self.ports: Dict[str, FMUPort] = {}
@@ -103,15 +106,18 @@ class EmbeddedFMU(OperationAbstract):
103
106
  for capability in self.capability_list:
104
107
  self.capabilities[capability] = attrs.get(capability, "false")
105
108
 
106
- def experiment_attrs(self, attrs):
109
+ def experiment_attrs(self, attrs: Dict[str, str]):
107
110
  try:
108
111
  self.step_size = float(attrs['stepSize'])
109
112
  except KeyError:
110
113
  logger.warning(f"FMU '{self.name}' does not specify preferred step size")
111
- pass
114
+ self.start_time = float(attrs.get("startTime", 0.0))
115
+ self.stop_time = float(attrs.get("stopTime", self.start_time + 1.0))
112
116
 
113
117
  def scalar_type(self, type_name, attrs):
114
118
  if self.current_port:
119
+ if type_name == "Enumeration":
120
+ type_name = "Integer"
115
121
  self.current_port.set_port_type(type_name, attrs)
116
122
  self.current_port = None
117
123
 
@@ -148,7 +154,7 @@ class ContainerPort:
148
154
 
149
155
  class Local:
150
156
  def __init__(self, cport_from: ContainerPort):
151
- self.name = cport_from.fmu.name[:-4] + "." + cport_from.port.name # strip .fmu suffix
157
+ self.name = cport_from.fmu.id + "." + cport_from.port.name # strip .fmu suffix
152
158
  self.cport_from = cport_from
153
159
  self.cport_to_list: List[ContainerPort] = []
154
160
  self.vr = None
@@ -195,6 +201,9 @@ class FMUContainer:
195
201
 
196
202
  self.description_pathname = description_pathname
197
203
 
204
+ self.start_time = None
205
+ self.stop_time = None
206
+
198
207
  # Rules
199
208
  self.inputs: Dict[str, ContainerPort] = {}
200
209
  self.outputs: Dict[str, ContainerPort] = {}
@@ -299,13 +308,19 @@ class FMUContainer:
299
308
  return ContainerPort(fmu, port.name)
300
309
  return None
301
310
 
302
- def add_implicit_rule(self, auto_input: bool = True, auto_output: bool = True, auto_link: bool = True):
311
+ def add_implicit_rule(self, auto_input=True, auto_output=True, auto_link=True, auto_parameter=False,
312
+ auto_local=False):
303
313
  # Auto Link outputs
304
314
  for fmu in self.execution_order:
305
315
  for port_name in fmu.ports:
306
316
  cport = ContainerPort(fmu, port_name)
307
317
  if cport not in self.rules:
308
- if cport.port.causality == 'output':
318
+ if cport.port.causality == 'parameter' and auto_parameter:
319
+ parameter_name = cport.fmu.model_identifier+"."+cport.port.name
320
+ logger.info(f"AUTO PARAMETER: {cport} as {parameter_name}")
321
+ self.inputs[parameter_name] = cport
322
+ self.mark_ruled(cport, 'PARAMETER')
323
+ elif cport.port.causality == 'output':
309
324
  candidate_cport = self.find_input(cport.port)
310
325
  if auto_link and candidate_cport:
311
326
  local = Local(cport)
@@ -314,11 +329,21 @@ class FMUContainer:
314
329
  self.mark_ruled(cport, 'LINK')
315
330
  self.mark_ruled(candidate_cport, 'LINK')
316
331
  self.locals[cport] = local
317
- else:
318
- if auto_output:
319
- self.mark_ruled(cport, 'OUTPUT')
320
- self.outputs[port_name] = cport
321
- logger.info(f"AUTO OUTPUT: Expose {cport}")
332
+ elif auto_output:
333
+ self.mark_ruled(cport, 'OUTPUT')
334
+ self.outputs[port_name] = cport
335
+ logger.info(f"AUTO OUTPUT: Expose {cport}")
336
+ elif cport.port.causality == 'local':
337
+ local_portname = None
338
+ if port_name.startswith("container."):
339
+ local_portname = "container." + cport.fmu.id + "." + port_name[10:]
340
+ logger.info(f"PROFILING: Expose {cport}")
341
+ elif auto_local:
342
+ local_portname = cport.fmu.id + "." + port_name
343
+ logger.info(f"AUTO LOCAL: Expose {cport}")
344
+ if local_portname:
345
+ self.mark_ruled(cport, 'OUTPUT')
346
+ self.outputs[local_portname] = cport
322
347
 
323
348
  if auto_input:
324
349
  # Auto link inputs
@@ -406,6 +431,18 @@ class FMUContainer:
406
431
  if fmu.capabilities[capability] == "true":
407
432
  capabilities[capability] = "true"
408
433
 
434
+ if self.start_time is None:
435
+ self.start_time = self.execution_order[0].start_time
436
+ logger.info(f"start_time={self.start_time} (deduced from '{self.execution_order[0].name}')")
437
+ else:
438
+ logger.info(f"start_time={self.start_time}")
439
+
440
+ if self.stop_time is None:
441
+ self.stop_time = self.execution_order[0].stop_time
442
+ logger.info(f"stop_time={self.stop_time} (deduced from '{self.execution_order[0].name}')")
443
+ else:
444
+ logger.info(f"stop_time={self.stop_time}")
445
+
409
446
  xml_file.write(f"""<?xml version="1.0" encoding="ISO-8859-1"?>
410
447
  <fmiModelDescription
411
448
  fmiVersion="2.0"
@@ -434,14 +471,14 @@ class FMUContainer:
434
471
  <Category name="fmucontainer"/>
435
472
  </LogCategories>
436
473
 
437
- <DefaultExperiment stepSize="{step_size}"/>
474
+ <DefaultExperiment stepSize="{step_size}" startTime="{self.start_time}" stopTime="{self.stop_time}"/>
438
475
 
439
476
  <ModelVariables>
440
477
  """)
441
478
  if profiling:
442
479
  for fmu in self.execution_order:
443
480
  vr = vr_table.add_vr("Real")
444
- name = f"container.{fmu.model_identifier}.rt_ratio"
481
+ name = f"container.{fmu.id}.rt_ratio"
445
482
  print(f'<ScalarVariable valueReference="{vr}" name="{name}" causality="local">'
446
483
  f'<Real /></ScalarVariable>', file=xml_file)
447
484
 
@@ -591,6 +628,14 @@ class FMUContainer:
591
628
  for cport, vr in outputs_fmu_per_type[type_name][fmu.name].items():
592
629
  print(f"{vr} {cport.port.vr}", file=txt_file)
593
630
 
631
+ @staticmethod
632
+ def long_path(path: Union[str, Path]) -> str:
633
+ # https://stackoverflow.com/questions/14075465/copy-a-file-with-a-too-long-path-to-another-directory-in-python
634
+ if os.name == 'nt':
635
+ return "\\\\?\\" + os.path.abspath(str(path))
636
+ else:
637
+ return path
638
+
594
639
  def make_fmu_skeleton(self, base_directory: Path) -> Path:
595
640
  logger.debug(f"Initialize directory '{base_directory}'")
596
641
 
@@ -617,8 +662,8 @@ class FMUContainer:
617
662
  shutil.copy(library_filename, binary_directory / f"{self.identifier}.dll")
618
663
 
619
664
  for fmu in self.involved_fmu.values():
620
- shutil.copytree(fmu.fmu.tmp_directory, resources_directory / fmu.name, dirs_exist_ok=True)
621
-
665
+ shutil.copytree(self.long_path(fmu.fmu.tmp_directory),
666
+ self.long_path(resources_directory / fmu.name), dirs_exist_ok=True)
622
667
  return resources_directory
623
668
 
624
669
  def make_fmu_package(self, base_directory: Path, fmu_filename: Path):
@@ -626,11 +671,10 @@ class FMUContainer:
626
671
  with zipfile.ZipFile(self.fmu_directory / fmu_filename, "w", zipfile.ZIP_DEFLATED) as zip_file:
627
672
  for root, dirs, files in os.walk(base_directory):
628
673
  for file in files:
629
- zip_file.write(os.path.join(root, file),
674
+ zip_file.write(self.long_path(os.path.join(root, file)),
630
675
  os.path.relpath(os.path.join(root, file), base_directory))
631
676
  logger.info(f"'{fmu_filename}' is available.")
632
677
 
633
- @staticmethod
634
- def make_fmu_cleanup(base_directory: Path):
678
+ def make_fmu_cleanup(self, base_directory: Path):
635
679
  logger.debug(f"Delete directory '{base_directory}'")
636
- shutil.rmtree(base_directory)
680
+ shutil.rmtree(self.long_path(base_directory))
@@ -89,9 +89,8 @@ class Manipulation:
89
89
  self.remove_port(attrs['name'])
90
90
  else:
91
91
  self.keep_port(attrs['name'])
92
- else:
92
+ else: # Keep ScalarVariable as it is.
93
93
  self.keep_port(attrs['name'])
94
- self.skip_until = name # do not read inner tags
95
94
  elif name == 'CoSimulation':
96
95
  self.operation.cosimulation_attrs(attrs)
97
96
  elif name == 'DefaultExperiment':
@@ -100,7 +99,7 @@ class Manipulation:
100
99
  self.operation.fmi_attrs(attrs)
101
100
  elif name == 'Unknown':
102
101
  self.unknown_attrs(attrs)
103
- elif name in ('Real', 'Integer', 'String', 'Boolean'):
102
+ elif name in ('Real', 'Integer', 'String', 'Boolean', 'Enumeration'):
104
103
  self.operation.scalar_type(name, attrs)
105
104
 
106
105
  except ManipulationSkipTag: