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.
Files changed (48) hide show
  1. {turbo_design-1.3.8.dist-info → turbo_design-1.3.10.dist-info}/METADATA +2 -1
  2. turbo_design-1.3.10.dist-info/RECORD +46 -0
  3. {turbo_design-1.3.8.dist-info → turbo_design-1.3.10.dist-info}/WHEEL +1 -1
  4. turbodesign/__init__.py +57 -4
  5. turbodesign/agf.py +346 -0
  6. turbodesign/arrayfuncs.py +31 -1
  7. turbodesign/bladerow.py +238 -155
  8. turbodesign/compressor_math.py +386 -0
  9. turbodesign/compressor_spool.py +941 -0
  10. turbodesign/coolant.py +18 -6
  11. turbodesign/deviation/__init__.py +5 -0
  12. turbodesign/deviation/axial_compressor.py +3 -0
  13. turbodesign/deviation/carter_deviation.py +79 -0
  14. turbodesign/deviation/deviation_base.py +20 -0
  15. turbodesign/deviation/fixed_deviation.py +42 -0
  16. turbodesign/enums.py +5 -6
  17. turbodesign/flow_math.py +158 -0
  18. turbodesign/inlet.py +126 -56
  19. turbodesign/isentropic.py +59 -15
  20. turbodesign/loss/__init__.py +3 -1
  21. turbodesign/loss/compressor/OTAC_README.md +39 -0
  22. turbodesign/loss/compressor/__init__.py +54 -0
  23. turbodesign/loss/compressor/diffusion.py +61 -0
  24. turbodesign/loss/compressor/lieblein.py +1 -0
  25. turbodesign/loss/compressor/otac.py +799 -0
  26. turbodesign/loss/compressor/references/schobeiri-2012-shock-loss-model-for-transonic-and-supersonic-axial-compressors-with-curved-blades.pdf +0 -0
  27. turbodesign/loss/fixedpolytropic.py +27 -0
  28. turbodesign/loss/fixedpressureloss.py +30 -0
  29. turbodesign/loss/losstype.py +2 -30
  30. turbodesign/loss/turbine/TD2.py +25 -29
  31. turbodesign/loss/turbine/__init__.py +0 -1
  32. turbodesign/loss/turbine/ainleymathieson.py +6 -5
  33. turbodesign/loss/turbine/craigcox.py +6 -5
  34. turbodesign/loss/turbine/fixedefficiency.py +8 -7
  35. turbodesign/loss/turbine/kackerokapuu.py +7 -5
  36. turbodesign/loss/turbine/traupel.py +17 -16
  37. turbodesign/outlet.py +81 -22
  38. turbodesign/passage.py +98 -63
  39. turbodesign/row_factory.py +129 -0
  40. turbodesign/solve_radeq.py +9 -10
  41. turbodesign/{td_math.py → turbine_math.py} +144 -185
  42. turbodesign/turbine_spool.py +1219 -0
  43. turbo_design-1.3.8.dist-info/RECORD +0 -33
  44. turbodesign/compressorspool.py +0 -60
  45. turbodesign/loss/turbine/fixedpressureloss.py +0 -25
  46. turbodesign/rotor.py +0 -38
  47. turbodesign/spool.py +0 -317
  48. 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)