flowgraph 0.1.2__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.
Files changed (56) hide show
  1. flowgraph/__init__.py +9 -0
  2. flowgraph/__main__.py +6 -0
  3. flowgraph/adapters/__init__.py +141 -0
  4. flowgraph/adapters/controls.py +96 -0
  5. flowgraph/adapters/cosapp.py +195 -0
  6. flowgraph/adapters/data_types.py +59 -0
  7. flowgraph/adapters/documentation.py +45 -0
  8. flowgraph/adapters/field_ops.py +121 -0
  9. flowgraph/adapters/file_conversion.py +393 -0
  10. flowgraph/adapters/files.py +191 -0
  11. flowgraph/adapters/filters.py +173 -0
  12. flowgraph/adapters/image_tools.py +394 -0
  13. flowgraph/adapters/mesh_creation_tools.py +431 -0
  14. flowgraph/adapters/mesh_generations.py +58 -0
  15. flowgraph/adapters/mesh_ops.py +362 -0
  16. flowgraph/adapters/paraview.py +39 -0
  17. flowgraph/adapters/plaid_bridge.py +205 -0
  18. flowgraph/adapters/readers.py +215 -0
  19. flowgraph/adapters/remote_files.py +118 -0
  20. flowgraph/adapters/simple_sources.py +199 -0
  21. flowgraph/adapters/sinks.py +80 -0
  22. flowgraph/adapters/user_code.py +196 -0
  23. flowgraph/adapters/workflow.py +176 -0
  24. flowgraph/adapters/workflow_interfaces.py +78 -0
  25. flowgraph/adapters/writers.py +159 -0
  26. flowgraph/application/__init__.py +1 -0
  27. flowgraph/application/node_registry.py +14 -0
  28. flowgraph/application/port_conversions.py +63 -0
  29. flowgraph/application/wasm_execution.py +132 -0
  30. flowgraph/application/workflow_cli.py +417 -0
  31. flowgraph/application/workflow_core.py +1193 -0
  32. flowgraph/application/workflow_history.py +93 -0
  33. flowgraph/application/workflow_io.py +393 -0
  34. flowgraph/application/workflow_layout.py +170 -0
  35. flowgraph/application/workflow_web_catalog.py +143 -0
  36. flowgraph/application/workflows.py +7 -0
  37. flowgraph/core_cli.py +81 -0
  38. flowgraph/domain/__init__.py +5 -0
  39. flowgraph/domain/mesh_document.py +91 -0
  40. flowgraph/examples/AllFilters.py +19 -0
  41. flowgraph/examples/convert_format.py +21 -0
  42. flowgraph/resources.py +29 -0
  43. flowgraph/testdata/AllFilters.json +390 -0
  44. flowgraph/testdata/FlowGraph.h5 +0 -0
  45. flowgraph/testdata/FlowGraph.stl +0 -0
  46. flowgraph/testdata/FlowGraph.xdmf +22 -0
  47. flowgraph/testdata/SimpleReadMesh.json +70 -0
  48. flowgraph/testdata/SpherePoints.csv +83 -0
  49. flowgraph/testdata/muscat-muscat-mesh-conversion.json +101 -0
  50. flowgraph/version.py +3 -0
  51. flowgraph-0.1.2.dist-info/METADATA +150 -0
  52. flowgraph-0.1.2.dist-info/RECORD +56 -0
  53. flowgraph-0.1.2.dist-info/WHEEL +5 -0
  54. flowgraph-0.1.2.dist-info/entry_points.txt +2 -0
  55. flowgraph-0.1.2.dist-info/licenses/LICENSE +29 -0
  56. flowgraph-0.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,362 @@
1
+ """Workflow nodes for mesh transformations and tag operations."""
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ import numpy as np
7
+ from Muscat.LinAlg.Transform import Transform
8
+ from Muscat.MeshContainers import ElementsDescription as ED
9
+ from Muscat.MeshContainers.ElementsContainers import AllElements, ElementsContainer
10
+ from Muscat.MeshContainers.Filters.FilterObjects import ElementFilter
11
+ from Muscat.MeshContainers.Filters.FilterOperators import FilterOperatorBase
12
+ from Muscat.MeshTools.MeshCreationTools import QuadToLin
13
+ from Muscat.Types import MuscatIndex
14
+ from scipy.spatial import Delaunay
15
+
16
+ from flowgraph.adapters.data_types import MESH_DOCUMENT, MUSCAT_ELEMENT_FILTER, TRANSFORM_T
17
+ from flowgraph.application.workflow_core import (
18
+ NodeDefinition,
19
+ ParameterDefinition,
20
+ ParameterOption,
21
+ PortDefinition,
22
+ PortDirection,
23
+ )
24
+ from flowgraph.domain.mesh_document import MeshDocument
25
+
26
+ from .data_types import ParameterKind
27
+
28
+
29
+ def _transform_mesh(_inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
30
+ """Build a :class:`Muscat.LinAlg.Transform.Transform` from node parameters."""
31
+ res = Transform()
32
+ res.keepNormalized = parameters.get("keepNormalized", True)
33
+ res.keepOrthogonal = parameters.get("keepOrthogonal", True)
34
+ res.SetOffset(parameters.get("translation", [0, 0, 0]))
35
+ res.SetFirst(parameters.get("first", [1, 0, 0]))
36
+ res.SetSecond(parameters.get("second", [0, 1, 0]))
37
+
38
+ return {"Transform": res}
39
+
40
+
41
+ def _apply_transform(inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
42
+ """Apply a Muscat transform to a mesh document without mutating its input."""
43
+ document: MeshDocument = inputs["mesh"]
44
+ transform: Transform = inputs["Transform"]
45
+ inverse = parameters.get("inverse", False)
46
+
47
+ transformed_document = MeshDocument(document.mesh.View())
48
+ apply_transform = transform.ApplyInvTransform if inverse else transform.ApplyTransform
49
+ apply_transform_direction = (
50
+ transform.ApplyInvTransformDirection if inverse else transform.ApplyTransformDirection
51
+ )
52
+ transformed_document.mesh.nodes = np.ascontiguousarray(
53
+ apply_transform(transformed_document.mesh.nodes)
54
+ )
55
+
56
+ if parameters.get("onVectorFields", True):
57
+ for fields in (
58
+ transformed_document.mesh.nodeFields,
59
+ transformed_document.mesh.elemFields,
60
+ ):
61
+ for name, field in fields.items():
62
+ if np.ndim(field) == 2 and np.shape(field)[1] == 3:
63
+ fields[name] = np.ascontiguousarray(apply_transform_direction(field))
64
+
65
+ return {"mesh": transformed_document}
66
+
67
+
68
+ def _delaunay_3d(inputs: Mapping[str, Any], _parameters: Mapping[str, Any]) -> Mapping[str, Any]:
69
+ """Tetrahedralize a document's points while preserving node metadata."""
70
+ source = inputs["meshDocument"]
71
+ points = np.asarray(source.mesh.nodes)
72
+ triangulation = Delaunay(points)
73
+
74
+ mesh = source.mesh.View()
75
+ mesh.elements = AllElements()
76
+ mesh.elemFields = {}
77
+ elements = ElementsContainer(ED.ElementType.Tetrahedron_4)
78
+ elements.AddNewElements(np.asarray(triangulation.simplices, dtype=MuscatIndex))
79
+ mesh.elements.AddContainer(elements)
80
+ return {"meshDocument": MeshDocument(mesh)}
81
+
82
+
83
+ def _quad_to_lin(inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
84
+ """Convert a quadratic mesh document to a linear mesh document."""
85
+ input_document: MeshDocument = inputs["inputMesh"]
86
+ mesh = QuadToLin(
87
+ input_document.mesh,
88
+ divideQuadElements=parameters.get("divideQuadElements", True),
89
+ linearizedMiddlePoints=parameters.get("linearizedMiddlePoints", False),
90
+ )
91
+ return {"outputMesh": MeshDocument(mesh)}
92
+
93
+
94
+ _TAG_ENTITIES = ("element", "node")
95
+ _TAG_ENTITY_OPTIONS = tuple(ParameterOption(entity, entity.title()) for entity in _TAG_ENTITIES)
96
+
97
+
98
+ def _tag_collections(document: MeshDocument, entity: str):
99
+ """Return the Muscat tag collections for a node or element tag domain."""
100
+ if entity not in _TAG_ENTITIES:
101
+ raise ValueError(f"tag entity must be one of {_TAG_ENTITIES!r}, got {entity!r}")
102
+ if entity == "node":
103
+ return (document.mesh.nodesTags,)
104
+ return tuple(elements.tags for elements in document.mesh.elements.values())
105
+
106
+
107
+ def _validate_tag_name(name: Any, parameter: str) -> str:
108
+ if not isinstance(name, str) or not name.strip():
109
+ raise ValueError(f"{parameter} must be a non-empty string")
110
+ return name.strip()
111
+
112
+
113
+ def _require_tag(collections, name: str) -> None:
114
+ if not any(name in collection for collection in collections):
115
+ raise ValueError(f"Tag {name!r} does not exist")
116
+
117
+
118
+ def _copy_document(document: MeshDocument) -> MeshDocument:
119
+ return MeshDocument(document.mesh.View())
120
+
121
+
122
+ def _rename_tag(inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
123
+ """Rename a node or element tag without mutating the input document."""
124
+ document = _copy_document(inputs["mesh"])
125
+ collections = _tag_collections(document, parameters.get("entity", "element"))
126
+ name = _validate_tag_name(parameters.get("tag"), "tag")
127
+ new_name = _validate_tag_name(parameters.get("newTag"), "newTag")
128
+ _require_tag(collections, name)
129
+ if name != new_name and any(new_name in collection for collection in collections):
130
+ raise ValueError(f"Tag {new_name!r} already exists")
131
+ for collection in collections:
132
+ if name in collection:
133
+ collection.RenameTag(name, new_name)
134
+ return {"mesh": document}
135
+
136
+
137
+ def _merge_tags(inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
138
+ """Merge source node or element tags into a destination tag."""
139
+ document = _copy_document(inputs["mesh"])
140
+ collections = _tag_collections(document, parameters.get("entity", "element"))
141
+ target = _validate_tag_name(parameters.get("targetTag"), "targetTag")
142
+ sources = parameters.get("sourceTags")
143
+ if not isinstance(sources, list) or not sources:
144
+ raise ValueError("sourceTags must be a non-empty list of strings")
145
+ source_names = [_validate_tag_name(source, "sourceTags item") for source in sources]
146
+ if len(set(source_names)) != len(source_names):
147
+ raise ValueError("sourceTags must not contain duplicates")
148
+ if target in source_names:
149
+ raise ValueError("targetTag must not be one of sourceTags")
150
+ for source in source_names:
151
+ _require_tag(collections, source)
152
+ for collection in collections:
153
+ matching = [collection[source] for source in source_names if source in collection]
154
+ if matching:
155
+ destination = collection.CreateTag(target, False)
156
+ for source in matching:
157
+ destination.Merge(source)
158
+ collection.DeleteTags(source_names)
159
+ return {"mesh": document}
160
+
161
+
162
+ def _remove_tag(inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
163
+ """Remove a node or element tag without mutating the input document."""
164
+ document = _copy_document(inputs["mesh"])
165
+ collections = _tag_collections(document, parameters.get("entity", "element"))
166
+ name = _validate_tag_name(parameters.get("tag"), "tag")
167
+ _require_tag(collections, name)
168
+ for collection in collections:
169
+ collection.DeleteTags(name)
170
+ return {"mesh": document}
171
+
172
+
173
+ def _create_tag(inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
174
+ """Create a tag containing the entities selected by an element filter."""
175
+ document = _copy_document(inputs["mesh"])
176
+ name = _validate_tag_name(parameters.get("tagName"), "tagName")
177
+ element_filter: ElementFilter | FilterOperatorBase = inputs["filter"]
178
+ overwrite = parameters.get("overwrite", False)
179
+
180
+ if parameters.get("nodeTag", False):
181
+ if name in document.mesh.nodesTags and not overwrite:
182
+ raise ValueError(f"Tag {name!r} already exists")
183
+ document.mesh.nodesTags.CreateTag(name).SetIds(
184
+ element_filter.GetNodesIndices(document.mesh)
185
+ )
186
+ else:
187
+ element_types = tuple(document.mesh.elements.keys())
188
+ if (
189
+ any(name in document.mesh.elements[element_type].tags for element_type in element_types)
190
+ and not overwrite
191
+ ):
192
+ raise ValueError(f"Tag {name!r} already exists")
193
+ for element_type in element_types:
194
+ ids = element_filter.GetIdsToTreat(document.mesh, element_type)
195
+ document.mesh.elements[element_type].tags.CreateTag(name).SetIds(ids)
196
+
197
+ return {"mesh": document}
198
+
199
+
200
+ def _tag_parameters(*, merge: bool = False, rename: bool = False):
201
+ parameters = [
202
+ ParameterDefinition(
203
+ "entity",
204
+ ParameterKind.STR_SELECT,
205
+ "Tag entity",
206
+ "element",
207
+ options=_TAG_ENTITY_OPTIONS,
208
+ port=False,
209
+ ),
210
+ ]
211
+ if merge:
212
+ parameters.extend(
213
+ [
214
+ ParameterDefinition("sourceTags", ParameterKind.LIST_STR, "Source tags", []),
215
+ ParameterDefinition("targetTag", ParameterKind.TEXT, "Target tag", "merged"),
216
+ ]
217
+ )
218
+ else:
219
+ parameters.append(ParameterDefinition("tag", ParameterKind.TEXT, "Tag", ""))
220
+ if rename:
221
+ parameters.append(ParameterDefinition("newTag", ParameterKind.TEXT, "New tag", ""))
222
+ return tuple(parameters)
223
+
224
+
225
+ def _tag_node(node_id: str, label: str, executor, parameters) -> NodeDefinition:
226
+ icons = {
227
+ "Rename Tag": "mdi-tag-edit-outline",
228
+ "Merge Tags": "mdi-tag-multiple-outline",
229
+ "Remove Tag": "mdi-tag-remove-outline",
230
+ }
231
+ return NodeDefinition(
232
+ id=node_id,
233
+ icon=icons[label],
234
+ label=label,
235
+ description=f"{label} for node or element tags.",
236
+ ports=(
237
+ PortDefinition("mesh", PortDirection.INPUT, MESH_DOCUMENT, "Mesh"),
238
+ PortDefinition("mesh", PortDirection.OUTPUT, MESH_DOCUMENT, "Mesh"),
239
+ ),
240
+ parameters=parameters,
241
+ executor=executor,
242
+ )
243
+
244
+
245
+ RENAME_TAG_NODE = _tag_node("rename-tag", "Rename Tag", _rename_tag, _tag_parameters(rename=True))
246
+ MERGE_TAGS_NODE = _tag_node("merge-tags", "Merge Tags", _merge_tags, _tag_parameters(merge=True))
247
+ REMOVE_TAG_NODE = _tag_node("remove-tag", "Remove Tag", _remove_tag, _tag_parameters())
248
+
249
+ CREATE_TAG_NODE = NodeDefinition(
250
+ id="create-tag",
251
+ icon="mdi-tag-plus-outline",
252
+ label="Create Tag",
253
+ description="Creates a node or element tag from an element filter.",
254
+ ports=(
255
+ PortDefinition("mesh", PortDirection.INPUT, MESH_DOCUMENT, "Mesh"),
256
+ PortDefinition("filter", PortDirection.INPUT, MUSCAT_ELEMENT_FILTER, "Element Filter"),
257
+ PortDefinition("mesh", PortDirection.OUTPUT, MESH_DOCUMENT, "Mesh"),
258
+ ),
259
+ parameters=(
260
+ ParameterDefinition("tagName", ParameterKind.TEXT, "Tag name", ""),
261
+ ParameterDefinition(
262
+ "nodeTag", ParameterKind.BOOLEAN, "Node tag (Element tag if False)", False, port=False
263
+ ),
264
+ ParameterDefinition(
265
+ "overwrite", ParameterKind.BOOLEAN, "Overwrite tag if exists", False, port=False
266
+ ),
267
+ ),
268
+ executor=_create_tag,
269
+ )
270
+
271
+
272
+ TRANSFORM_NODE = NodeDefinition(
273
+ id="transform",
274
+ icon="mdi-axis-arrow-lock",
275
+ label="Mesh Transform",
276
+ description="Creates a Muscat coordinate transformation.",
277
+ ports=(PortDefinition("Transform", PortDirection.OUTPUT, TRANSFORM_T, "Transform"),),
278
+ executor=_transform_mesh,
279
+ parameters=(
280
+ ParameterDefinition("translation", ParameterKind.VEC3D, "Translation", [0, 0, 0]),
281
+ ParameterDefinition("first", ParameterKind.VEC3D, "First direction", [1, 0, 0]),
282
+ ParameterDefinition("second", ParameterKind.VEC3D, "Second direction", [0, 1, 0]),
283
+ ParameterDefinition(
284
+ "keepNormalized", ParameterKind.BOOLEAN, "Keep normalized", True, port=False
285
+ ),
286
+ ParameterDefinition(
287
+ "keepOrthogonal", ParameterKind.BOOLEAN, "Keep orthogonal", True, port=False
288
+ ),
289
+ ),
290
+ )
291
+
292
+ APPLY_TRANSFORM_NODE = NodeDefinition(
293
+ id="apply-transform",
294
+ icon="mdi-vector-combine",
295
+ label="Apply Transform",
296
+ description="Applies a coordinate transformation to a mesh.",
297
+ ports=(
298
+ PortDefinition("mesh", PortDirection.INPUT, MESH_DOCUMENT, "Mesh"),
299
+ PortDefinition("Transform", PortDirection.INPUT, TRANSFORM_T, "Transform"),
300
+ PortDefinition("mesh", PortDirection.OUTPUT, MESH_DOCUMENT, "Transformed mesh"),
301
+ ),
302
+ parameters=(
303
+ ParameterDefinition(
304
+ "onVectorFields", ParameterKind.BOOLEAN, "Apply on Vector Fields", True, port=False
305
+ ),
306
+ ParameterDefinition(
307
+ "inverse", ParameterKind.BOOLEAN, "Apply inverse transform", False, port=False
308
+ ),
309
+ ),
310
+ executor=_apply_transform,
311
+ )
312
+
313
+ DELAUNAY_3D_NODE = NodeDefinition(
314
+ id="delaunay-3d",
315
+ icon="mdi-vector-triangle",
316
+ label="Delaunay 3D",
317
+ description="Create a 3D tetrahedralization from the mesh document's points.",
318
+ ports=(
319
+ PortDefinition("meshDocument", PortDirection.INPUT, MESH_DOCUMENT, "Mesh document"),
320
+ PortDefinition("meshDocument", PortDirection.OUTPUT, MESH_DOCUMENT, "Mesh document"),
321
+ ),
322
+ executor=_delaunay_3d,
323
+ )
324
+
325
+ QUAD_TO_LIN_NODE = NodeDefinition(
326
+ id="quad-to-lin",
327
+ icon="mdi-vector-square-remove",
328
+ label="Quad To Lin",
329
+ description="Convert quadratic mesh elements to linear elements.",
330
+ ports=(
331
+ PortDefinition("inputMesh", PortDirection.INPUT, MESH_DOCUMENT, "Input mesh"),
332
+ PortDefinition("outputMesh", PortDirection.OUTPUT, MESH_DOCUMENT, "Output mesh"),
333
+ ),
334
+ parameters=(
335
+ ParameterDefinition(
336
+ "divideQuadElements",
337
+ ParameterKind.BOOLEAN,
338
+ "Divide quadratic elements",
339
+ True,
340
+ port=False,
341
+ ),
342
+ ParameterDefinition(
343
+ "linearizedMiddlePoints",
344
+ ParameterKind.BOOLEAN,
345
+ "Linearize middle points",
346
+ False,
347
+ port=False,
348
+ ),
349
+ ),
350
+ executor=_quad_to_lin,
351
+ )
352
+
353
+ AVAILABLE_NODES = (
354
+ TRANSFORM_NODE,
355
+ APPLY_TRANSFORM_NODE,
356
+ DELAUNAY_3D_NODE,
357
+ QUAD_TO_LIN_NODE,
358
+ RENAME_TAG_NODE,
359
+ MERGE_TAGS_NODE,
360
+ REMOVE_TAG_NODE,
361
+ CREATE_TAG_NODE,
362
+ )
@@ -0,0 +1,39 @@
1
+ """Simple workflow sink to send the files to paraview."""
2
+
3
+ import json
4
+ import socket
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from flowgraph.adapters.readers import MESH_DOCUMENT
9
+ from flowgraph.application.workflow_core import (
10
+ DataType,
11
+ NodeDefinition,
12
+ PortDefinition,
13
+ PortDirection,
14
+ )
15
+
16
+ STRING = DataType("string", "Text", str)
17
+
18
+
19
+ def SendMeshToParaView(
20
+ inputs: Mapping[str, Any], parameters: Mapping[str, Any]
21
+ ) -> Mapping[str, Any]:
22
+
23
+ data = {"command": "test", "value": 123}
24
+ host = parameters.get("host", "127.0.0.1")
25
+ port = int(parameters.get("port", 9999))
26
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
27
+ sock.connect((host, port))
28
+ sock.sendall(json.dumps(data).encode("utf-8"))
29
+ return {}
30
+
31
+
32
+ PARAIEW_BRIDGE = NodeDefinition(
33
+ id="write-muscat",
34
+ icon="mdi-file-export-outline",
35
+ label="Write with Muscat",
36
+ description="Writes the canonical mesh document to a filename selected by extension.",
37
+ ports=(PortDefinition("mesh", PortDirection.INPUT, MESH_DOCUMENT, "Mesh"),),
38
+ executor=SendMeshToParaView,
39
+ )
@@ -0,0 +1,205 @@
1
+ """Workflow nodes backed by the Plaid external dataset library."""
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ from flowgraph.application.workflow_core import (
7
+ DataType,
8
+ NodeDefinition,
9
+ ParameterDefinition,
10
+ PortDefinition,
11
+ PortDirection,
12
+ )
13
+ from flowgraph.domain.mesh_document import MeshDocument
14
+
15
+ from .data_types import MESH_DOCUMENT, ParameterKind
16
+
17
+ PLAID_SAMPLE = DataType("plaid-dataset", "Plaid sample", object)
18
+ PLAID_INFO = DataType("plaid-info", "Plaid info", object)
19
+ PLAID_DATA_PRODUCTION = DataType("plaid-data-production", "Plaid data production", object)
20
+ PLAID_OPTIONAL_STRING = DataType("plaid-optional-string", "Optional Text", (str, type(None)))
21
+ PLAID_OPTIONAL_TABLE = DataType("plaid-optional-table", "Optional Table", (dict, type(None)))
22
+
23
+
24
+ def _load_plaid_sample(
25
+ _inputs: Mapping[str, Any], parameters: Mapping[str, Any]
26
+ ) -> Mapping[str, Any]:
27
+ """Load one sample from a Plaid dataset stored on disk."""
28
+ path = parameters.get("path", "")
29
+
30
+ if not isinstance(path, str) or not path:
31
+ raise ValueError("The Plaid dataset path must be a non-empty string")
32
+
33
+ try:
34
+ from plaid.storage.reader import init_from_disk, load_infos_from_disk
35
+ except ImportError as error:
36
+ raise RuntimeError(
37
+ "The Plaid library is required to load Plaid samples; "
38
+ "install the package providing plaid.storage.reader. "
39
+ "> uv pip install pyplaid"
40
+ ) from error
41
+
42
+ infos = load_infos_from_disk(path)
43
+ datasetdict, converterdict = init_from_disk(path)
44
+
45
+ return {"dataset": (datasetdict, converterdict), "infos": infos}
46
+
47
+
48
+ LOAD_PLAID_DATASET = NodeDefinition(
49
+ id="load-plaid-dataset",
50
+ icon="mdi-database-arrow-down-outline",
51
+ label="Load Plaid Dataset",
52
+ description="Loads dataset from a Plaid dataset on disk.",
53
+ ports=(
54
+ PortDefinition("dataset", PortDirection.OUTPUT, PLAID_SAMPLE, "Sample"),
55
+ PortDefinition("infos", PortDirection.OUTPUT, PLAID_INFO, "Infos"),
56
+ ),
57
+ executor=_load_plaid_sample,
58
+ parameters=(ParameterDefinition("path", ParameterKind.FILE, "Path", "", "/path/to/plaid"),),
59
+ )
60
+
61
+
62
+ def _extract_plaid_sample(
63
+ inputs: Mapping[str, Any], parameters: Mapping[str, Any]
64
+ ) -> Mapping[str, Any]:
65
+ """Convert one stored Plaid dataset item into a Plaid ``Sample``."""
66
+ dataset_and_converters = inputs.get("dataset")
67
+ infos = inputs.get("infos")
68
+ split = parameters.get("split", "")
69
+ sample_number = parameters.get("sample_number", 0)
70
+
71
+ if (
72
+ not isinstance(dataset_and_converters, tuple)
73
+ or len(dataset_and_converters) != 2
74
+ or not isinstance(dataset_and_converters[0], Mapping)
75
+ or not isinstance(dataset_and_converters[1], Mapping)
76
+ ):
77
+ raise TypeError("The PLAID_SAMPLE input must contain dataset and converter mappings")
78
+ if infos is None:
79
+ raise ValueError("The PLAID_INFO input is required")
80
+ if not isinstance(split, str) or not split:
81
+ raise ValueError("The Plaid split must be a non-empty string")
82
+ if isinstance(sample_number, bool) or not isinstance(sample_number, int):
83
+ raise TypeError("The Plaid sample number must be an integer")
84
+ if sample_number < 0:
85
+ raise ValueError("The Plaid sample number must not be negative")
86
+
87
+ datasetdict, converterdict = dataset_and_converters
88
+ try:
89
+ dataset = datasetdict[split]
90
+ converter = converterdict[split]
91
+ sample = converter.to_plaid(dataset, sample_number)
92
+ except (KeyError, IndexError, TypeError, ValueError, OSError) as error:
93
+ raise ValueError(
94
+ f"Could not extract Plaid sample: split={split!r}, sample_number={sample_number}"
95
+ ) from error
96
+
97
+ return {"sample": sample}
98
+
99
+
100
+ EXTRACT_PLAID_SAMPLE = NodeDefinition(
101
+ id="extract-plaid-sample",
102
+ icon="mdi-database-search-outline",
103
+ label="Extract Plaid sample",
104
+ description="Converts one item from a loaded Plaid dataset into a Plaid Sample.",
105
+ ports=(
106
+ PortDefinition("dataset", PortDirection.INPUT, PLAID_SAMPLE, "PLAID_SAMPLE"),
107
+ PortDefinition("infos", PortDirection.INPUT, PLAID_INFO, "PLAID_INFO"),
108
+ PortDefinition("sample", PortDirection.OUTPUT, PLAID_SAMPLE, "Sample"),
109
+ ),
110
+ executor=_extract_plaid_sample,
111
+ parameters=(
112
+ ParameterDefinition("split", ParameterKind.TEXT, "Split", "", "train"),
113
+ ParameterDefinition("sample_number", ParameterKind.INTEGER, "Sample number", 0, "0"),
114
+ ),
115
+ )
116
+
117
+
118
+ def _extract_plaid_time_step(
119
+ inputs: Mapping[str, Any], parameters: Mapping[str, Any]
120
+ ) -> Mapping[str, Any]:
121
+ """Extract one Plaid sample time step as a FlowGraph mesh document."""
122
+ sample = inputs.get("sample")
123
+ time = parameters.get("time", 0.0)
124
+
125
+ if sample is None:
126
+ raise ValueError("The PLAID_SAMPLE input is required")
127
+ if isinstance(time, bool) or not isinstance(time, (int, float)):
128
+ raise TypeError("The Plaid time step must be numeric")
129
+
130
+ try:
131
+ from Muscat.Bridges.CGNSBridge import CGNSToMesh
132
+
133
+ tree = sample.get_tree(time=float(time), only_mesh=False)
134
+ if tree is None:
135
+ raise ValueError(f"Plaid sample has no mesh at time step {time}")
136
+ mesh = CGNSToMesh(tree)
137
+ return {"mesh": MeshDocument(mesh=mesh)}
138
+ except (AttributeError, TypeError, ValueError, IndexError, KeyError) as error:
139
+ raise ValueError(f"Could not extract Plaid time step {time}: {error}") from error
140
+
141
+
142
+ EXTRACT_PLAID_TIME_STEP = NodeDefinition(
143
+ id="extract-plaid-time-step",
144
+ icon="mdi-calendar-clock-outline",
145
+ label="Extract Plaid time step",
146
+ description="Converts one Plaid sample time step into a FlowGraph mesh document.",
147
+ ports=(
148
+ PortDefinition("sample", PortDirection.INPUT, PLAID_SAMPLE, "PLAID_SAMPLE"),
149
+ PortDefinition("mesh", PortDirection.OUTPUT, MESH_DOCUMENT, "Mesh"),
150
+ ),
151
+ executor=_extract_plaid_time_step,
152
+ parameters=(ParameterDefinition("time", ParameterKind.FLOAT, "Time step", 0.0, "0.0"),),
153
+ )
154
+
155
+
156
+ def _extract_plaid_info(
157
+ inputs: Mapping[str, Any], _parameters: Mapping[str, Any]
158
+ ) -> Mapping[str, Any]:
159
+ """Expose each top-level attribute of a Plaid ``Infos`` object."""
160
+ infos = inputs.get("infos")
161
+ if infos is None:
162
+ raise ValueError("The PLAID_INFO input is required")
163
+
164
+ return {
165
+ "owner": getattr(infos, "owner", None),
166
+ "license": getattr(infos, "license", None),
167
+ "data_production": getattr(infos, "data_production", None),
168
+ "data_description": getattr(infos, "data_description", None),
169
+ "num_samples": getattr(infos, "num_samples", None),
170
+ "storage_backend": getattr(infos, "storage_backend", None),
171
+ }
172
+
173
+
174
+ EXTRACT_PLAID_INFO = NodeDefinition(
175
+ id="extract-plaid-info",
176
+ icon="mdi-information-outline",
177
+ label="Extract Plaid info",
178
+ description="Exposes the top-level attributes of a Plaid dataset info object.",
179
+ ports=(
180
+ PortDefinition("infos", PortDirection.INPUT, PLAID_INFO, "PLAID_INFO"),
181
+ PortDefinition("owner", PortDirection.OUTPUT, PLAID_OPTIONAL_STRING, "Owner"),
182
+ PortDefinition("license", PortDirection.OUTPUT, PLAID_OPTIONAL_STRING, "License"),
183
+ PortDefinition(
184
+ "data_production", PortDirection.OUTPUT, PLAID_DATA_PRODUCTION, "Data production"
185
+ ),
186
+ PortDefinition(
187
+ "data_description", PortDirection.OUTPUT, PLAID_OPTIONAL_STRING, "Data description"
188
+ ),
189
+ PortDefinition(
190
+ "num_samples", PortDirection.OUTPUT, PLAID_OPTIONAL_TABLE, "Number of samples"
191
+ ),
192
+ PortDefinition(
193
+ "storage_backend", PortDirection.OUTPUT, PLAID_OPTIONAL_STRING, "Storage backend"
194
+ ),
195
+ ),
196
+ executor=_extract_plaid_info,
197
+ )
198
+
199
+
200
+ AVAILABLE_NODES = (
201
+ LOAD_PLAID_DATASET,
202
+ EXTRACT_PLAID_SAMPLE,
203
+ EXTRACT_PLAID_TIME_STEP,
204
+ EXTRACT_PLAID_INFO,
205
+ )