combaero-gui 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- combaero_gui-0.2.0/PKG-INFO +73 -0
- combaero_gui-0.2.0/README.md +48 -0
- combaero_gui-0.2.0/__init__.py +0 -0
- combaero_gui-0.2.0/backend/__init__.py +0 -0
- combaero_gui-0.2.0/backend/graph_builder.py +443 -0
- combaero_gui-0.2.0/backend/main.py +359 -0
- combaero_gui-0.2.0/backend/schemas.py +340 -0
- combaero_gui-0.2.0/backend/units.py +135 -0
- combaero_gui-0.2.0/combaero_gui.egg-info/PKG-INFO +73 -0
- combaero_gui-0.2.0/combaero_gui.egg-info/SOURCES.txt +76 -0
- combaero_gui-0.2.0/combaero_gui.egg-info/dependency_links.txt +1 -0
- combaero_gui-0.2.0/combaero_gui.egg-info/entry_points.txt +2 -0
- combaero_gui-0.2.0/combaero_gui.egg-info/requires.txt +5 -0
- combaero_gui-0.2.0/combaero_gui.egg-info/top_level.txt +1 -0
- combaero_gui-0.2.0/frontend/.gitignore +24 -0
- combaero_gui-0.2.0/frontend/README.md +38 -0
- combaero_gui-0.2.0/frontend/eslint.config.js +23 -0
- combaero_gui-0.2.0/frontend/index.html +13 -0
- combaero_gui-0.2.0/frontend/package.json +44 -0
- combaero_gui-0.2.0/frontend/pnpm-lock.yaml +3344 -0
- combaero_gui-0.2.0/frontend/postcss.config.js +6 -0
- combaero_gui-0.2.0/frontend/public/favicon.svg +1 -0
- combaero_gui-0.2.0/frontend/public/icons.svg +24 -0
- combaero_gui-0.2.0/frontend/src/App.css +1 -0
- combaero_gui-0.2.0/frontend/src/App.tsx +169 -0
- combaero_gui-0.2.0/frontend/src/api.ts +48 -0
- combaero_gui-0.2.0/frontend/src/assets/hero.png +0 -0
- combaero_gui-0.2.0/frontend/src/assets/react.svg +1 -0
- combaero_gui-0.2.0/frontend/src/assets/vite.svg +1 -0
- combaero_gui-0.2.0/frontend/src/components/AreaInput.tsx +56 -0
- combaero_gui-0.2.0/frontend/src/components/CompositionEditor.tsx +225 -0
- combaero_gui-0.2.0/frontend/src/components/DiagnosticsPanel.tsx +231 -0
- combaero_gui-0.2.0/frontend/src/components/FlowEdge.tsx +53 -0
- combaero_gui-0.2.0/frontend/src/components/Inspector.tsx +1700 -0
- combaero_gui-0.2.0/frontend/src/components/LengthInput.tsx +63 -0
- combaero_gui-0.2.0/frontend/src/components/NetworkCanvas.tsx +141 -0
- combaero_gui-0.2.0/frontend/src/components/NodeDiagRows.tsx +62 -0
- combaero_gui-0.2.0/frontend/src/components/NumericInput.tsx +107 -0
- combaero_gui-0.2.0/frontend/src/components/Sidebar.tsx +383 -0
- combaero_gui-0.2.0/frontend/src/components/SolverStatusBadge.tsx +54 -0
- combaero_gui-0.2.0/frontend/src/components/SurfaceEnhancementInspector.tsx +280 -0
- combaero_gui-0.2.0/frontend/src/components/ThermalEdge.tsx +171 -0
- combaero_gui-0.2.0/frontend/src/components/UnitInput.tsx +53 -0
- combaero_gui-0.2.0/frontend/src/components/WallLayersEditor.tsx +198 -0
- combaero_gui-0.2.0/frontend/src/components/flowTypes.ts +32 -0
- combaero_gui-0.2.0/frontend/src/components/nodes/AreaChangeNode.tsx +73 -0
- combaero_gui-0.2.0/frontend/src/components/nodes/ChannelNode.tsx +63 -0
- combaero_gui-0.2.0/frontend/src/components/nodes/CombustorNode.tsx +61 -0
- combaero_gui-0.2.0/frontend/src/components/nodes/DiscreteLossNode.tsx +78 -0
- combaero_gui-0.2.0/frontend/src/components/nodes/LosslessNode.tsx +40 -0
- combaero_gui-0.2.0/frontend/src/components/nodes/MassBoundaryNode.tsx +52 -0
- combaero_gui-0.2.0/frontend/src/components/nodes/MomentumChamberNode.tsx +61 -0
- combaero_gui-0.2.0/frontend/src/components/nodes/OrificeNode.tsx +63 -0
- combaero_gui-0.2.0/frontend/src/components/nodes/PlenumNode.tsx +61 -0
- combaero_gui-0.2.0/frontend/src/components/nodes/PressureBoundaryNode.tsx +54 -0
- combaero_gui-0.2.0/frontend/src/components/nodes/ProbeNode.tsx +124 -0
- combaero_gui-0.2.0/frontend/src/index.css +154 -0
- combaero_gui-0.2.0/frontend/src/main.tsx +10 -0
- combaero_gui-0.2.0/frontend/src/node-25-regression.test.ts +55 -0
- combaero_gui-0.2.0/frontend/src/store/useStore.ts +424 -0
- combaero_gui-0.2.0/frontend/src/utils/diagnostics.ts +96 -0
- combaero_gui-0.2.0/frontend/src/utils/nodeUtils.ts +11 -0
- combaero_gui-0.2.0/frontend/src/utils/quantities.ts +125 -0
- combaero_gui-0.2.0/frontend/src/utils/validation.ts +102 -0
- combaero_gui-0.2.0/frontend/tailwind.config.js +8 -0
- combaero_gui-0.2.0/frontend/tsconfig.app.json +28 -0
- combaero_gui-0.2.0/frontend/tsconfig.json +7 -0
- combaero_gui-0.2.0/frontend/tsconfig.node.json +26 -0
- combaero_gui-0.2.0/frontend/vite.config.ts +7 -0
- combaero_gui-0.2.0/frontend/vitest.config.ts +11 -0
- combaero_gui-0.2.0/pyproject.toml +53 -0
- combaero_gui-0.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: combaero-gui
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: CombAero Network GUI — React/FastAPI desktop application
|
|
5
|
+
Author-email: thiemom <2641939+thiemom@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/thiemom/combaero
|
|
8
|
+
Project-URL: Repository, https://github.com/thiemom/combaero
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/thiemom/combaero/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/thiemom/combaero/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: aerodynamics,combustion,aerothermal,propulsion,gui,network
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: fastapi
|
|
21
|
+
Requires-Dist: uvicorn
|
|
22
|
+
Requires-Dist: pydantic
|
|
23
|
+
Requires-Dist: pandas
|
|
24
|
+
Requires-Dist: combaero~=0.2
|
|
25
|
+
|
|
26
|
+
# CombAero Network Designer GUI
|
|
27
|
+
|
|
28
|
+
**Interactive design and diagnostic environment for fluid-thermal networks.**
|
|
29
|
+
|
|
30
|
+
The CombAero GUI allows for rapid construction of complex aerospace networks using a drag-and-drop interface powered by React Flow and a high-performance C++ backend.
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
### 1. Environment Setup
|
|
35
|
+
Ensure the GUI components are installed:
|
|
36
|
+
```bash
|
|
37
|
+
uv pip install -e ".[gui]"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Launch
|
|
41
|
+
The GUI can be launched with a single command:
|
|
42
|
+
```bash
|
|
43
|
+
combaero-gui
|
|
44
|
+
```
|
|
45
|
+
This starts both the FastAPI backend and the Vite-based frontend.
|
|
46
|
+
|
|
47
|
+
## Key Workflows
|
|
48
|
+
|
|
49
|
+
- **Build**: Drag nodes (Plenums, Boundaries) and elements (Orifices, Channels) from the sidebar.
|
|
50
|
+
- **Connect**: Click and drag from node handles to element ports.
|
|
51
|
+
- **Solve**: Click the **Solve** button in the header. Successful solves turn node borders green.
|
|
52
|
+
- **Inspect**: Click on any node or element to view real-time diagnostics in the Inspector sidebar.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Agent Reference (UI Automation Map)
|
|
57
|
+
|
|
58
|
+
For agents with screen control (multimodal LLMs), the following selectors and IDs are provided to assist in navigation:
|
|
59
|
+
|
|
60
|
+
| UI Component | Selector / ID | Description |
|
|
61
|
+
| :--- | :--- | :--- |
|
|
62
|
+
| **Solve Button** | `#solve-btn` | Triggers the network solve. |
|
|
63
|
+
| **Inspector Sidebar** | `.inspector-panel` | Properties for selected node/element. |
|
|
64
|
+
| **Network Canvas** | `#rf-canvas` | The main React Flow workspace. |
|
|
65
|
+
| **Node (General)** | `.react-flow__node` | Any node on the canvas. |
|
|
66
|
+
| **Solved Indicator** | `.solved-status-ok` | Appears on nodes after a successful solve. |
|
|
67
|
+
| **Error Console** | `#error-output` | Displays solver convergence failures. |
|
|
68
|
+
|
|
69
|
+
> [!TIP]
|
|
70
|
+
> **Automation Hint**: To add a node via script, use the `gui.backend` API or generate a network JSON and load it via `File -> Import`.
|
|
71
|
+
|
|
72
|
+
## Technical Reference
|
|
73
|
+
For internal data structures and JSON schemas, see [docs/GUI_TECHNICAL.md](../docs/GUI_TECHNICAL.md).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# CombAero Network Designer GUI
|
|
2
|
+
|
|
3
|
+
**Interactive design and diagnostic environment for fluid-thermal networks.**
|
|
4
|
+
|
|
5
|
+
The CombAero GUI allows for rapid construction of complex aerospace networks using a drag-and-drop interface powered by React Flow and a high-performance C++ backend.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
### 1. Environment Setup
|
|
10
|
+
Ensure the GUI components are installed:
|
|
11
|
+
```bash
|
|
12
|
+
uv pip install -e ".[gui]"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### 2. Launch
|
|
16
|
+
The GUI can be launched with a single command:
|
|
17
|
+
```bash
|
|
18
|
+
combaero-gui
|
|
19
|
+
```
|
|
20
|
+
This starts both the FastAPI backend and the Vite-based frontend.
|
|
21
|
+
|
|
22
|
+
## Key Workflows
|
|
23
|
+
|
|
24
|
+
- **Build**: Drag nodes (Plenums, Boundaries) and elements (Orifices, Channels) from the sidebar.
|
|
25
|
+
- **Connect**: Click and drag from node handles to element ports.
|
|
26
|
+
- **Solve**: Click the **Solve** button in the header. Successful solves turn node borders green.
|
|
27
|
+
- **Inspect**: Click on any node or element to view real-time diagnostics in the Inspector sidebar.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Agent Reference (UI Automation Map)
|
|
32
|
+
|
|
33
|
+
For agents with screen control (multimodal LLMs), the following selectors and IDs are provided to assist in navigation:
|
|
34
|
+
|
|
35
|
+
| UI Component | Selector / ID | Description |
|
|
36
|
+
| :--- | :--- | :--- |
|
|
37
|
+
| **Solve Button** | `#solve-btn` | Triggers the network solve. |
|
|
38
|
+
| **Inspector Sidebar** | `.inspector-panel` | Properties for selected node/element. |
|
|
39
|
+
| **Network Canvas** | `#rf-canvas` | The main React Flow workspace. |
|
|
40
|
+
| **Node (General)** | `.react-flow__node` | Any node on the canvas. |
|
|
41
|
+
| **Solved Indicator** | `.solved-status-ok` | Appears on nodes after a successful solve. |
|
|
42
|
+
| **Error Console** | `#error-output` | Displays solver convergence failures. |
|
|
43
|
+
|
|
44
|
+
> [!TIP]
|
|
45
|
+
> **Automation Hint**: To add a node via script, use the `gui.backend` API or generate a network JSON and load it via `File -> Import`.
|
|
46
|
+
|
|
47
|
+
## Technical Reference
|
|
48
|
+
For internal data structures and JSON schemas, see [docs/GUI_TECHNICAL.md](../docs/GUI_TECHNICAL.md).
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
from combaero.network import (
|
|
2
|
+
AreaChangeElement,
|
|
3
|
+
ChannelElement,
|
|
4
|
+
CombustorNode,
|
|
5
|
+
ConstantFractionLoss,
|
|
6
|
+
ConstantHeadLoss,
|
|
7
|
+
ConvectiveSurface,
|
|
8
|
+
DimpledModel,
|
|
9
|
+
FlowNetwork,
|
|
10
|
+
ImpingementModel,
|
|
11
|
+
LinearThetaFractionLoss,
|
|
12
|
+
LinearThetaHeadLoss,
|
|
13
|
+
LosslessConnectionElement,
|
|
14
|
+
MassFlowBoundary,
|
|
15
|
+
MomentumChamberNode,
|
|
16
|
+
OrificeElement,
|
|
17
|
+
PinFinModel,
|
|
18
|
+
PlenumNode,
|
|
19
|
+
PressureBoundary,
|
|
20
|
+
PressureLossElement,
|
|
21
|
+
RibbedModel,
|
|
22
|
+
SmoothModel,
|
|
23
|
+
ThermalWall,
|
|
24
|
+
WallLayer,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from .schemas import (
|
|
28
|
+
AreaChangeData,
|
|
29
|
+
ChannelData,
|
|
30
|
+
CombustorData,
|
|
31
|
+
CompositionData,
|
|
32
|
+
ConstantFractionLossData,
|
|
33
|
+
ConstantHeadLossData,
|
|
34
|
+
DimpledModelData,
|
|
35
|
+
DiscreteLossData,
|
|
36
|
+
ImpingementModelData,
|
|
37
|
+
LinearThetaFractionLossData,
|
|
38
|
+
LinearThetaHeadLossData,
|
|
39
|
+
MassBoundaryData,
|
|
40
|
+
MomentumChamberData,
|
|
41
|
+
NetworkGraphSchema,
|
|
42
|
+
OrificeData,
|
|
43
|
+
PinFinModelData,
|
|
44
|
+
PlenumData,
|
|
45
|
+
PressureBoundaryData,
|
|
46
|
+
RibbedModelData,
|
|
47
|
+
SmoothModelData,
|
|
48
|
+
ThermalWallData,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_pressure_loss(data, area: float = 0.1):
|
|
53
|
+
"""Converts PressureLossData schema to a pressure-loss callable."""
|
|
54
|
+
if data is None:
|
|
55
|
+
return None
|
|
56
|
+
t = getattr(data, "type", None)
|
|
57
|
+
if isinstance(data, ConstantFractionLossData) or t == "constant_fraction":
|
|
58
|
+
return ConstantFractionLoss(xi=data.xi)
|
|
59
|
+
if isinstance(data, LinearThetaFractionLossData) or t == "linear_theta_fraction":
|
|
60
|
+
return LinearThetaFractionLoss(k=data.k, xi0=data.xi0)
|
|
61
|
+
if isinstance(data, ConstantHeadLossData) or t == "constant_head":
|
|
62
|
+
return ConstantHeadLoss(zeta=data.zeta, area=area)
|
|
63
|
+
if isinstance(data, LinearThetaHeadLossData) or t == "linear_theta_head":
|
|
64
|
+
return LinearThetaHeadLoss(k=data.k, zeta0=data.zeta0, area=area)
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def map_surface_model(data):
|
|
69
|
+
"""Maps UI SurfaceModelData to combaero.network models."""
|
|
70
|
+
if isinstance(data, SmoothModelData) or data.type == "smooth":
|
|
71
|
+
return SmoothModel()
|
|
72
|
+
elif isinstance(data, RibbedModelData) or data.type == "ribbed":
|
|
73
|
+
return RibbedModel(
|
|
74
|
+
e_D=data.e_D,
|
|
75
|
+
pitch_to_height=data.pitch_to_height,
|
|
76
|
+
alpha_deg=data.alpha_deg,
|
|
77
|
+
)
|
|
78
|
+
elif isinstance(data, DimpledModelData) or data.type == "dimpled":
|
|
79
|
+
return DimpledModel(
|
|
80
|
+
d_Dh=data.d_Dh,
|
|
81
|
+
h_d=data.h_d,
|
|
82
|
+
S_d=data.S_d,
|
|
83
|
+
)
|
|
84
|
+
elif isinstance(data, PinFinModelData) or data.type == "pin_fin":
|
|
85
|
+
return PinFinModel(
|
|
86
|
+
pin_diameter=data.pin_diameter,
|
|
87
|
+
channel_height=data.channel_height,
|
|
88
|
+
S_D=data.S_D,
|
|
89
|
+
X_D=data.X_D,
|
|
90
|
+
N_rows=data.N_rows,
|
|
91
|
+
is_staggered=data.is_staggered,
|
|
92
|
+
)
|
|
93
|
+
elif isinstance(data, ImpingementModelData) or data.type == "impingement":
|
|
94
|
+
return ImpingementModel(
|
|
95
|
+
d_jet=data.d_jet,
|
|
96
|
+
z_D=data.z_D,
|
|
97
|
+
x_D=data.x_D,
|
|
98
|
+
y_D=data.y_D,
|
|
99
|
+
A_target=data.A_target,
|
|
100
|
+
Cd_jet=data.Cd_jet,
|
|
101
|
+
)
|
|
102
|
+
return SmoothModel()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def resolve_composition(comp: CompositionData, T: float, P: float) -> list[float]:
|
|
106
|
+
"""
|
|
107
|
+
Converts UI CompositionData into a mass fraction list (Y)
|
|
108
|
+
using combaero.species utilities.
|
|
109
|
+
"""
|
|
110
|
+
import combaero as cb
|
|
111
|
+
|
|
112
|
+
if comp.source == "dry_air":
|
|
113
|
+
Y = cb.species.dry_air_mass()
|
|
114
|
+
elif comp.source == "humid_air":
|
|
115
|
+
# humid_air_mass takes ambient T, P, RH
|
|
116
|
+
Y = cb.species.humid_air_mass(comp.ambient_T, comp.ambient_P, comp.relative_humidity)
|
|
117
|
+
elif comp.source == "fuel":
|
|
118
|
+
# Default fuel to CH4 for now, or use a specific fuel if provided
|
|
119
|
+
Y = cb.species.pure_species("CH4")
|
|
120
|
+
Y = cb.species.to_mass(Y)
|
|
121
|
+
elif comp.source == "custom" and comp.custom_fractions:
|
|
122
|
+
vec = cb.species.from_mapping(comp.custom_fractions)
|
|
123
|
+
Y = cb.species.to_mass(vec) if comp.mode == "mole" else vec
|
|
124
|
+
else:
|
|
125
|
+
Y = cb.species.dry_air_mass()
|
|
126
|
+
|
|
127
|
+
return [float(y) for y in Y]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _expand_initial_guess(short_guess: dict, prefix: str) -> dict:
|
|
131
|
+
"""Translate short GUI guess keys (``P``, ``T``, ``m_dot``) into the fully
|
|
132
|
+
qualified solver unknown names (e.g. ``"plenum1.P"``). ``P`` and ``T`` are
|
|
133
|
+
broadcast to both static and total variants because the solver exposes
|
|
134
|
+
them as separate unknowns on some node types; the solver silently ignores
|
|
135
|
+
entries that do not correspond to an actual unknown.
|
|
136
|
+
Any key already containing a ``.`` is passed through unchanged so that
|
|
137
|
+
hand-authored qualified guesses still work. Explicit short keys
|
|
138
|
+
(e.g. ``Pt``) take precedence over broadcast values from ``P``/``T``.
|
|
139
|
+
"""
|
|
140
|
+
if not short_guess:
|
|
141
|
+
return {}
|
|
142
|
+
result: dict = {}
|
|
143
|
+
# Pass 1 — broadcast P and T to both static and total variants.
|
|
144
|
+
for key, val in short_guess.items():
|
|
145
|
+
if "." in key:
|
|
146
|
+
continue
|
|
147
|
+
if key == "P":
|
|
148
|
+
result[f"{prefix}.P"] = val
|
|
149
|
+
result[f"{prefix}.Pt"] = val
|
|
150
|
+
elif key == "T":
|
|
151
|
+
result[f"{prefix}.T"] = val
|
|
152
|
+
result[f"{prefix}.Tt"] = val
|
|
153
|
+
# Pass 2 — explicit short keys (Pt, Tt, m_dot, X[i], etc.)
|
|
154
|
+
# are applied after the broadcast so they win on conflict.
|
|
155
|
+
for key, val in short_guess.items():
|
|
156
|
+
if "." in key:
|
|
157
|
+
result[key] = val
|
|
158
|
+
continue
|
|
159
|
+
if key in ("P", "T"):
|
|
160
|
+
continue # already handled in pass 1
|
|
161
|
+
result[f"{prefix}.{key}"] = val
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _build_discrete_loss_correlation(
|
|
166
|
+
corr_type: str,
|
|
167
|
+
xi: float,
|
|
168
|
+
k: float,
|
|
169
|
+
xi0: float,
|
|
170
|
+
zeta: float,
|
|
171
|
+
zeta0: float,
|
|
172
|
+
area: float,
|
|
173
|
+
):
|
|
174
|
+
"""Build a pressure loss correlation object from discrete loss params."""
|
|
175
|
+
if corr_type == "constant_fraction":
|
|
176
|
+
return ConstantFractionLoss(xi=xi)
|
|
177
|
+
if corr_type == "linear_theta_fraction":
|
|
178
|
+
return LinearThetaFractionLoss(k=k, xi0=xi0)
|
|
179
|
+
if corr_type == "constant_head":
|
|
180
|
+
return ConstantHeadLoss(zeta=zeta, area=area)
|
|
181
|
+
if corr_type == "linear_theta_head":
|
|
182
|
+
return LinearThetaHeadLoss(k=k, zeta0=zeta0, area=area)
|
|
183
|
+
return ConstantFractionLoss(xi=xi)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
187
|
+
net = FlowNetwork()
|
|
188
|
+
nodes_map = {} # ID -> Physical Node
|
|
189
|
+
plenum_node_ids: set[str] = set()
|
|
190
|
+
element_nodes = [] # List of node schemas that are actually elements
|
|
191
|
+
|
|
192
|
+
# 1. First Pass: Create Physical Nodes
|
|
193
|
+
for node_schema in schema.nodes:
|
|
194
|
+
node_id = node_schema.id
|
|
195
|
+
node_data = node_schema.data
|
|
196
|
+
node_type = node_schema.type
|
|
197
|
+
|
|
198
|
+
if node_type == "plenum":
|
|
199
|
+
data = PlenumData(**node_data)
|
|
200
|
+
node = PlenumNode(node_id)
|
|
201
|
+
node.initial_guess = _expand_initial_guess(data.initial_guess, node_id)
|
|
202
|
+
net.add_node(node)
|
|
203
|
+
nodes_map[node_id] = node
|
|
204
|
+
plenum_node_ids.add(node_id)
|
|
205
|
+
elif node_type == "mass_boundary":
|
|
206
|
+
data = MassBoundaryData(**node_data)
|
|
207
|
+
Y = resolve_composition(data.composition, data.Tt, 101325.0)
|
|
208
|
+
node = MassFlowBoundary(node_id, m_dot=data.m_dot, Tt=data.Tt, Y=Y)
|
|
209
|
+
node.initial_guess = _expand_initial_guess(data.initial_guess, node_id)
|
|
210
|
+
net.add_node(node)
|
|
211
|
+
nodes_map[node_id] = node
|
|
212
|
+
elif node_type == "pressure_boundary":
|
|
213
|
+
data = PressureBoundaryData(**node_data)
|
|
214
|
+
Y = resolve_composition(data.composition, data.Tt, data.Pt)
|
|
215
|
+
node = PressureBoundary(node_id, Pt=data.Pt, Tt=data.Tt, Y=Y)
|
|
216
|
+
node.initial_guess = _expand_initial_guess(data.initial_guess, node_id)
|
|
217
|
+
net.add_node(node)
|
|
218
|
+
nodes_map[node_id] = node
|
|
219
|
+
elif node_type == "combustor":
|
|
220
|
+
data = CombustorData(**node_data)
|
|
221
|
+
node = CombustorNode(
|
|
222
|
+
node_id,
|
|
223
|
+
method=data.method,
|
|
224
|
+
area=data.area,
|
|
225
|
+
Dh=data.Dh,
|
|
226
|
+
surface=ConvectiveSurface(
|
|
227
|
+
area=data.area,
|
|
228
|
+
model=map_surface_model(data.surface),
|
|
229
|
+
Nu_multiplier=data.Nu_multiplier,
|
|
230
|
+
f_multiplier=data.f_multiplier,
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
node.initial_guess = _expand_initial_guess(data.initial_guess, node_id)
|
|
234
|
+
net.add_node(node)
|
|
235
|
+
nodes_map[node_id] = node
|
|
236
|
+
elif node_type == "momentum_chamber":
|
|
237
|
+
data = MomentumChamberData(**node_data)
|
|
238
|
+
node = MomentumChamberNode(
|
|
239
|
+
node_id,
|
|
240
|
+
area=data.area,
|
|
241
|
+
surface=ConvectiveSurface(
|
|
242
|
+
area=data.area,
|
|
243
|
+
model=map_surface_model(data.surface),
|
|
244
|
+
Nu_multiplier=data.Nu_multiplier,
|
|
245
|
+
f_multiplier=data.f_multiplier,
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
node.initial_guess = _expand_initial_guess(data.initial_guess, node_id)
|
|
249
|
+
net.add_node(node)
|
|
250
|
+
nodes_map[node_id] = node
|
|
251
|
+
elif node_type == "probe":
|
|
252
|
+
pass # Display-only diagnostic nodes -- not part of the solver graph
|
|
253
|
+
else:
|
|
254
|
+
# This is an element node (Channel, Orifice, etc.)
|
|
255
|
+
element_nodes.append(node_schema)
|
|
256
|
+
|
|
257
|
+
element_ids = {node.id for node in element_nodes}
|
|
258
|
+
|
|
259
|
+
# 1b. Create implicit junction nodes for element -> element visual links.
|
|
260
|
+
edge_junction_map: dict[tuple[str, str], str] = {}
|
|
261
|
+
for edge in schema.edges:
|
|
262
|
+
if edge.data and edge.data.get("type") == "thermal":
|
|
263
|
+
continue
|
|
264
|
+
if edge.source in element_ids and edge.target in element_ids:
|
|
265
|
+
junction_id = f"__junction__{edge.source}__{edge.target}"
|
|
266
|
+
if junction_id not in nodes_map:
|
|
267
|
+
junction_node = MomentumChamberNode(junction_id)
|
|
268
|
+
net.add_node(junction_node)
|
|
269
|
+
nodes_map[junction_id] = junction_node
|
|
270
|
+
edge_junction_map[(edge.source, edge.target)] = junction_id
|
|
271
|
+
|
|
272
|
+
# 2. Second Pass: Create Elements
|
|
273
|
+
for elem_node_schema in element_nodes:
|
|
274
|
+
elem_id = elem_node_schema.id
|
|
275
|
+
elem_data = elem_node_schema.data
|
|
276
|
+
|
|
277
|
+
source_id = None
|
|
278
|
+
target_id = None
|
|
279
|
+
incoming_count = 0
|
|
280
|
+
outgoing_count = 0
|
|
281
|
+
|
|
282
|
+
for edge in schema.edges:
|
|
283
|
+
if edge.data and edge.data.get("type") == "thermal":
|
|
284
|
+
continue
|
|
285
|
+
if edge.target == elem_id:
|
|
286
|
+
incoming_count += 1
|
|
287
|
+
if incoming_count > 1:
|
|
288
|
+
raise ValueError(f"Element '{elem_id}' has multiple upstream links.")
|
|
289
|
+
if edge.source in nodes_map:
|
|
290
|
+
source_id = edge.source
|
|
291
|
+
elif edge.source in element_ids:
|
|
292
|
+
source_id = edge_junction_map.get((edge.source, elem_id))
|
|
293
|
+
if edge.source == elem_id:
|
|
294
|
+
outgoing_count += 1
|
|
295
|
+
if outgoing_count > 1:
|
|
296
|
+
raise ValueError(f"Element '{elem_id}' has multiple downstream links.")
|
|
297
|
+
if edge.target in nodes_map:
|
|
298
|
+
target_id = edge.target
|
|
299
|
+
elif edge.target in element_ids:
|
|
300
|
+
target_id = edge_junction_map.get((elem_id, edge.target))
|
|
301
|
+
|
|
302
|
+
if not source_id or not target_id:
|
|
303
|
+
raise ValueError(f"Element '{elem_id}' is not fully connected.")
|
|
304
|
+
|
|
305
|
+
elem_type = elem_node_schema.type
|
|
306
|
+
if elem_type == "channel":
|
|
307
|
+
data = ChannelData(**elem_data)
|
|
308
|
+
conv_area = 3.1415926535 * data.D * data.L
|
|
309
|
+
elem = ChannelElement(
|
|
310
|
+
elem_id,
|
|
311
|
+
from_node=source_id,
|
|
312
|
+
to_node=target_id,
|
|
313
|
+
length=data.L,
|
|
314
|
+
diameter=data.D,
|
|
315
|
+
roughness=data.roughness,
|
|
316
|
+
surface=ConvectiveSurface(
|
|
317
|
+
area=conv_area,
|
|
318
|
+
model=map_surface_model(data.surface),
|
|
319
|
+
Nu_multiplier=data.Nu_multiplier,
|
|
320
|
+
f_multiplier=data.f_multiplier,
|
|
321
|
+
),
|
|
322
|
+
)
|
|
323
|
+
regime = (
|
|
324
|
+
data.regime if data.regime != "default" else schema.solver_settings.global_regime
|
|
325
|
+
)
|
|
326
|
+
elem.regime = regime
|
|
327
|
+
elem.initial_guess = _expand_initial_guess(data.initial_guess, elem_id)
|
|
328
|
+
net.add_element(elem)
|
|
329
|
+
elif elem_type == "orifice":
|
|
330
|
+
data = OrificeData(**elem_data)
|
|
331
|
+
elem = OrificeElement(
|
|
332
|
+
elem_id,
|
|
333
|
+
from_node=source_id,
|
|
334
|
+
to_node=target_id,
|
|
335
|
+
diameter=data.diameter,
|
|
336
|
+
Cd=data.Cd,
|
|
337
|
+
correlation=data.correlation,
|
|
338
|
+
plate_thickness=data.plate_thickness,
|
|
339
|
+
edge_radius=data.edge_radius,
|
|
340
|
+
)
|
|
341
|
+
regime = (
|
|
342
|
+
data.regime if data.regime != "default" else schema.solver_settings.global_regime
|
|
343
|
+
)
|
|
344
|
+
elem.regime = regime
|
|
345
|
+
elem.initial_guess = _expand_initial_guess(data.initial_guess, elem_id)
|
|
346
|
+
net.add_element(elem)
|
|
347
|
+
elif elem_type == "area_change":
|
|
348
|
+
data = AreaChangeData(**elem_data)
|
|
349
|
+
elem = AreaChangeElement(
|
|
350
|
+
id=elem_id,
|
|
351
|
+
from_node=source_id,
|
|
352
|
+
to_node=target_id,
|
|
353
|
+
F0=data.F0,
|
|
354
|
+
F1=data.F1,
|
|
355
|
+
model_type=data.model_type,
|
|
356
|
+
length=data.length,
|
|
357
|
+
D_h=data.D_h,
|
|
358
|
+
)
|
|
359
|
+
elem.initial_guess = _expand_initial_guess(data.initial_guess, elem_id)
|
|
360
|
+
net.add_element(elem)
|
|
361
|
+
elif elem_type == "lossless_connection":
|
|
362
|
+
elem = LosslessConnectionElement(elem_id, from_node=source_id, to_node=target_id)
|
|
363
|
+
net.add_element(elem)
|
|
364
|
+
elif elem_type == "discrete_loss":
|
|
365
|
+
data = DiscreteLossData(**elem_data)
|
|
366
|
+
# Infer area from upstream node if not explicitly set
|
|
367
|
+
area = data.area
|
|
368
|
+
if area is None and source_id in nodes_map:
|
|
369
|
+
area = getattr(nodes_map[source_id], "area", 0.1)
|
|
370
|
+
area = area if area and area > 0 else 0.1
|
|
371
|
+
correlation = _build_discrete_loss_correlation(
|
|
372
|
+
data.correlation_type, data.xi, data.k, data.xi0, data.zeta, data.zeta0, area
|
|
373
|
+
)
|
|
374
|
+
theta_source = data.theta_source if data.theta_source not in (None, "none") else None
|
|
375
|
+
surface = ConvectiveSurface(
|
|
376
|
+
area=area,
|
|
377
|
+
model=map_surface_model(data.surface),
|
|
378
|
+
Nu_multiplier=data.Nu_multiplier,
|
|
379
|
+
f_multiplier=data.f_multiplier,
|
|
380
|
+
)
|
|
381
|
+
elem = PressureLossElement(
|
|
382
|
+
elem_id,
|
|
383
|
+
from_node=source_id,
|
|
384
|
+
to_node=target_id,
|
|
385
|
+
correlation=correlation,
|
|
386
|
+
theta_source=theta_source,
|
|
387
|
+
area=area,
|
|
388
|
+
surface=surface,
|
|
389
|
+
)
|
|
390
|
+
elem.initial_guess = _expand_initial_guess(data.initial_guess, elem_id)
|
|
391
|
+
net.add_element(elem)
|
|
392
|
+
|
|
393
|
+
# 3. Third Pass: Auto-connections
|
|
394
|
+
for edge in schema.edges:
|
|
395
|
+
if (
|
|
396
|
+
edge.source in nodes_map
|
|
397
|
+
and edge.target in nodes_map
|
|
398
|
+
and not (edge.data or {}).get("type")
|
|
399
|
+
):
|
|
400
|
+
auto_id = f"__auto_link__{edge.source}__{edge.target}"
|
|
401
|
+
if auto_id not in net.elements:
|
|
402
|
+
elem = LosslessConnectionElement(
|
|
403
|
+
auto_id, from_node=edge.source, to_node=edge.target
|
|
404
|
+
)
|
|
405
|
+
net.add_element(elem)
|
|
406
|
+
|
|
407
|
+
# 4. Fourth Pass: Thermal Walls
|
|
408
|
+
for edge in schema.edges:
|
|
409
|
+
if edge.data and edge.data.get("type") == "thermal":
|
|
410
|
+
if edge.source in plenum_node_ids or edge.target in plenum_node_ids:
|
|
411
|
+
continue
|
|
412
|
+
wall_id = edge.id
|
|
413
|
+
data = ThermalWallData(**edge.data)
|
|
414
|
+
|
|
415
|
+
# Build layers list
|
|
416
|
+
layers = []
|
|
417
|
+
if data.layers:
|
|
418
|
+
# New multi-layer format
|
|
419
|
+
layers = [
|
|
420
|
+
WallLayer(
|
|
421
|
+
thickness=L.thickness, conductivity=L.conductivity, material=L.material
|
|
422
|
+
)
|
|
423
|
+
for L in data.layers
|
|
424
|
+
]
|
|
425
|
+
else:
|
|
426
|
+
# Legacy scalar format
|
|
427
|
+
t = data.thickness if data.thickness is not None else 0.003
|
|
428
|
+
k = data.conductivity if data.conductivity is not None else 20.0
|
|
429
|
+
layers = [WallLayer(thickness=t, conductivity=k)]
|
|
430
|
+
|
|
431
|
+
net.add_wall(
|
|
432
|
+
ThermalWall(
|
|
433
|
+
id=wall_id,
|
|
434
|
+
element_a=edge.source,
|
|
435
|
+
element_b=edge.target,
|
|
436
|
+
layers=layers,
|
|
437
|
+
contact_area=data.area,
|
|
438
|
+
R_fouling=data.R_fouling,
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
net.thermal_coupling_enabled = True
|
|
442
|
+
|
|
443
|
+
return net
|