turbo-design 1.3.7__py3-none-any.whl → 1.3.9__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.

Potentially problematic release.


This version of turbo-design might be problematic. Click here for more details.

Files changed (49) hide show
  1. {turbo_design-1.3.7.dist-info → turbo_design-1.3.9.dist-info}/METADATA +2 -1
  2. turbo_design-1.3.9.dist-info/RECORD +46 -0
  3. {turbo_design-1.3.7.dist-info → turbo_design-1.3.9.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 +237 -155
  8. turbodesign/compressor_math.py +374 -0
  9. turbodesign/compressor_spool.py +837 -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 +159 -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/radeq.py +3 -2
  40. turbodesign/row_factory.py +129 -0
  41. turbodesign/solve_radeq.py +9 -10
  42. turbodesign/{td_math.py → turbine_math.py} +125 -175
  43. turbodesign/turbine_spool.py +984 -0
  44. turbo_design-1.3.7.dist-info/RECORD +0 -33
  45. turbodesign/compressorspool.py +0 -60
  46. turbodesign/loss/turbine/fixedpressureloss.py +0 -25
  47. turbodesign/rotor.py +0 -38
  48. turbodesign/spool.py +0 -317
  49. turbodesign/turbinespool.py +0 -543
@@ -0,0 +1,374 @@
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
+ T0R_local = upstream.T0R
248
+
249
+ P_local = P0R_local / IsenP(M_rel, row.gamma)
250
+ U_local = row.omega * row.r
251
+
252
+ P0R_P = P0R_local / P_local
253
+ T0R_T = P0R_P ** ((row.gamma - 1) / row.gamma)
254
+ T_local = T0R_local / T0R_T
255
+ W_local = np.sqrt(2 * row.Cp * (T0R_local - T_local))
256
+
257
+ if np.isnan(W_local).any() or np.any(T_local >= T0R_local):
258
+ return np.inf
259
+
260
+ Vr_local = W_local * np.sin(row.phi)
261
+ beta2_eff = row.beta2 + deviation_rad
262
+ Vm_local = W_local * np.cos(beta2_eff)
263
+ Wt_local = W_local * np.sin(beta2_eff)
264
+ Vx_local = Vm_local * np.cos(row.phi)
265
+ Vt_local = Wt_local + U_local
266
+ V_local = np.sqrt(Vr_local ** 2 + Vt_local ** 2 + Vx_local ** 2)
267
+ M_local = V_local / np.sqrt(row.gamma * row.R * T_local)
268
+ T0_local = T_local + V_local ** 2 / (2 * row.Cp)
269
+ P0_local = P_local * (T0_local / T_local) ** (row.gamma / (row.gamma - 1))
270
+
271
+ # compute massflow using locals (include blockage and optional coolant)
272
+ rho_local = P_local / (row.R * T_local)
273
+ total_area, streamline_area = compute_streamline_areas(row)
274
+ n_streams = len(row.percent_hub_shroud)
275
+ massflow_local = np.zeros(n_streams, dtype=float)
276
+ massflow_fraction = np.array([1.0]) if n_streams <= 1 else np.linspace(0, 1, n_streams)
277
+ if n_streams <= 1:
278
+ massflow_local[0] = Vm_local[0] * rho_local[0] * streamline_area[0] * (1 - row.blockage)
279
+ total_massflow_no_coolant = massflow_local[0]
280
+ if row.coolant is not None:
281
+ massflow_local += massflow_fraction * row.coolant.massflow_percentage * total_massflow_no_coolant
282
+ total_massflow_local = massflow_local[-1]
283
+ else:
284
+ for j in range(1, len(row.percent_hub_shroud)):
285
+ Vm_seg = 0.5 * (Vm_local[j] + Vm_local[j - 1])
286
+ rho_seg = 0.5 * (rho_local[j] + rho_local[j - 1])
287
+ massflow_local[j] = Vm_seg * rho_seg * streamline_area[j] * (1 - row.blockage) + massflow_local[j - 1]
288
+ total_massflow_no_coolant = massflow_local[-1]
289
+ if row.coolant is not None:
290
+ massflow_local += massflow_fraction * row.coolant.massflow_percentage * total_massflow_no_coolant
291
+ total_massflow_local = massflow_local[-1]
292
+
293
+ if apply:
294
+ row.P = P_local
295
+ row.T = T_local
296
+ row.W = W_local
297
+ row.Vr = Vr_local
298
+ row.Vm = Vm_local
299
+ row.Wt = Wt_local
300
+ row.Vx = Vx_local
301
+ row.Vt = Vt_local
302
+ row.V = V_local
303
+ row.M = M_local
304
+ row.U = U_local
305
+ row.T0 = T0_local
306
+ row.P0 = P0_local
307
+ row.P0R = P0R_local
308
+ row.T0R = T0R_local
309
+ row.alpha2 = np.arctan2(row.Vt, row.Vm)
310
+ row.M_rel = W_local / np.sqrt(row.gamma * row.R * T_local)
311
+ row.total_massflow = total_massflow_local
312
+ row.total_massflow_no_coolant = total_massflow_no_coolant
313
+ row.massflow = massflow_local
314
+ row.total_area = total_area
315
+ row.area = streamline_area
316
+ row.entropy_rise = 0.5 * (row.Cp + upstream.Cp) * np.log(T_local / upstream.T) - row.R * np.log(P_local / upstream.P)
317
+ row.deviation[:] = deviation_rad
318
+ # pi_local: stage total-pressure ratio (pt_out/pt_in); tau_local: total-temperature ratio (Tt_out/Tt_in)
319
+ pi_local = float(np.mean(row.P0) / np.mean(upstream.P0)) if np.all(row.P0) else 1.0
320
+ tau_local = float(np.mean(row.T0) / np.mean(upstream.T0)) if np.all(row.T0) else 1.0
321
+ row.eta_poly = polytropic_efficiency(pi_local, tau_local, row.gamma)
322
+ tau_is = (row.P0_is / upstream.P0) ** ((row.gamma - 1.0) / row.gamma)
323
+ row.T0_is = upstream.T0 * tau_is
324
+
325
+ return np.abs(upstream.total_massflow - total_massflow_local)
326
+
327
+ def solve_massflow_for_current_loss() -> None:
328
+ res = minimize_scalar(calculate_vm_func, bounds=[0.01, 1], method="bounded")
329
+ calculate_vm_func(res.x, apply=True)
330
+
331
+ if calculate_vm:
332
+ if loss_type == LossType.Polytropic and target_eta_poly is not None:
333
+ def obj(y: float) -> float:
334
+ row.Yp[:] = y
335
+ res_local = minimize_scalar(calculate_vm_func, bounds=[0.01, 1], method="bounded")
336
+ calculate_vm_func(res_local.x, apply=True)
337
+ return abs(float(row.eta_poly) - target_eta_poly)
338
+
339
+ res_y = minimize_scalar(obj, bounds=[0.0, 0.95], method="bounded")
340
+ row.Yp[:] = res_y.x
341
+ solve_massflow_for_current_loss()
342
+ elif loss_type == LossType.Entropy and target_entropy is not None:
343
+ def obj_entropy(y: float) -> float:
344
+ row.Yp[:] = y
345
+ res_local = minimize_scalar(calculate_vm_func, bounds=[0.01, 1], method="bounded")
346
+ calculate_vm_func(res_local.x, apply=True)
347
+ return abs(float(np.mean(row.entropy_rise)) - target_entropy)
348
+
349
+ res_y = minimize_scalar(obj_entropy, bounds=[0.0, 0.95], method="bounded")
350
+ row.Yp[:] = res_y.x
351
+ solve_massflow_for_current_loss()
352
+ else:
353
+ solve_massflow_for_current_loss()
354
+ else: # We know Vm, P0, T0
355
+ deviation_func = getattr(row, "deviation_function", None)
356
+ deviation_val = deviation_func(row, upstream) if callable(deviation_func) else 0.0
357
+ deviation_rad = np.radians(deviation_val)
358
+ beta2_eff = row.beta2 + deviation_rad
359
+ row.Vr = row.Vm*np.sin(row.phi)
360
+ row.Vx = row.Vm*np.cos(row.phi)
361
+ row.W = np.sqrt(2*row.Cp*(row.T0R-row.T))
362
+ row.Wt = row.W*np.sin(beta2_eff)
363
+ row.Vt = row.Wt+row.U
364
+
365
+ row.alpha2 = np.arctan2(row.Vt,row.Vm)
366
+ row.V = np.sqrt(row.Vm**2*(1+np.tan(row.alpha2)**2))
367
+
368
+ row.M = row.V/np.sqrt(row.gamma*row.R*row.T)
369
+ T0_T = (1+(row.gamma-1)/2 * row.M**2)
370
+ row.P0 = row.P * T0_T**(row.gamma/(row.gamma-1))
371
+
372
+ row.M_rel = row.W/np.sqrt(row.gamma*row.R*row.T)
373
+ row.T0 = row.T+row.V**2/(2*row.Cp)
374
+ compute_gas_constants(row)