combaero-gui 0.2.6__tar.gz → 0.3.1.dev2__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.6 → combaero_gui-0.3.1.dev2}/PKG-INFO +2 -2
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/backend/graph_builder.py +221 -13
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/backend/main.py +16 -164
- combaero_gui-0.3.1.dev2/backend/runner.py +654 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/backend/schemas.py +49 -5
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/backend/units.py +7 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/combaero_gui.egg-info/PKG-INFO +2 -2
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/combaero_gui.egg-info/SOURCES.txt +8 -4
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/combaero_gui.egg-info/requires.txt +1 -1
- combaero_gui-0.3.1.dev2/frontend/dist/assets/index-BACdja6I.js +19 -0
- combaero_gui-0.3.1.dev2/frontend/dist/assets/index-mUR2pMTx.css +2 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/dist/index.html +2 -2
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/package.json +18 -18
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/pnpm-lock.yaml +518 -500
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/CompositionEditor.tsx +28 -5
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/FlowEdge.tsx +34 -3
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/Inspector.tsx +408 -13
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/LengthInput.tsx +7 -1
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/NetworkCanvas.tsx +19 -10
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/NumericInput.tsx +8 -1
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/Sidebar.tsx +184 -0
- combaero_gui-0.3.1.dev2/frontend/src/components/SolverStatusBadge.tsx +71 -0
- combaero_gui-0.3.1.dev2/frontend/src/components/ThermalEdge.tsx +305 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/UnitInput.tsx +5 -2
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/flowTypes.ts +4 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/nodes/AreaChangeNode.tsx +26 -1
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/nodes/ChannelNode.tsx +26 -1
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/nodes/CombustorNode.tsx +26 -1
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/nodes/DiscreteLossNode.tsx +26 -1
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/nodes/LosslessNode.tsx +12 -6
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/nodes/MomentumChamberNode.tsx +26 -1
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/nodes/OrificeNode.tsx +26 -1
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/nodes/PlenumNode.tsx +27 -1
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/nodes/ProbeNode.tsx +4 -3
- combaero_gui-0.3.1.dev2/frontend/src/components/nodes/TeeJunctionNode.tsx +136 -0
- combaero_gui-0.3.1.dev2/frontend/src/components/nodes/VortexNode.tsx +47 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/store/useStore.ts +25 -1
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/utils/diagnostics.ts +7 -1
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/utils/quantities.ts +11 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/utils/validation.ts +12 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/pyproject.toml +1 -1
- combaero_gui-0.2.6/frontend/dist/assets/index-C41dzTWj.js +0 -19
- combaero_gui-0.2.6/frontend/dist/assets/index-DNc5NTHP.css +0 -2
- combaero_gui-0.2.6/frontend/src/components/SolverStatusBadge.tsx +0 -54
- combaero_gui-0.2.6/frontend/src/components/ThermalEdge.tsx +0 -171
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/MANIFEST.in +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/README.md +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/__init__.py +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/backend/__init__.py +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/combaero_gui.egg-info/dependency_links.txt +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/combaero_gui.egg-info/entry_points.txt +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/combaero_gui.egg-info/top_level.txt +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/.gitignore +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/README.md +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/dist/favicon.svg +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/dist/icons.svg +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/eslint.config.js +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/index.html +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/postcss.config.js +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/public/favicon.svg +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/public/icons.svg +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/App.css +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/App.tsx +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/api.ts +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/assets/hero.png +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/assets/react.svg +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/assets/vite.svg +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/AreaInput.tsx +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/DiagnosticsPanel.tsx +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/NodeDiagRows.tsx +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/SurfaceEnhancementInspector.tsx +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/WallLayersEditor.tsx +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/nodes/MassBoundaryNode.tsx +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/components/nodes/PressureBoundaryNode.tsx +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/index.css +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/main.tsx +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/node-25-regression.test.ts +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/src/utils/nodeUtils.ts +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/tailwind.config.js +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/tsconfig.app.json +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/tsconfig.json +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/tsconfig.node.json +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/vite.config.ts +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/frontend/vitest.config.ts +0 -0
- {combaero_gui-0.2.6 → combaero_gui-0.3.1.dev2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: combaero-gui
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1.dev2
|
|
4
4
|
Summary: CombAero Network GUI — React/FastAPI desktop application
|
|
5
5
|
Author-email: thiemom <2641939+thiemom@users.noreply.github.com>
|
|
6
6
|
License: MIT
|
|
@@ -21,7 +21,7 @@ Requires-Dist: fastapi
|
|
|
21
21
|
Requires-Dist: uvicorn
|
|
22
22
|
Requires-Dist: pydantic
|
|
23
23
|
Requires-Dist: pandas
|
|
24
|
-
Requires-Dist: combaero~=0.
|
|
24
|
+
Requires-Dist: combaero~=0.3
|
|
25
25
|
Provides-Extra: gui
|
|
26
26
|
|
|
27
27
|
# CombAero Network Designer GUI
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import math as _math
|
|
2
|
+
|
|
1
3
|
from combaero.network import (
|
|
2
4
|
AreaChangeElement,
|
|
3
5
|
ChannelElement,
|
|
@@ -20,7 +22,9 @@ from combaero.network import (
|
|
|
20
22
|
PressureLossElement,
|
|
21
23
|
RibbedModel,
|
|
22
24
|
SmoothModel,
|
|
25
|
+
TeeJunctionElement,
|
|
23
26
|
ThermalWall,
|
|
27
|
+
VortexElement,
|
|
24
28
|
WallLayer,
|
|
25
29
|
)
|
|
26
30
|
|
|
@@ -45,7 +49,9 @@ from .schemas import (
|
|
|
45
49
|
PressureBoundaryData,
|
|
46
50
|
RibbedModelData,
|
|
47
51
|
SmoothModelData,
|
|
52
|
+
TeeJunctionData,
|
|
48
53
|
ThermalWallData,
|
|
54
|
+
VortexData,
|
|
49
55
|
)
|
|
50
56
|
|
|
51
57
|
|
|
@@ -102,7 +108,9 @@ def map_surface_model(data):
|
|
|
102
108
|
return SmoothModel()
|
|
103
109
|
|
|
104
110
|
|
|
105
|
-
def resolve_composition(
|
|
111
|
+
def resolve_composition(
|
|
112
|
+
comp: CompositionData, T: float, P: float, solver_settings=None
|
|
113
|
+
) -> list[float]:
|
|
106
114
|
"""
|
|
107
115
|
Converts UI CompositionData into a mass fraction list (Y)
|
|
108
116
|
using combaero.species utilities.
|
|
@@ -112,8 +120,23 @@ def resolve_composition(comp: CompositionData, T: float, P: float) -> list[float
|
|
|
112
120
|
if comp.source == "dry_air":
|
|
113
121
|
Y = cb.species.dry_air_mass()
|
|
114
122
|
elif comp.source == "humid_air":
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
s = solver_settings
|
|
124
|
+
ambient_T = (
|
|
125
|
+
comp.ambient_T
|
|
126
|
+
if comp.ambient_T is not None
|
|
127
|
+
else (s.ambient_T if s is not None and s.ambient_T is not None else 288.15)
|
|
128
|
+
)
|
|
129
|
+
ambient_P = (
|
|
130
|
+
comp.ambient_P
|
|
131
|
+
if comp.ambient_P is not None
|
|
132
|
+
else (s.ambient_P if s is not None and s.ambient_P is not None else 101325.0)
|
|
133
|
+
)
|
|
134
|
+
ambient_RH = (
|
|
135
|
+
comp.relative_humidity
|
|
136
|
+
if comp.relative_humidity is not None
|
|
137
|
+
else (s.ambient_RH if s is not None and s.ambient_RH is not None else 0.6)
|
|
138
|
+
)
|
|
139
|
+
Y = cb.species.humid_air_mass(ambient_T, ambient_P, ambient_RH)
|
|
117
140
|
elif comp.source == "fuel":
|
|
118
141
|
# Default fuel to CH4 for now, or use a specific fuel if provided
|
|
119
142
|
Y = cb.species.pure_species("CH4")
|
|
@@ -162,6 +185,35 @@ def _expand_initial_guess(short_guess: dict, prefix: str) -> dict:
|
|
|
162
185
|
return result
|
|
163
186
|
|
|
164
187
|
|
|
188
|
+
def _guess_from_prior_result(node_data: dict, prefix: str) -> dict:
|
|
189
|
+
"""Build an initial-guess dict from a prior solve result stored in node_data.
|
|
190
|
+
|
|
191
|
+
The GUI frontend writes the last solve result into node_data["result"].
|
|
192
|
+
When data.initial_guess is empty (no explicit user override), this
|
|
193
|
+
provides a warm-start from the previously found state so that repeated
|
|
194
|
+
solves on a difficult network benefit from accumulated Newton progress.
|
|
195
|
+
Works for both pressure-node results (state.P/T/Pt/Tt) and element
|
|
196
|
+
results (m_dot stored at the top level).
|
|
197
|
+
"""
|
|
198
|
+
prior = node_data.get("result")
|
|
199
|
+
if not prior:
|
|
200
|
+
return {}
|
|
201
|
+
short: dict[str, float] = {}
|
|
202
|
+
state = prior.get("state")
|
|
203
|
+
if state:
|
|
204
|
+
if (p := state.get("P")) is not None:
|
|
205
|
+
short["P"] = float(p)
|
|
206
|
+
if (t := state.get("T")) is not None:
|
|
207
|
+
short["T"] = float(t)
|
|
208
|
+
if (pt := state.get("Pt")) is not None:
|
|
209
|
+
short["Pt"] = float(pt)
|
|
210
|
+
if (tt := state.get("Tt")) is not None:
|
|
211
|
+
short["Tt"] = float(tt)
|
|
212
|
+
if (m_dot := prior.get("m_dot")) is not None:
|
|
213
|
+
short["m_dot"] = float(m_dot)
|
|
214
|
+
return _expand_initial_guess(short, prefix)
|
|
215
|
+
|
|
216
|
+
|
|
165
217
|
def _build_discrete_loss_correlation(
|
|
166
218
|
corr_type: str,
|
|
167
219
|
xi: float,
|
|
@@ -183,6 +235,61 @@ def _build_discrete_loss_correlation(
|
|
|
183
235
|
return ConstantFractionLoss(xi=xi)
|
|
184
236
|
|
|
185
237
|
|
|
238
|
+
def _port_from_handle(handle: str | None) -> str | None:
|
|
239
|
+
"""Extract port name from a tee handle string.
|
|
240
|
+
|
|
241
|
+
'port-common-source' -> 'common', 'port-branch-target' -> 'branch', etc.
|
|
242
|
+
Returns None for non-tee handles.
|
|
243
|
+
"""
|
|
244
|
+
if (
|
|
245
|
+
handle
|
|
246
|
+
and handle.startswith("port-")
|
|
247
|
+
and (handle.endswith("-source") or handle.endswith("-target"))
|
|
248
|
+
):
|
|
249
|
+
return handle.split("-")[1]
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _find_tee_port(
|
|
254
|
+
edges: list,
|
|
255
|
+
tee_id: str,
|
|
256
|
+
port: str,
|
|
257
|
+
nodes_map: dict,
|
|
258
|
+
tee_port_node_map: dict | None = None,
|
|
259
|
+
) -> str:
|
|
260
|
+
"""Return the physical-node ID connected to a named tee port.
|
|
261
|
+
|
|
262
|
+
Checks both edge directions: tee as source (sourceHandle == port-X-source)
|
|
263
|
+
and tee as target (targetHandle == port-X-target).
|
|
264
|
+
Falls back to *tee_port_node_map* for auto-created junction nodes when an
|
|
265
|
+
element is connected directly to a tee port without an explicit plenum.
|
|
266
|
+
"""
|
|
267
|
+
src_handle = f"port-{port}-source"
|
|
268
|
+
tgt_handle = f"port-{port}-target"
|
|
269
|
+
for edge in edges:
|
|
270
|
+
if edge.data and edge.data.get("type") == "thermal":
|
|
271
|
+
continue
|
|
272
|
+
if edge.source == tee_id and edge.sourceHandle == src_handle:
|
|
273
|
+
nid = edge.target
|
|
274
|
+
elif edge.target == tee_id and edge.targetHandle == tgt_handle:
|
|
275
|
+
nid = edge.source
|
|
276
|
+
else:
|
|
277
|
+
continue
|
|
278
|
+
if nid in nodes_map:
|
|
279
|
+
return nid
|
|
280
|
+
if tee_port_node_map and (tee_id, port) in tee_port_node_map:
|
|
281
|
+
return tee_port_node_map[(tee_id, port)]
|
|
282
|
+
raise ValueError(
|
|
283
|
+
f"Tee '{tee_id}' port '{port}' connects to '{nid}', which is not a "
|
|
284
|
+
"physical node (plenum or boundary). All tee ports must connect directly "
|
|
285
|
+
"to plena or boundaries."
|
|
286
|
+
)
|
|
287
|
+
raise ValueError(
|
|
288
|
+
f"Tee '{tee_id}' port '{port}' is not connected. "
|
|
289
|
+
"All three tee ports (common, straight, branch) must be wired to a node."
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
186
293
|
def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
187
294
|
net = FlowNetwork()
|
|
188
295
|
nodes_map = {} # ID -> Physical Node
|
|
@@ -199,19 +306,23 @@ def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
|
199
306
|
data = PlenumData(**node_data)
|
|
200
307
|
node = PlenumNode(node_id)
|
|
201
308
|
node.initial_guess = _expand_initial_guess(data.initial_guess, node_id)
|
|
309
|
+
if not node.initial_guess:
|
|
310
|
+
node.initial_guess = _guess_from_prior_result(node_data, node_id)
|
|
202
311
|
net.add_node(node)
|
|
203
312
|
nodes_map[node_id] = node
|
|
204
313
|
plenum_node_ids.add(node_id)
|
|
205
314
|
elif node_type == "mass_boundary":
|
|
206
315
|
data = MassBoundaryData(**node_data)
|
|
207
|
-
Y = resolve_composition(data.composition, data.Tt, 101325.0)
|
|
316
|
+
Y = resolve_composition(data.composition, data.Tt, 101325.0, schema.solver_settings)
|
|
208
317
|
node = MassFlowBoundary(node_id, m_dot=data.m_dot, Tt=data.Tt, Y=Y)
|
|
209
318
|
node.initial_guess = _expand_initial_guess(data.initial_guess, node_id)
|
|
319
|
+
if not node.initial_guess:
|
|
320
|
+
node.initial_guess = _guess_from_prior_result(node_data, node_id)
|
|
210
321
|
net.add_node(node)
|
|
211
322
|
nodes_map[node_id] = node
|
|
212
323
|
elif node_type == "pressure_boundary":
|
|
213
324
|
data = PressureBoundaryData(**node_data)
|
|
214
|
-
Y = resolve_composition(data.composition, data.Tt, data.Pt)
|
|
325
|
+
Y = resolve_composition(data.composition, data.Tt, data.Pt, schema.solver_settings)
|
|
215
326
|
node = PressureBoundary(node_id, Pt=data.Pt, Tt=data.Tt, Y=Y)
|
|
216
327
|
node.initial_guess = _expand_initial_guess(data.initial_guess, node_id)
|
|
217
328
|
net.add_node(node)
|
|
@@ -226,11 +337,14 @@ def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
|
226
337
|
surface=ConvectiveSurface(
|
|
227
338
|
area=data.area,
|
|
228
339
|
model=map_surface_model(data.surface),
|
|
229
|
-
Nu_multiplier=data.Nu_multiplier
|
|
230
|
-
|
|
340
|
+
Nu_multiplier=data.Nu_multiplier
|
|
341
|
+
* (schema.solver_settings.Nu_multiplier or 1.0),
|
|
342
|
+
f_multiplier=data.f_multiplier * (schema.solver_settings.f_multiplier or 1.0),
|
|
231
343
|
),
|
|
232
344
|
)
|
|
233
345
|
node.initial_guess = _expand_initial_guess(data.initial_guess, node_id)
|
|
346
|
+
if not node.initial_guess:
|
|
347
|
+
node.initial_guess = _guess_from_prior_result(node_data, node_id)
|
|
234
348
|
net.add_node(node)
|
|
235
349
|
nodes_map[node_id] = node
|
|
236
350
|
elif node_type == "momentum_chamber":
|
|
@@ -241,11 +355,14 @@ def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
|
241
355
|
surface=ConvectiveSurface(
|
|
242
356
|
area=data.area,
|
|
243
357
|
model=map_surface_model(data.surface),
|
|
244
|
-
Nu_multiplier=data.Nu_multiplier
|
|
245
|
-
|
|
358
|
+
Nu_multiplier=data.Nu_multiplier
|
|
359
|
+
* (schema.solver_settings.Nu_multiplier or 1.0),
|
|
360
|
+
f_multiplier=data.f_multiplier * (schema.solver_settings.f_multiplier or 1.0),
|
|
246
361
|
),
|
|
247
362
|
)
|
|
248
363
|
node.initial_guess = _expand_initial_guess(data.initial_guess, node_id)
|
|
364
|
+
if not node.initial_guess:
|
|
365
|
+
node.initial_guess = _guess_from_prior_result(node_data, node_id)
|
|
249
366
|
net.add_node(node)
|
|
250
367
|
nodes_map[node_id] = node
|
|
251
368
|
elif node_type == "probe":
|
|
@@ -255,13 +372,24 @@ def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
|
255
372
|
element_nodes.append(node_schema)
|
|
256
373
|
|
|
257
374
|
element_ids = {node.id for node in element_nodes}
|
|
375
|
+
tee_ids = {node.id for node in element_nodes if node.type == "tee_junction"}
|
|
258
376
|
|
|
259
377
|
# 1b. Create implicit junction nodes for element -> element visual links.
|
|
378
|
+
# For direct element<->element connections we auto-insert a MomentumChamberNode.
|
|
379
|
+
# For direct element<->tee-port connections we auto-insert a PlenumNode so that
|
|
380
|
+
# users don't need to manually place a plenum on every tee arm.
|
|
260
381
|
edge_junction_map: dict[tuple[str, str], str] = {}
|
|
382
|
+
tee_port_node_map: dict[tuple[str, str], str] = {} # (tee_id, port) -> node_id
|
|
261
383
|
for edge in schema.edges:
|
|
262
384
|
if edge.data and edge.data.get("type") == "thermal":
|
|
263
385
|
continue
|
|
264
|
-
|
|
386
|
+
src_elem = edge.source in element_ids
|
|
387
|
+
tgt_elem = edge.target in element_ids
|
|
388
|
+
src_tee = edge.source in tee_ids
|
|
389
|
+
tgt_tee = edge.target in tee_ids
|
|
390
|
+
|
|
391
|
+
if src_elem and tgt_elem and not src_tee and not tgt_tee:
|
|
392
|
+
# Standard element-to-element: auto-insert MomentumChamberNode
|
|
265
393
|
junction_id = f"__junction__{edge.source}__{edge.target}"
|
|
266
394
|
if junction_id not in nodes_map:
|
|
267
395
|
junction_node = MomentumChamberNode(junction_id)
|
|
@@ -269,10 +397,60 @@ def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
|
269
397
|
nodes_map[junction_id] = junction_node
|
|
270
398
|
edge_junction_map[(edge.source, edge.target)] = junction_id
|
|
271
399
|
|
|
400
|
+
elif src_tee and tgt_elem:
|
|
401
|
+
# Tee port → element (e.g. tee.common-source → channel): auto-insert PlenumNode
|
|
402
|
+
port = _port_from_handle(edge.sourceHandle)
|
|
403
|
+
if port and (edge.source, port) not in tee_port_node_map:
|
|
404
|
+
jid = f"__tee_jct__{edge.source}_{port}"
|
|
405
|
+
if jid not in nodes_map:
|
|
406
|
+
jn = PlenumNode(jid)
|
|
407
|
+
net.add_node(jn)
|
|
408
|
+
nodes_map[jid] = jn
|
|
409
|
+
tee_port_node_map[(edge.source, port)] = jid
|
|
410
|
+
edge_junction_map[(edge.source, edge.target)] = jid
|
|
411
|
+
|
|
412
|
+
elif src_elem and tgt_tee:
|
|
413
|
+
# Element → tee port (e.g. channel → tee.straight-target): auto-insert PlenumNode
|
|
414
|
+
port = _port_from_handle(edge.targetHandle)
|
|
415
|
+
if port and (edge.target, port) not in tee_port_node_map:
|
|
416
|
+
jid = f"__tee_jct__{edge.target}_{port}"
|
|
417
|
+
if jid not in nodes_map:
|
|
418
|
+
jn = PlenumNode(jid)
|
|
419
|
+
net.add_node(jn)
|
|
420
|
+
nodes_map[jid] = jn
|
|
421
|
+
tee_port_node_map[(edge.target, port)] = jid
|
|
422
|
+
edge_junction_map[(edge.source, edge.target)] = jid
|
|
423
|
+
|
|
272
424
|
# 2. Second Pass: Create Elements
|
|
273
425
|
for elem_node_schema in element_nodes:
|
|
274
426
|
elem_id = elem_node_schema.id
|
|
275
427
|
elem_data = elem_node_schema.data
|
|
428
|
+
elem_type = elem_node_schema.type
|
|
429
|
+
|
|
430
|
+
# 3-port element: bypass the 2-port source/target logic entirely.
|
|
431
|
+
if elem_type == "tee_junction":
|
|
432
|
+
_d = TeeJunctionData(**elem_data)
|
|
433
|
+
_tee = TeeJunctionElement(
|
|
434
|
+
id=elem_id,
|
|
435
|
+
common_node=_find_tee_port(
|
|
436
|
+
schema.edges, elem_id, "common", nodes_map, tee_port_node_map
|
|
437
|
+
),
|
|
438
|
+
straight_node=_find_tee_port(
|
|
439
|
+
schema.edges, elem_id, "straight", nodes_map, tee_port_node_map
|
|
440
|
+
),
|
|
441
|
+
branch_node=_find_tee_port(
|
|
442
|
+
schema.edges, elem_id, "branch", nodes_map, tee_port_node_map
|
|
443
|
+
),
|
|
444
|
+
theta=_math.radians(_d.theta_deg),
|
|
445
|
+
F_C=_d.F_C,
|
|
446
|
+
psi=_d.psi,
|
|
447
|
+
tee_type=_d.tee_type,
|
|
448
|
+
)
|
|
449
|
+
_tee.initial_guess = _expand_initial_guess(_d.initial_guess, elem_id)
|
|
450
|
+
if not _tee.initial_guess:
|
|
451
|
+
_tee.initial_guess = _guess_from_prior_result(elem_data, elem_id)
|
|
452
|
+
net.add_element(_tee)
|
|
453
|
+
continue
|
|
276
454
|
|
|
277
455
|
source_id = None
|
|
278
456
|
target_id = None
|
|
@@ -302,7 +480,6 @@ def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
|
302
480
|
if not source_id or not target_id:
|
|
303
481
|
raise ValueError(f"Element '{elem_id}' is not fully connected.")
|
|
304
482
|
|
|
305
|
-
elem_type = elem_node_schema.type
|
|
306
483
|
if elem_type == "channel":
|
|
307
484
|
data = ChannelData(**elem_data)
|
|
308
485
|
conv_area = 3.1415926535 * data.D * data.L
|
|
@@ -313,11 +490,13 @@ def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
|
313
490
|
length=data.L,
|
|
314
491
|
diameter=data.D,
|
|
315
492
|
roughness=data.roughness,
|
|
493
|
+
friction_model=data.friction_model,
|
|
316
494
|
surface=ConvectiveSurface(
|
|
317
495
|
area=conv_area,
|
|
318
496
|
model=map_surface_model(data.surface),
|
|
319
|
-
Nu_multiplier=data.Nu_multiplier
|
|
320
|
-
|
|
497
|
+
Nu_multiplier=data.Nu_multiplier
|
|
498
|
+
* (schema.solver_settings.Nu_multiplier or 1.0),
|
|
499
|
+
f_multiplier=data.f_multiplier * (schema.solver_settings.f_multiplier or 1.0),
|
|
321
500
|
),
|
|
322
501
|
)
|
|
323
502
|
regime = (
|
|
@@ -325,6 +504,8 @@ def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
|
325
504
|
)
|
|
326
505
|
elem.regime = regime
|
|
327
506
|
elem.initial_guess = _expand_initial_guess(data.initial_guess, elem_id)
|
|
507
|
+
if not elem.initial_guess:
|
|
508
|
+
elem.initial_guess = _guess_from_prior_result(elem_data, elem_id)
|
|
328
509
|
net.add_element(elem)
|
|
329
510
|
elif elem_type == "orifice":
|
|
330
511
|
data = OrificeData(**elem_data)
|
|
@@ -343,6 +524,8 @@ def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
|
343
524
|
)
|
|
344
525
|
elem.regime = regime
|
|
345
526
|
elem.initial_guess = _expand_initial_guess(data.initial_guess, elem_id)
|
|
527
|
+
if not elem.initial_guess:
|
|
528
|
+
elem.initial_guess = _guess_from_prior_result(elem_data, elem_id)
|
|
346
529
|
net.add_element(elem)
|
|
347
530
|
elif elem_type == "area_change":
|
|
348
531
|
data = AreaChangeData(**elem_data)
|
|
@@ -357,6 +540,8 @@ def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
|
357
540
|
D_h=data.D_h,
|
|
358
541
|
)
|
|
359
542
|
elem.initial_guess = _expand_initial_guess(data.initial_guess, elem_id)
|
|
543
|
+
if not elem.initial_guess:
|
|
544
|
+
elem.initial_guess = _guess_from_prior_result(elem_data, elem_id)
|
|
360
545
|
net.add_element(elem)
|
|
361
546
|
elif elem_type == "lossless_connection":
|
|
362
547
|
elem = LosslessConnectionElement(elem_id, from_node=source_id, to_node=target_id)
|
|
@@ -388,6 +573,29 @@ def build_network_from_schema(schema: NetworkGraphSchema) -> FlowNetwork:
|
|
|
388
573
|
surface=surface,
|
|
389
574
|
)
|
|
390
575
|
elem.initial_guess = _expand_initial_guess(data.initial_guess, elem_id)
|
|
576
|
+
if not elem.initial_guess:
|
|
577
|
+
elem.initial_guess = _guess_from_prior_result(elem_data, elem_id)
|
|
578
|
+
net.add_element(elem)
|
|
579
|
+
elif elem_type == "vortex":
|
|
580
|
+
data = VortexData(**elem_data)
|
|
581
|
+
omega = (
|
|
582
|
+
data.omega_rpm
|
|
583
|
+
if data.omega_rpm is not None
|
|
584
|
+
else (schema.solver_settings.omega_rpm or 0.0)
|
|
585
|
+
)
|
|
586
|
+
elem = VortexElement(
|
|
587
|
+
elem_id,
|
|
588
|
+
from_node=source_id,
|
|
589
|
+
to_node=target_id,
|
|
590
|
+
r_c=data.r_c,
|
|
591
|
+
r_out=data.r_out,
|
|
592
|
+
r_in=data.r_in,
|
|
593
|
+
omega_rpm=float(omega),
|
|
594
|
+
n=data.n,
|
|
595
|
+
)
|
|
596
|
+
elem.initial_guess = _expand_initial_guess(data.initial_guess, elem_id)
|
|
597
|
+
if not elem.initial_guess:
|
|
598
|
+
elem.initial_guess = _guess_from_prior_result(elem_data, elem_id)
|
|
391
599
|
net.add_element(elem)
|
|
392
600
|
|
|
393
601
|
# 3. Third Pass: Auto-connections
|
|
@@ -11,12 +11,10 @@ import combaero as cb
|
|
|
11
11
|
from combaero.network import NetworkSolver
|
|
12
12
|
|
|
13
13
|
from .graph_builder import build_network_from_schema
|
|
14
|
+
from .runner import NetworkResult, _build_result_objects, _schema_maps
|
|
14
15
|
from .schemas import (
|
|
15
|
-
ElementResult,
|
|
16
16
|
NetworkGraphSchema,
|
|
17
|
-
NodeResult,
|
|
18
17
|
SolveResponse,
|
|
19
|
-
StateResult,
|
|
20
18
|
)
|
|
21
19
|
|
|
22
20
|
_FRONTEND_DIST = Path(__file__).parent.parent / "frontend" / "dist"
|
|
@@ -129,62 +127,7 @@ def _solve_sync(schema: NetworkGraphSchema):
|
|
|
129
127
|
"initial_guesses": {n.id: n.data.get("initial_guess", {}) for n in schema.nodes},
|
|
130
128
|
}
|
|
131
129
|
|
|
132
|
-
node_results =
|
|
133
|
-
for node_id in net.nodes:
|
|
134
|
-
# Pull all node diagnostics and state variables directly from sol_dict
|
|
135
|
-
# flat keys are like node_id.T, node_id.P, node_id.mach, node_id.rho, etc.
|
|
136
|
-
prefix = f"{node_id}."
|
|
137
|
-
node_vals = {k[len(prefix) :]: v for k, v in result.items() if k.startswith(prefix)}
|
|
138
|
-
|
|
139
|
-
# Handle composition lists (Y and X)
|
|
140
|
-
y_vals = []
|
|
141
|
-
x_vals = []
|
|
142
|
-
i = 0
|
|
143
|
-
while f"Y[{i}]" in node_vals:
|
|
144
|
-
y_vals.append(float(node_vals.pop(f"Y[{i}]")))
|
|
145
|
-
if f"X[{i}]" in node_vals:
|
|
146
|
-
x_vals.append(float(node_vals.pop(f"X[{i}]")))
|
|
147
|
-
i += 1
|
|
148
|
-
|
|
149
|
-
state_res = StateResult(
|
|
150
|
-
T=float(node_vals.pop("T", 300.0)),
|
|
151
|
-
P=float(node_vals.pop("P", 101325.0)),
|
|
152
|
-
Pt=node_vals.pop("Pt", None),
|
|
153
|
-
Tt=node_vals.pop("Tt", None),
|
|
154
|
-
Y=y_vals,
|
|
155
|
-
X=x_vals or None,
|
|
156
|
-
**node_vals, # All remaining diagnostics (rho, mach, mu, k, etc.)
|
|
157
|
-
)
|
|
158
|
-
node_results[node_id] = NodeResult(state=state_res, success=success)
|
|
159
|
-
|
|
160
|
-
element_results = {}
|
|
161
|
-
elem_diags = result.get("__element_diag__", {})
|
|
162
|
-
for elem_id in net.elements:
|
|
163
|
-
m_dot = float(result.get(f"{elem_id}.m_dot", 0.0))
|
|
164
|
-
# Pull element diagnostics from the structured dict aggregated by solve()
|
|
165
|
-
diag = elem_diags.get(elem_id, {}).copy()
|
|
166
|
-
diag.pop("m_dot", None)
|
|
167
|
-
element_results[elem_id] = ElementResult(m_dot=m_dot, success=success, **diag)
|
|
168
|
-
|
|
169
|
-
edge_results = {}
|
|
170
|
-
|
|
171
|
-
def _safe_float(v):
|
|
172
|
-
try:
|
|
173
|
-
if isinstance(v, (list, tuple, dict)):
|
|
174
|
-
return v
|
|
175
|
-
return float(v)
|
|
176
|
-
except (TypeError, ValueError):
|
|
177
|
-
return v
|
|
178
|
-
|
|
179
|
-
for edge_id in net.walls:
|
|
180
|
-
edge_keys = {
|
|
181
|
-
k.split(".", 1)[1]: _safe_float(v)
|
|
182
|
-
for k, v in result.items()
|
|
183
|
-
if k.startswith(f"{edge_id}.")
|
|
184
|
-
}
|
|
185
|
-
if edge_keys:
|
|
186
|
-
edge_results[edge_id] = edge_keys
|
|
187
|
-
|
|
130
|
+
node_results, element_results, edge_results = _build_result_objects(result, net)
|
|
188
131
|
return result, node_results, element_results, edge_results, net
|
|
189
132
|
|
|
190
133
|
|
|
@@ -228,115 +171,24 @@ async def export_results(schema: NetworkGraphSchema):
|
|
|
228
171
|
hard_timeout = soft_timeout + 10.0
|
|
229
172
|
try:
|
|
230
173
|
# Solve quickly to get states
|
|
231
|
-
|
|
174
|
+
raw, node_results, element_results, edge_results, net = await asyncio.wait_for(
|
|
232
175
|
asyncio.to_thread(_solve_sync, schema),
|
|
233
176
|
timeout=hard_timeout,
|
|
234
177
|
)
|
|
235
178
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
lbl = (e.data or {}).get("label") or ""
|
|
250
|
-
id_to_label[e.id] = lbl
|
|
251
|
-
id_to_kind[e.id] = (e.data or {}).get("type") or "flow"
|
|
252
|
-
# Also map solver auto-link IDs
|
|
253
|
-
auto_id = f"__auto_link__{e.source}__{e.target}"
|
|
254
|
-
id_to_label[auto_id] = lbl
|
|
255
|
-
id_to_kind[auto_id] = "lossless_connection"
|
|
256
|
-
|
|
257
|
-
_BOUNDARY_KINDS = {"mass_boundary", "pressure_boundary"}
|
|
258
|
-
|
|
259
|
-
# Per-node combustion method (empty string for non-combustor rows).
|
|
260
|
-
id_to_method: dict[str, str] = {}
|
|
261
|
-
for n in schema.nodes:
|
|
262
|
-
if n.type == "combustor":
|
|
263
|
-
id_to_method[n.id] = n.data.get("method", "complete")
|
|
264
|
-
|
|
265
|
-
# Species labels for composition column headers (``Y[N2]`` not ``Y[0]``).
|
|
266
|
-
import combaero as cb
|
|
267
|
-
|
|
268
|
-
species_labels: list[str] = list(cb.species.names)
|
|
269
|
-
|
|
270
|
-
data = []
|
|
271
|
-
|
|
272
|
-
# 1. Process Nodes
|
|
273
|
-
for node_id, res in node_results.items():
|
|
274
|
-
# Include all fields from StateResult (rho, mach, k, etc.)
|
|
275
|
-
state_data = res.state.model_dump()
|
|
276
|
-
# Handle Y/X list expansion
|
|
277
|
-
Y = state_data.pop("Y", [])
|
|
278
|
-
X = state_data.pop("X", None)
|
|
279
|
-
|
|
280
|
-
kind = id_to_kind.get(node_id, "")
|
|
281
|
-
base_data = {
|
|
282
|
-
"type": "node",
|
|
283
|
-
"kind": kind,
|
|
284
|
-
"is_boundary": kind in _BOUNDARY_KINDS,
|
|
285
|
-
"combustion_method": id_to_method.get(node_id, ""),
|
|
286
|
-
"id": node_id,
|
|
287
|
-
"label": id_to_label.get(node_id, ""),
|
|
288
|
-
**state_data,
|
|
289
|
-
}
|
|
290
|
-
# Flatten species fractions with species-name indexing.
|
|
291
|
-
for i, y_val in enumerate(Y):
|
|
292
|
-
name = species_labels[i] if i < len(species_labels) else str(i)
|
|
293
|
-
base_data[f"Y[{name}]"] = y_val
|
|
294
|
-
if X:
|
|
295
|
-
for i, x_val in enumerate(X):
|
|
296
|
-
name = species_labels[i] if i < len(species_labels) else str(i)
|
|
297
|
-
base_data[f"X[{name}]"] = x_val
|
|
298
|
-
|
|
299
|
-
data.append(base_data)
|
|
300
|
-
|
|
301
|
-
# 2. Process Flow Elements
|
|
302
|
-
for elem_id, res in element_results.items():
|
|
303
|
-
# Include all analytic/diagnostic fields (Re, Nu, h, q_dot, f, etc.)
|
|
304
|
-
elem_data = res.model_dump()
|
|
305
|
-
data.append(
|
|
306
|
-
{
|
|
307
|
-
"type": "element",
|
|
308
|
-
"kind": id_to_kind.get(elem_id, ""),
|
|
309
|
-
"is_boundary": False,
|
|
310
|
-
"combustion_method": "",
|
|
311
|
-
"id": elem_id,
|
|
312
|
-
"label": id_to_label.get(elem_id, ""),
|
|
313
|
-
**elem_data,
|
|
314
|
-
}
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
# 3. Process Thermal Walls (edge_results)
|
|
318
|
-
for wall_id, res in edge_results.items():
|
|
319
|
-
# thermal wall results are already a dict from _solve_sync
|
|
320
|
-
data.append(
|
|
321
|
-
{
|
|
322
|
-
"type": "thermal_wall",
|
|
323
|
-
"kind": "thermal_wall",
|
|
324
|
-
"is_boundary": False,
|
|
325
|
-
"combustion_method": "",
|
|
326
|
-
"id": wall_id,
|
|
327
|
-
"label": id_to_label.get(wall_id, ""),
|
|
328
|
-
**res,
|
|
329
|
-
}
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
df = pd.DataFrame(data)
|
|
333
|
-
|
|
334
|
-
# Rename columns with unit annotations (e.g. ``P`` -> ``P [Pa]``).
|
|
335
|
-
# Meta columns (``id``, ``type``, ``kind``, ``is_boundary``, ...) are
|
|
336
|
-
# passed through unchanged; unknown columns also remain untouched.
|
|
337
|
-
from gui.backend.units import label_with_unit
|
|
338
|
-
|
|
339
|
-
df = df.rename(columns={col: label_with_unit(col) for col in df.columns})
|
|
179
|
+
id_to_label, id_to_kind, id_to_method = _schema_maps(schema)
|
|
180
|
+
result_obj = NetworkResult(
|
|
181
|
+
raw=raw,
|
|
182
|
+
node_results=node_results,
|
|
183
|
+
element_results=element_results,
|
|
184
|
+
edge_results=edge_results,
|
|
185
|
+
net=net,
|
|
186
|
+
schema=schema,
|
|
187
|
+
id_to_label=id_to_label,
|
|
188
|
+
id_to_kind=id_to_kind,
|
|
189
|
+
id_to_method=id_to_method,
|
|
190
|
+
)
|
|
191
|
+
df = result_obj.to_dataframe()
|
|
340
192
|
|
|
341
193
|
# Stream as CSV
|
|
342
194
|
stream = io.StringIO()
|