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,386 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import numpy.typing as npt
|
|
7
|
+
from scipy.optimize import minimize_scalar, minimize
|
|
8
|
+
|
|
9
|
+
from pyturbo.helper import convert_to_ndarray
|
|
10
|
+
|
|
11
|
+
from .bladerow import BladeRow, compute_gas_constants
|
|
12
|
+
from .enums import LossType, RowType
|
|
13
|
+
from .isentropic import IsenP, IsenT, solve_for_mach
|
|
14
|
+
from .turbine_math import T0_coolant_weighted_average
|
|
15
|
+
from .flow_math import compute_massflow, compute_streamline_areas
|
|
16
|
+
from .outlet import OutletType
|
|
17
|
+
|
|
18
|
+
__all__ = ["stator_calc", "rotor_calc", "polytropic_efficiency"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def polytropic_efficiency(pi: float, tau: float, gamma: float) -> float:
|
|
22
|
+
"""Compute polytropic efficiency from pressure/temperature ratios.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
pi: Total-pressure ratio (pt,out / pt,in).
|
|
26
|
+
tau: Total-temperature ratio (Tt,out / Tt,in).
|
|
27
|
+
gamma: Ratio of specific heats.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Polytropic efficiency consistent with the provided ratios.
|
|
31
|
+
"""
|
|
32
|
+
pi_safe = max(pi, 1e-8)
|
|
33
|
+
tau_safe = max(tau, 1e-8)
|
|
34
|
+
ln_tau = np.log(tau_safe)
|
|
35
|
+
if abs(ln_tau) < 1e-12:
|
|
36
|
+
return 1.0
|
|
37
|
+
return ((gamma - 1.0) / gamma) * np.log(pi_safe) / ln_tau
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def stator_calc(row: BladeRow, upstream: BladeRow, calculate_vm: bool = True) -> None:
|
|
41
|
+
"""Solve compressor stator exit conditions by matching exit massflow."""
|
|
42
|
+
loss_fn = getattr(row, "loss_function", None)
|
|
43
|
+
loss_type = getattr(loss_fn, "loss_type", LossType.Pressure)
|
|
44
|
+
target_eta_poly = None
|
|
45
|
+
target_entropy = None
|
|
46
|
+
|
|
47
|
+
if loss_type == LossType.Pressure and callable(loss_fn):
|
|
48
|
+
row.Yp = loss_fn(row, upstream) # type: ignore[arg-type]
|
|
49
|
+
else:
|
|
50
|
+
row.Yp[:] = 0
|
|
51
|
+
if loss_type == LossType.Polytropic:
|
|
52
|
+
if np.any(row.eta_poly):
|
|
53
|
+
target_eta_poly = float(np.mean(row.eta_poly))
|
|
54
|
+
elif callable(loss_fn):
|
|
55
|
+
target_eta_poly = float(loss_fn(row, upstream)) # type: ignore[arg-type]
|
|
56
|
+
elif loss_type == LossType.Entropy:
|
|
57
|
+
if np.any(row.entropy_rise):
|
|
58
|
+
target_entropy = float(np.mean(row.entropy_rise))
|
|
59
|
+
elif callable(loss_fn):
|
|
60
|
+
target_entropy = float(loss_fn(row, upstream)) # type: ignore[arg-type]
|
|
61
|
+
|
|
62
|
+
def calculate_vm_func(M_guess: float, apply: bool = False) -> float:
|
|
63
|
+
"""Solve stator for a guessed Mach; returns massflow residual."""
|
|
64
|
+
T0_coolant_local = T0_coolant_weighted_average(row) if row.coolant is not None else 0.0
|
|
65
|
+
T0_local = upstream.T0 - T0_coolant_local
|
|
66
|
+
|
|
67
|
+
P0_local = row.P0
|
|
68
|
+
if row.row_type == RowType.IGV:
|
|
69
|
+
row.P0 = row.P0_is - row.Yp * (upstream.P0 - upstream.P)
|
|
70
|
+
else:
|
|
71
|
+
row.P0_is = P0_local + row.Yp * (upstream.P0 - upstream.P)
|
|
72
|
+
|
|
73
|
+
deviation_func = getattr(row, "deviation_function", None)
|
|
74
|
+
deviation = deviation_func(row, upstream) if callable(deviation_func) else 0.0
|
|
75
|
+
deviation_rad = np.radians(deviation)
|
|
76
|
+
|
|
77
|
+
M_local = np.full_like(row.area, M_guess, dtype=float)
|
|
78
|
+
P0_P = IsenP(M_local, row.gamma)
|
|
79
|
+
P_local = P0_local / P0_P
|
|
80
|
+
T0_T = IsenT(M_local, row.gamma)
|
|
81
|
+
T_local = T0_local / T0_T
|
|
82
|
+
V_local = M_local * np.sqrt(row.gamma * row.R * T_local)
|
|
83
|
+
Vm_local = V_local * np.cos(row.alpha2)
|
|
84
|
+
Vx_local = Vm_local * np.cos(row.phi)
|
|
85
|
+
Vr_local = Vm_local * np.sin(row.phi)
|
|
86
|
+
Vt_local = Vm_local * np.tan(row.alpha2 + deviation_rad)
|
|
87
|
+
|
|
88
|
+
rho_local = P_local / (row.R * T_local)
|
|
89
|
+
U_local = row.omega * row.r
|
|
90
|
+
Wt_local = Vt_local - U_local
|
|
91
|
+
alpha1_local = upstream.alpha2 + upstream.deviation if upstream.row_type == RowType.Rotor else row.alpha1
|
|
92
|
+
entropy_rise_local = 0.5 * (row.Cp + upstream.Cp) * np.log(T_local / upstream.T) - row.R * np.log(P_local / upstream.P)
|
|
93
|
+
|
|
94
|
+
# massflow integration (include blockage and optional coolant)
|
|
95
|
+
total_area, streamline_area = compute_streamline_areas(row)
|
|
96
|
+
n_streams = len(row.percent_hub_shroud)
|
|
97
|
+
massflow_local = np.zeros(n_streams, dtype=float)
|
|
98
|
+
massflow_fraction = np.array([1.0]) if n_streams <= 1 else np.linspace(0, 1, n_streams)
|
|
99
|
+
if n_streams <= 1:
|
|
100
|
+
massflow_local[0] = Vm_local[0] * rho_local[0] * streamline_area[0] * (1 - row.blockage)
|
|
101
|
+
total_massflow_no_coolant = massflow_local[0]
|
|
102
|
+
if row.coolant is not None:
|
|
103
|
+
massflow_local += massflow_fraction * row.coolant.massflow_percentage * total_massflow_no_coolant
|
|
104
|
+
total_massflow_local = massflow_local[-1]
|
|
105
|
+
else:
|
|
106
|
+
for j in range(1, len(row.percent_hub_shroud)):
|
|
107
|
+
Vm_seg = 0.5 * (Vm_local[j] + Vm_local[j - 1])
|
|
108
|
+
rho_seg = 0.5 * (rho_local[j] + rho_local[j - 1])
|
|
109
|
+
massflow_local[j] = Vm_seg * rho_seg * streamline_area[j] * (1 - row.blockage) + massflow_local[j - 1]
|
|
110
|
+
total_massflow_no_coolant = massflow_local[-1]
|
|
111
|
+
if row.coolant is not None:
|
|
112
|
+
massflow_local += massflow_fraction * row.coolant.massflow_percentage * total_massflow_no_coolant
|
|
113
|
+
total_massflow_local = massflow_local[-1]
|
|
114
|
+
|
|
115
|
+
if apply:
|
|
116
|
+
row.T0 = T0_local
|
|
117
|
+
row.P0 = P0_local
|
|
118
|
+
row.M = M_local
|
|
119
|
+
row.P = P_local
|
|
120
|
+
row.T = T_local
|
|
121
|
+
row.V = V_local
|
|
122
|
+
row.Vm = Vm_local
|
|
123
|
+
row.Vx = Vx_local
|
|
124
|
+
row.Vr = Vr_local
|
|
125
|
+
row.Vt = Vt_local
|
|
126
|
+
row.alpha1 = alpha1_local
|
|
127
|
+
row.beta1 = upstream.beta2
|
|
128
|
+
row.deviation[:] = deviation_rad
|
|
129
|
+
row.rho = rho_local
|
|
130
|
+
row.U = U_local
|
|
131
|
+
row.Wt = Wt_local
|
|
132
|
+
row.P0_stator_inlet = upstream.P0
|
|
133
|
+
row.entropy_rise = entropy_rise_local
|
|
134
|
+
row.total_area = total_area
|
|
135
|
+
row.area = streamline_area
|
|
136
|
+
row.massflow = massflow_local
|
|
137
|
+
row.total_massflow_no_coolant = total_massflow_no_coolant
|
|
138
|
+
row.total_massflow = total_massflow_local
|
|
139
|
+
# pi_local: stage total-pressure ratio (pt_out/pt_in); tau_local: total-temperature ratio (Tt_out/Tt_in)
|
|
140
|
+
pi_local = float(np.mean(row.P0) / np.mean(upstream.P0)) if np.all(row.P0) else 1.0
|
|
141
|
+
tau_local = float(np.mean(row.T0) / np.mean(upstream.T0)) if np.all(row.T0) else 1.0
|
|
142
|
+
row.eta_poly = polytropic_efficiency(pi_local, tau_local, row.gamma)
|
|
143
|
+
tau_is = (row.P0_is / upstream.P0) ** ((row.gamma - 1.0) / row.gamma)
|
|
144
|
+
row.T0_is = upstream.T0 * tau_is
|
|
145
|
+
target_massflow = getattr(upstream, "total_massflow", total_massflow_local)
|
|
146
|
+
return abs(target_massflow - total_massflow_local)
|
|
147
|
+
|
|
148
|
+
def solve_massflow_for_current_loss() -> None:
|
|
149
|
+
res = minimize_scalar(calculate_vm_func, bounds=[0.01, 1], method="bounded")
|
|
150
|
+
calculate_vm_func(res.x, apply=True)
|
|
151
|
+
|
|
152
|
+
if calculate_vm:
|
|
153
|
+
if loss_type == LossType.Polytropic and target_eta_poly is not None:
|
|
154
|
+
def obj(y: float) -> float:
|
|
155
|
+
row.Yp[:] = y
|
|
156
|
+
res_local = minimize_scalar(calculate_vm_func, bounds=[0.01, 1], method="bounded")
|
|
157
|
+
calculate_vm_func(res_local.x, apply=True)
|
|
158
|
+
return abs(float(row.eta_poly) - target_eta_poly)
|
|
159
|
+
|
|
160
|
+
res_y = minimize_scalar(obj, bounds=[0.0, 0.95], method="bounded")
|
|
161
|
+
row.Yp[:] = res_y.x
|
|
162
|
+
solve_massflow_for_current_loss()
|
|
163
|
+
elif loss_type == LossType.Entropy and target_entropy is not None:
|
|
164
|
+
def obj_entropy(y: float) -> float:
|
|
165
|
+
row.Yp[:] = y
|
|
166
|
+
res_local = minimize_scalar(calculate_vm_func, bounds=[0.01, 1], method="bounded")
|
|
167
|
+
calculate_vm_func(res_local.x, apply=True)
|
|
168
|
+
return abs(float(np.mean(row.entropy_rise)) - target_entropy)
|
|
169
|
+
|
|
170
|
+
res_y = minimize_scalar(obj_entropy, bounds=[0.0, 0.95], method="bounded")
|
|
171
|
+
row.Yp[:] = res_y.x
|
|
172
|
+
solve_massflow_for_current_loss()
|
|
173
|
+
else:
|
|
174
|
+
solve_massflow_for_current_loss()
|
|
175
|
+
else: # We know Vm, P0, T0, P
|
|
176
|
+
row.Vx = row.Vm * np.cos(row.phi)
|
|
177
|
+
row.Vr = row.Vm * np.sin(row.phi)
|
|
178
|
+
row.Vt = row.Vm * np.tan(row.alpha2)
|
|
179
|
+
row.V = np.sqrt(row.Vx ** 2 + row.Vr ** 2 + row.Vt ** 2)
|
|
180
|
+
row.T = row.P / (row.R * row.rho) # We know P, this is a guess
|
|
181
|
+
row.M = row.V / np.sqrt(row.gamma * row.R * row.T)
|
|
182
|
+
|
|
183
|
+
def rotor_calc(
|
|
184
|
+
row: BladeRow,
|
|
185
|
+
upstream: BladeRow,
|
|
186
|
+
calculate_vm: bool = True,
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Solve compressor rotor exit conditions.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
row: Rotor blade row being solved.
|
|
192
|
+
upstream: Upstream blade row providing inlet relative/absolute conditions.
|
|
193
|
+
calculate_vm: If True, iterates Mach to satisfy massflow; if False, assumes Vm known.
|
|
194
|
+
"""
|
|
195
|
+
loss_fn = getattr(row, "loss_function", None)
|
|
196
|
+
loss_type = getattr(loss_fn, "loss_type", LossType.Pressure)
|
|
197
|
+
target_eta_poly = None
|
|
198
|
+
target_entropy = None
|
|
199
|
+
|
|
200
|
+
if loss_type == LossType.Pressure and callable(loss_fn):
|
|
201
|
+
row.Yp = loss_fn(row, upstream) # type: ignore[arg-type]
|
|
202
|
+
else:
|
|
203
|
+
row.Yp[:] = 0
|
|
204
|
+
if loss_type == LossType.Polytropic:
|
|
205
|
+
if np.any(row.eta_poly):
|
|
206
|
+
target_eta_poly = float(np.mean(row.eta_poly))
|
|
207
|
+
elif callable(loss_fn):
|
|
208
|
+
target_eta_poly = float(loss_fn(row, upstream)) # type: ignore[arg-type]
|
|
209
|
+
elif loss_type == LossType.Entropy:
|
|
210
|
+
if np.any(row.entropy_rise):
|
|
211
|
+
target_entropy = float(np.mean(row.entropy_rise))
|
|
212
|
+
elif callable(loss_fn):
|
|
213
|
+
target_entropy = float(loss_fn(row, upstream)) # type: ignore[arg-type]
|
|
214
|
+
|
|
215
|
+
# Use the frozen target (if available) so diagnostic code can overwrite row.P0_ratio
|
|
216
|
+
# without changing the initial guess used by this solver.
|
|
217
|
+
P0_ratio_target = getattr(row, "P0_ratio_target", 0.0) or row.P0_ratio
|
|
218
|
+
row.P0 = upstream.P0 * P0_ratio_target
|
|
219
|
+
row.P0_is = row.P0 + row.Yp * (upstream.P0 - upstream.P)
|
|
220
|
+
|
|
221
|
+
# Upstream relative frame
|
|
222
|
+
upstream.U = upstream.rpm * np.pi / 30 * upstream.r
|
|
223
|
+
upstream.Wt = upstream.Vt - upstream.U
|
|
224
|
+
upstream.W = np.sqrt(upstream.Vx ** 2 + upstream.Wt ** 2 + upstream.Vr ** 2)
|
|
225
|
+
upstream.beta2 = np.arctan2(upstream.Wt, upstream.Vm)
|
|
226
|
+
upstream.T0R = upstream.T + upstream.W ** 2 / (2 * upstream.Cp)
|
|
227
|
+
upstream.P0R = upstream.P * (upstream.T0R / upstream.T) ** (upstream.gamma / (upstream.gamma - 1))
|
|
228
|
+
upstream.M_rel = upstream.W / np.sqrt(upstream.gamma * upstream.R * upstream.T)
|
|
229
|
+
upstream_rothalpy = upstream.T0R * upstream.Cp - 0.5 * upstream.U ** 2
|
|
230
|
+
|
|
231
|
+
if np.any(upstream_rothalpy < 0):
|
|
232
|
+
print('U is too high, reduce RPM or radius')
|
|
233
|
+
|
|
234
|
+
def calculate_vm_func(M_rel: float, apply: bool = False):
|
|
235
|
+
"""Compute Vm of rotor locally guessing relative mach number at rotor exit. This calculation is done to balance the massflow
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
M (float): Relative mach number at rotor exit
|
|
239
|
+
apply (bool, optional): Apply calculations. Defaults to False.
|
|
240
|
+
"""
|
|
241
|
+
# Use local scratch copies to avoid polluting row during optimizer iterations
|
|
242
|
+
deviation_func = getattr(row, "deviation_function", None)
|
|
243
|
+
deviation_val = deviation_func(row, upstream) if callable(deviation_func) else 0.0
|
|
244
|
+
deviation_rad = np.radians(deviation_val)
|
|
245
|
+
|
|
246
|
+
P0R_local = upstream.P0R - row.Yp * (upstream.P0R - upstream.P)
|
|
247
|
+
# Use rothalpy conservation: I = Cp*T0R - U^2/2 = const across rotor
|
|
248
|
+
U_local = row.omega * row.r
|
|
249
|
+
T0R_local = (upstream_rothalpy + 0.5 * U_local ** 2) / row.Cp
|
|
250
|
+
|
|
251
|
+
P_local = P0R_local / IsenP(M_rel, row.gamma)
|
|
252
|
+
|
|
253
|
+
P0R_P = P0R_local / P_local
|
|
254
|
+
T0R_T = P0R_P ** ((row.gamma - 1) / row.gamma)
|
|
255
|
+
T_local = T0R_local / T0R_T
|
|
256
|
+
W_local = np.sqrt(2 * row.Cp * (T0R_local - T_local))
|
|
257
|
+
|
|
258
|
+
if np.isnan(W_local).any() or np.any(T_local >= T0R_local):
|
|
259
|
+
return np.inf
|
|
260
|
+
|
|
261
|
+
Vr_local = W_local * np.sin(row.phi)
|
|
262
|
+
beta2_eff = row.beta2 + deviation_rad
|
|
263
|
+
Vm_local = W_local * np.cos(beta2_eff)
|
|
264
|
+
Wt_local = W_local * np.sin(beta2_eff)
|
|
265
|
+
Vx_local = Vm_local * np.cos(row.phi)
|
|
266
|
+
Vt_local = Wt_local + U_local
|
|
267
|
+
V_local = np.sqrt(Vr_local ** 2 + Vt_local ** 2 + Vx_local ** 2)
|
|
268
|
+
M_local = V_local / np.sqrt(row.gamma * row.R * T_local)
|
|
269
|
+
T0_local = T_local + V_local ** 2 / (2 * row.Cp)
|
|
270
|
+
P0_local = P_local * (T0_local / T_local) ** (row.gamma / (row.gamma - 1))
|
|
271
|
+
|
|
272
|
+
# compute massflow using locals (include blockage and optional coolant)
|
|
273
|
+
rho_local = P_local / (row.R * T_local)
|
|
274
|
+
total_area, streamline_area = compute_streamline_areas(row)
|
|
275
|
+
n_streams = len(row.percent_hub_shroud)
|
|
276
|
+
massflow_local = np.zeros(n_streams, dtype=float)
|
|
277
|
+
massflow_fraction = np.array([1.0]) if n_streams <= 1 else np.linspace(0, 1, n_streams)
|
|
278
|
+
if n_streams <= 1:
|
|
279
|
+
massflow_local[0] = Vm_local[0] * rho_local[0] * streamline_area[0] * (1 - row.blockage)
|
|
280
|
+
total_massflow_no_coolant = massflow_local[0]
|
|
281
|
+
if row.coolant is not None:
|
|
282
|
+
massflow_local += massflow_fraction * row.coolant.massflow_percentage * total_massflow_no_coolant
|
|
283
|
+
total_massflow_local = massflow_local[-1]
|
|
284
|
+
else:
|
|
285
|
+
for j in range(1, len(row.percent_hub_shroud)):
|
|
286
|
+
Vm_seg = 0.5 * (Vm_local[j] + Vm_local[j - 1])
|
|
287
|
+
rho_seg = 0.5 * (rho_local[j] + rho_local[j - 1])
|
|
288
|
+
massflow_local[j] = Vm_seg * rho_seg * streamline_area[j] * (1 - row.blockage) + massflow_local[j - 1]
|
|
289
|
+
total_massflow_no_coolant = massflow_local[-1]
|
|
290
|
+
if row.coolant is not None:
|
|
291
|
+
massflow_local += massflow_fraction * row.coolant.massflow_percentage * total_massflow_no_coolant
|
|
292
|
+
total_massflow_local = massflow_local[-1]
|
|
293
|
+
|
|
294
|
+
if apply:
|
|
295
|
+
row.P = P_local
|
|
296
|
+
row.T = T_local
|
|
297
|
+
row.W = W_local
|
|
298
|
+
row.Vr = Vr_local
|
|
299
|
+
row.Vm = Vm_local
|
|
300
|
+
row.Wt = Wt_local
|
|
301
|
+
row.Vx = Vx_local
|
|
302
|
+
row.Vt = Vt_local
|
|
303
|
+
row.V = V_local
|
|
304
|
+
row.M = M_local
|
|
305
|
+
row.U = U_local
|
|
306
|
+
row.T0 = T0_local
|
|
307
|
+
row.P0 = P0_local
|
|
308
|
+
row.P0R = P0R_local
|
|
309
|
+
row.T0R = T0R_local
|
|
310
|
+
row.alpha2 = np.arctan2(row.Vt, row.Vm)
|
|
311
|
+
row.M_rel = W_local / np.sqrt(row.gamma * row.R * T_local)
|
|
312
|
+
row.total_massflow = total_massflow_local
|
|
313
|
+
row.total_massflow_no_coolant = total_massflow_no_coolant
|
|
314
|
+
row.massflow = massflow_local
|
|
315
|
+
row.total_area = total_area
|
|
316
|
+
row.area = streamline_area
|
|
317
|
+
row.entropy_rise = 0.5 * (row.Cp + upstream.Cp) * np.log(T_local / upstream.T) - row.R * np.log(P_local / upstream.P)
|
|
318
|
+
row.deviation[:] = deviation_rad
|
|
319
|
+
# pi_local: stage total-pressure ratio (pt_out/pt_in); tau_local: total-temperature ratio (Tt_out/Tt_in)
|
|
320
|
+
pi_local = float(np.mean(row.P0) / np.mean(upstream.P0)) if np.all(row.P0) else 1.0
|
|
321
|
+
tau_local = float(np.mean(row.T0) / np.mean(upstream.T0)) if np.all(row.T0) else 1.0
|
|
322
|
+
row.eta_poly = polytropic_efficiency(pi_local, tau_local, row.gamma)
|
|
323
|
+
tau_is = (row.P0_is / upstream.P0) ** ((row.gamma - 1.0) / row.gamma)
|
|
324
|
+
row.T0_is = upstream.T0 * tau_is
|
|
325
|
+
|
|
326
|
+
return np.abs(upstream.total_massflow - total_massflow_local)
|
|
327
|
+
|
|
328
|
+
def solve_massflow_for_current_loss() -> None:
|
|
329
|
+
res = minimize_scalar(calculate_vm_func, bounds=[0.01, 1], method="bounded")
|
|
330
|
+
calculate_vm_func(res.x, apply=True)
|
|
331
|
+
|
|
332
|
+
if calculate_vm:
|
|
333
|
+
if loss_type == LossType.Polytropic and target_eta_poly is not None:
|
|
334
|
+
def obj(y: float) -> float:
|
|
335
|
+
row.Yp[:] = y
|
|
336
|
+
res_local = minimize_scalar(calculate_vm_func, bounds=[0.01, 1], method="bounded")
|
|
337
|
+
calculate_vm_func(res_local.x, apply=True)
|
|
338
|
+
return abs(float(row.eta_poly) - target_eta_poly)
|
|
339
|
+
|
|
340
|
+
res_y = minimize_scalar(obj, bounds=[0.0, 0.95], method="bounded")
|
|
341
|
+
row.Yp[:] = res_y.x
|
|
342
|
+
solve_massflow_for_current_loss()
|
|
343
|
+
elif loss_type == LossType.Entropy and target_entropy is not None:
|
|
344
|
+
def obj_entropy(y: float) -> float:
|
|
345
|
+
row.Yp[:] = y
|
|
346
|
+
res_local = minimize_scalar(calculate_vm_func, bounds=[0.01, 1], method="bounded")
|
|
347
|
+
calculate_vm_func(res_local.x, apply=True)
|
|
348
|
+
return abs(float(np.mean(row.entropy_rise)) - target_entropy)
|
|
349
|
+
|
|
350
|
+
res_y = minimize_scalar(obj_entropy, bounds=[0.0, 0.95], method="bounded")
|
|
351
|
+
row.Yp[:] = res_y.x
|
|
352
|
+
solve_massflow_for_current_loss()
|
|
353
|
+
else:
|
|
354
|
+
solve_massflow_for_current_loss()
|
|
355
|
+
else: # We know Vm from radeq, beta2 from blade angle, T0R from rothalpy
|
|
356
|
+
deviation_func = getattr(row, "deviation_function", None)
|
|
357
|
+
deviation_val = deviation_func(row, upstream) if callable(deviation_func) else 0.0
|
|
358
|
+
deviation_rad = np.radians(deviation_val)
|
|
359
|
+
beta2_eff = row.beta2 + deviation_rad
|
|
360
|
+
|
|
361
|
+
row.U = row.omega * row.r
|
|
362
|
+
row.T0R = (upstream_rothalpy + 0.5 * row.U ** 2) / row.Cp
|
|
363
|
+
row.P0R = upstream.P0R - row.Yp * (upstream.P0R - upstream.P)
|
|
364
|
+
|
|
365
|
+
row.Vr = row.Vm * np.sin(row.phi)
|
|
366
|
+
row.Vx = row.Vm * np.cos(row.phi)
|
|
367
|
+
|
|
368
|
+
# Compute W from velocity triangle (geometric closure)
|
|
369
|
+
row.W = row.Vm / np.cos(beta2_eff)
|
|
370
|
+
row.Wt = row.W * np.sin(beta2_eff)
|
|
371
|
+
row.Vt = row.Wt + row.U
|
|
372
|
+
|
|
373
|
+
row.alpha2 = np.arctan2(row.Vt, row.Vm)
|
|
374
|
+
row.V = np.sqrt(row.Vm ** 2 * (1 + np.tan(row.alpha2) ** 2))
|
|
375
|
+
|
|
376
|
+
# Update T from energy conservation in rotating frame
|
|
377
|
+
row.T = row.T0R - row.W ** 2 / (2 * row.Cp)
|
|
378
|
+
|
|
379
|
+
row.M = row.V / np.sqrt(row.gamma * row.R * row.T)
|
|
380
|
+
|
|
381
|
+
# Compute T0 first, then derive P0 from P0R to keep the velocity triangle consistent.
|
|
382
|
+
# P0/P0R = (T0/T0R)^(gamma/(gamma-1)) always holds since both reference the same static state.
|
|
383
|
+
row.M_rel = row.W / np.sqrt(row.gamma * row.R * row.T)
|
|
384
|
+
row.T0 = row.T + row.V ** 2 / (2 * row.Cp)
|
|
385
|
+
row.P0 = row.P0R * (row.T0 / row.T0R) ** (row.gamma / (row.gamma - 1))
|
|
386
|
+
compute_gas_constants(row)
|