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.
- flowgraph/__init__.py +9 -0
- flowgraph/__main__.py +6 -0
- flowgraph/adapters/__init__.py +141 -0
- flowgraph/adapters/controls.py +96 -0
- flowgraph/adapters/cosapp.py +195 -0
- flowgraph/adapters/data_types.py +59 -0
- flowgraph/adapters/documentation.py +45 -0
- flowgraph/adapters/field_ops.py +121 -0
- flowgraph/adapters/file_conversion.py +393 -0
- flowgraph/adapters/files.py +191 -0
- flowgraph/adapters/filters.py +173 -0
- flowgraph/adapters/image_tools.py +394 -0
- flowgraph/adapters/mesh_creation_tools.py +431 -0
- flowgraph/adapters/mesh_generations.py +58 -0
- flowgraph/adapters/mesh_ops.py +362 -0
- flowgraph/adapters/paraview.py +39 -0
- flowgraph/adapters/plaid_bridge.py +205 -0
- flowgraph/adapters/readers.py +215 -0
- flowgraph/adapters/remote_files.py +118 -0
- flowgraph/adapters/simple_sources.py +199 -0
- flowgraph/adapters/sinks.py +80 -0
- flowgraph/adapters/user_code.py +196 -0
- flowgraph/adapters/workflow.py +176 -0
- flowgraph/adapters/workflow_interfaces.py +78 -0
- flowgraph/adapters/writers.py +159 -0
- flowgraph/application/__init__.py +1 -0
- flowgraph/application/node_registry.py +14 -0
- flowgraph/application/port_conversions.py +63 -0
- flowgraph/application/wasm_execution.py +132 -0
- flowgraph/application/workflow_cli.py +417 -0
- flowgraph/application/workflow_core.py +1193 -0
- flowgraph/application/workflow_history.py +93 -0
- flowgraph/application/workflow_io.py +393 -0
- flowgraph/application/workflow_layout.py +170 -0
- flowgraph/application/workflow_web_catalog.py +143 -0
- flowgraph/application/workflows.py +7 -0
- flowgraph/core_cli.py +81 -0
- flowgraph/domain/__init__.py +5 -0
- flowgraph/domain/mesh_document.py +91 -0
- flowgraph/examples/AllFilters.py +19 -0
- flowgraph/examples/convert_format.py +21 -0
- flowgraph/resources.py +29 -0
- flowgraph/testdata/AllFilters.json +390 -0
- flowgraph/testdata/FlowGraph.h5 +0 -0
- flowgraph/testdata/FlowGraph.stl +0 -0
- flowgraph/testdata/FlowGraph.xdmf +22 -0
- flowgraph/testdata/SimpleReadMesh.json +70 -0
- flowgraph/testdata/SpherePoints.csv +83 -0
- flowgraph/testdata/muscat-muscat-mesh-conversion.json +101 -0
- flowgraph/version.py +3 -0
- flowgraph-0.1.2.dist-info/METADATA +150 -0
- flowgraph-0.1.2.dist-info/RECORD +56 -0
- flowgraph-0.1.2.dist-info/WHEEL +5 -0
- flowgraph-0.1.2.dist-info/entry_points.txt +2 -0
- flowgraph-0.1.2.dist-info/licenses/LICENSE +29 -0
- 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
|
+
)
|