turbo-design 1.3.8__py3-none-any.whl → 1.3.10__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.
- {turbo_design-1.3.8.dist-info → turbo_design-1.3.10.dist-info}/METADATA +2 -1
- turbo_design-1.3.10.dist-info/RECORD +46 -0
- {turbo_design-1.3.8.dist-info → turbo_design-1.3.10.dist-info}/WHEEL +1 -1
- turbodesign/__init__.py +57 -4
- turbodesign/agf.py +346 -0
- turbodesign/arrayfuncs.py +31 -1
- turbodesign/bladerow.py +238 -155
- turbodesign/compressor_math.py +386 -0
- turbodesign/compressor_spool.py +941 -0
- turbodesign/coolant.py +18 -6
- turbodesign/deviation/__init__.py +5 -0
- turbodesign/deviation/axial_compressor.py +3 -0
- turbodesign/deviation/carter_deviation.py +79 -0
- turbodesign/deviation/deviation_base.py +20 -0
- turbodesign/deviation/fixed_deviation.py +42 -0
- turbodesign/enums.py +5 -6
- turbodesign/flow_math.py +158 -0
- turbodesign/inlet.py +126 -56
- turbodesign/isentropic.py +59 -15
- turbodesign/loss/__init__.py +3 -1
- turbodesign/loss/compressor/OTAC_README.md +39 -0
- turbodesign/loss/compressor/__init__.py +54 -0
- turbodesign/loss/compressor/diffusion.py +61 -0
- turbodesign/loss/compressor/lieblein.py +1 -0
- turbodesign/loss/compressor/otac.py +799 -0
- turbodesign/loss/compressor/references/schobeiri-2012-shock-loss-model-for-transonic-and-supersonic-axial-compressors-with-curved-blades.pdf +0 -0
- turbodesign/loss/fixedpolytropic.py +27 -0
- turbodesign/loss/fixedpressureloss.py +30 -0
- turbodesign/loss/losstype.py +2 -30
- turbodesign/loss/turbine/TD2.py +25 -29
- turbodesign/loss/turbine/__init__.py +0 -1
- turbodesign/loss/turbine/ainleymathieson.py +6 -5
- turbodesign/loss/turbine/craigcox.py +6 -5
- turbodesign/loss/turbine/fixedefficiency.py +8 -7
- turbodesign/loss/turbine/kackerokapuu.py +7 -5
- turbodesign/loss/turbine/traupel.py +17 -16
- turbodesign/outlet.py +81 -22
- turbodesign/passage.py +98 -63
- turbodesign/row_factory.py +129 -0
- turbodesign/solve_radeq.py +9 -10
- turbodesign/{td_math.py → turbine_math.py} +144 -185
- turbodesign/turbine_spool.py +1219 -0
- turbo_design-1.3.8.dist-info/RECORD +0 -33
- turbodesign/compressorspool.py +0 -60
- turbodesign/loss/turbine/fixedpressureloss.py +0 -25
- turbodesign/rotor.py +0 -38
- turbodesign/spool.py +0 -317
- turbodesign/turbinespool.py +0 -543
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
"""OTAC compressor loss model translations (best effort).
|
|
2
|
+
|
|
3
|
+
This module ports the OTAC ``*.int`` compressor loss models into the current
|
|
4
|
+
Python API. Geometry/flow mappings assume ``upstream`` is ``FL_IR`` and ``row``
|
|
5
|
+
is ``FL_OR``. Many correlations require design parameters (e.g., blade counts,
|
|
6
|
+
clearances); these are exposed as constructor arguments with pragmatic defaults.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import warnings
|
|
12
|
+
import numpy as np
|
|
13
|
+
import numpy.typing as npt
|
|
14
|
+
|
|
15
|
+
from ...bladerow import BladeRow
|
|
16
|
+
from ...enums import LossType
|
|
17
|
+
from ..losstype import LossBaseClass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _mean(val, default: float = 0.0) -> float:
|
|
21
|
+
try:
|
|
22
|
+
arr = np.asarray(val)
|
|
23
|
+
return float(np.mean(arr)) if arr.size else default
|
|
24
|
+
except Exception:
|
|
25
|
+
return default
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _mag(val) -> float:
|
|
29
|
+
arr = np.asarray(val)
|
|
30
|
+
return float(np.linalg.norm(arr))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _span(row: BladeRow) -> float:
|
|
34
|
+
r = np.asarray(row.r)
|
|
35
|
+
return float(np.max(r) - np.min(r))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _hub(row: BladeRow) -> float:
|
|
39
|
+
return float(np.min(np.asarray(row.r)))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _tip(row: BladeRow) -> float:
|
|
43
|
+
return float(np.max(np.asarray(row.r)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# High-level placeholders for axial/turbine/NASA correlations
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AxialCompressorAungier(LossBaseClass):
|
|
52
|
+
"""Aungier axial-compressor pressure-loss (omega)."""
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
super().__init__(LossType.Pressure)
|
|
56
|
+
|
|
57
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
58
|
+
Vm1 = _mean(getattr(upstream, "Vm", upstream.V))
|
|
59
|
+
Vm2 = _mean(getattr(row, "Vm", row.V))
|
|
60
|
+
Vt1 = _mean(getattr(upstream, "Vt", upstream.V))
|
|
61
|
+
Vt2 = _mean(getattr(row, "Vt", row.V))
|
|
62
|
+
M2 = _mean(getattr(row, "M", 0.0))
|
|
63
|
+
|
|
64
|
+
Vm1 = max(Vm1, 1e-6)
|
|
65
|
+
turning = abs(np.arctan2(Vt2, Vm2) - np.arctan2(Vt1, Vm1))
|
|
66
|
+
|
|
67
|
+
Df = abs(Vt2 - Vt1) / Vm1 + max(0.0, 1 - Vm2 / Vm1)
|
|
68
|
+
Deq = Df # stand-in for equivalent diffusion factor
|
|
69
|
+
|
|
70
|
+
omega_prof = 0.04 + 0.6 * Df**2 + 0.2 * max(0.0, Deq - 0.5)
|
|
71
|
+
omega_sec = 0.01 * (turning**2)
|
|
72
|
+
tip_clearance = float(getattr(row, "tip_clearance", 0.0))
|
|
73
|
+
omega_tip = 0.02 * tip_clearance
|
|
74
|
+
omega_shock = 0.0
|
|
75
|
+
if M2 > 0.9:
|
|
76
|
+
omega_shock = 0.02 * (M2 - 0.9) ** 2
|
|
77
|
+
|
|
78
|
+
omega = omega_prof + omega_sec + omega_tip + omega_shock
|
|
79
|
+
omega = np.maximum(omega, 0.0)
|
|
80
|
+
return np.full_like(row.r, omega, dtype=float)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AxialCompressorEntropy(LossBaseClass):
|
|
84
|
+
"""Koch-Smith axial-compressor loss (entropy-based)."""
|
|
85
|
+
|
|
86
|
+
def __init__(self):
|
|
87
|
+
super().__init__(LossType.Entropy)
|
|
88
|
+
|
|
89
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
90
|
+
Vm1 = _mean(getattr(upstream, "Vm", upstream.V))
|
|
91
|
+
Vm2 = _mean(getattr(row, "Vm", row.V))
|
|
92
|
+
Vt1 = _mean(getattr(upstream, "Vt", upstream.V))
|
|
93
|
+
Vt2 = _mean(getattr(row, "Vt", row.V))
|
|
94
|
+
M2 = _mean(getattr(row, "M", 0.0))
|
|
95
|
+
|
|
96
|
+
Vm1 = max(Vm1, 1e-6)
|
|
97
|
+
turning = abs(np.arctan2(Vt2, Vm2) - np.arctan2(Vt1, Vm1))
|
|
98
|
+
Df = abs(Vt2 - Vt1) / Vm1 + max(0.0, 1 - Vm2 / Vm1)
|
|
99
|
+
|
|
100
|
+
ds_over_cp = 0.03 + 0.5 * Df**2 + 0.1 * (turning**2)
|
|
101
|
+
if M2 > 1.0:
|
|
102
|
+
ds_over_cp += 0.05 * (M2 - 1.0) ** 2
|
|
103
|
+
|
|
104
|
+
return np.full_like(row.r, ds_over_cp, dtype=float)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AxialCompressorWrightMiller(LossBaseClass):
|
|
108
|
+
"""Wright-Miller axial compressor loss (pressure coefficient)."""
|
|
109
|
+
|
|
110
|
+
def __init__(self):
|
|
111
|
+
super().__init__(LossType.Pressure)
|
|
112
|
+
|
|
113
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
114
|
+
beta1 = _mean(np.degrees(getattr(row, "beta1", upstream.beta2)))
|
|
115
|
+
beta2 = _mean(np.degrees(getattr(row, "beta2", row.beta2)))
|
|
116
|
+
turning = abs(beta2 - beta1)
|
|
117
|
+
V_ratio = _mean(getattr(row, "V", row.V)) / max(_mean(getattr(upstream, "V", upstream.V)), 1e-6)
|
|
118
|
+
omega = abs(np.tan(np.radians(beta1)) - np.tan(np.radians(beta2)))
|
|
119
|
+
omega *= (0.5 + 0.5 * V_ratio) * (1 + turning / 90.0)
|
|
120
|
+
omega = np.maximum(omega, 0.0)
|
|
121
|
+
return np.full_like(row.r, omega, dtype=float)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AxialTurbineAinleyMathiesonOTAC(LossBaseClass):
|
|
125
|
+
"""Placeholder for OTAC axial turbine Ainley-Mathieson."""
|
|
126
|
+
|
|
127
|
+
def __init__(self):
|
|
128
|
+
super().__init__(LossType.Pressure)
|
|
129
|
+
|
|
130
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
131
|
+
warnings.warn("AxialTurbineAinleyMathiesonOTAC not yet translated; returning zeros.", stacklevel=2)
|
|
132
|
+
return np.zeros_like(row.r)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class AxialTurbineKackerOkapuuOTAC(LossBaseClass):
|
|
136
|
+
"""Placeholder for OTAC axial turbine Kacker-Okapuu."""
|
|
137
|
+
|
|
138
|
+
def __init__(self):
|
|
139
|
+
super().__init__(LossType.Pressure)
|
|
140
|
+
|
|
141
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
142
|
+
warnings.warn("AxialTurbineKackerOkapuuOTAC not yet translated; returning zeros.", stacklevel=2)
|
|
143
|
+
return np.zeros_like(row.r)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class NASA23B20(LossBaseClass):
|
|
147
|
+
"""NASA23B/20 compressor loss correlation (simplified profile + incidence)."""
|
|
148
|
+
|
|
149
|
+
def __init__(self):
|
|
150
|
+
super().__init__(LossType.Pressure)
|
|
151
|
+
|
|
152
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
153
|
+
beta1 = _mean(np.degrees(upstream.beta2))
|
|
154
|
+
beta2 = _mean(np.degrees(row.beta2))
|
|
155
|
+
turning = abs(beta2 - beta1)
|
|
156
|
+
Vm1 = _mean(getattr(upstream, "Vm", upstream.V))
|
|
157
|
+
Vm2 = _mean(getattr(row, "Vm", row.V))
|
|
158
|
+
Vm1 = max(Vm1, 1e-6)
|
|
159
|
+
DF = abs(_mean(getattr(row, "Vt", row.V)) - _mean(getattr(upstream, "Vt", upstream.V))) / Vm1 + max(
|
|
160
|
+
0.0, 1 - Vm2 / Vm1
|
|
161
|
+
)
|
|
162
|
+
omega_min = 0.06 + 0.4 * DF**2
|
|
163
|
+
i_ml = 1.5
|
|
164
|
+
incidence = abs(beta1 - i_ml)
|
|
165
|
+
omega = omega_min * (1 + (incidence / 15) ** 2) * (1 + turning / 120)
|
|
166
|
+
return np.full_like(row.r, omega, dtype=float)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class NASA74A(LossBaseClass):
|
|
170
|
+
"""NASA 74A 5-stage axial compressor loss (simplified)."""
|
|
171
|
+
|
|
172
|
+
def __init__(self):
|
|
173
|
+
super().__init__(LossType.Pressure)
|
|
174
|
+
|
|
175
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
176
|
+
Vm1 = _mean(getattr(upstream, "Vm", upstream.V))
|
|
177
|
+
Vm2 = _mean(getattr(row, "Vm", row.V))
|
|
178
|
+
Vm1 = max(Vm1, 1e-6)
|
|
179
|
+
Vt1 = _mean(getattr(upstream, "Vt", upstream.V))
|
|
180
|
+
Vt2 = _mean(getattr(row, "Vt", row.V))
|
|
181
|
+
DF = abs(Vt2 - Vt1) / Vm1 + max(0.0, 1 - Vm2 / Vm1)
|
|
182
|
+
if len(np.asarray(row.r)) > 1:
|
|
183
|
+
pct_rad = np.linspace(0, 1, len(row.r))
|
|
184
|
+
else:
|
|
185
|
+
pct_rad = np.array([0.5])
|
|
186
|
+
base = 0.02 + 0.08 * DF**2
|
|
187
|
+
radial_factor = 1 + 0.3 * pct_rad
|
|
188
|
+
omega = base * radial_factor
|
|
189
|
+
return np.array(omega, dtype=float)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class RadialInput(LossBaseClass):
|
|
193
|
+
"""Radial input helper (no additional loss)."""
|
|
194
|
+
|
|
195
|
+
def __init__(self):
|
|
196
|
+
super().__init__(LossType.Pressure)
|
|
197
|
+
|
|
198
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
199
|
+
return np.zeros_like(row.r)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class DiffuserVanelessStanitz(LossBaseClass):
|
|
203
|
+
"""Stanitz vaneless diffuser loss (pressure)."""
|
|
204
|
+
|
|
205
|
+
def __init__(self):
|
|
206
|
+
super().__init__(LossType.Enthalpy)
|
|
207
|
+
|
|
208
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
209
|
+
r_in = _tip(upstream)
|
|
210
|
+
r_out = _tip(row)
|
|
211
|
+
length = max(r_out - r_in, 1e-6)
|
|
212
|
+
Dh = 2 * _span(row) if _span(row) > 0 else 1.0
|
|
213
|
+
Re = abs(_mean(row.V)) * Dh * _mean(getattr(row, "rho", 1.0)) / max(getattr(row, "mu", 1.0), 1e-6)
|
|
214
|
+
Cf = 0.026 / (Re ** 0.2) if Re > 0 else 0.0
|
|
215
|
+
loss = Cf * length / Dh * (_mean(row.V) ** 2) / 2
|
|
216
|
+
loss = np.maximum(loss, 0.0)
|
|
217
|
+
return np.full_like(row.r, loss, dtype=float)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Impeller correlations (translated)
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class ImpellerBladeLoadingAungier(LossBaseClass):
|
|
226
|
+
"""Aungier blade loading loss."""
|
|
227
|
+
|
|
228
|
+
def __init__(self, number_of_blades: int = 12, splitter_le: float = 0.0, loading_coefficient: float = 1.0):
|
|
229
|
+
super().__init__(LossType.Enthalpy)
|
|
230
|
+
self.number_of_blades = number_of_blades
|
|
231
|
+
self.splitter_le = splitter_le
|
|
232
|
+
self.loading_coefficient = loading_coefficient
|
|
233
|
+
|
|
234
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
235
|
+
mean_blades = self.number_of_blades / 2 + (self.number_of_blades / 2) * (1 - self.splitter_le)
|
|
236
|
+
radius_exit = _tip(row)
|
|
237
|
+
radius_inlet = _hub(upstream)
|
|
238
|
+
lb = float(getattr(row, "chord", radius_exit - radius_inlet))
|
|
239
|
+
|
|
240
|
+
dVrel = 2 * np.pi * (2 * radius_exit) * _mean(row.U) * self.loading_coefficient / (mean_blades * lb)
|
|
241
|
+
dh_blade = dVrel**2 / 48.0
|
|
242
|
+
|
|
243
|
+
kbar = (_mean(row.phi) - _mean(upstream.phi)) / max(lb, 1e-6)
|
|
244
|
+
bbar = ((radius_exit - radius_inlet) + _span(row)) / 2.0
|
|
245
|
+
Vrelbar = (_mag(getattr(upstream, "W", upstream.V)) + _mag(getattr(row, "W", row.V))) / 2.0
|
|
246
|
+
dh_hub_shroud = (kbar * bbar * Vrelbar) ** 2 / 12.0
|
|
247
|
+
|
|
248
|
+
loss = dh_blade + dh_hub_shroud
|
|
249
|
+
loss = np.maximum(loss, 0.0)
|
|
250
|
+
return np.full_like(row.r, loss, dtype=float)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class ImpellerBladeLoadingCoppage(LossBaseClass):
|
|
254
|
+
"""Coppage blade loading loss."""
|
|
255
|
+
|
|
256
|
+
def __init__(
|
|
257
|
+
self,
|
|
258
|
+
number_of_blades: int = 12,
|
|
259
|
+
splitter_le: float = 0.0,
|
|
260
|
+
loading_coefficient: float = 1.0,
|
|
261
|
+
surge_vrel: float = 1.0,
|
|
262
|
+
):
|
|
263
|
+
super().__init__(LossType.Enthalpy)
|
|
264
|
+
self.number_of_blades = number_of_blades
|
|
265
|
+
self.splitter_le = splitter_le
|
|
266
|
+
self.loading_coefficient = loading_coefficient
|
|
267
|
+
self.surge_vrel = surge_vrel
|
|
268
|
+
|
|
269
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
270
|
+
Vrel_out = _mag(getattr(row, "W", row.V))
|
|
271
|
+
Kbl = 0.75 if self.splitter_le == 0 else 0.6
|
|
272
|
+
r_tip_in = _tip(upstream)
|
|
273
|
+
r_exit = _tip(row)
|
|
274
|
+
denom = (self.surge_vrel / max(Vrel_out, 1e-6)) * (
|
|
275
|
+
(self.number_of_blades / np.pi) * (1 - r_tip_in / max(r_exit, 1e-6)) + 2 * r_tip_in / max(r_exit, 1e-6)
|
|
276
|
+
)
|
|
277
|
+
Df = 1 - Vrel_out / max(self.surge_vrel, 1e-6) + Kbl * self.loading_coefficient / max(denom, 1e-6)
|
|
278
|
+
dh = 0.05 * Df**2 * (_mean(row.U) ** 2)
|
|
279
|
+
dh = np.maximum(dh, 0.0)
|
|
280
|
+
return np.full_like(row.r, dh, dtype=float)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class ImpellerClearanceJansen(LossBaseClass):
|
|
284
|
+
"""Jansen clearance loss."""
|
|
285
|
+
|
|
286
|
+
def __init__(
|
|
287
|
+
self,
|
|
288
|
+
number_of_blades: int = 12,
|
|
289
|
+
tip_clearance_axial: float = 0.0,
|
|
290
|
+
exit_blade_height: float | None = None,
|
|
291
|
+
loss_modifier: float = 1.0,
|
|
292
|
+
):
|
|
293
|
+
super().__init__(LossType.Enthalpy)
|
|
294
|
+
self.number_of_blades = number_of_blades
|
|
295
|
+
self.tip_clearance_axial = tip_clearance_axial
|
|
296
|
+
self.exit_blade_height = exit_blade_height
|
|
297
|
+
self.loss_modifier = loss_modifier
|
|
298
|
+
|
|
299
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
300
|
+
h_exit = self.exit_blade_height if self.exit_blade_height is not None else _span(row)
|
|
301
|
+
Vtheta = _mean(getattr(row, "Vt", row.V))
|
|
302
|
+
Vm = _mean(getattr(row, "Vm", row.V))
|
|
303
|
+
r_tip_in = _tip(upstream)
|
|
304
|
+
r_hub_in = _hub(upstream)
|
|
305
|
+
r_exit = _tip(row)
|
|
306
|
+
ratio = abs(r_exit - r_tip_in)
|
|
307
|
+
rho_ratio = _mean(getattr(row, "rho", 1.0)) / max(_mean(getattr(upstream, "rho", 1.0)), 1e-6)
|
|
308
|
+
inner = (4 * np.pi) / (h_exit * self.number_of_blades) * (
|
|
309
|
+
(r_tip_in**2 - r_hub_in**2) / (ratio * (1 + rho_ratio))
|
|
310
|
+
) * Vtheta * Vm
|
|
311
|
+
dh = 0.6 * (self.tip_clearance_axial / max(h_exit, 1e-6)) * Vtheta * np.sqrt(max(inner, 0.0))
|
|
312
|
+
dh *= self.loss_modifier
|
|
313
|
+
dh = np.maximum(dh, 0.0)
|
|
314
|
+
return np.full_like(row.r, dh, dtype=float)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class ImpellerDiscFrictionDaily(LossBaseClass):
|
|
318
|
+
"""Daily & Nece disc friction loss."""
|
|
319
|
+
|
|
320
|
+
def __init__(self, bf_gap: float | None = None, loss_modifier: float = 1.0):
|
|
321
|
+
super().__init__(LossType.Enthalpy)
|
|
322
|
+
self.bf_gap = bf_gap
|
|
323
|
+
self.loss_modifier = loss_modifier
|
|
324
|
+
|
|
325
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
326
|
+
radius_exit = _tip(row)
|
|
327
|
+
span = _span(row)
|
|
328
|
+
gap = self.bf_gap if self.bf_gap is not None else float(getattr(row, "tip_clearance", 0.0) * span)
|
|
329
|
+
gap = gap if gap > 0 else 1e-5
|
|
330
|
+
|
|
331
|
+
rho_avg = _mean([getattr(upstream, "rho", 0.0), getattr(row, "rho", 0.0)], 1.0)
|
|
332
|
+
mu = float(getattr(row, "mu", 0.0) or getattr(upstream, "mu", 0.0) or 1.0)
|
|
333
|
+
|
|
334
|
+
U = np.asarray(getattr(row, "U", row.omega * row.r))
|
|
335
|
+
W_in = _mag(getattr(upstream, "W", upstream.V))
|
|
336
|
+
if W_in == 0:
|
|
337
|
+
W_in = _mag(getattr(upstream, "V", 1e-6))
|
|
338
|
+
|
|
339
|
+
Re = np.abs(U) * radius_exit * rho_avg / mu
|
|
340
|
+
Re = np.maximum(Re, 1e-6)
|
|
341
|
+
|
|
342
|
+
f_df = np.where(
|
|
343
|
+
Re < 3e5,
|
|
344
|
+
3.7 * (gap / radius_exit) ** 0.1 / np.sqrt(Re),
|
|
345
|
+
0.102 * (gap / radius_exit) ** 0.1 / (Re ** 0.2),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
dh_disc = f_df * rho_avg * (radius_exit**2) * (U**3) / (4 * W_in)
|
|
349
|
+
loss = self.loss_modifier * dh_disc
|
|
350
|
+
|
|
351
|
+
cp = _mean([row.Cp, upstream.Cp], row.Cp)
|
|
352
|
+
ht_delta = cp * (np.asarray(row.T0) - np.asarray(upstream.T0))
|
|
353
|
+
cap = 0.25 * ht_delta
|
|
354
|
+
loss = np.minimum(loss, cap)
|
|
355
|
+
loss = np.maximum(loss, 0.0)
|
|
356
|
+
|
|
357
|
+
return np.array(loss, dtype=float)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class ImpellerIncidenceAungier(LossBaseClass):
|
|
361
|
+
"""Aungier incidence loss."""
|
|
362
|
+
|
|
363
|
+
def __init__(self, blade_inlet_angle_deg: float | None = None, loss_modifier: float = 1.0):
|
|
364
|
+
super().__init__(LossType.Enthalpy)
|
|
365
|
+
self.blade_inlet_angle_deg = blade_inlet_angle_deg
|
|
366
|
+
self.loss_modifier = loss_modifier
|
|
367
|
+
|
|
368
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
369
|
+
r_tip = _tip(upstream)
|
|
370
|
+
r_hub = _hub(upstream)
|
|
371
|
+
omega = float(getattr(upstream, "omega", 0.0))
|
|
372
|
+
V = _mag(upstream.V)
|
|
373
|
+
Vm = _mean(getattr(upstream, "Vm", upstream.V))
|
|
374
|
+
alpha = _mean(np.degrees(upstream.alpha1))
|
|
375
|
+
|
|
376
|
+
U_tip = omega * r_tip
|
|
377
|
+
U_hub = omega * r_hub
|
|
378
|
+
Vtheta_tip_rel = U_tip - _mean(getattr(upstream, "Vt", 0.0))
|
|
379
|
+
Vtheta_hub_rel = U_hub - _mean(getattr(upstream, "Vt", 0.0))
|
|
380
|
+
Vrel_tip = np.hypot(V * np.cos(np.radians(alpha)), Vtheta_tip_rel)
|
|
381
|
+
Vrel_hub = np.hypot(V * np.cos(np.radians(alpha)), Vtheta_hub_rel)
|
|
382
|
+
|
|
383
|
+
blade_beta = self.blade_inlet_angle_deg
|
|
384
|
+
if blade_beta is None:
|
|
385
|
+
beta_metal = getattr(upstream, "beta1_metal", None)
|
|
386
|
+
blade_beta = _mean(beta_metal if beta_metal is not None else np.degrees(upstream.beta1))
|
|
387
|
+
|
|
388
|
+
target = abs(Vm / max(np.cos(np.radians(blade_beta)), 1e-6))
|
|
389
|
+
|
|
390
|
+
dh_hub = 0.4 * (Vrel_hub - target) ** 2
|
|
391
|
+
dh_tip = 0.4 * (Vrel_tip - target) ** 2
|
|
392
|
+
dh_mean = 0.4 * (_mag(getattr(upstream, "W", upstream.V)) - target) ** 2
|
|
393
|
+
dh_inc = (dh_hub + dh_tip + 10 * dh_mean) / 12.0
|
|
394
|
+
dh_inc = np.maximum(dh_inc * self.loss_modifier, 0.0)
|
|
395
|
+
return np.full_like(row.r, dh_inc, dtype=float)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class ImpellerIncidenceConrad(LossBaseClass):
|
|
399
|
+
"""Conrad incidence loss."""
|
|
400
|
+
|
|
401
|
+
def __init__(
|
|
402
|
+
self,
|
|
403
|
+
leading_edge_thickness: float | None = None,
|
|
404
|
+
number_of_blades: int = 12,
|
|
405
|
+
blade_inlet_angle_deg: float | None = None,
|
|
406
|
+
radius_tip_inlet: float | None = None,
|
|
407
|
+
radius_hub_inlet: float | None = None,
|
|
408
|
+
f_incidence: float = 1.0,
|
|
409
|
+
loss_modifier_inc: float = 1.0,
|
|
410
|
+
):
|
|
411
|
+
super().__init__(LossType.Enthalpy)
|
|
412
|
+
self.leading_edge_thickness = leading_edge_thickness
|
|
413
|
+
self.number_of_blades = number_of_blades
|
|
414
|
+
self.blade_inlet_angle_deg = blade_inlet_angle_deg
|
|
415
|
+
self.radius_tip_inlet = radius_tip_inlet
|
|
416
|
+
self.radius_hub_inlet = radius_hub_inlet
|
|
417
|
+
self.f_incidence = f_incidence
|
|
418
|
+
self.loss_modifier_inc = loss_modifier_inc
|
|
419
|
+
|
|
420
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
421
|
+
r_tip = self.radius_tip_inlet if self.radius_tip_inlet is not None else _tip(upstream)
|
|
422
|
+
r_hub = self.radius_hub_inlet if self.radius_hub_inlet is not None else _hub(upstream)
|
|
423
|
+
le_thickness = self.leading_edge_thickness
|
|
424
|
+
if le_thickness is None:
|
|
425
|
+
chord = float(getattr(row, "chord", 0.0) or 1.0)
|
|
426
|
+
le_thickness = 0.02 * chord
|
|
427
|
+
|
|
428
|
+
blade_inlet_angle = self.blade_inlet_angle_deg
|
|
429
|
+
if blade_inlet_angle is None:
|
|
430
|
+
beta_metal = getattr(upstream, "beta1_metal", None)
|
|
431
|
+
blade_inlet_angle = _mean(beta_metal if beta_metal is not None else np.degrees(upstream.beta1))
|
|
432
|
+
|
|
433
|
+
area = float(np.pi * (r_tip**2 - r_hub**2))
|
|
434
|
+
beta_opt_rad = np.arctan(
|
|
435
|
+
area
|
|
436
|
+
/ (area - le_thickness * (r_tip - r_hub) * self.number_of_blades / 2.0)
|
|
437
|
+
* np.tan(np.radians(blade_inlet_angle))
|
|
438
|
+
)
|
|
439
|
+
beta_opt_deg = np.degrees(beta_opt_rad)
|
|
440
|
+
|
|
441
|
+
beta_in_deg = _mean(np.degrees(upstream.beta1))
|
|
442
|
+
Vrel = _mag(getattr(upstream, "W", upstream.V))
|
|
443
|
+
if Vrel == 0:
|
|
444
|
+
Vrel = _mag(upstream.V)
|
|
445
|
+
|
|
446
|
+
Wui = Vrel * np.sin(np.radians(abs(beta_in_deg - beta_opt_deg)))
|
|
447
|
+
dh_incidence = self.f_incidence * 0.5 * Wui**2
|
|
448
|
+
|
|
449
|
+
loss = self.loss_modifier_inc * dh_incidence
|
|
450
|
+
loss = np.maximum(loss, 0.0)
|
|
451
|
+
return np.full_like(row.r, loss, dtype=float)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class ImpellerLeakageAungier(LossBaseClass):
|
|
455
|
+
"""Aungier tip-leakage loss."""
|
|
456
|
+
|
|
457
|
+
def __init__(
|
|
458
|
+
self,
|
|
459
|
+
number_of_blades: int = 12,
|
|
460
|
+
splitter_le: float = 0.0,
|
|
461
|
+
seal_clearance: float | None = None,
|
|
462
|
+
loading_coefficient: float = 1.0,
|
|
463
|
+
loss_modifier: float = 1.0,
|
|
464
|
+
):
|
|
465
|
+
super().__init__(LossType.Enthalpy)
|
|
466
|
+
self.number_of_blades = number_of_blades
|
|
467
|
+
self.splitter_le = splitter_le
|
|
468
|
+
self.seal_clearance = seal_clearance
|
|
469
|
+
self.loading_coefficient = loading_coefficient
|
|
470
|
+
self.loss_modifier = loss_modifier
|
|
471
|
+
|
|
472
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
473
|
+
mean_blades = self.number_of_blades / 2 + (self.number_of_blades / 2) * (1 - self.splitter_le)
|
|
474
|
+
r_exit = _tip(row)
|
|
475
|
+
r_in = _hub(upstream)
|
|
476
|
+
r_tip_in = _tip(upstream)
|
|
477
|
+
bwidth = _span(row)
|
|
478
|
+
rbar = (r_in + r_exit) / 2.0
|
|
479
|
+
bbar = ((r_tip_in - r_in) + bwidth) / 2.0
|
|
480
|
+
lb = float(getattr(row, "chord", r_exit - r_in))
|
|
481
|
+
|
|
482
|
+
Vtheta_out = _mean(getattr(row, "Vt", row.V))
|
|
483
|
+
Vtheta_in = _mean(getattr(upstream, "Vt", upstream.V))
|
|
484
|
+
rho_out = _mean(getattr(row, "rho", 1.0))
|
|
485
|
+
deltaP = rho_out * abs(r_exit * Vtheta_out - r_in * Vtheta_in) / max(lb, 1e-6)
|
|
486
|
+
seal_gap = self.seal_clearance if self.seal_clearance is not None else float(getattr(row, "tip_clearance", 0.0) * bwidth)
|
|
487
|
+
|
|
488
|
+
Ucl = 0.816 * np.sqrt(max(2 * deltaP / max(rho_out, 1e-6), 0.0))
|
|
489
|
+
mdot_cl = rho_out * mean_blades * seal_gap * lb * Ucl
|
|
490
|
+
W_out = _mag(getattr(row, "W", row.V))
|
|
491
|
+
U_out = _mean(row.U)
|
|
492
|
+
dh_leakage = (mdot_cl * Ucl) / max(2 * W_out * max(U_out, 1e-6), 1e-6) * U_out**2
|
|
493
|
+
|
|
494
|
+
loss = self.loss_modifier * dh_leakage
|
|
495
|
+
loss = np.maximum(loss, 0.0)
|
|
496
|
+
return np.full_like(row.r, loss, dtype=float)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class ImpellerMixingAungier(LossBaseClass):
|
|
500
|
+
"""Aungier & Dean mixing loss."""
|
|
501
|
+
|
|
502
|
+
def __init__(
|
|
503
|
+
self,
|
|
504
|
+
number_of_blades: int = 12,
|
|
505
|
+
splitter_le: float = 0.0,
|
|
506
|
+
loading_coefficient: float = 1.0,
|
|
507
|
+
area_exit_factor: float = 0.9,
|
|
508
|
+
loss_modifier: float = 1.0,
|
|
509
|
+
):
|
|
510
|
+
super().__init__(LossType.Enthalpy)
|
|
511
|
+
self.number_of_blades = number_of_blades
|
|
512
|
+
self.splitter_le = splitter_le
|
|
513
|
+
self.loading_coefficient = loading_coefficient
|
|
514
|
+
self.area_exit_factor = area_exit_factor
|
|
515
|
+
self.loss_modifier = loss_modifier
|
|
516
|
+
|
|
517
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
518
|
+
mean_blades = self.number_of_blades / 2 + (self.number_of_blades / 2) * (1 - self.splitter_le)
|
|
519
|
+
r_exit = _tip(row)
|
|
520
|
+
lb = float(getattr(row, "chord", r_exit - _hub(upstream)))
|
|
521
|
+
dVrel = 2 * np.pi * (2 * r_exit) * _mean(row.U) * self.loading_coefficient / (mean_blades * lb)
|
|
522
|
+
Vrel_max = (_mag(getattr(upstream, "W", upstream.V)) + _mag(getattr(row, "W", row.V)) + dVrel) / 2
|
|
523
|
+
Vrel_out = _mag(getattr(row, "W", row.V))
|
|
524
|
+
Deq = Vrel_max / max(Vrel_out, 1e-6)
|
|
525
|
+
|
|
526
|
+
if Deq <= 2:
|
|
527
|
+
Vrel_sep = Vrel_out
|
|
528
|
+
else:
|
|
529
|
+
Vrel_sep = Vrel_out * Deq / 2
|
|
530
|
+
|
|
531
|
+
Vm = _mean(getattr(row, "Vm", row.V))
|
|
532
|
+
Vtheta_rel = _mean(getattr(row, "Wt", row.Vt if hasattr(row, "Vt") else row.V))
|
|
533
|
+
area_exit = float(np.pi * (r_exit**2 - _hub(row) ** 2))
|
|
534
|
+
Vrel_out_eff = np.hypot(Vm * area_exit * self.area_exit_factor / max(np.pi * _span(row) * r_exit * 2, 1e-6), Vtheta_rel)
|
|
535
|
+
|
|
536
|
+
dh_mix = 0.5 * (Vrel_sep - Vrel_out_eff) ** 2
|
|
537
|
+
cp = _mean([row.Cp, upstream.Cp], row.Cp)
|
|
538
|
+
cap = 0.3 * cp * (_mean(row.T0) - _mean(upstream.T0))
|
|
539
|
+
dh_mix = np.clip(dh_mix, 0.0, cap)
|
|
540
|
+
|
|
541
|
+
loss = self.loss_modifier * dh_mix
|
|
542
|
+
return np.full_like(row.r, loss, dtype=float)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class ImpellerMixingJohnston(LossBaseClass):
|
|
546
|
+
"""Johnston & Dean mixing loss."""
|
|
547
|
+
|
|
548
|
+
def __init__(self, bstar: float = 1.0, loss_modifier: float = 1.0):
|
|
549
|
+
super().__init__(LossType.Enthalpy)
|
|
550
|
+
self.bstar = bstar
|
|
551
|
+
self.loss_modifier = loss_modifier
|
|
552
|
+
|
|
553
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
554
|
+
omega = float(getattr(upstream, "omega", 0.0))
|
|
555
|
+
r_tip = _tip(upstream)
|
|
556
|
+
Vtheta = _mean(getattr(upstream, "Vt", upstream.V))
|
|
557
|
+
V = _mag(upstream.V)
|
|
558
|
+
alpha = _mean(np.degrees(upstream.alpha1))
|
|
559
|
+
Vtheta_tip_rel = omega * r_tip - Vtheta
|
|
560
|
+
Vrel_tip = np.hypot(V * np.cos(np.radians(alpha)), Vtheta_tip_rel)
|
|
561
|
+
Vrel_out = _mag(getattr(row, "W", row.V))
|
|
562
|
+
wake = 1 - (1 / 0.45) * (Vrel_out / max(Vrel_tip, 1e-6))
|
|
563
|
+
wake = np.clip(wake, 0.0, 0.99)
|
|
564
|
+
|
|
565
|
+
Vtheta_out = _mean(getattr(row, "Vt", row.V))
|
|
566
|
+
Vm_out = _mean(getattr(row, "Vm", row.V))
|
|
567
|
+
coeff = 1.0 / (1 + (Vtheta_out / max(Vm_out, 1e-6)) ** 2)
|
|
568
|
+
dh = coeff * ((1 - wake - self.bstar) / max(1 - wake, 1e-6)) ** 2 * (_mag(row.V) ** 2 / 2)
|
|
569
|
+
dh = np.maximum(dh, 0.0) * self.loss_modifier
|
|
570
|
+
return np.full_like(row.r, dh, dtype=float)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
class ImpellerPrescribed(LossBaseClass):
|
|
574
|
+
"""Conrad incidence loss (prescribed percentage of enthalpy rise)."""
|
|
575
|
+
|
|
576
|
+
def __init__(self, loss_pct: float = 0.0):
|
|
577
|
+
super().__init__(LossType.Enthalpy)
|
|
578
|
+
self.loss_pct = loss_pct
|
|
579
|
+
|
|
580
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
581
|
+
cp = _mean([row.Cp, upstream.Cp], row.Cp)
|
|
582
|
+
ht_in = cp * np.asarray(upstream.T0)
|
|
583
|
+
ht_out = cp * np.asarray(row.T0)
|
|
584
|
+
dh = ht_out - ht_in
|
|
585
|
+
loss = self.loss_pct * dh
|
|
586
|
+
loss = np.maximum(loss, 0) # clamp negative due to numerical noise
|
|
587
|
+
return np.array(loss, dtype=float)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
class ImpellerRecirculationAungier(LossBaseClass):
|
|
591
|
+
"""Aungier recirculation loss."""
|
|
592
|
+
|
|
593
|
+
def __init__(
|
|
594
|
+
self,
|
|
595
|
+
number_of_blades: int = 12,
|
|
596
|
+
splitter_le: float = 0.0,
|
|
597
|
+
loading_coefficient: float = 1.0,
|
|
598
|
+
lb: float | None = None,
|
|
599
|
+
loss_modifier: float = 1.0,
|
|
600
|
+
):
|
|
601
|
+
super().__init__(LossType.Enthalpy)
|
|
602
|
+
self.number_of_blades = number_of_blades
|
|
603
|
+
self.splitter_le = splitter_le
|
|
604
|
+
self.loading_coefficient = loading_coefficient
|
|
605
|
+
self.lb = lb
|
|
606
|
+
self.loss_modifier = loss_modifier
|
|
607
|
+
|
|
608
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
609
|
+
mean_blades = self.number_of_blades / 2 + (self.number_of_blades / 2) * (1 - self.splitter_le)
|
|
610
|
+
r_exit = _tip(row)
|
|
611
|
+
lb = self.lb if self.lb is not None else float(getattr(row, "chord", r_exit - _hub(upstream)))
|
|
612
|
+
dVrel = 2 * np.pi * (2 * r_exit) * _mean(row.U) * self.loading_coefficient / (mean_blades * lb)
|
|
613
|
+
Vrel_max = (_mag(getattr(upstream, "W", upstream.V)) + _mag(getattr(row, "W", row.V)) + dVrel) / 2
|
|
614
|
+
Deq = Vrel_max / max(_mag(getattr(row, "W", row.V)), 1e-6)
|
|
615
|
+
|
|
616
|
+
Vtheta_rel = _mean(getattr(row, "Wt", row.Vt if hasattr(row, "Vt") else row.V))
|
|
617
|
+
Vm = _mean(getattr(row, "Vm", row.V))
|
|
618
|
+
beta = np.degrees(np.arctan2(Vtheta_rel, max(Vm, 1e-6)))
|
|
619
|
+
dh = (Deq / 2 - 1) * (abs(Vtheta_rel) / max(Vm, 1e-6) - 2 * (1 / max(np.tan(np.radians(beta)), 1e-6))) * (
|
|
620
|
+
_mean(row.U) ** 2
|
|
621
|
+
)
|
|
622
|
+
cp = _mean([row.Cp, upstream.Cp], row.Cp)
|
|
623
|
+
cap = 0.25 * cp * (_mean(row.T0) - _mean(upstream.T0))
|
|
624
|
+
dh = np.clip(dh, 0.0, cap) * self.loss_modifier
|
|
625
|
+
return np.full_like(row.r, dh, dtype=float)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
class ImpellerRecirculationOh(LossBaseClass):
|
|
629
|
+
"""Oh recirculation loss."""
|
|
630
|
+
|
|
631
|
+
def __init__(
|
|
632
|
+
self,
|
|
633
|
+
splitter_le: float = 0.0,
|
|
634
|
+
loading_coefficient: float = 1.0,
|
|
635
|
+
number_of_blades: int = 12,
|
|
636
|
+
radius_tip_inlet: float | None = None,
|
|
637
|
+
radius_exit: float | None = None,
|
|
638
|
+
surge_vrel: float = 1.0,
|
|
639
|
+
loss_modifier: float = 1.0,
|
|
640
|
+
):
|
|
641
|
+
super().__init__(LossType.Enthalpy)
|
|
642
|
+
self.splitter_le = splitter_le
|
|
643
|
+
self.loading_coefficient = loading_coefficient
|
|
644
|
+
self.number_of_blades = number_of_blades
|
|
645
|
+
self.radius_tip_inlet = radius_tip_inlet
|
|
646
|
+
self.radius_exit = radius_exit
|
|
647
|
+
self.surge_vrel = surge_vrel
|
|
648
|
+
self.loss_modifier = loss_modifier
|
|
649
|
+
|
|
650
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
651
|
+
r_tip_in = self.radius_tip_inlet if self.radius_tip_inlet is not None else _tip(upstream)
|
|
652
|
+
r_exit = self.radius_exit if self.radius_exit is not None else _tip(row)
|
|
653
|
+
omega = float(getattr(upstream, "omega", 0.0))
|
|
654
|
+
Vtheta = _mean(getattr(upstream, "Vt", upstream.V))
|
|
655
|
+
alpha = _mean(np.degrees(upstream.alpha1))
|
|
656
|
+
V = _mag(upstream.V)
|
|
657
|
+
|
|
658
|
+
U_tip = omega * r_tip_in
|
|
659
|
+
Vtheta_tip_rel = U_tip - Vtheta
|
|
660
|
+
Vrel_tip = np.hypot(V * np.cos(np.radians(alpha)), Vtheta_tip_rel)
|
|
661
|
+
|
|
662
|
+
Kbl = 0.75 if self.splitter_le == 0 else 0.6
|
|
663
|
+
Df = 1 - _mag(getattr(row, "W", row.V)) / max(self.surge_vrel, 1e-6) + Kbl * self.loading_coefficient / max(
|
|
664
|
+
(self.surge_vrel / max(_mag(getattr(row, "W", row.V)), 1e-6))
|
|
665
|
+
* ((self.number_of_blades / np.pi) * (1 - r_tip_in / max(r_exit, 1e-6)) + 2 * r_tip_in / max(r_exit, 1e-6)),
|
|
666
|
+
1e-6,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
dh = 8e-5 * np.sinh(3.5 * (np.radians(_mean(row.alpha2)) ** 3)) * Df**2 * (_mean(row.U) ** 2)
|
|
670
|
+
cp = _mean([row.Cp, upstream.Cp], row.Cp)
|
|
671
|
+
cap = 0.5 * cp * (_mean(row.T0) - _mean(upstream.T0))
|
|
672
|
+
dh = np.clip(dh * self.loss_modifier, 0.0, cap)
|
|
673
|
+
return np.full_like(row.r, dh, dtype=float)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class ImpellerSkinFrictionCoppage(LossBaseClass):
|
|
677
|
+
"""Coppage skin friction loss (simplified)."""
|
|
678
|
+
|
|
679
|
+
def __init__(
|
|
680
|
+
self,
|
|
681
|
+
number_of_blades: int = 12,
|
|
682
|
+
roughness: float = 1e-5,
|
|
683
|
+
loss_modifier: float = 1.0,
|
|
684
|
+
splitters: bool = False,
|
|
685
|
+
):
|
|
686
|
+
super().__init__(LossType.Enthalpy)
|
|
687
|
+
self.number_of_blades = number_of_blades
|
|
688
|
+
self.roughness = roughness
|
|
689
|
+
self.loss_modifier = loss_modifier
|
|
690
|
+
self.splitters = splitters
|
|
691
|
+
|
|
692
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
693
|
+
r_exit = _tip(row)
|
|
694
|
+
bwidth = _span(row)
|
|
695
|
+
lambda_ratio = _hub(upstream) / max(_tip(upstream), 1e-6)
|
|
696
|
+
Dh = 2 * bwidth if bwidth > 0 else 1.0
|
|
697
|
+
U = _mean(row.U)
|
|
698
|
+
mu = float(getattr(row, "mu", 0.0) or getattr(upstream, "mu", 0.0) or 1.0)
|
|
699
|
+
rho = _mean(getattr(row, "rho", 1.0))
|
|
700
|
+
Re = abs(U) * Dh * rho / mu
|
|
701
|
+
Re = max(Re, 1e3)
|
|
702
|
+
Cf = 0.26 / (Re ** 0.25)
|
|
703
|
+
if self.roughness > 0:
|
|
704
|
+
Cf += 0.11 * (self.roughness / max(Dh, 1e-6)) ** 0.25
|
|
705
|
+
Wbar = 0.125 * (
|
|
706
|
+
_mag(upstream.V)
|
|
707
|
+
+ _mag(row.V)
|
|
708
|
+
+ _mag(getattr(upstream, "W", upstream.V))
|
|
709
|
+
+ 2 * _mag(getattr(row, "W", row.V))
|
|
710
|
+
+ 3 * _mag(getattr(row, "W", row.V))
|
|
711
|
+
)
|
|
712
|
+
Ksf = 7.0 if self.splitters else 5.6
|
|
713
|
+
dh = Ksf * Cf * (Wbar**2) / 2
|
|
714
|
+
dh = np.maximum(dh * self.loss_modifier, 0.0)
|
|
715
|
+
return np.full_like(row.r, dh, dtype=float)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
class ImpellerSkinFrictionJansen(LossBaseClass):
|
|
719
|
+
"""Jansen skin friction loss (simplified Casey/Colebrook form)."""
|
|
720
|
+
|
|
721
|
+
def __init__(self, roughness: float = 1e-5, loss_modifier: float = 1.0):
|
|
722
|
+
super().__init__(LossType.Enthalpy)
|
|
723
|
+
self.roughness = roughness
|
|
724
|
+
self.loss_modifier = loss_modifier
|
|
725
|
+
|
|
726
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
727
|
+
bwidth = _span(row)
|
|
728
|
+
Dh = 2 * bwidth if bwidth > 0 else 1.0
|
|
729
|
+
U = _mean(row.U)
|
|
730
|
+
mu = float(getattr(row, "mu", 0.0) or getattr(upstream, "mu", 0.0) or 1.0)
|
|
731
|
+
rho = _mean(getattr(row, "rho", 1.0))
|
|
732
|
+
Re = abs(U) * Dh * rho / mu
|
|
733
|
+
Re = max(Re, 1e3)
|
|
734
|
+
# Swamee-Jain approximation for Colebrook
|
|
735
|
+
Cf = 0.25 / (np.log10(self.roughness / (3.7 * Dh) + 5.74 / (Re**0.9)) ** 2)
|
|
736
|
+
Wbar = _mag(getattr(row, "W", row.V))
|
|
737
|
+
dh = Cf * (Wbar**2) / 2
|
|
738
|
+
dh = np.maximum(dh * self.loss_modifier, 0.0)
|
|
739
|
+
return np.full_like(row.r, dh, dtype=float)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
class ImpellerVarious(LossBaseClass):
|
|
743
|
+
"""Aggregate loss using multiple sub-correlations."""
|
|
744
|
+
|
|
745
|
+
def __init__(self):
|
|
746
|
+
super().__init__(LossType.Enthalpy)
|
|
747
|
+
# Compose key submodels with default parameters
|
|
748
|
+
self.blade_loading = ImpellerBladeLoadingCoppage()
|
|
749
|
+
self.clearance = ImpellerClearanceJansen()
|
|
750
|
+
self.mixing = ImpellerMixingJohnston()
|
|
751
|
+
self.disc_friction = ImpellerDiscFrictionDaily()
|
|
752
|
+
self.leakage = ImpellerLeakageAungier()
|
|
753
|
+
self.recirculation = ImpellerRecirculationOh()
|
|
754
|
+
self.incidence = ImpellerIncidenceConrad()
|
|
755
|
+
self.skin_friction = ImpellerSkinFrictionJansen()
|
|
756
|
+
|
|
757
|
+
def __call__(self, row: BladeRow, upstream: BladeRow) -> npt.NDArray:
|
|
758
|
+
components = [
|
|
759
|
+
self.blade_loading(row, upstream),
|
|
760
|
+
self.clearance(row, upstream),
|
|
761
|
+
self.mixing(row, upstream),
|
|
762
|
+
self.disc_friction(row, upstream),
|
|
763
|
+
self.leakage(row, upstream),
|
|
764
|
+
self.recirculation(row, upstream),
|
|
765
|
+
self.incidence(row, upstream),
|
|
766
|
+
self.skin_friction(row, upstream),
|
|
767
|
+
]
|
|
768
|
+
total = np.zeros_like(row.r, dtype=float)
|
|
769
|
+
for comp in components:
|
|
770
|
+
total = total + np.asarray(comp, dtype=float)
|
|
771
|
+
return total
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
__all__ = [
|
|
775
|
+
"AxialCompressorAungier",
|
|
776
|
+
"AxialCompressorEntropy",
|
|
777
|
+
"AxialCompressorWrightMiller",
|
|
778
|
+
"AxialTurbineAinleyMathiesonOTAC",
|
|
779
|
+
"AxialTurbineKackerOkapuuOTAC",
|
|
780
|
+
"DiffuserVanelessStanitz",
|
|
781
|
+
"ImpellerBladeLoadingAungier",
|
|
782
|
+
"ImpellerBladeLoadingCoppage",
|
|
783
|
+
"ImpellerClearanceJansen",
|
|
784
|
+
"ImpellerDiscFrictionDaily",
|
|
785
|
+
"ImpellerIncidenceAungier",
|
|
786
|
+
"ImpellerIncidenceConrad",
|
|
787
|
+
"ImpellerLeakageAungier",
|
|
788
|
+
"ImpellerMixingAungier",
|
|
789
|
+
"ImpellerMixingJohnston",
|
|
790
|
+
"ImpellerPrescribed",
|
|
791
|
+
"ImpellerRecirculationAungier",
|
|
792
|
+
"ImpellerRecirculationOh",
|
|
793
|
+
"ImpellerSkinFrictionCoppage",
|
|
794
|
+
"ImpellerSkinFrictionJansen",
|
|
795
|
+
"ImpellerVarious",
|
|
796
|
+
"NASA23B20",
|
|
797
|
+
"NASA74A",
|
|
798
|
+
"RadialInput",
|
|
799
|
+
]
|