fmu-manipulation-toolbox 1.8.1__tar.gz → 1.8.2.dev2__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 (64) hide show
  1. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/PKG-INFO +8 -3
  2. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/README.md +2 -1
  3. fmu_manipulation_toolbox-1.8.2.dev2/fmu_manipulation_toolbox/__version__.py +1 -0
  4. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/assembly.py +120 -41
  5. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/cli.py +5 -2
  6. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/fmu_container.py +53 -15
  7. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/fmu_operations.py +2 -3
  8. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/gui.py +330 -79
  9. fmu_manipulation_toolbox-1.8.2.dev2/fmu_manipulation_toolbox/gui_style.py +129 -0
  10. fmu_manipulation_toolbox-1.8.2.dev2/fmu_manipulation_toolbox/resources/container.png +0 -0
  11. fmu_manipulation_toolbox-1.8.2.dev2/fmu_manipulation_toolbox/resources/drop_fmu.png +0 -0
  12. fmu_manipulation_toolbox-1.8.2.dev2/fmu_manipulation_toolbox/resources/fmu.png +0 -0
  13. fmu_manipulation_toolbox-1.8.2.dev2/fmu_manipulation_toolbox/resources/fmu_manipulation_toolbox.png +0 -0
  14. fmu_manipulation_toolbox-1.8.2.dev2/fmu_manipulation_toolbox/resources/icon_fmu.png +0 -0
  15. fmu_manipulation_toolbox-1.8.2.dev2/fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
  16. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
  17. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
  18. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
  19. fmu_manipulation_toolbox-1.8.2.dev2/fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
  20. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
  21. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox.egg-info/PKG-INFO +8 -3
  22. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox.egg-info/SOURCES.txt +3 -0
  23. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox.egg-info/requires.txt +1 -1
  24. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/setup.py +2 -1
  25. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/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-1.8.2.dev2}/LICENSE.txt +0 -0
  33. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/__init__.py +0 -0
  34. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/__main__.py +0 -0
  35. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/checker.py +0 -0
  36. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/help.py +0 -0
  37. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/checkbox-checked-disabled.png +0 -0
  38. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/checkbox-checked-hover.png +0 -0
  39. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/checkbox-checked.png +0 -0
  40. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/checkbox-unchecked-disabled.png +0 -0
  41. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/checkbox-unchecked-hover.png +0 -0
  42. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/checkbox-unchecked.png +0 -0
  43. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Annotation.xsd +0 -0
  44. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2AttributeGroups.xsd +0 -0
  45. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ModelDescription.xsd +0 -0
  46. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ScalarVariable.xsd +0 -0
  47. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Type.xsd +0 -0
  48. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Unit.xsd +0 -0
  49. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2VariableDependency.xsd +0 -0
  50. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/help.png +0 -0
  51. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/icon-round.png +0 -0
  52. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/icon.png +0 -0
  53. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/license.txt +0 -0
  54. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
  55. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/linux32/server_sm +0 -0
  56. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
  57. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/linux64/server_sm +0 -0
  58. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/mask.png +0 -0
  59. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/resources/model.png +0 -0
  60. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox/version.py +0 -0
  61. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox.egg-info/dependency_links.txt +0 -0
  62. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox.egg-info/entry_points.txt +0 -0
  63. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/fmu_manipulation_toolbox.egg-info/top_level.txt +0 -0
  64. {fmu_manipulation_toolbox-1.8.1 → fmu_manipulation_toolbox-1.8.2.dev2}/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.dev2
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):
36
42
  self.name = name
37
43
  if step_size:
38
44
  try:
@@ -47,12 +53,13 @@ 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
50
57
 
51
58
  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] = {}
59
+ self.children: Dict[str, AssemblyNode] = {} # sub-containers
60
+ self.fmu_names_list: List[str] = [] # FMUs contained at this level (ordered list)
61
+ self.input_ports: Dict[Port, str] = {} # value is input port name, key is the source
62
+ self.output_ports: Dict[Port, str] = {} # value is output port name, key is the origin
56
63
  self.start_values: Dict[Port, str] = {}
57
64
  self.drop_ports: List[Port] = []
58
65
  self.links: List[Connection] = []
@@ -64,13 +71,17 @@ class AssemblyNode:
64
71
  if sub_node.parent is not None:
65
72
  raise AssemblyError(f"Internal Error: AssemblyNode {sub_node.name} is already parented.")
66
73
 
67
- sub_node.parent = self
68
- self.fmu_names_list.add(sub_node.name)
69
- self.children.append(sub_node)
74
+ if sub_node.name in self.children:
75
+ raise AssemblyError(f"Internal Error: AssemblyNode {sub_node.name} is already child of {self.name}")
70
76
 
77
+ sub_node.parent = self
78
+ if sub_node.name not in self.fmu_names_list:
79
+ self.fmu_names_list.append(sub_node.name)
80
+ self.children[sub_node.name] = sub_node
71
81
 
72
82
  def add_fmu(self, fmu_name: str):
73
- self.fmu_names_list.add(fmu_name)
83
+ if fmu_name not in self.fmu_names_list:
84
+ self.fmu_names_list.append(fmu_name)
74
85
 
75
86
  def add_input(self, from_port_name: str, to_fmu_filename: str, to_port_name: str):
76
87
  self.input_ports[Port(to_fmu_filename, to_port_name)] = from_port_name
@@ -89,12 +100,13 @@ class AssemblyNode:
89
100
  self.start_values[Port(fmu_filename, port_name)] = value
90
101
 
91
102
  def make_fmu(self, fmu_directory: Path, debug=False, description_pathname=None):
92
- for node in self.children:
103
+ for node in self.children.values():
93
104
  node.make_fmu(fmu_directory, debug=debug)
94
105
 
95
- container = FMUContainer(self.name, fmu_directory, description_pathname=description_pathname)
106
+ identifier = str(Path(self.name).stem)
107
+ container = FMUContainer(identifier, fmu_directory, description_pathname=description_pathname)
96
108
 
97
- for fmu_name in sorted(self.fmu_names_list):
109
+ for fmu_name in self.fmu_names_list:
98
110
  container.get_fmu(fmu_name)
99
111
 
100
112
  for port, source in self.input_ports.items():
@@ -115,14 +127,77 @@ class AssemblyNode:
115
127
 
116
128
  container.add_implicit_rule(auto_input=self.auto_input,
117
129
  auto_output=self.auto_output,
118
- auto_link=self.auto_link)
130
+ auto_link=self.auto_link,
131
+ auto_parameter=self.auto_parameter)
119
132
 
120
133
  container.make_fmu(self.name, self.step_size, mt=self.mt, profiling=self.profiling, debug=debug)
121
134
 
122
- for node in self.children:
135
+ for node in self.children.values():
123
136
  logger.info(f"Deleting transient FMU Container '{node.name}'")
124
137
  (fmu_directory / node.name).unlink()
125
138
 
139
+ def get_final_from(self, port: Port) -> Port:
140
+ if port in self.input_ports:
141
+ ancestor = Port(self.name, self.input_ports[port])
142
+ if self.parent:
143
+ return self.parent.get_final_from(ancestor) # input port
144
+ else:
145
+ return ancestor # TOPLEVEL input port
146
+ elif port.fmu_name in self.fmu_names_list:
147
+ if port.fmu_name in self.children:
148
+ child = self.children[port.fmu_name]
149
+ ancestors = [key for key, val in child.output_ports.items() if val == port.port_name]
150
+ if len(ancestors) == 1:
151
+ return child.get_final_from(ancestors[0]) # child output port
152
+ else:
153
+ return port # embedded FMU
154
+
155
+ raise AssemblyError(f"{self.name}: Port {port} is not connected upstream.")
156
+
157
+ def get_final_to(self, port: Port) -> Port:
158
+ if port in self.output_ports:
159
+ successor = Port(self.name, self.output_ports[port])
160
+ if self.parent:
161
+ return self.parent.get_final_to(successor) # Output port
162
+ else:
163
+ return successor # TOLEVEL output port
164
+ elif port.fmu_name in self.fmu_names_list:
165
+ if port.fmu_name in self.children:
166
+ child = self.children[port.fmu_name]
167
+ successors = [key for key, val in child.input_ports.items() if val == port.port_name]
168
+ if len(successors) == 1:
169
+ return child.get_final_to(successors[0]) # Child input port
170
+ else:
171
+ return port # embedded FMU
172
+
173
+ raise AssemblyError(f"Node {self.name}: Port {port} is not connected downstream.")
174
+
175
+ def get_fmu_connections(self, fmu_name: str) -> List[Connection]:
176
+ connections = []
177
+ if fmu_name not in self.fmu_names_list:
178
+ raise AssemblyError(f"Internal Error: FMU {fmu_name} is not embedded by {self.name}.")
179
+ for link in self.links:
180
+ if link.from_port.fmu_name == fmu_name:
181
+ connections.append(Connection(link.from_port, self.get_final_to(link.to_port)))
182
+ elif link.to_port.fmu_name == fmu_name:
183
+ connections.append(Connection(self.get_final_from(link.from_port), link.to_port))
184
+
185
+ for to_port, input_port_name in self.input_ports.items():
186
+ if to_port.fmu_name == fmu_name:
187
+ if self.parent:
188
+ connections.append(Connection(self.parent.get_final_from(Port(self.name, input_port_name)), to_port))
189
+ else:
190
+ connections.append(Connection(Port(self.name, input_port_name), to_port))
191
+
192
+ for from_port, output_port_name in self.output_ports.items():
193
+ if from_port.fmu_name == fmu_name:
194
+ if self.parent:
195
+ connections.append(Connection(from_port, self.parent.get_final_to(Port(self.name, output_port_name))))
196
+ else:
197
+ connections.append(Connection(from_port, Port(self.name, output_port_name))) ###HERE
198
+
199
+ return connections
200
+
126
201
 
127
202
  class AssemblyError(Exception):
128
203
  def __init__(self, reason: str):
@@ -134,13 +209,14 @@ class AssemblyError(Exception):
134
209
 
135
210
  class Assembly:
136
211
  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 = "."):
212
+ auto_output=True, mt=False, profiling=False, fmu_directory: Path = Path("."), auto_parameter=False):
138
213
  self.filename = Path(filename)
139
214
  self.default_auto_input = auto_input
140
215
  self.debug = debug
141
216
  self.default_auto_output = auto_output
142
217
  self.default_step_size = step_size
143
218
  self.default_auto_link = auto_link
219
+ self.default_auto_parameter = auto_parameter
144
220
  self.default_mt = mt
145
221
  self.default_profiling = profiling
146
222
  self.fmu_directory = fmu_directory
@@ -197,7 +273,8 @@ class Assembly:
197
273
  name = str(self.filename.with_suffix(".fmu"))
198
274
  self.root = AssemblyNode(name, step_size=self.default_step_size, auto_link=self.default_auto_link,
199
275
  mt=self.default_mt, profiling=self.default_profiling,
200
- auto_input=self.default_auto_input, auto_output=self.default_auto_output)
276
+ auto_input=self.default_auto_input, auto_output=self.default_auto_output,
277
+ auto_parameter=self.default_auto_parameter)
201
278
 
202
279
  with open(self.input_pathname) as file:
203
280
  reader = csv.reader(file, delimiter=';')
@@ -292,49 +369,51 @@ class Assembly:
292
369
  raise AssemblyError(f"Cannot read json: {e}")
293
370
  self.root = self._json_decode_node(data)
294
371
  if not self.root.name:
295
- self.root.name = str(self.filename.with_suffix(".fmu"))
372
+ self.root.name = str(self.filename.with_suffix(".fmu").name)
296
373
 
297
- def _json_decode_node(self, data:Dict) -> AssemblyNode:
374
+ def _json_decode_node(self, data: Dict) -> AssemblyNode:
298
375
  name = data.get("name", None) # 1
299
- step_size = data.get("step_size", self.default_step_size) # 7
376
+ mt = data.get("mt", self.default_mt) # 2
377
+ profiling = data.get("profiling", self.default_profiling) # 3
300
378
  auto_link = data.get("auto_link", self.default_auto_link) # 4
301
379
  auto_input = data.get("auto_input", self.default_auto_input) # 5
302
380
  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
381
+ auto_parameter = data.get("auto_parameter", self.default_auto_parameter) # 6b
382
+ step_size = data.get("step_size", self.default_step_size) # 7
305
383
 
306
384
  node = AssemblyNode(name, step_size=step_size, auto_link=auto_link, mt=mt, profiling=profiling,
307
- auto_input=auto_input, auto_output=auto_output)
385
+ auto_input=auto_input, auto_output=auto_output, auto_parameter=auto_parameter)
308
386
 
309
387
  for key, value in data.items():
310
- if key in ('name', 'step_size', 'auto_link', 'auto_input', 'auto_output', 'mt', 'profiling'):
388
+ if key in ('name', 'step_size', 'auto_link', 'auto_input', 'auto_output', 'mt', 'profiling',
389
+ 'auto_parameter'):
311
390
  continue # Already read
312
391
 
313
- elif key == "container": # 8
392
+ elif key == "container": # 8
314
393
  if not isinstance(value, list):
315
394
  raise AssemblyError("JSON: 'container' keyword should define a list.")
316
395
  for sub_data in value:
317
396
  node.add_sub_node(self._json_decode_node(sub_data))
318
397
 
319
- elif key == "fmu": # 9
398
+ elif key == "fmu": # 9
320
399
  if not isinstance(value, list):
321
400
  raise AssemblyError("JSON: 'fmu' keyword should define a list.")
322
401
  for fmu in value:
323
402
  node.add_fmu(fmu)
324
403
 
325
- elif key == "input": # 10
404
+ elif key == "input": # 10
326
405
  self._json_decode_keyword('input', value, node.add_input)
327
406
 
328
- elif key == "output": # 11
407
+ elif key == "output": # 11
329
408
  self._json_decode_keyword('output', value, node.add_output)
330
409
 
331
- elif key == "link": # 12
410
+ elif key == "link": # 12
332
411
  self._json_decode_keyword('link', value, node.add_link)
333
412
 
334
- elif key == "start": # 13
413
+ elif key == "start": # 13
335
414
  self._json_decode_keyword('start', value, node.add_start_value)
336
415
 
337
- elif key == "drop": #14
416
+ elif key == "drop": # 14
338
417
  self._json_decode_keyword('drop', value, node.add_drop_port)
339
418
 
340
419
  else:
@@ -354,7 +433,6 @@ class Assembly:
354
433
  except TypeError:
355
434
  raise AssemblyError(f"JSON: '{keyword}' value does not contain right number of fields: {line}.")
356
435
 
357
-
358
436
  def write_json(self, filename: Union[str, Path]):
359
437
  with open(self.fmu_directory / filename, "wt") as file:
360
438
  data = self._json_encode_node(self.root)
@@ -362,21 +440,22 @@ class Assembly:
362
440
 
363
441
  def _json_encode_node(self, node: AssemblyNode) -> Dict[str, Any]:
364
442
  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
443
+ json_node["name"] = node.name # 1
444
+ json_node["mt"] = node.mt # 2
445
+ json_node["profiling"] = node.profiling # 3
446
+ json_node["auto_link"] = node.auto_link # 4
447
+ json_node["auto_input"] = node.auto_input # 5
448
+ json_node["auto_output"] = node.auto_output # 6
449
+ json_node["auto_parameter"] = node.auto_parameter # 6b
371
450
 
372
451
  if node.step_size:
373
- json_node["step_size"] = node.step_size # 7
452
+ json_node["step_size"] = node.step_size # 7
374
453
 
375
454
  if node.children:
376
- json_node["container"] = [self._json_encode_node(child) for child in node.children] # 8
455
+ json_node["container"] = [self._json_encode_node(child) for child in node.children.values()] # 8
377
456
 
378
457
  if node.fmu_names_list:
379
- json_node["fmu"] = [f"{fmu_name}" for fmu_name in sorted(node.fmu_names_list)] #9
458
+ json_node["fmu"] = [f"{fmu_name}" for fmu_name in sorted(node.fmu_names_list)] # 9
380
459
 
381
460
  if node.input_ports:
382
461
  json_node["input"] = [[f"{source}", f"{port.fmu_name}", f"{port.port_name}"] # 10
@@ -396,7 +475,7 @@ class Assembly:
396
475
  for port, value in node.start_values.items()]
397
476
 
398
477
  if node.drop_ports:
399
- json_node["drop"] = [[f"{port.fmu_name}", f"{port.port_name}"] for port in node.drop_ports] # 14
478
+ json_node["drop"] = [[f"{port.fmu_name}", f"{port.port_name}"] for port in node.drop_ports] # 14
400
479
 
401
480
  return json_node
402
481
 
@@ -176,6 +176,9 @@ 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
+
179
182
  parser.add_argument("-no-auto-link", action="store_false", dest="auto_link", default=True,
180
183
  help="Create ONLY explicit links.")
181
184
 
@@ -206,8 +209,8 @@ def fmucontainer():
206
209
  try:
207
210
  assembly = Assembly(filename, step_size=step_size, auto_link=config.auto_link,
208
211
  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
-
212
+ profiling=config.profiling, fmu_directory=fmu_directory, debug=config.debug,
213
+ auto_parameter=config.auto_parameter)
211
214
  except FileNotFoundError as e:
212
215
  logger.fatal(f"Cannot read file: {e}")
213
216
  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,18 @@ 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):
303
312
  # Auto Link outputs
304
313
  for fmu in self.execution_order:
305
314
  for port_name in fmu.ports:
306
315
  cport = ContainerPort(fmu, port_name)
307
316
  if cport not in self.rules:
308
- if cport.port.causality == 'output':
317
+ if cport.port.causality == 'parameter' and auto_parameter:
318
+ parameter_name = cport.fmu.model_identifier+"."+cport.port.name
319
+ logger.info(f"AUTO PARAMETER: {cport} as {parameter_name}")
320
+ self.inputs[parameter_name] = cport
321
+ self.mark_ruled(cport, 'PARAMETER')
322
+ elif cport.port.causality == 'output':
309
323
  candidate_cport = self.find_input(cport.port)
310
324
  if auto_link and candidate_cport:
311
325
  local = Local(cport)
@@ -314,11 +328,15 @@ class FMUContainer:
314
328
  self.mark_ruled(cport, 'LINK')
315
329
  self.mark_ruled(candidate_cport, 'LINK')
316
330
  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}")
331
+ elif auto_output:
332
+ self.mark_ruled(cport, 'OUTPUT')
333
+ self.outputs[port_name] = cport
334
+ logger.info(f"AUTO OUTPUT: Expose {cport}")
335
+ elif cport.port.causality == 'local' and port_name.startswith("container."):
336
+ local_portname = "container." + cport.fmu.id + "." + port_name[10:]
337
+ self.mark_ruled(cport, 'OUTPUT')
338
+ self.outputs[local_portname] = cport
339
+ logger.info(f"PROFILING: Expose {cport}")
322
340
 
323
341
  if auto_input:
324
342
  # Auto link inputs
@@ -406,6 +424,18 @@ class FMUContainer:
406
424
  if fmu.capabilities[capability] == "true":
407
425
  capabilities[capability] = "true"
408
426
 
427
+ if self.start_time is None:
428
+ self.start_time = self.execution_order[0].start_time
429
+ logger.info(f"start_time={self.start_time} (deduced from '{self.execution_order[0].name}')")
430
+ else:
431
+ logger.info(f"start_time={self.start_time}")
432
+
433
+ if self.stop_time is None:
434
+ self.stop_time = self.execution_order[0].stop_time
435
+ logger.info(f"stop_time={self.stop_time} (deduced from '{self.execution_order[0].name}')")
436
+ else:
437
+ logger.info(f"stop_time={self.stop_time}")
438
+
409
439
  xml_file.write(f"""<?xml version="1.0" encoding="ISO-8859-1"?>
410
440
  <fmiModelDescription
411
441
  fmiVersion="2.0"
@@ -434,14 +464,14 @@ class FMUContainer:
434
464
  <Category name="fmucontainer"/>
435
465
  </LogCategories>
436
466
 
437
- <DefaultExperiment stepSize="{step_size}"/>
467
+ <DefaultExperiment stepSize="{step_size}" startTime="{self.start_time}" stopTime="{self.stop_time}"/>
438
468
 
439
469
  <ModelVariables>
440
470
  """)
441
471
  if profiling:
442
472
  for fmu in self.execution_order:
443
473
  vr = vr_table.add_vr("Real")
444
- name = f"container.{fmu.model_identifier}.rt_ratio"
474
+ name = f"container.{fmu.id}.rt_ratio"
445
475
  print(f'<ScalarVariable valueReference="{vr}" name="{name}" causality="local">'
446
476
  f'<Real /></ScalarVariable>', file=xml_file)
447
477
 
@@ -591,6 +621,14 @@ class FMUContainer:
591
621
  for cport, vr in outputs_fmu_per_type[type_name][fmu.name].items():
592
622
  print(f"{vr} {cport.port.vr}", file=txt_file)
593
623
 
624
+ @staticmethod
625
+ def long_path(path: Union[str, Path]) -> str:
626
+ # https://stackoverflow.com/questions/14075465/copy-a-file-with-a-too-long-path-to-another-directory-in-python
627
+ if os.name == 'nt':
628
+ return "\\\\?\\" + os.path.abspath(str(path))
629
+ else:
630
+ return path
631
+
594
632
  def make_fmu_skeleton(self, base_directory: Path) -> Path:
595
633
  logger.debug(f"Initialize directory '{base_directory}'")
596
634
 
@@ -617,8 +655,8 @@ class FMUContainer:
617
655
  shutil.copy(library_filename, binary_directory / f"{self.identifier}.dll")
618
656
 
619
657
  for fmu in self.involved_fmu.values():
620
- shutil.copytree(fmu.fmu.tmp_directory, resources_directory / fmu.name, dirs_exist_ok=True)
621
-
658
+ shutil.copytree(self.long_path(fmu.fmu.tmp_directory),
659
+ self.long_path(resources_directory / fmu.name), dirs_exist_ok=True)
622
660
  return resources_directory
623
661
 
624
662
  def make_fmu_package(self, base_directory: Path, fmu_filename: Path):
@@ -626,7 +664,7 @@ class FMUContainer:
626
664
  with zipfile.ZipFile(self.fmu_directory / fmu_filename, "w", zipfile.ZIP_DEFLATED) as zip_file:
627
665
  for root, dirs, files in os.walk(base_directory):
628
666
  for file in files:
629
- zip_file.write(os.path.join(root, file),
667
+ zip_file.write(self.long_path(os.path.join(root, file)),
630
668
  os.path.relpath(os.path.join(root, file), base_directory))
631
669
  logger.info(f"'{fmu_filename}' is available.")
632
670
 
@@ -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: