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
flowgraph/__init__.py
ADDED
flowgraph/__main__.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Grouped catalog of workflow adapters exposed to application composition."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from flowgraph.adapters.controls import AVAILABLE_NODES as CONTROL_AVAILABLE_NODES
|
|
6
|
+
from flowgraph.adapters.cosapp import AVAILABLE_NODES as COSAPP_AVAILABLE_NODES
|
|
7
|
+
from flowgraph.adapters.documentation import AVAILABLE_NODES as DOCUMENTATION_AVAILABLE_NODES
|
|
8
|
+
from flowgraph.adapters.field_ops import AVAILABLE_NODES as FIELD_OPS_AVAILABLE_NODES
|
|
9
|
+
from flowgraph.adapters.file_conversion import MESH_FILE_CONVERSION
|
|
10
|
+
from flowgraph.adapters.files import (
|
|
11
|
+
READ_DIRECTORY_FILES,
|
|
12
|
+
SELECT_FILE,
|
|
13
|
+
SELECT_SERVER_FILE,
|
|
14
|
+
)
|
|
15
|
+
from flowgraph.adapters.filters import (
|
|
16
|
+
CREATE_MUSCAT_ELEMENT_FILTER,
|
|
17
|
+
FILTER_MESH_WITH_MUSCAT,
|
|
18
|
+
)
|
|
19
|
+
from flowgraph.adapters.image_tools import AVAILABLE_NODES as IMAGE_TOOLS_AVAILABLE_NODES
|
|
20
|
+
from flowgraph.adapters.mesh_creation_tools import AVAILABLE_NODES as MESH_CREATION_AVAILABLE_NODES
|
|
21
|
+
from flowgraph.adapters.mesh_generations import CSV_TO_MESH
|
|
22
|
+
from flowgraph.adapters.mesh_ops import AVAILABLE_NODES as MESH_OPS_AVAILABLE_NODES
|
|
23
|
+
from flowgraph.adapters.plaid_bridge import AVAILABLE_NODES as PLAID_AVAILABLE_NODES
|
|
24
|
+
from flowgraph.adapters.readers import (
|
|
25
|
+
LOAD_CSV,
|
|
26
|
+
LOAD_MESHIO,
|
|
27
|
+
LOAD_MESHLANE,
|
|
28
|
+
LOAD_MUSCAT,
|
|
29
|
+
)
|
|
30
|
+
from flowgraph.adapters.remote_files import DOWNLOAD_URL
|
|
31
|
+
from flowgraph.adapters.simple_sources import (
|
|
32
|
+
SET_FLOAT,
|
|
33
|
+
SET_INT,
|
|
34
|
+
SET_LIST_FLOAT,
|
|
35
|
+
SET_LIST_INT,
|
|
36
|
+
SET_LIST_STR,
|
|
37
|
+
SET_STRING,
|
|
38
|
+
SET_VEC3D,
|
|
39
|
+
)
|
|
40
|
+
from flowgraph.adapters.sinks import AVAILABLE_NODES as SINKS_AVAILABLE_NODES
|
|
41
|
+
from flowgraph.adapters.user_code import AVAILABLE_NODES as USER_CODE_AVAILABLE_NODES
|
|
42
|
+
from flowgraph.adapters.workflow import AVAILABLE_NODES as WORKFLOW_AVAILABLE_NODES
|
|
43
|
+
from flowgraph.adapters.workflow_interfaces import (
|
|
44
|
+
AVAILABLE_NODES as WORKFLOW_INTERFACE_AVAILABLE_NODES,
|
|
45
|
+
)
|
|
46
|
+
from flowgraph.adapters.writers import WRITE_MESHIO, WRITE_MESHLANE, WRITE_MUSCAT
|
|
47
|
+
from flowgraph.application.workflow_core import NodeDefinition
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class AdapterGroup:
|
|
52
|
+
"""A named, ordered group of workflow nodes presented in one UI menu."""
|
|
53
|
+
|
|
54
|
+
label: str
|
|
55
|
+
node_definitions: tuple[NodeDefinition, ...] = ()
|
|
56
|
+
subgroups: tuple["AdapterGroup", ...] = ()
|
|
57
|
+
|
|
58
|
+
def all_node_definitions(self) -> tuple[NodeDefinition, ...]:
|
|
59
|
+
"""Return definitions in this group and all nested groups."""
|
|
60
|
+
return self.node_definitions + tuple(
|
|
61
|
+
definition
|
|
62
|
+
for subgroup in self.subgroups
|
|
63
|
+
for definition in subgroup.all_node_definitions()
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class AdapterCatalog:
|
|
69
|
+
"""Single application-facing catalog for adapter nodes and their UI metadata."""
|
|
70
|
+
|
|
71
|
+
groups: tuple[AdapterGroup, ...]
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def node_definitions(self) -> tuple[NodeDefinition, ...]:
|
|
75
|
+
"""Return all adapter definitions in group and menu order."""
|
|
76
|
+
return tuple(
|
|
77
|
+
definition for group in self.groups for definition in group.all_node_definitions()
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def require(self, definition_id: str) -> NodeDefinition:
|
|
81
|
+
"""Return an adapter definition by ID or raise a descriptive key error."""
|
|
82
|
+
definition = next(
|
|
83
|
+
(candidate for candidate in self.node_definitions if candidate.id == definition_id),
|
|
84
|
+
None,
|
|
85
|
+
)
|
|
86
|
+
if definition is None:
|
|
87
|
+
raise KeyError(f"Unknown adapter node definition: {definition_id!r}")
|
|
88
|
+
return definition
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
ADAPTERS = AdapterCatalog(
|
|
92
|
+
groups=(
|
|
93
|
+
AdapterGroup(
|
|
94
|
+
"Inputs",
|
|
95
|
+
subgroups=(
|
|
96
|
+
AdapterGroup("Scalars", (SET_STRING, SET_INT, SET_FLOAT)),
|
|
97
|
+
AdapterGroup(
|
|
98
|
+
"Lists/Vectors", (SET_LIST_STR, SET_LIST_INT, SET_LIST_FLOAT, SET_VEC3D)
|
|
99
|
+
),
|
|
100
|
+
AdapterGroup(
|
|
101
|
+
"Files",
|
|
102
|
+
(SELECT_FILE, SELECT_SERVER_FILE, DOWNLOAD_URL, READ_DIRECTORY_FILES),
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
AdapterGroup(
|
|
107
|
+
"Mesh Tools",
|
|
108
|
+
subgroups=(
|
|
109
|
+
AdapterGroup(
|
|
110
|
+
"Readers",
|
|
111
|
+
(LOAD_MUSCAT, LOAD_MESHIO, LOAD_MESHLANE, LOAD_CSV),
|
|
112
|
+
),
|
|
113
|
+
AdapterGroup("Mesh Gens", (CSV_TO_MESH,)),
|
|
114
|
+
AdapterGroup("Mesh Creation", MESH_CREATION_AVAILABLE_NODES),
|
|
115
|
+
AdapterGroup(
|
|
116
|
+
"Filters",
|
|
117
|
+
(CREATE_MUSCAT_ELEMENT_FILTER, FILTER_MESH_WITH_MUSCAT),
|
|
118
|
+
),
|
|
119
|
+
AdapterGroup("Mesh Ops", MESH_OPS_AVAILABLE_NODES),
|
|
120
|
+
AdapterGroup("Field Ops", FIELD_OPS_AVAILABLE_NODES),
|
|
121
|
+
AdapterGroup("File Conversion", (MESH_FILE_CONVERSION,)),
|
|
122
|
+
AdapterGroup("Writers", (WRITE_MUSCAT, WRITE_MESHLANE, WRITE_MESHIO)),
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
AdapterGroup("Controls", CONTROL_AVAILABLE_NODES),
|
|
126
|
+
AdapterGroup("Image Tools", IMAGE_TOOLS_AVAILABLE_NODES),
|
|
127
|
+
AdapterGroup("Code", USER_CODE_AVAILABLE_NODES),
|
|
128
|
+
AdapterGroup("Workflow", WORKFLOW_INTERFACE_AVAILABLE_NODES + WORKFLOW_AVAILABLE_NODES),
|
|
129
|
+
AdapterGroup("Sinks", SINKS_AVAILABLE_NODES),
|
|
130
|
+
AdapterGroup("Doc", DOCUMENTATION_AVAILABLE_NODES),
|
|
131
|
+
AdapterGroup(
|
|
132
|
+
"External Tools",
|
|
133
|
+
subgroups=(
|
|
134
|
+
AdapterGroup("CoSApp", COSAPP_AVAILABLE_NODES),
|
|
135
|
+
AdapterGroup("Plaid", PLAID_AVAILABLE_NODES),
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
__all__ = ["ADAPTERS"]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Headless workflow nodes whose values are presented with GUI controls."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from flowgraph.application.workflow_core import (
|
|
8
|
+
NodeDefinition,
|
|
9
|
+
ParameterDefinition,
|
|
10
|
+
PortDefinition,
|
|
11
|
+
PortDirection,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .data_types import FLOAT, INTEGER, ParameterKind
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _slider_bounds(
|
|
18
|
+
parameters: Mapping[str, Any], *, integer: bool
|
|
19
|
+
) -> tuple[int | float, int | float, int | float]:
|
|
20
|
+
"""Validate and return a slider's minimum, maximum, and current value."""
|
|
21
|
+
minimum = parameters.get("min")
|
|
22
|
+
maximum = parameters.get("max")
|
|
23
|
+
value = parameters.get("value")
|
|
24
|
+
expected_type = int if integer else (int, float)
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
isinstance(minimum, bool)
|
|
28
|
+
or not isinstance(minimum, expected_type)
|
|
29
|
+
or isinstance(maximum, bool)
|
|
30
|
+
or not isinstance(maximum, expected_type)
|
|
31
|
+
or isinstance(value, bool)
|
|
32
|
+
or not isinstance(value, expected_type)
|
|
33
|
+
):
|
|
34
|
+
value_type = "integer" if integer else "number"
|
|
35
|
+
raise TypeError(f"The slider min, max, and value must be {value_type}s")
|
|
36
|
+
|
|
37
|
+
if integer:
|
|
38
|
+
if minimum >= maximum:
|
|
39
|
+
raise ValueError("The slider minimum must be less than its maximum")
|
|
40
|
+
else:
|
|
41
|
+
minimum = float(minimum)
|
|
42
|
+
maximum = float(maximum)
|
|
43
|
+
value = float(value)
|
|
44
|
+
if not all(math.isfinite(item) for item in (minimum, maximum, value)):
|
|
45
|
+
raise ValueError("The slider min, max, and value must be finite")
|
|
46
|
+
if minimum >= maximum:
|
|
47
|
+
raise ValueError("The slider minimum must be less than its maximum")
|
|
48
|
+
|
|
49
|
+
if not minimum <= value <= maximum:
|
|
50
|
+
raise ValueError("The slider value must be between its minimum and maximum")
|
|
51
|
+
return minimum, maximum, value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _float_slider(_inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
55
|
+
"""Output the current finite floating-point slider value."""
|
|
56
|
+
return {"value": _slider_bounds(parameters, integer=False)[2]}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
FLOAT_SLIDER = NodeDefinition(
|
|
60
|
+
id="float-slider",
|
|
61
|
+
icon="mdi-tune-vertical",
|
|
62
|
+
label="Float Slider",
|
|
63
|
+
description="Outputs a floating-point value selected with a slider.",
|
|
64
|
+
ports=(PortDefinition("value", PortDirection.OUTPUT, FLOAT, "Value"),),
|
|
65
|
+
executor=_float_slider,
|
|
66
|
+
presentation="slider",
|
|
67
|
+
parameters=(
|
|
68
|
+
ParameterDefinition("min", ParameterKind.FLOAT, "Minimum", 0.0, "0.0"),
|
|
69
|
+
ParameterDefinition("max", ParameterKind.FLOAT, "Maximum", 1.0, "1.0"),
|
|
70
|
+
ParameterDefinition("value", ParameterKind.FLOAT, "Value", 0.5, "0.5", port=False),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _int_slider(_inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
76
|
+
"""Output the current integer slider value."""
|
|
77
|
+
return {"value": _slider_bounds(parameters, integer=True)[2]}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
INT_SLIDER = NodeDefinition(
|
|
81
|
+
id="int-slider",
|
|
82
|
+
icon="mdi-tune-vertical",
|
|
83
|
+
label="Integer Slider",
|
|
84
|
+
description="Outputs an integer value selected with a slider.",
|
|
85
|
+
ports=(PortDefinition("value", PortDirection.OUTPUT, INTEGER, "Value"),),
|
|
86
|
+
executor=_int_slider,
|
|
87
|
+
presentation="slider",
|
|
88
|
+
parameters=(
|
|
89
|
+
ParameterDefinition("min", ParameterKind.INTEGER, "Minimum", 0, "0"),
|
|
90
|
+
ParameterDefinition("max", ParameterKind.INTEGER, "Maximum", 100, "100"),
|
|
91
|
+
ParameterDefinition("value", ParameterKind.INTEGER, "Value", 50, "50", port=False),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
AVAILABLE_NODES = (FLOAT_SLIDER, INT_SLIDER)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""CoSApp workflow adapter for executing external trusted Python models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from flowgraph.application.workflow_core import (
|
|
12
|
+
NodeDefinition,
|
|
13
|
+
ParameterDefinition,
|
|
14
|
+
PortDefinition,
|
|
15
|
+
PortDirection,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .data_types import ANY, FILE, STRING
|
|
19
|
+
|
|
20
|
+
COSAPP_RESULT_PORT = PortDefinition("result", PortDirection.OUTPUT, ANY, "CoSApp system")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _path_mapping(value: object, parameter_name: str) -> dict[str, str]:
|
|
24
|
+
"""Parse one JSON mapping of FlowGraph port names to CoSApp attribute paths."""
|
|
25
|
+
if not isinstance(value, str):
|
|
26
|
+
raise TypeError(f"The {parameter_name} parameter must be a JSON object")
|
|
27
|
+
try:
|
|
28
|
+
parsed = json.loads(value)
|
|
29
|
+
except json.JSONDecodeError as error:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"The {parameter_name} parameter must be valid JSON: {error.msg}"
|
|
32
|
+
) from error
|
|
33
|
+
if not isinstance(parsed, dict):
|
|
34
|
+
raise TypeError(f"The {parameter_name} parameter must be a JSON object")
|
|
35
|
+
|
|
36
|
+
paths: dict[str, str] = {}
|
|
37
|
+
for port_name, attribute_path in parsed.items():
|
|
38
|
+
if not isinstance(port_name, str) or not port_name.strip():
|
|
39
|
+
raise ValueError(f"The {parameter_name} mapping contains an empty port name")
|
|
40
|
+
if not isinstance(attribute_path, str) or not attribute_path.strip():
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"The {parameter_name} mapping for {port_name!r} must contain a non-empty path"
|
|
43
|
+
)
|
|
44
|
+
if any(not segment.isidentifier() for segment in attribute_path.split(".")):
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"The {parameter_name} path for {port_name!r} must be dot-separated identifiers"
|
|
47
|
+
)
|
|
48
|
+
paths[port_name] = attribute_path
|
|
49
|
+
return paths
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _ports(parameters: Mapping[str, Any]) -> tuple[PortDefinition, ...]:
|
|
53
|
+
"""Return ports declared by the node's persisted CoSApp interface mappings."""
|
|
54
|
+
input_paths = _path_mapping(parameters.get("inputs", "{}"), "inputs")
|
|
55
|
+
output_paths = _path_mapping(parameters.get("outputs", "{}"), "outputs")
|
|
56
|
+
_validate_output_paths(output_paths)
|
|
57
|
+
return (
|
|
58
|
+
*(PortDefinition(name, PortDirection.INPUT, ANY, name) for name in input_paths),
|
|
59
|
+
COSAPP_RESULT_PORT,
|
|
60
|
+
*(PortDefinition(name, PortDirection.OUTPUT, ANY, name) for name in output_paths),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _validate_output_paths(output_paths: Mapping[str, str]) -> None:
|
|
65
|
+
"""Reject output names that conflict with the permanent CoSApp result port."""
|
|
66
|
+
if "result" in output_paths:
|
|
67
|
+
raise ValueError("The outputs mapping cannot use reserved output name 'result'")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _load_model(module_path: object, factory_name: object) -> Any:
|
|
71
|
+
"""Load and construct a root CoSApp ``System`` from a trusted Python file."""
|
|
72
|
+
if not isinstance(module_path, str) or not module_path.strip():
|
|
73
|
+
raise ValueError("The CoSApp model file must be a non-empty Python file path")
|
|
74
|
+
if not isinstance(factory_name, str) or not factory_name.strip():
|
|
75
|
+
raise ValueError("The CoSApp factory name must be a non-empty attribute name")
|
|
76
|
+
|
|
77
|
+
path = Path(module_path).expanduser()
|
|
78
|
+
if not path.is_file():
|
|
79
|
+
raise FileNotFoundError(f"CoSApp model file does not exist: {path}")
|
|
80
|
+
specification = importlib.util.spec_from_file_location(
|
|
81
|
+
f"flowgraph_cosapp_model_{abs(hash(path.resolve()))}", path
|
|
82
|
+
)
|
|
83
|
+
if specification is None or specification.loader is None:
|
|
84
|
+
raise ImportError(f"Could not load the CoSApp model file: {path}")
|
|
85
|
+
module = importlib.util.module_from_spec(specification)
|
|
86
|
+
specification.loader.exec_module(module)
|
|
87
|
+
|
|
88
|
+
factory = getattr(module, factory_name, None)
|
|
89
|
+
if not callable(factory):
|
|
90
|
+
raise TypeError(f"CoSApp model file must define callable {factory_name!r}")
|
|
91
|
+
model = factory()
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
from cosapp.base import System
|
|
95
|
+
except ImportError as error:
|
|
96
|
+
raise ImportError(
|
|
97
|
+
"CoSApp is required to execute this node. Install FlowGraph with its 'cosapp' extra."
|
|
98
|
+
) from error
|
|
99
|
+
if not isinstance(model, System):
|
|
100
|
+
raise TypeError(f"CoSApp factory {factory_name!r} must return a cosapp.base.System")
|
|
101
|
+
return model
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _resolve_parent(model: object, path: str) -> tuple[object, str]:
|
|
105
|
+
"""Return the object and final attribute addressed by a declared CoSApp path."""
|
|
106
|
+
parent = model
|
|
107
|
+
*parents, name = path.split(".")
|
|
108
|
+
for segment in parents:
|
|
109
|
+
try:
|
|
110
|
+
parent = getattr(parent, segment)
|
|
111
|
+
except AttributeError as error:
|
|
112
|
+
raise ValueError(f"CoSApp path {path!r} cannot resolve {segment!r}") from error
|
|
113
|
+
return parent, name
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _set_path(model: object, path: str, value: Any) -> None:
|
|
117
|
+
"""Set a declared CoSApp input attribute path."""
|
|
118
|
+
parent, name = _resolve_parent(model, path)
|
|
119
|
+
if not hasattr(parent, name):
|
|
120
|
+
raise ValueError(f"CoSApp input path {path!r} does not exist")
|
|
121
|
+
setattr(parent, name, value)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _get_path(model: object, path: str) -> Any:
|
|
125
|
+
"""Read a declared CoSApp output attribute path."""
|
|
126
|
+
parent, name = _resolve_parent(model, path)
|
|
127
|
+
try:
|
|
128
|
+
return getattr(parent, name)
|
|
129
|
+
except AttributeError as error:
|
|
130
|
+
raise ValueError(f"CoSApp output path {path!r} does not exist") from error
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _execute_cosapp(inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
134
|
+
"""Construct, configure, execute, and export one CoSApp model instance."""
|
|
135
|
+
input_paths = _path_mapping(parameters.get("inputs", "{}"), "inputs")
|
|
136
|
+
output_paths = _path_mapping(parameters.get("outputs", "{}"), "outputs")
|
|
137
|
+
_validate_output_paths(output_paths)
|
|
138
|
+
model = _load_model(parameters.get("model_file"), parameters.get("factory"))
|
|
139
|
+
for port_name, path in input_paths.items():
|
|
140
|
+
_set_path(model, path, inputs[port_name])
|
|
141
|
+
model.run_drivers()
|
|
142
|
+
return {
|
|
143
|
+
"result": model,
|
|
144
|
+
**{name: _get_path(model, path) for name, path in output_paths.items()},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
COSAPP_WORKFLOW = NodeDefinition(
|
|
149
|
+
id="cosapp-workflow",
|
|
150
|
+
icon="mdi-play-network-outline",
|
|
151
|
+
label="CoSApp Workflow",
|
|
152
|
+
description="Executes a trusted external CoSApp System and exports declared result paths.",
|
|
153
|
+
ports=(COSAPP_RESULT_PORT,),
|
|
154
|
+
executor=_execute_cosapp,
|
|
155
|
+
parameters=(
|
|
156
|
+
ParameterDefinition(
|
|
157
|
+
"model_file",
|
|
158
|
+
FILE,
|
|
159
|
+
"CoSApp model file",
|
|
160
|
+
"",
|
|
161
|
+
"Path to a trusted Python file defining the CoSApp model",
|
|
162
|
+
file_patterns=("*.py",),
|
|
163
|
+
port=False,
|
|
164
|
+
),
|
|
165
|
+
ParameterDefinition(
|
|
166
|
+
"factory",
|
|
167
|
+
STRING,
|
|
168
|
+
"Model factory",
|
|
169
|
+
"create_model",
|
|
170
|
+
"Zero-argument callable returning the root CoSApp System",
|
|
171
|
+
port=False,
|
|
172
|
+
),
|
|
173
|
+
ParameterDefinition(
|
|
174
|
+
"inputs",
|
|
175
|
+
STRING,
|
|
176
|
+
"Input paths",
|
|
177
|
+
"{}",
|
|
178
|
+
'JSON mapping of FlowGraph input names to CoSApp paths, e.g. {"current": "source.I"}',
|
|
179
|
+
port=False,
|
|
180
|
+
),
|
|
181
|
+
ParameterDefinition(
|
|
182
|
+
"outputs",
|
|
183
|
+
STRING,
|
|
184
|
+
"Output paths",
|
|
185
|
+
"{}",
|
|
186
|
+
'JSON mapping of FlowGraph output names to CoSApp paths, e.g. {"voltage": "circuit.n1.V"}',
|
|
187
|
+
port=False,
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
port_resolver=_ports,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
AVAILABLE_NODES = (COSAPP_WORKFLOW,)
|
|
194
|
+
|
|
195
|
+
__all__ = ["AVAILABLE_NODES", "COSAPP_WORKFLOW"]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from Muscat.LinAlg.Transform import Transform
|
|
2
|
+
from Muscat.MeshContainers.Filters.FilterObjects import ElementFilter
|
|
3
|
+
from Muscat.MeshContainers.Filters.FilterOperators import FilterOperatorBase
|
|
4
|
+
from PIL.Image import Image
|
|
5
|
+
|
|
6
|
+
from flowgraph.application.workflow_core import DataType
|
|
7
|
+
from flowgraph.domain.mesh_document import MeshDocument
|
|
8
|
+
|
|
9
|
+
ANY = DataType("any", "Any", object)
|
|
10
|
+
STRING = DataType("string", "Text", str)
|
|
11
|
+
STR_SELECT = DataType("str", "select str", str)
|
|
12
|
+
BOOLEAN = DataType("boolean", "Boolean", bool)
|
|
13
|
+
INTEGER = DataType("integer", "Integer", int)
|
|
14
|
+
FLOAT = DataType("float", "Float", float)
|
|
15
|
+
LIST_STR = DataType("list[str]", "List Text", list[str])
|
|
16
|
+
LIST_INT = DataType("list[int]", "List Ints", list[int])
|
|
17
|
+
LIST_FLOAT = DataType("list[float]", "List Floats", list[float])
|
|
18
|
+
LIST_ANY = DataType("list[any]", "List Any", list)
|
|
19
|
+
FILE = DataType("filename", "Path", str)
|
|
20
|
+
|
|
21
|
+
MESH_DOCUMENT = DataType("mesh-document", "Mesh document", MeshDocument)
|
|
22
|
+
TABLE_DOCUMENT = DataType("table-document", "Table", dict)
|
|
23
|
+
IMAGE = DataType("image", "Image", Image)
|
|
24
|
+
MUSCAT_ELEMENT_FILTER = DataType(
|
|
25
|
+
"filter-like",
|
|
26
|
+
"Selector",
|
|
27
|
+
(ElementFilter, FilterOperatorBase),
|
|
28
|
+
)
|
|
29
|
+
VEC3D = DataType("VEC3D", "3D Vector", list[float])
|
|
30
|
+
VEC2D = DataType("VEC2D", "2D Vector", list[float])
|
|
31
|
+
VEC3DSTR = DataType("VEC3DSTR", "3D Text Vector", list[str])
|
|
32
|
+
|
|
33
|
+
VEC3DI = DataType("VEC3D", "3D Vector", list[int])
|
|
34
|
+
VEC2DI = DataType("VEC2D", "2D Vector", list[int])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
MULTI_SELECT = DataType("list[bool]", "List bool", list[bool])
|
|
38
|
+
SELECT_SERVER_FILENAME = DataType("ServerFile", "Text", str)
|
|
39
|
+
|
|
40
|
+
TRANSFORM_T = DataType("Transform", "Transform", Transform)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ParameterKind:
|
|
44
|
+
"""UI-neutral editor semantics for a persisted node parameter."""
|
|
45
|
+
|
|
46
|
+
TEXT = STRING
|
|
47
|
+
STR_SELECT = STR_SELECT
|
|
48
|
+
BOOLEAN = BOOLEAN
|
|
49
|
+
INTEGER = INTEGER
|
|
50
|
+
FLOAT = FLOAT
|
|
51
|
+
LIST_STR = LIST_STR
|
|
52
|
+
LIST_INT = LIST_INT
|
|
53
|
+
LIST_FLOAT = LIST_FLOAT
|
|
54
|
+
FILE = FILE
|
|
55
|
+
VEC3D = VEC3D
|
|
56
|
+
VEC2D = VEC2D
|
|
57
|
+
VEC3DSTR = VEC3DSTR
|
|
58
|
+
MULTI_SELECT = MULTI_SELECT
|
|
59
|
+
SELECT_SERVER_FILENAME = SELECT_SERVER_FILENAME
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Nodes for documentation and grouping ."""
|
|
2
|
+
|
|
3
|
+
from flowgraph.adapters.data_types import STRING
|
|
4
|
+
from flowgraph.application.workflow_core import NodeDefinition, ParameterDefinition
|
|
5
|
+
|
|
6
|
+
DOC_TITLE = NodeDefinition(
|
|
7
|
+
id="title-doc",
|
|
8
|
+
icon="",
|
|
9
|
+
label="Title",
|
|
10
|
+
description="Just a Title",
|
|
11
|
+
ports=(),
|
|
12
|
+
executor=lambda x, y: None,
|
|
13
|
+
parameters=(
|
|
14
|
+
ParameterDefinition(
|
|
15
|
+
name="title",
|
|
16
|
+
kind=STRING,
|
|
17
|
+
label="Title",
|
|
18
|
+
default="Title",
|
|
19
|
+
port=False,
|
|
20
|
+
),
|
|
21
|
+
),
|
|
22
|
+
presentation="title",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
DOC_PARAGRAPH = NodeDefinition(
|
|
26
|
+
id="paragraph-doc",
|
|
27
|
+
icon="",
|
|
28
|
+
label="Paragraph",
|
|
29
|
+
description="Long-form documentation text",
|
|
30
|
+
ports=(),
|
|
31
|
+
executor=lambda x, y: None,
|
|
32
|
+
parameters=(
|
|
33
|
+
ParameterDefinition(
|
|
34
|
+
name="text",
|
|
35
|
+
kind=STRING,
|
|
36
|
+
label="Text",
|
|
37
|
+
default="Write documentation here.",
|
|
38
|
+
port=False,
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
presentation="paragraph",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
AVAILABLE_NODES = (DOC_TITLE, DOC_PARAGRAPH)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Workflow nodes for transferring fields between meshes."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from Muscat.FE.DofNumbering import ComputeDofNumbering
|
|
8
|
+
from Muscat.FE.Fields.FEField import FEField
|
|
9
|
+
from Muscat.FE.Spaces.FESpaces import LagrangeSpaceP0
|
|
10
|
+
from Muscat.MeshContainers.Filters.FilterObjects import ElementFilter
|
|
11
|
+
from Muscat.MeshContainers.Mesh import Mesh
|
|
12
|
+
from Muscat.MeshTools.MeshFieldOperations import GetFieldTransferOp
|
|
13
|
+
from Muscat.MeshTools.MeshTools import GetElementsCenters
|
|
14
|
+
|
|
15
|
+
from flowgraph.adapters.data_types import MESH_DOCUMENT
|
|
16
|
+
from flowgraph.application.workflow_core import (
|
|
17
|
+
NodeDefinition,
|
|
18
|
+
ParameterDefinition,
|
|
19
|
+
PortDefinition,
|
|
20
|
+
PortDirection,
|
|
21
|
+
)
|
|
22
|
+
from flowgraph.domain.mesh_document import MeshDocument
|
|
23
|
+
|
|
24
|
+
from .data_types import ParameterKind
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _transfer_fields(
|
|
28
|
+
inputs: Mapping[str, Any], _parameters: Mapping[str, Any]
|
|
29
|
+
) -> Mapping[str, Any]:
|
|
30
|
+
"""Transfer all nodal fields from ``source_data`` onto ``target``."""
|
|
31
|
+
source_mesh: Mesh = inputs["source_data"].mesh
|
|
32
|
+
target_mesh: Mesh = inputs["target"].mesh
|
|
33
|
+
outpout_mesh = target_mesh.View()
|
|
34
|
+
|
|
35
|
+
transfer_nodes = _parameters.get("transferNodeData", True)
|
|
36
|
+
transfer_elements = _parameters.get("transferElementData", True)
|
|
37
|
+
transfer_nodes_tags = _parameters.get("transferNodeTags", True)
|
|
38
|
+
transfer_elements_tags = _parameters.get("transferElementTags", True)
|
|
39
|
+
|
|
40
|
+
if transfer_nodes or transfer_nodes_tags:
|
|
41
|
+
field = FEField(name="source", mesh=source_mesh)
|
|
42
|
+
operator_nodal, _, _ = GetFieldTransferOp(field, target_mesh.nodes)
|
|
43
|
+
if transfer_nodes and source_mesh.nodeFields:
|
|
44
|
+
for name, data in source_mesh.nodeFields.items():
|
|
45
|
+
if name == "flowgraph_node_id" or np.issubdtype(data.dtype, np.str_):
|
|
46
|
+
continue
|
|
47
|
+
outpout_mesh.nodeFields[name] = np.ascontiguousarray(
|
|
48
|
+
(operator_nodal @ data).astype(data.dtype, copy=False)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if transfer_nodes_tags:
|
|
52
|
+
mask = np.zeros(source_mesh.GetNumberOfNodes())
|
|
53
|
+
for tag in source_mesh.nodesTags:
|
|
54
|
+
mask.fill(0)
|
|
55
|
+
ids = tag.GetIds()
|
|
56
|
+
mask[ids] = 1
|
|
57
|
+
|
|
58
|
+
outpout_mesh.nodesTags.CreateTag(tag.name, False).SetIds(
|
|
59
|
+
np.where(operator_nodal.dot(mask) >= 0.999999999)[0], tag.name
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if transfer_elements or transfer_elements_tags:
|
|
63
|
+
numbering = ComputeDofNumbering(source_mesh, LagrangeSpaceP0)
|
|
64
|
+
field = FEField(name="source", mesh=source_mesh, space=LagrangeSpaceP0, numbering=numbering)
|
|
65
|
+
operator_elem, _, _ = GetFieldTransferOp(
|
|
66
|
+
field, GetElementsCenters(target_mesh), elementFilter=ElementFilter()
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if transfer_elements and source_mesh.elemFields:
|
|
70
|
+
for name, data in source_mesh.elemFields.items():
|
|
71
|
+
if name == "flowgraph_element_id" or np.issubdtype(data.dtype, np.str_):
|
|
72
|
+
continue
|
|
73
|
+
outpout_mesh.elemFields[name] = np.ascontiguousarray(
|
|
74
|
+
(operator_elem @ data).astype(data.dtype, copy=False)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if transfer_elements_tags:
|
|
78
|
+
mask = np.zeros(source_mesh.GetNumberOfElements())
|
|
79
|
+
for tagname in source_mesh.elements.GetTagsNames():
|
|
80
|
+
mask.fill(0)
|
|
81
|
+
ids = source_mesh.GetElementsInTag(tagname)
|
|
82
|
+
mask[ids] = 1
|
|
83
|
+
|
|
84
|
+
outpout_mesh.AddElementsToTag(np.where(operator_elem.dot(mask))[0], tagname)
|
|
85
|
+
|
|
86
|
+
return {"mesh_document": MeshDocument(outpout_mesh)}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
TRANSFER_FIELDS_NODE = NodeDefinition(
|
|
90
|
+
id="transfer-fields",
|
|
91
|
+
icon="mdi-swap-horizontal-bold",
|
|
92
|
+
label="Transfer Fields",
|
|
93
|
+
description="Transfers nodal fields from one mesh onto another mesh.",
|
|
94
|
+
ports=(
|
|
95
|
+
PortDefinition("source_data", PortDirection.INPUT, MESH_DOCUMENT, "Source data"),
|
|
96
|
+
PortDefinition("target", PortDirection.INPUT, MESH_DOCUMENT, "Target"),
|
|
97
|
+
PortDefinition("mesh_document", PortDirection.OUTPUT, MESH_DOCUMENT, "Mesh document"),
|
|
98
|
+
),
|
|
99
|
+
parameters=(
|
|
100
|
+
ParameterDefinition(
|
|
101
|
+
"transferNodeData", ParameterKind.BOOLEAN, "Apply on node fields", True, port=False
|
|
102
|
+
),
|
|
103
|
+
ParameterDefinition(
|
|
104
|
+
"transferElementData",
|
|
105
|
+
ParameterKind.BOOLEAN,
|
|
106
|
+
"Apply on element fields",
|
|
107
|
+
True,
|
|
108
|
+
port=False,
|
|
109
|
+
),
|
|
110
|
+
ParameterDefinition(
|
|
111
|
+
"transferNodeTags", ParameterKind.BOOLEAN, "Transfer Node Tags", False, port=False
|
|
112
|
+
),
|
|
113
|
+
ParameterDefinition(
|
|
114
|
+
"transferElementTags", ParameterKind.BOOLEAN, "Transfer Element Tags", False, port=False
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
executor=_transfer_fields,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
AVAILABLE_NODES = (TRANSFER_FIELDS_NODE,)
|