flexCommunicator 0.1.0__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.
- flexCommunicator/__init__.py +0 -0
- flexCommunicator/clientLibraries/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/action/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/action/action.py +85 -0
- flexCommunicator/clientLibraries/flcpy/action/action_client.py +70 -0
- flexCommunicator/clientLibraries/flcpy/action/goal_handle.py +50 -0
- flexCommunicator/clientLibraries/flcpy/codec/ROS2Codec.py +227 -0
- flexCommunicator/clientLibraries/flcpy/codec/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/commands/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/commands/commandProcessor.py +113 -0
- flexCommunicator/clientLibraries/flcpy/commands/composer.py +25 -0
- flexCommunicator/clientLibraries/flcpy/communication/CommunicationManager.py +579 -0
- flexCommunicator/clientLibraries/flcpy/communication/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/contracts/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/contracts/contractVariable.py +93 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudConfiguration.py +16 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudManager.py +162 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudOrchestrator.py +248 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudVariable.py +130 -0
- flexCommunicator/clientLibraries/flcpy/flexNode.py +892 -0
- flexCommunicator/clientLibraries/flcpy/fmi/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/fmi/fmuNode.py +166 -0
- flexCommunicator/clientLibraries/flcpy/fmi/node.py +360 -0
- flexCommunicator/clientLibraries/flcpy/knowledge/KnowledgeManager.py +288 -0
- flexCommunicator/clientLibraries/flcpy/knowledge/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/logging/LoggingAndTracking.py +159 -0
- flexCommunicator/clientLibraries/flcpy/logging/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/messages/StandardizedMessages.py +64 -0
- flexCommunicator/clientLibraries/flcpy/messages/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/node.py +486 -0
- flexCommunicator/clientLibraries/flcpy/security/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/security/security.py +189 -0
- flexCommunicator/clientLibraries/flcpy/service/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/service/client.py +58 -0
- flexCommunicator/clientLibraries/flcpy/service/service.py +67 -0
- flexCommunicator/clientLibraries/flcpy/transport/PropertyBinding.py +108 -0
- flexCommunicator/clientLibraries/flcpy/transport/PropertyRegistry.py +142 -0
- flexCommunicator/clientLibraries/flcpy/transport/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/utils/Timer.py +22 -0
- flexCommunicator/clientLibraries/flcpy/utils/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/utils/auxiliary.py +261 -0
- flexCommunicator/clientLibraries/flcpy/utils/constants.py +276 -0
- flexCommunicator/clientLibraries/flcpy/utils/decorators.py +70 -0
- flexCommunicator/clientLibraries/flcpy/validityFrame/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/validityFrame/runtimeMonitor.py +101 -0
- flexCommunicator/clientLibraries/flcpy/validityFrame/validityFrameManager.py +958 -0
- flexcommunicator-0.1.0.dist-info/METADATA +568 -0
- flexcommunicator-0.1.0.dist-info/RECORD +53 -0
- flexcommunicator-0.1.0.dist-info/WHEEL +5 -0
- flexcommunicator-0.1.0.dist-info/licenses/LICENSE +201 -0
- flexcommunicator-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
#**************************************************************************
|
|
2
|
+
# * Copyright (C) 2025-present Bert Van Acker (B.MKR) <bva.bmkr@gmail.com>
|
|
3
|
+
# *
|
|
4
|
+
# * This file is part of the flexIA project.
|
|
5
|
+
# *
|
|
6
|
+
# * flexIA can not be copied and/or distributed without the express
|
|
7
|
+
# * permission of Bert Van Acker
|
|
8
|
+
# *************************************************************************
|
|
9
|
+
import tempfile
|
|
10
|
+
import zipfile
|
|
11
|
+
import json
|
|
12
|
+
import importlib.util
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from flexCommunicator.clientLibraries.flcpy.validityFrame.runtimeMonitor import *
|
|
16
|
+
|
|
17
|
+
class RuntimeMonitorObserverable(object):
|
|
18
|
+
def __init__(self, monitor_info, logger=None, custom_check_function=None, custom_source_path=None):
|
|
19
|
+
self.info = monitor_info
|
|
20
|
+
self.logger = logger
|
|
21
|
+
self.custom_check_function = custom_check_function
|
|
22
|
+
self.custom_source_path = custom_source_path
|
|
23
|
+
self.name = monitor_info["name"]
|
|
24
|
+
self.description = monitor_info.get("description")
|
|
25
|
+
self.guid = monitor_info.get("guid")
|
|
26
|
+
self.observes = monitor_info.get("observes", [])
|
|
27
|
+
self.spec_status = dict(monitor_info.get("spec_status", {}))
|
|
28
|
+
self.last_observed_values = dict(monitor_info.get("last_observed_values", {}))
|
|
29
|
+
self.status = monitor_info.get("status")
|
|
30
|
+
|
|
31
|
+
def __getitem__(self, key):
|
|
32
|
+
return self.info[key]
|
|
33
|
+
|
|
34
|
+
def get(self, key, default=None):
|
|
35
|
+
return self.info.get(key, default)
|
|
36
|
+
|
|
37
|
+
def set(self, value):
|
|
38
|
+
"""Set observed values and evaluate the monitor."""
|
|
39
|
+
if isinstance(value, dict):
|
|
40
|
+
data_vector = value
|
|
41
|
+
elif len(self.observes) == 1:
|
|
42
|
+
feature = self.observes[0].get("feature") or self.observes[0].get("name")
|
|
43
|
+
data_vector = {feature: value}
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError("Monitor '" + self.name + "' observes multiple features; pass a dict of feature values")
|
|
46
|
+
|
|
47
|
+
return self.check(data_vector)
|
|
48
|
+
|
|
49
|
+
def check(self, data_vector):
|
|
50
|
+
"""Evaluate observed values and update monitor status."""
|
|
51
|
+
evaluations = {}
|
|
52
|
+
if self.custom_check_function is not None:
|
|
53
|
+
custom_evaluations = self.custom_check_function(data_vector, monitor=self)
|
|
54
|
+
if custom_evaluations is None:
|
|
55
|
+
custom_evaluations = {}
|
|
56
|
+
if not isinstance(custom_evaluations, dict):
|
|
57
|
+
raise ValueError("Custom monitor check must return a feature-to-bool dictionary")
|
|
58
|
+
evaluations.update(custom_evaluations)
|
|
59
|
+
|
|
60
|
+
for observed_property in self.observes:
|
|
61
|
+
feature = observed_property.get("feature") or observed_property.get("name")
|
|
62
|
+
if feature not in data_vector:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
self.last_observed_values[feature] = data_vector[feature]
|
|
66
|
+
if feature not in evaluations:
|
|
67
|
+
evaluations[feature] = self._checkObservedProperty(observed_property, data_vector[feature])
|
|
68
|
+
|
|
69
|
+
for feature, is_valid in evaluations.items():
|
|
70
|
+
self.spec_status[feature] = "valid" if is_valid else "invalid"
|
|
71
|
+
|
|
72
|
+
if evaluations:
|
|
73
|
+
self.status = "valid" if all(evaluations.values()) else "invalid"
|
|
74
|
+
else:
|
|
75
|
+
self.status = "unknown"
|
|
76
|
+
|
|
77
|
+
return evaluations
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class validityFrameManager(object):
|
|
81
|
+
def __init__(self, name, logger, model_loader_mode="internal", model_loader_function=None, monitor_loader_mode="internal", monitor_loader_function=None):
|
|
82
|
+
self.name = name
|
|
83
|
+
self.logger = logger
|
|
84
|
+
self.path_to_fmu = None
|
|
85
|
+
self.path_to_vf = None
|
|
86
|
+
self._unpacked_vf_dir = None
|
|
87
|
+
self._temp_dir = None
|
|
88
|
+
self.vf_instance = None
|
|
89
|
+
self.model = None
|
|
90
|
+
self.runtime_monitors = {}
|
|
91
|
+
self.saifety_architecture = None
|
|
92
|
+
self.saifety_architecture_path = None
|
|
93
|
+
self.traceability_graph = None
|
|
94
|
+
self.traceability_graph_path = None
|
|
95
|
+
self._operational_data = None
|
|
96
|
+
self._operational_path = None
|
|
97
|
+
self._metadata_data = None
|
|
98
|
+
self._metadata_path = None
|
|
99
|
+
self.model_loader_mode = model_loader_mode
|
|
100
|
+
self.model_loader_function = model_loader_function
|
|
101
|
+
self.model_loader_path = None
|
|
102
|
+
self.monitor_loader_mode = monitor_loader_mode
|
|
103
|
+
self.monitor_loader_function = monitor_loader_function
|
|
104
|
+
self.monitor_loader_paths = {}
|
|
105
|
+
|
|
106
|
+
#flexIA runtime monitor instances
|
|
107
|
+
self.flexia_runtime_monitors = []
|
|
108
|
+
|
|
109
|
+
def _log(self, level, message):
|
|
110
|
+
if self.logger is None:
|
|
111
|
+
print(message)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
getattr(self.logger, level)(message)
|
|
115
|
+
|
|
116
|
+
def load(self, path_to_vf):
|
|
117
|
+
"""Load the validity frame from a folder or a zipped .vf archive."""
|
|
118
|
+
self.path_to_fmu = path_to_vf
|
|
119
|
+
self._log("info", "Loading validity frame: " + str(path_to_vf))
|
|
120
|
+
self.path_to_vf = self._resolveValidityFramePath(path_to_vf)
|
|
121
|
+
|
|
122
|
+
# TODO: instantiate/load the VF FMU once the concrete loader is selected.
|
|
123
|
+
self.vf_instance = None
|
|
124
|
+
self._loadRuntimeMonitors()
|
|
125
|
+
self.model = self.getModel()
|
|
126
|
+
self._loadMetadata()
|
|
127
|
+
self.generateSAIfetyArchitecture()
|
|
128
|
+
self.generateTraceabilityGraph()
|
|
129
|
+
self._registerRuntime()
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def dump(self):
|
|
133
|
+
"""Show metadata of the loaded validity frame."""
|
|
134
|
+
metadata = {
|
|
135
|
+
"name": self.name,
|
|
136
|
+
"path_to_fmu": self.path_to_fmu,
|
|
137
|
+
"path_to_vf": str(self.path_to_vf) if self.path_to_vf is not None else None,
|
|
138
|
+
"loaded": self.vf_instance is not None,
|
|
139
|
+
"model": self.model,
|
|
140
|
+
"model_loader_mode": self.model_loader_mode,
|
|
141
|
+
"model_loader_path": str(self.model_loader_path) if self.model_loader_path else None,
|
|
142
|
+
"monitor_loader_mode": self.monitor_loader_mode,
|
|
143
|
+
"monitor_loader_paths": {name: str(path) for name, path in self.monitor_loader_paths.items()},
|
|
144
|
+
"runtime_monitors": list(self.runtime_monitors.keys()),
|
|
145
|
+
"saifety_architecture_path": str(self.saifety_architecture_path) if self.saifety_architecture_path else None,
|
|
146
|
+
"traceability_graph_path": str(self.traceability_graph_path) if self.traceability_graph_path else None,
|
|
147
|
+
}
|
|
148
|
+
self._log("info", "Validity frame metadata: " + json.dumps(metadata, indent=2))
|
|
149
|
+
return metadata
|
|
150
|
+
|
|
151
|
+
def update(self, runtimeMonitorName, value):
|
|
152
|
+
"""Update a runtime monitor value."""
|
|
153
|
+
return self._setRuntimeMonitor(runtimeMonitorName, value)
|
|
154
|
+
|
|
155
|
+
def configureModelLoader(self, mode="internal", load_model_function=None):
|
|
156
|
+
"""Configure how getModel loads the AI model."""
|
|
157
|
+
supported_modes = ["internal", "linked", "auto"]
|
|
158
|
+
if mode not in supported_modes:
|
|
159
|
+
raise ValueError("Unsupported model loader mode: " + str(mode))
|
|
160
|
+
|
|
161
|
+
if mode == "linked" and load_model_function is None:
|
|
162
|
+
raise ValueError("linked model loader mode requires a load_model_function")
|
|
163
|
+
|
|
164
|
+
self.model_loader_mode = mode
|
|
165
|
+
self.model_loader_function = load_model_function
|
|
166
|
+
self.model_loader_path = None
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
def configureMonitorLoader(self, mode="internal", check_function=None):
|
|
170
|
+
"""Configure how runtime monitors use custom check code."""
|
|
171
|
+
supported_modes = ["internal", "linked", "auto"]
|
|
172
|
+
if mode not in supported_modes:
|
|
173
|
+
raise ValueError("Unsupported monitor loader mode: " + str(mode))
|
|
174
|
+
|
|
175
|
+
if mode == "linked" and check_function is None:
|
|
176
|
+
raise ValueError("linked monitor loader mode requires a check_function")
|
|
177
|
+
|
|
178
|
+
self.monitor_loader_mode = mode
|
|
179
|
+
self.monitor_loader_function = check_function
|
|
180
|
+
self.monitor_loader_paths = {}
|
|
181
|
+
for monitor in self.runtime_monitors.values():
|
|
182
|
+
custom_check_function, custom_source_path = self._getMonitorCheckFunction(monitor.info)
|
|
183
|
+
monitor.custom_check_function = custom_check_function
|
|
184
|
+
monitor.custom_source_path = custom_source_path
|
|
185
|
+
if custom_source_path is not None:
|
|
186
|
+
self.monitor_loader_paths[monitor.name] = custom_source_path
|
|
187
|
+
|
|
188
|
+
return self
|
|
189
|
+
|
|
190
|
+
def getModel(self):
|
|
191
|
+
"""Return the model contained by the validity frame instance."""
|
|
192
|
+
if self.model_loader_mode == "internal":
|
|
193
|
+
return self._getModelInternal(self)
|
|
194
|
+
|
|
195
|
+
if self.model_loader_mode == "linked":
|
|
196
|
+
return self._callLoadModelFunction(self.model_loader_function)
|
|
197
|
+
|
|
198
|
+
if self.model_loader_mode == "auto":
|
|
199
|
+
load_model_function = self._autoDiscoverLoadModelFunction()
|
|
200
|
+
return self._callLoadModelFunction(load_model_function)
|
|
201
|
+
|
|
202
|
+
raise ValueError("Unsupported model loader mode: " + str(self.model_loader_mode))
|
|
203
|
+
|
|
204
|
+
def _getModelInternal(self, vf_instance):
|
|
205
|
+
"""Internal model loading implementation."""
|
|
206
|
+
# TODO: extract and return the contained model from vf_instance.
|
|
207
|
+
self._log("info", "Using internal validity frame model loader")
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
def _callLoadModelFunction(self, load_model_function):
|
|
211
|
+
"""Call a custom load_model function with the expected VF context."""
|
|
212
|
+
if load_model_function is None:
|
|
213
|
+
raise ValueError("No load_model function configured")
|
|
214
|
+
|
|
215
|
+
model_location = self._getModelLocation()
|
|
216
|
+
self._log("info", "Using custom load_model function for model location: " + str(model_location))
|
|
217
|
+
original_logger = self.logger
|
|
218
|
+
if self.logger is None:
|
|
219
|
+
self.logger = _ConsoleLogger()
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
return load_model_function(str(model_location), validityframe=self)
|
|
223
|
+
finally:
|
|
224
|
+
self.logger = original_logger
|
|
225
|
+
|
|
226
|
+
def _autoDiscoverLoadModelFunction(self):
|
|
227
|
+
"""Discover a load_model function under Resources/loaders."""
|
|
228
|
+
loader_file = self._findModelLoaderFile()
|
|
229
|
+
self.model_loader_path = loader_file
|
|
230
|
+
|
|
231
|
+
module_name = "vf_model_loader_" + self._toIdentifier(str(loader_file))
|
|
232
|
+
spec = importlib.util.spec_from_file_location(module_name, loader_file)
|
|
233
|
+
if spec is None or spec.loader is None:
|
|
234
|
+
raise ImportError("Unable to import model loader from: " + str(loader_file))
|
|
235
|
+
|
|
236
|
+
module = importlib.util.module_from_spec(spec)
|
|
237
|
+
spec.loader.exec_module(module)
|
|
238
|
+
|
|
239
|
+
if not hasattr(module, "load_model"):
|
|
240
|
+
raise AttributeError("Model loader does not define load_model(): " + str(loader_file))
|
|
241
|
+
|
|
242
|
+
self._log("info", "Auto-discovered model loader: " + str(loader_file))
|
|
243
|
+
return module.load_model
|
|
244
|
+
|
|
245
|
+
def _loadPythonModuleFromFile(self, module_prefix, python_file):
|
|
246
|
+
"""Load a Python module from a VF resource file."""
|
|
247
|
+
module_name = module_prefix + "_" + self._toIdentifier(str(python_file))
|
|
248
|
+
spec = importlib.util.spec_from_file_location(module_name, python_file)
|
|
249
|
+
if spec is None or spec.loader is None:
|
|
250
|
+
raise ImportError("Unable to import Python module from: " + str(python_file))
|
|
251
|
+
|
|
252
|
+
module = importlib.util.module_from_spec(spec)
|
|
253
|
+
spec.loader.exec_module(module)
|
|
254
|
+
return module
|
|
255
|
+
|
|
256
|
+
def _findModelLoaderFile(self):
|
|
257
|
+
"""Find the most appropriate Resources/loaders/*.py file."""
|
|
258
|
+
loaders_dir = self._findResourcesDirectory() / "loaders"
|
|
259
|
+
if not loaders_dir.is_dir():
|
|
260
|
+
raise FileNotFoundError("Resources/loaders directory not found in validity frame: " + str(self.path_to_vf))
|
|
261
|
+
|
|
262
|
+
model_structure = self._getActiveModelStructure()
|
|
263
|
+
model_type = model_structure.get("_modelType")
|
|
264
|
+
if model_type:
|
|
265
|
+
preferred_loader = loaders_dir / (str(model_type) + ".py")
|
|
266
|
+
if preferred_loader.is_file() and self._pythonFileDefinesLoadModel(preferred_loader):
|
|
267
|
+
return preferred_loader
|
|
268
|
+
|
|
269
|
+
loader_files = []
|
|
270
|
+
for loader_file in loaders_dir.glob("*.py"):
|
|
271
|
+
if loader_file.name == "__init__.py":
|
|
272
|
+
continue
|
|
273
|
+
if self._pythonFileDefinesLoadModel(loader_file):
|
|
274
|
+
loader_files.append(loader_file)
|
|
275
|
+
|
|
276
|
+
if len(loader_files) == 1:
|
|
277
|
+
return loader_files[0]
|
|
278
|
+
|
|
279
|
+
if not loader_files:
|
|
280
|
+
raise FileNotFoundError("No load_model() function found in Resources/loaders")
|
|
281
|
+
|
|
282
|
+
raise ValueError("Multiple load_model() functions found in Resources/loaders; configure a linked loader explicitly")
|
|
283
|
+
|
|
284
|
+
def _pythonFileDefinesLoadModel(self, python_file):
|
|
285
|
+
"""Return True when a loader file appears to define load_model."""
|
|
286
|
+
with python_file.open("r", encoding="utf-8") as source_file:
|
|
287
|
+
for line in source_file:
|
|
288
|
+
if line.strip().startswith("def load_model("):
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
def _getMonitorCheckFunction(self, monitor_info):
|
|
294
|
+
"""Return a custom check function for a monitor according to configuration."""
|
|
295
|
+
if self.monitor_loader_mode == "internal":
|
|
296
|
+
return None, None
|
|
297
|
+
|
|
298
|
+
if self.monitor_loader_mode == "linked":
|
|
299
|
+
return self.monitor_loader_function, None
|
|
300
|
+
|
|
301
|
+
if self.monitor_loader_mode == "auto":
|
|
302
|
+
monitor_file = self._findMonitorCodeFile(monitor_info)
|
|
303
|
+
if monitor_file is None:
|
|
304
|
+
return None, None
|
|
305
|
+
|
|
306
|
+
module = self._loadPythonModuleFromFile("vf_monitor", monitor_file)
|
|
307
|
+
if hasattr(module, "custom_validate_vector"):
|
|
308
|
+
self._log("info", "Auto-discovered monitor custom code: " + str(monitor_file))
|
|
309
|
+
return module.custom_validate_vector, monitor_file
|
|
310
|
+
|
|
311
|
+
if hasattr(module, "check"):
|
|
312
|
+
self._log("info", "Auto-discovered monitor check function: " + str(monitor_file))
|
|
313
|
+
return module.check, monitor_file
|
|
314
|
+
|
|
315
|
+
raise AttributeError("Monitor custom code does not define custom_validate_vector() or check(): " + str(monitor_file))
|
|
316
|
+
|
|
317
|
+
raise ValueError("Unsupported monitor loader mode: " + str(self.monitor_loader_mode))
|
|
318
|
+
|
|
319
|
+
def _findMonitorCodeFile(self, monitor_info):
|
|
320
|
+
"""Find custom monitor code under Resources/monitors."""
|
|
321
|
+
monitors_dir = self._findResourcesDirectory() / "monitors"
|
|
322
|
+
if not monitors_dir.is_dir():
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
monitor_name = monitor_info["name"]
|
|
326
|
+
preferred_monitor = monitors_dir / (monitor_name + ".py")
|
|
327
|
+
if preferred_monitor.is_file() and self._pythonFileDefinesMonitorCheck(preferred_monitor):
|
|
328
|
+
return preferred_monitor
|
|
329
|
+
|
|
330
|
+
for monitor_file in monitors_dir.glob("*.py"):
|
|
331
|
+
if monitor_file.name == "__init__.py":
|
|
332
|
+
continue
|
|
333
|
+
if monitor_file.stem == monitor_name and self._pythonFileDefinesMonitorCheck(monitor_file):
|
|
334
|
+
return monitor_file
|
|
335
|
+
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
def _pythonFileDefinesMonitorCheck(self, python_file):
|
|
339
|
+
"""Return True when a monitor code file appears to define custom check logic."""
|
|
340
|
+
with python_file.open("r", encoding="utf-8") as source_file:
|
|
341
|
+
for line in source_file:
|
|
342
|
+
stripped_line = line.strip()
|
|
343
|
+
if stripped_line.startswith("def custom_validate_vector(") or stripped_line.startswith("def check("):
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
def _findResourcesDirectory(self):
|
|
349
|
+
"""Find the Resources directory in the loaded validity frame."""
|
|
350
|
+
resources_path = self.path_to_vf / "Resources"
|
|
351
|
+
if resources_path.is_dir():
|
|
352
|
+
return resources_path
|
|
353
|
+
|
|
354
|
+
resources_dirs = [path for path in self.path_to_vf.glob("*/Resources") if path.is_dir()]
|
|
355
|
+
if len(resources_dirs) == 1:
|
|
356
|
+
return resources_dirs[0]
|
|
357
|
+
|
|
358
|
+
if not resources_dirs:
|
|
359
|
+
raise FileNotFoundError("Resources directory not found in validity frame: " + str(self.path_to_vf))
|
|
360
|
+
|
|
361
|
+
raise ValueError("Multiple Resources directories found in validity frame: " + str(self.path_to_vf))
|
|
362
|
+
|
|
363
|
+
def _getModelLocation(self):
|
|
364
|
+
"""Resolve the model artifact location passed to custom load_model functions."""
|
|
365
|
+
model_structure = self._getActiveModelStructure()
|
|
366
|
+
model_location = model_structure.get("_modelRef") or self._operational_data.get("_modelReference")
|
|
367
|
+
if model_location is None:
|
|
368
|
+
return self.path_to_vf
|
|
369
|
+
|
|
370
|
+
model_location = Path(model_location)
|
|
371
|
+
if model_location.is_absolute():
|
|
372
|
+
return model_location
|
|
373
|
+
|
|
374
|
+
return self.path_to_vf / model_location
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _loadRuntimeMonitors(self):
|
|
378
|
+
"""Load runtime monitors from the validity frame."""
|
|
379
|
+
if self.path_to_vf is None:
|
|
380
|
+
raise ValueError("No validity frame has been loaded")
|
|
381
|
+
|
|
382
|
+
operational_path = self._findOperationalFile()
|
|
383
|
+
with operational_path.open("r", encoding="utf-8") as operational_file:
|
|
384
|
+
operational_data = json.load(operational_file)
|
|
385
|
+
|
|
386
|
+
self._operational_data = operational_data
|
|
387
|
+
self._operational_path = operational_path
|
|
388
|
+
|
|
389
|
+
runtime_monitor_definitions = operational_data.get("_runtime_monitors", [])
|
|
390
|
+
if not isinstance(runtime_monitor_definitions, list):
|
|
391
|
+
raise ValueError("Operational.json field '_runtime_monitors' must be a list")
|
|
392
|
+
|
|
393
|
+
self.runtime_monitors = {}
|
|
394
|
+
for monitor_definition in runtime_monitor_definitions:
|
|
395
|
+
monitor_info = self._extractRuntimeMonitorInfo(monitor_definition)
|
|
396
|
+
|
|
397
|
+
# -- flexIA runtime monitor instantiation --
|
|
398
|
+
_monitor = runtimeMonitor(name=monitor_info['name'], initial_value=0.0, type=VF_RUNTIME_MONITOR_TYPE.LOCAL,check=CONTRACT_CHECK_TYPE.BOUND_CHECK,bound=[0.7, 0.9])
|
|
399
|
+
#TODO: implement specific monitors here! (BOUND, SET, CUSTOM_CODE,...)
|
|
400
|
+
|
|
401
|
+
# store for registration
|
|
402
|
+
self.flexia_runtime_monitors.append(_monitor)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
_monitorObserverable = RuntimeMonitorObserverable(
|
|
406
|
+
monitor_info=monitor_info,
|
|
407
|
+
logger=self.logger,
|
|
408
|
+
custom_check_function="", #TODO: check for custom code
|
|
409
|
+
custom_source_path="", #TODO: check for custom code source path
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
self.runtime_monitors[monitor_info["name"]] = _monitorObserverable
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
self._logRuntimeMonitorSummary(operational_path)
|
|
417
|
+
|
|
418
|
+
# TODO: translate the extracted runtime monitor metadata into runtime
|
|
419
|
+
# monitors in the flexIA framework.
|
|
420
|
+
return self.runtime_monitors
|
|
421
|
+
|
|
422
|
+
def _loadMetadata(self):
|
|
423
|
+
"""Load metadata from the validity frame."""
|
|
424
|
+
if self.path_to_vf is None:
|
|
425
|
+
raise ValueError("No validity frame has been loaded")
|
|
426
|
+
|
|
427
|
+
metadata_path = self._findMetadataFile()
|
|
428
|
+
with metadata_path.open("r", encoding="utf-8") as metadata_file:
|
|
429
|
+
metadata_data = json.load(metadata_file)
|
|
430
|
+
|
|
431
|
+
self._metadata_data = metadata_data
|
|
432
|
+
self._metadata_path = metadata_path
|
|
433
|
+
self._log("info", "Loaded validity frame metadata from " + str(metadata_path))
|
|
434
|
+
return metadata_data
|
|
435
|
+
|
|
436
|
+
def generateSAIfetyArchitecture(self):
|
|
437
|
+
"""Generate the SAIfety architecture JSON from Operational.json."""
|
|
438
|
+
if self._operational_data is None:
|
|
439
|
+
raise ValueError("Operational.json data must be loaded before generating the SAIfety architecture")
|
|
440
|
+
|
|
441
|
+
model_structure = self._getActiveModelStructure()
|
|
442
|
+
model_name = model_structure.get("_name") or self.name
|
|
443
|
+
model_id = "model_" + self._toIdentifier(model_name)
|
|
444
|
+
|
|
445
|
+
inputs = self._extractSAIfetyPorts(model_structure.get("_inports", []), "input")
|
|
446
|
+
outputs = self._extractSAIfetyPorts(model_structure.get("_outports", []), "output")
|
|
447
|
+
|
|
448
|
+
component = {
|
|
449
|
+
"id": self._toIdentifier(model_name),
|
|
450
|
+
"name": model_name,
|
|
451
|
+
"description": model_structure.get("_description"),
|
|
452
|
+
"inputs": inputs,
|
|
453
|
+
"outputs": outputs,
|
|
454
|
+
"ai_architecture": {
|
|
455
|
+
"pattern": "single_ai_component",
|
|
456
|
+
"models": [
|
|
457
|
+
{
|
|
458
|
+
"id": model_id,
|
|
459
|
+
"name": model_name,
|
|
460
|
+
"type": model_structure.get("_modelType"),
|
|
461
|
+
}
|
|
462
|
+
],
|
|
463
|
+
"flow": self._generateSAIfetyFlow(inputs, outputs, model_id),
|
|
464
|
+
},
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
architecture = {
|
|
468
|
+
"component": component,
|
|
469
|
+
"monitors": self._extractSAIfetyMonitors(inputs, outputs),
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
self.saifety_architecture = architecture
|
|
473
|
+
self.saifety_architecture_path = self._getSAIfetyArchitecturePath()
|
|
474
|
+
with self.saifety_architecture_path.open("w", encoding="utf-8") as architecture_file:
|
|
475
|
+
json.dump(architecture, architecture_file, indent=2)
|
|
476
|
+
architecture_file.write("\n")
|
|
477
|
+
|
|
478
|
+
self._log("info", "Generated SAIfety architecture: " + str(self.saifety_architecture_path))
|
|
479
|
+
return architecture
|
|
480
|
+
|
|
481
|
+
def generateTraceabilityGraph(self):
|
|
482
|
+
"""Generate the traceability graph JSON from Metadata.json and Operational.json."""
|
|
483
|
+
if self._metadata_data is None:
|
|
484
|
+
raise ValueError("Metadata.json data must be loaded before generating the traceability graph")
|
|
485
|
+
if self._operational_data is None:
|
|
486
|
+
raise ValueError("Operational.json data must be loaded before generating the traceability graph")
|
|
487
|
+
|
|
488
|
+
graph = {
|
|
489
|
+
"directed": True,
|
|
490
|
+
"multigraph": False,
|
|
491
|
+
"graph": {},
|
|
492
|
+
"nodes": [],
|
|
493
|
+
"edges": [],
|
|
494
|
+
}
|
|
495
|
+
node_ids = set()
|
|
496
|
+
edge_ids = set()
|
|
497
|
+
|
|
498
|
+
specifications_by_guid = {}
|
|
499
|
+
for specification in self._metadata_data.get("_specifications", []):
|
|
500
|
+
specification_id = self._getTraceabilitySpecificationId(specification)
|
|
501
|
+
specifications_by_guid[specification.get("_GUID")] = specification_id
|
|
502
|
+
self._addTraceabilityNode(graph, node_ids, {
|
|
503
|
+
"type": "specification",
|
|
504
|
+
"status": specification.get("_status"),
|
|
505
|
+
"layer": 0,
|
|
506
|
+
"guid": specification.get("_GUID"),
|
|
507
|
+
"id": specification_id,
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
for property_definition in self._metadata_data.get("_properties", []):
|
|
511
|
+
property_id = self._getTraceabilityPropertyId(property_definition)
|
|
512
|
+
satisfies = property_definition.get("_satisfies") or []
|
|
513
|
+
self._addTraceabilityNode(graph, node_ids, {
|
|
514
|
+
"type": "property",
|
|
515
|
+
"status": None,
|
|
516
|
+
"layer": 1,
|
|
517
|
+
"satisfies": satisfies,
|
|
518
|
+
"guid": property_definition.get("_GUID"),
|
|
519
|
+
"id": property_id,
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
for specification_guid in satisfies:
|
|
523
|
+
specification_id = specifications_by_guid.get(specification_guid)
|
|
524
|
+
self._addTraceabilityEdge(graph, edge_ids, property_id, specification_id)
|
|
525
|
+
|
|
526
|
+
monitor_links_by_specification_guid = self._addTraceabilityMonitorNodes(
|
|
527
|
+
graph,
|
|
528
|
+
node_ids,
|
|
529
|
+
edge_ids,
|
|
530
|
+
specifications_by_guid,
|
|
531
|
+
)
|
|
532
|
+
self._addTraceabilityValueNodes(
|
|
533
|
+
graph,
|
|
534
|
+
node_ids,
|
|
535
|
+
edge_ids,
|
|
536
|
+
specifications_by_guid,
|
|
537
|
+
monitor_links_by_specification_guid,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
self.traceability_graph = graph
|
|
541
|
+
self.traceability_graph_path = self._getTraceabilityGraphPath()
|
|
542
|
+
with self.traceability_graph_path.open("w", encoding="utf-8") as graph_file:
|
|
543
|
+
json.dump(graph, graph_file, indent=2)
|
|
544
|
+
graph_file.write("\n")
|
|
545
|
+
|
|
546
|
+
self._log("info", "Generated traceability graph: " + str(self.traceability_graph_path))
|
|
547
|
+
return graph
|
|
548
|
+
|
|
549
|
+
def _addTraceabilityMonitorNodes(self, graph, node_ids, edge_ids, specifications_by_guid):
|
|
550
|
+
"""Add runtime and design-time monitor nodes and their traceability edges."""
|
|
551
|
+
monitor_links_by_specification_guid = {}
|
|
552
|
+
monitor_definitions = []
|
|
553
|
+
monitor_definitions.extend(self._operational_data.get("_runtime_monitors", []))
|
|
554
|
+
monitor_definitions.extend(self._operational_data.get("_design_time_monitors", []))
|
|
555
|
+
|
|
556
|
+
for monitor_definition in monitor_definitions:
|
|
557
|
+
monitor_id = monitor_definition.get("_description") or monitor_definition.get("_name")
|
|
558
|
+
self._addTraceabilityNode(graph, node_ids, {
|
|
559
|
+
"type": "monitor",
|
|
560
|
+
"status": monitor_definition.get("_status"),
|
|
561
|
+
"layer": 2,
|
|
562
|
+
"guid": monitor_definition.get("_GUID"),
|
|
563
|
+
"id": monitor_id,
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
for observed_property in monitor_definition.get("observes", []):
|
|
567
|
+
specification_guid = observed_property.get("_GUID")
|
|
568
|
+
monitor_links_by_specification_guid.setdefault(specification_guid, []).append(monitor_id)
|
|
569
|
+
|
|
570
|
+
property_id = self._findTraceabilityPropertyForSpecification(specification_guid)
|
|
571
|
+
self._addTraceabilityEdge(graph, edge_ids, property_id, monitor_id)
|
|
572
|
+
|
|
573
|
+
specification_id = specifications_by_guid.get(specification_guid)
|
|
574
|
+
self._addTraceabilityEdge(graph, edge_ids, monitor_id, specification_id)
|
|
575
|
+
|
|
576
|
+
return monitor_links_by_specification_guid
|
|
577
|
+
|
|
578
|
+
def _addTraceabilityValueNodes(self, graph, node_ids, edge_ids, specifications_by_guid, monitor_links_by_specification_guid):
|
|
579
|
+
"""Add value nodes derived from specification constraints."""
|
|
580
|
+
for specification in self._metadata_data.get("_specifications", []):
|
|
581
|
+
value_id = self._formatTraceabilityValue(specification)
|
|
582
|
+
if value_id is None:
|
|
583
|
+
continue
|
|
584
|
+
|
|
585
|
+
specification_guid = specification.get("_GUID")
|
|
586
|
+
self._addTraceabilityNode(graph, node_ids, {
|
|
587
|
+
"type": "value",
|
|
588
|
+
"status": None,
|
|
589
|
+
"layer": 1,
|
|
590
|
+
"guid": specification_guid,
|
|
591
|
+
"id": value_id,
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
specification_id = specifications_by_guid.get(specification_guid)
|
|
595
|
+
self._addTraceabilityEdge(graph, edge_ids, value_id, specification_id)
|
|
596
|
+
|
|
597
|
+
for monitor_id in monitor_links_by_specification_guid.get(specification_guid, []):
|
|
598
|
+
self._addTraceabilityEdge(graph, edge_ids, value_id, monitor_id)
|
|
599
|
+
|
|
600
|
+
def _addTraceabilityNode(self, graph, node_ids, node):
|
|
601
|
+
"""Add a graph node once."""
|
|
602
|
+
node_id = node.get("id")
|
|
603
|
+
if node_id in node_ids:
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
node_ids.add(node_id)
|
|
607
|
+
graph["nodes"].append(node)
|
|
608
|
+
|
|
609
|
+
def _addTraceabilityEdge(self, graph, edge_ids, source, target):
|
|
610
|
+
"""Add a directed graph edge once."""
|
|
611
|
+
if source is None or target is None:
|
|
612
|
+
return
|
|
613
|
+
|
|
614
|
+
edge_id = (source, target)
|
|
615
|
+
if edge_id in edge_ids:
|
|
616
|
+
return
|
|
617
|
+
|
|
618
|
+
edge_ids.add(edge_id)
|
|
619
|
+
graph["edges"].append({
|
|
620
|
+
"source": source,
|
|
621
|
+
"target": target,
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
def _findTraceabilityPropertyForSpecification(self, specification_guid):
|
|
625
|
+
"""Find the metadata property that satisfies a specification."""
|
|
626
|
+
for property_definition in self._metadata_data.get("_properties", []):
|
|
627
|
+
if specification_guid in (property_definition.get("_satisfies") or []):
|
|
628
|
+
return self._getTraceabilityPropertyId(property_definition)
|
|
629
|
+
|
|
630
|
+
return None
|
|
631
|
+
|
|
632
|
+
def _getTraceabilityPropertyId(self, property_definition):
|
|
633
|
+
"""Return a display id for a property node."""
|
|
634
|
+
return property_definition.get("_name") or property_definition.get("_GUID")
|
|
635
|
+
|
|
636
|
+
def _getTraceabilitySpecificationId(self, specification):
|
|
637
|
+
"""Return a display id for a specification node."""
|
|
638
|
+
return (
|
|
639
|
+
specification.get("_description")
|
|
640
|
+
or specification.get("_feature")
|
|
641
|
+
or specification.get("_name")
|
|
642
|
+
or specification.get("_GUID")
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
def _formatTraceabilityValue(self, specification):
|
|
646
|
+
"""Format specification values as traceability value nodes."""
|
|
647
|
+
value = specification.get("_value") or {}
|
|
648
|
+
specification_type = specification.get("_type")
|
|
649
|
+
|
|
650
|
+
if specification_type == "property_range":
|
|
651
|
+
return "[" + str(value.get("_valueMin")) + " - " + str(value.get("_valueMax")) + "]"
|
|
652
|
+
|
|
653
|
+
if specification_type == "property_min":
|
|
654
|
+
return ">=" + str(value.get("_valueMin"))
|
|
655
|
+
|
|
656
|
+
if specification_type == "property_max":
|
|
657
|
+
return "<=" + str(value.get("_valueMax"))
|
|
658
|
+
|
|
659
|
+
if specification_type == "property_mean":
|
|
660
|
+
return str(value.get("_average")) + " +/- " + str(value.get("_deviation"))
|
|
661
|
+
|
|
662
|
+
return None
|
|
663
|
+
|
|
664
|
+
def _getActiveModelStructure(self):
|
|
665
|
+
"""Return the active model structure, or the first model for now."""
|
|
666
|
+
model_structures = self._operational_data.get("_modelStructures", [])
|
|
667
|
+
if not model_structures:
|
|
668
|
+
raise ValueError("Operational.json does not contain any model structures")
|
|
669
|
+
|
|
670
|
+
active_model_structure = self._operational_data.get("_activeModelStructure")
|
|
671
|
+
if active_model_structure:
|
|
672
|
+
for model_structure in model_structures:
|
|
673
|
+
if model_structure.get("_name") == active_model_structure or model_structure.get("_GUID") == active_model_structure:
|
|
674
|
+
return model_structure
|
|
675
|
+
|
|
676
|
+
return model_structures[0]
|
|
677
|
+
|
|
678
|
+
def _extractSAIfetyPorts(self, port_definitions, port_prefix):
|
|
679
|
+
"""Extract SAIfety input/output port definitions."""
|
|
680
|
+
ports = []
|
|
681
|
+
for port_definition in port_definitions:
|
|
682
|
+
port_name = port_definition.get("_name") or port_definition.get("_GUID") or "unnamed"
|
|
683
|
+
port = {
|
|
684
|
+
"id": port_prefix + "_" + self._toIdentifier(port_name),
|
|
685
|
+
"name": port_name,
|
|
686
|
+
"datatype": port_definition.get("_datatype"),
|
|
687
|
+
"dimensions": None,
|
|
688
|
+
"shape": [],
|
|
689
|
+
"unit": port_definition.get("_unit"),
|
|
690
|
+
"value_range": self._extractValueRange(port_definition),
|
|
691
|
+
"description": port_definition.get("_description"),
|
|
692
|
+
"source_guid": port_definition.get("_GUID"),
|
|
693
|
+
"mapping": port_definition.get("_mapping"),
|
|
694
|
+
}
|
|
695
|
+
ports.append(port)
|
|
696
|
+
|
|
697
|
+
return ports
|
|
698
|
+
|
|
699
|
+
def _extractValueRange(self, port_definition):
|
|
700
|
+
"""Extract a SAIfety value range from an Operational.json port."""
|
|
701
|
+
min_value = port_definition.get("_min")
|
|
702
|
+
max_value = port_definition.get("_max")
|
|
703
|
+
if min_value is None and max_value is None:
|
|
704
|
+
return None
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
"min": min_value,
|
|
708
|
+
"max": max_value,
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
def _generateSAIfetyFlow(self, inputs, outputs, model_id):
|
|
712
|
+
"""Build the single-component AI flow."""
|
|
713
|
+
flow = []
|
|
714
|
+
for input_port in inputs:
|
|
715
|
+
flow.append({
|
|
716
|
+
"from": input_port["id"],
|
|
717
|
+
"to": model_id,
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
for output_port in outputs:
|
|
721
|
+
flow.append({
|
|
722
|
+
"from": model_id,
|
|
723
|
+
"to": output_port["id"],
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
return flow
|
|
727
|
+
|
|
728
|
+
def _extractSAIfetyMonitors(self, inputs, outputs):
|
|
729
|
+
"""Link runtime monitors to known component ports where possible."""
|
|
730
|
+
port_targets = {}
|
|
731
|
+
for port in inputs + outputs:
|
|
732
|
+
port_targets[port["name"]] = port["id"]
|
|
733
|
+
port_targets[self._toIdentifier(port["name"])] = port["id"]
|
|
734
|
+
|
|
735
|
+
monitors = []
|
|
736
|
+
for monitor_name, monitor_info in self.runtime_monitors.items():
|
|
737
|
+
if monitor_info["observes"]:
|
|
738
|
+
for observed_property in monitor_info["observes"]:
|
|
739
|
+
feature = observed_property.get("feature") or observed_property.get("name") or monitor_name
|
|
740
|
+
monitors.append({
|
|
741
|
+
"name": feature,
|
|
742
|
+
"monitor": monitor_name,
|
|
743
|
+
"target": self._resolveSAIfetyMonitorTarget(feature, port_targets),
|
|
744
|
+
"expression": self._buildSAIfetyMonitorExpression(observed_property),
|
|
745
|
+
})
|
|
746
|
+
else:
|
|
747
|
+
monitors.append({
|
|
748
|
+
"name": monitor_name,
|
|
749
|
+
"monitor": monitor_name,
|
|
750
|
+
"target": "environment",
|
|
751
|
+
"expression": None,
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
return monitors
|
|
755
|
+
|
|
756
|
+
def _resolveSAIfetyMonitorTarget(self, feature, port_targets):
|
|
757
|
+
"""Resolve a monitor target to an input/output id when names match."""
|
|
758
|
+
if feature in port_targets:
|
|
759
|
+
return port_targets[feature]
|
|
760
|
+
|
|
761
|
+
feature_id = self._toIdentifier(feature)
|
|
762
|
+
if feature_id in port_targets:
|
|
763
|
+
return port_targets[feature_id]
|
|
764
|
+
|
|
765
|
+
return "environment"
|
|
766
|
+
|
|
767
|
+
def _buildSAIfetyMonitorExpression(self, observed_property):
|
|
768
|
+
"""Build a readable monitor expression from observed property metadata."""
|
|
769
|
+
feature = observed_property.get("feature") or observed_property.get("name") or "value"
|
|
770
|
+
property_type = observed_property.get("type")
|
|
771
|
+
value = observed_property.get("value") or {}
|
|
772
|
+
|
|
773
|
+
if property_type == "property_range":
|
|
774
|
+
min_value = value.get("_valueMin")
|
|
775
|
+
max_value = value.get("_valueMax")
|
|
776
|
+
return str(min_value) + " <= " + feature + " <= " + str(max_value)
|
|
777
|
+
|
|
778
|
+
if property_type == "property_min":
|
|
779
|
+
return feature + " >= " + str(value.get("_valueMin"))
|
|
780
|
+
|
|
781
|
+
if property_type == "property_max":
|
|
782
|
+
return feature + " <= " + str(value.get("_valueMax"))
|
|
783
|
+
|
|
784
|
+
if property_type == "property_mean":
|
|
785
|
+
average = value.get("_average")
|
|
786
|
+
deviation = value.get("_deviation")
|
|
787
|
+
if average is not None and deviation is not None:
|
|
788
|
+
return str(average - deviation) + " <= " + feature + " <= " + str(average + deviation)
|
|
789
|
+
|
|
790
|
+
return None
|
|
791
|
+
|
|
792
|
+
def _getSAIfetyArchitecturePath(self):
|
|
793
|
+
"""Return the output path for the generated SAIfety architecture."""
|
|
794
|
+
if self._operational_path is not None:
|
|
795
|
+
return self._operational_path.parents[1] / "SAIfetyArchitecture.json"
|
|
796
|
+
|
|
797
|
+
return self.path_to_vf / "SAIfetyArchitecture.json"
|
|
798
|
+
|
|
799
|
+
def _getTraceabilityGraphPath(self):
|
|
800
|
+
"""Return the output path for the generated traceability graph."""
|
|
801
|
+
if self._metadata_path is not None:
|
|
802
|
+
return self._metadata_path.parents[1] / "TraceabilityGraph.json"
|
|
803
|
+
|
|
804
|
+
return self.path_to_vf / "TraceabilityGraph.json"
|
|
805
|
+
|
|
806
|
+
def _toIdentifier(self, value):
|
|
807
|
+
"""Convert names from Operational.json into stable JSON ids."""
|
|
808
|
+
identifier = []
|
|
809
|
+
for character in str(value).strip():
|
|
810
|
+
if character.isalnum():
|
|
811
|
+
identifier.append(character.lower())
|
|
812
|
+
else:
|
|
813
|
+
identifier.append("_")
|
|
814
|
+
|
|
815
|
+
compact_identifier = "_".join(part for part in "".join(identifier).split("_") if part)
|
|
816
|
+
return compact_identifier or "unnamed"
|
|
817
|
+
|
|
818
|
+
def _findOperationalFile(self):
|
|
819
|
+
"""Find the Operational.json file in the loaded validity frame."""
|
|
820
|
+
operational_path = self.path_to_vf / "Operational" / "Operational.json"
|
|
821
|
+
if operational_path.is_file():
|
|
822
|
+
return operational_path
|
|
823
|
+
|
|
824
|
+
operational_files = list(self.path_to_vf.glob("*/Operational/Operational.json"))
|
|
825
|
+
if len(operational_files) == 1:
|
|
826
|
+
return operational_files[0]
|
|
827
|
+
|
|
828
|
+
if not operational_files:
|
|
829
|
+
raise FileNotFoundError("Operational.json not found in validity frame: " + str(self.path_to_vf))
|
|
830
|
+
|
|
831
|
+
raise ValueError("Multiple Operational.json files found in validity frame: " + str(self.path_to_vf))
|
|
832
|
+
|
|
833
|
+
def _findMetadataFile(self):
|
|
834
|
+
"""Find the Metadata.json file in the loaded validity frame."""
|
|
835
|
+
metadata_path = self.path_to_vf / "Metadata" / "Metadata.json"
|
|
836
|
+
if metadata_path.is_file():
|
|
837
|
+
return metadata_path
|
|
838
|
+
|
|
839
|
+
metadata_files = list(self.path_to_vf.glob("*/Metadata/Metadata.json"))
|
|
840
|
+
if len(metadata_files) == 1:
|
|
841
|
+
return metadata_files[0]
|
|
842
|
+
|
|
843
|
+
if not metadata_files:
|
|
844
|
+
raise FileNotFoundError("Metadata.json not found in validity frame: " + str(self.path_to_vf))
|
|
845
|
+
|
|
846
|
+
raise ValueError("Multiple Metadata.json files found in validity frame: " + str(self.path_to_vf))
|
|
847
|
+
|
|
848
|
+
def _extractRuntimeMonitorInfo(self, monitor_definition):
|
|
849
|
+
"""Extract the runtime monitor metadata used for flexIA translation."""
|
|
850
|
+
monitor_name = monitor_definition.get("_name")
|
|
851
|
+
if not monitor_name:
|
|
852
|
+
raise ValueError("Runtime monitor without '_name' found in Operational.json")
|
|
853
|
+
|
|
854
|
+
observes = []
|
|
855
|
+
for observed_property in monitor_definition.get("observes", []):
|
|
856
|
+
observes.append({
|
|
857
|
+
"name": observed_property.get("_name"),
|
|
858
|
+
"description": observed_property.get("_description"),
|
|
859
|
+
"guid": observed_property.get("_GUID"),
|
|
860
|
+
"feature": observed_property.get("_feature"),
|
|
861
|
+
"status": observed_property.get("_status"),
|
|
862
|
+
"type": observed_property.get("_type"),
|
|
863
|
+
"value": observed_property.get("_value"),
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
"name": monitor_name,
|
|
868
|
+
"description": monitor_definition.get("_description"),
|
|
869
|
+
"guid": monitor_definition.get("_GUID"),
|
|
870
|
+
"timestamp": monitor_definition.get("_timestamp"),
|
|
871
|
+
"status": monitor_definition.get("_status"),
|
|
872
|
+
"type": monitor_definition.get("_type"),
|
|
873
|
+
"validate_point_function": monitor_definition.get("_validate_point_function"),
|
|
874
|
+
"package_name": monitor_definition.get("_packageName"),
|
|
875
|
+
"observes": observes,
|
|
876
|
+
"spec_status": monitor_definition.get("spec_status", {}),
|
|
877
|
+
"last_observed_values": monitor_definition.get("last_observed_values", {}),
|
|
878
|
+
"parent": monitor_definition.get("parent"),
|
|
879
|
+
"source": monitor_definition,
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
def _logRuntimeMonitorSummary(self, operational_path):
|
|
883
|
+
"""Print a compact summary of the loaded runtime monitors."""
|
|
884
|
+
self._log(
|
|
885
|
+
"info",
|
|
886
|
+
"Loaded " + str(len(self.runtime_monitors)) +
|
|
887
|
+
" runtime monitor(s) from " + str(operational_path),
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
for monitor_name, monitor_info in self.runtime_monitors.items():
|
|
891
|
+
observed_features = []
|
|
892
|
+
for observed_property in monitor_info["observes"]:
|
|
893
|
+
feature = observed_property.get("feature") or "<unnamed>"
|
|
894
|
+
property_type = observed_property.get("type") or "<unknown>"
|
|
895
|
+
observed_features.append(feature + " (" + property_type + ")")
|
|
896
|
+
|
|
897
|
+
if observed_features:
|
|
898
|
+
observes_summary = ", ".join(observed_features)
|
|
899
|
+
else:
|
|
900
|
+
observes_summary = "no observed properties"
|
|
901
|
+
|
|
902
|
+
self._log(
|
|
903
|
+
"info",
|
|
904
|
+
" - " + monitor_name +
|
|
905
|
+
" [" + str(monitor_info.get("type")) + "] " +
|
|
906
|
+
"status=" + str(monitor_info.get("status")) +
|
|
907
|
+
", observes: " + observes_summary,
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
def _registerRuntime(self):
|
|
911
|
+
"""Register loaded runtime monitors."""
|
|
912
|
+
# TODO: register runtime monitors with the owning runtime/node.
|
|
913
|
+
return True
|
|
914
|
+
|
|
915
|
+
def _setRuntimeMonitor(self, runtimeMonitorName, value):
|
|
916
|
+
"""Set a runtime monitor value."""
|
|
917
|
+
if runtimeMonitorName not in self.runtime_monitors:
|
|
918
|
+
self._log("warning", "Runtime monitor not found: " + str(runtimeMonitorName))
|
|
919
|
+
return False
|
|
920
|
+
|
|
921
|
+
monitor = self.runtime_monitors[runtimeMonitorName]
|
|
922
|
+
if not hasattr(monitor, "set"):
|
|
923
|
+
self._log("warning", "Runtime monitor is not translated to a flexIA runtime monitor yet: " + str(runtimeMonitorName))
|
|
924
|
+
return False
|
|
925
|
+
|
|
926
|
+
monitor.set(value)
|
|
927
|
+
return True
|
|
928
|
+
|
|
929
|
+
def _resolveValidityFramePath(self, path_to_fmu):
|
|
930
|
+
"""Return the VF folder path, unpacking .vf archives when needed."""
|
|
931
|
+
vf_path = Path(path_to_fmu)
|
|
932
|
+
if vf_path.is_dir():
|
|
933
|
+
return vf_path
|
|
934
|
+
|
|
935
|
+
if vf_path.is_file() and vf_path.suffix == ".vf":
|
|
936
|
+
return self._unpackValidityFrame(vf_path)
|
|
937
|
+
|
|
938
|
+
raise ValueError("Validity frame must be a folder or a .vf archive: " + str(path_to_fmu))
|
|
939
|
+
|
|
940
|
+
def _unpackValidityFrame(self, vf_archive):
|
|
941
|
+
"""Unpack a zipped .vf archive and return the unpacked folder."""
|
|
942
|
+
if not zipfile.is_zipfile(vf_archive):
|
|
943
|
+
raise ValueError("Validity frame archive is not a valid zip file: " + str(vf_archive))
|
|
944
|
+
|
|
945
|
+
self._temp_dir = tempfile.TemporaryDirectory(prefix="validity_frame_")
|
|
946
|
+
unpack_dir = Path(self._temp_dir.name) / vf_archive.stem
|
|
947
|
+
unpack_dir.mkdir(parents=True, exist_ok=True)
|
|
948
|
+
|
|
949
|
+
with zipfile.ZipFile(vf_archive, "r") as archive:
|
|
950
|
+
for member in archive.infolist():
|
|
951
|
+
member_path = unpack_dir / member.filename
|
|
952
|
+
if not member_path.resolve().is_relative_to(unpack_dir.resolve()):
|
|
953
|
+
raise ValueError("Unsafe path found in validity frame archive: " + member.filename)
|
|
954
|
+
archive.extractall(unpack_dir)
|
|
955
|
+
|
|
956
|
+
self._unpacked_vf_dir = unpack_dir
|
|
957
|
+
self._log("info", "Unpacked validity frame archive to: " + str(unpack_dir))
|
|
958
|
+
return unpack_dir
|