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

@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: turbo-design
3
- Version: 1.3.6
3
+ Version: 1.3.8
4
4
  Summary: TurboDesign is a library used to design turbines and compressors using radial equilibrium.
5
5
  Author: Paht Juangphanich
6
6
  Author-email: paht.juangphanich@nasa.gov
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.10
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
14
15
  Requires-Dist: cantera
15
16
  Requires-Dist: findiff
16
17
  Requires-Dist: matplotlib
@@ -1,33 +1,33 @@
1
1
  turbodesign/__init__.py,sha256=N8Nu0I1vrlEHYJZZ7yhhg-FtbNbLyrgjy7hoicygUqg,325
2
2
  turbodesign/arrayfuncs.py,sha256=GHIlTyLfeNsNCQQoh5m1aXK3iyvU6RjdYQipkqpU_ps,519
3
- turbodesign/bladerow.py,sha256=351FMt9UtbFMAKtU-mDkJ12hmIdaaa4qF6K6Q-ZYkBM,25063
3
+ turbodesign/bladerow.py,sha256=Uad3UUnaRqCaEQBV1mHD04eU_lkz-BWr4n1vjFsX-eA,25472
4
4
  turbodesign/cantera_gas/co2.yaml,sha256=M2o_RzxV9B9rDkgkXJC-l3voKraFZguTZuKKt4F7S_c,887
5
5
  turbodesign/compressorspool.py,sha256=z8ZVczJ-EdZvIqqZArC6UdwC5MbU2SZh_MT2CGq5avY,2600
6
6
  turbodesign/coolant.py,sha256=evDtUFOYhfZKTVAsDWzxRzUX20gTvfjj0uybaWg4CsI,427
7
7
  turbodesign/enums.py,sha256=WC6VPmHw3UF1zR1SLjj8z00gIWSNso66MShFGoYRY4U,1164
8
8
  turbodesign/inlet.py,sha256=5uja906IA-_gca4FJykZvFLJ1he6HRy5pZDNQE1D2_g,7945
9
9
  turbodesign/isentropic.py,sha256=YLTmNx69e_M5fTuLOmk_KvaQ6ABv0aZsityfBihJOmI,2003
10
- turbodesign/loss/__init__.py,sha256=ZJomzXa6ElduFruURukCrFwJQXMWS2aW8JSaVQ-M2r8,46
10
+ turbodesign/loss/__init__.py,sha256=jltNIIbUntGKq2IfX3EKUNEajIcHdOB7pWuj5tLtmGw,66
11
11
  turbodesign/loss/compressor/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
12
- turbodesign/loss/losstype.py,sha256=Vzj3UPk1_5IA3DtL56Eb5Z7LkYyMQ97DGZvgfki8ByA,708
12
+ turbodesign/loss/losstype.py,sha256=ViUf1ue3DZ3qFAhLUP3ps2fyygNH6udekAr53vEzrY8,1776
13
13
  turbodesign/loss/turbine/TD2.py,sha256=evyTG4K6s56NGdjxrZFD3G7zsB8_2lJTBOxT3Ju2uG8,5207
14
14
  turbodesign/loss/turbine/__init__.py,sha256=d1UrbiIicO60FhDlOemo_irYW2asUaYoqlXUz-UG9zk,285
15
- turbodesign/loss/turbine/ainleymathieson.py,sha256=8UbugNmxI12I68HGctJe2lQpqjKZN1b40UOTzqU6hQQ,5861
16
- turbodesign/loss/turbine/craigcox.py,sha256=TrbNdpGdyD5kTRFKfNmbRDhCOjA0-GT5xoluLQpO1gA,9664
15
+ turbodesign/loss/turbine/ainleymathieson.py,sha256=uTl1wXcJZmNjeerGTYDYCs0vJhyMPjG_0C9xlvSKYqY,5911
16
+ turbodesign/loss/turbine/craigcox.py,sha256=2dydHVvxTKhmJsRHO7Vf2JFKM6xaUSeT_CPxf8PCtWs,9942
17
17
  turbodesign/loss/turbine/fixedefficiency.py,sha256=JJv4GUS5q9pvuf3akjBtlcHOykRYmRVFfDDrdWqH6S4,774
18
18
  turbodesign/loss/turbine/fixedpressureloss.py,sha256=sgkm6uQwz6Qz2LDCEmir46aJRSPLAdCKpDyR1hNdgQ8,682
19
- turbodesign/loss/turbine/kackerokapuu.py,sha256=hUmZdyA1hAhN3_xbwBhka2dyk0Aq9rqq62S2ghuKSOk,5071
20
- turbodesign/loss/turbine/traupel.py,sha256=aZxFE9JDcCfi1gy-fqS1SxjjlhuDG-H-g3LPurty1M0,4164
19
+ turbodesign/loss/turbine/kackerokapuu.py,sha256=TQkTKG6ZdpsdEp1-qJfacQR8EEZSQ1qjUmOTf3t7mws,7263
20
+ turbodesign/loss/turbine/traupel.py,sha256=8KDjalPgBW3GoZUL7jgsyPFoDwNfcQhzmeRNrsZA0XA,4369
21
21
  turbodesign/lossinterp.py,sha256=B2KEobp-nD9jwD6UINgBmTlH9kKyWg3UNvXxqfUsr-k,6198
22
22
  turbodesign/outlet.py,sha256=MaZJ-mDuLvh19AashnMQLx2XlR-Rk1r0HQS4kE0OxeI,2191
23
23
  turbodesign/passage.py,sha256=RZ8giKMcpJI7EnO4SEnbGYcPFmKdGyzWC0UZTXHnS6g,12181
24
- turbodesign/radeq.py,sha256=KiZybA8EtOKMzszstdlaR-sF5KI9nUBgYOGZYLU1x4I,6516
24
+ turbodesign/radeq.py,sha256=LEqYxZZocCZyVPhY09MNFTnAkEXM740-SR6QxXW8Ssg,6522
25
25
  turbodesign/rotor.py,sha256=tHl9o5H4aQ6Etd4gqa8Ime1UK7k0de4GLt5Yb1sJdGs,1376
26
26
  turbodesign/solve_radeq.py,sha256=nLYlRtXElPgHaoUP9jwMulRmYKTJs_uQ1eCulk2V59I,1938
27
27
  turbodesign/spool.py,sha256=81UFbBi5JKm0RJ_hjB40bKWeCXx0V2lt0b2VAwOtTDU,14376
28
28
  turbodesign/stage.py,sha256=UP45sDKDLsAkO_WfDWJ6kqXU7cYKh_4QO01QZnSN1oQ,166
29
- turbodesign/td_math.py,sha256=qlLlAMB3kHUMdkvr9sOKl-rPShDx-XQCqG9DHOr7Vis,16268
30
- turbodesign/turbinespool.py,sha256=vpmrNmZ23k2wU7b9GAZrT3dOpX3otkpTtjAlfCY92sQ,25271
31
- turbo_design-1.3.6.dist-info/METADATA,sha256=T0hurxrmU0EtnW6pWQzml9KF7o22etumb68C3wWU0gA,734
32
- turbo_design-1.3.6.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
33
- turbo_design-1.3.6.dist-info/RECORD,,
29
+ turbodesign/td_math.py,sha256=aL5lbRl7nB8hQA6HyyFTtNpAbJ1ybn1s8iybXIfqiO8,17549
30
+ turbodesign/turbinespool.py,sha256=REA5QmpRE58o-3cf4dtVS1ChCoriCcQmzCOssv9CYvc,25259
31
+ turbo_design-1.3.8.dist-info/METADATA,sha256=9XmBcAOWQZPo5yR2zQVkL-rVxLK8YKjkOQI-CGtaFjU,785
32
+ turbo_design-1.3.8.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
33
+ turbo_design-1.3.8.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
turbodesign/bladerow.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from dataclasses import field, Field
2
- from typing import Any, Callable, List, Optional, Tuple, Union
2
+ from typing import Any, Callable, List, Optional, Sequence, Tuple, Union
3
3
  from .enums import RowType, PowerType
4
4
  import numpy as np
5
5
  import numpy.typing as npt
@@ -9,7 +9,7 @@ from cantera import Solution, composite
9
9
  from .coolant import Coolant
10
10
  from pyturbo.helper import line2D
11
11
  from pyturbo.aero.airfoil2D import Airfoil2D
12
- from .loss import LossBaseClass
12
+ from .loss import LossBaseClass, CompositeLossModel
13
13
  from .passage import Passage
14
14
 
15
15
 
@@ -406,19 +406,25 @@ class BladeRow:
406
406
  return self.loss_function
407
407
 
408
408
  @loss_model.setter
409
- def loss_model(self, model:Callable[[Any], Any]):
410
- """Add in custom loss model
409
+ def loss_model(self, model:Union[LossBaseClass, Sequence[LossBaseClass]]):
410
+ """Assign one or more loss models that inherit :class:`LossBaseClass`.
411
411
 
412
412
  Args:
413
- model (function): custom loss function. Input will be of the format blade row
414
-
415
- Example:
416
-
417
- def mylossfunction(row:BladeRow) -> float
418
- code to do something with machine learning
419
- return pressure loss
413
+ model: Either a single loss model or a sequence of models.
420
414
  """
421
- self.loss_function = model # type: ignore
415
+ if isinstance(model, LossBaseClass):
416
+ self.loss_function = model
417
+ return
418
+
419
+ if isinstance(model, Sequence):
420
+ if len(model) == 0:
421
+ raise ValueError("At least one loss model must be provided.")
422
+ if not all(isinstance(m, LossBaseClass) for m in model):
423
+ raise TypeError("All entries must inherit LossBaseClass.")
424
+ self.loss_function = CompositeLossModel(model)
425
+ return
426
+
427
+ raise TypeError("Loss models must inherit LossBaseClass.")
422
428
 
423
429
  @property
424
430
  def te_pitch(self):
@@ -631,4 +637,4 @@ def compute_gas_constants(row:BladeRow,fluid:Optional[Solution]=None) -> None:
631
637
  row.gamma = row.Cp/row.Cv
632
638
  # Use Ideal Gas
633
639
  row.rho = row.P/(row.T*row.R)
634
- row.mu = sutherland(row.T) # type: ignore
640
+ row.mu = sutherland(row.T) # type: ignore
@@ -1 +1 @@
1
- from .losstype import LossType, LossBaseClass
1
+ from .losstype import LossType, LossBaseClass, CompositeLossModel
@@ -1,9 +1,10 @@
1
- from typing import Any, Dict
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict, Iterable, Tuple
2
3
  from ..lossinterp import LossInterp
3
4
  import os
4
5
  from ..enums import LossType
5
6
 
6
- class LossBaseClass:
7
+ class LossBaseClass(ABC):
7
8
  data: Dict[str,LossInterp]
8
9
  _loss_type:LossType
9
10
 
@@ -15,11 +16,40 @@ class LossBaseClass:
15
16
 
16
17
  self._loss_type = lossType
17
18
 
18
- def __call__(self, row:Any, upstream:Any):
19
- raise Exception("Method needs to be overridden")
19
+ @abstractmethod
20
+ def __call__(self, row:Any, upstream:Any) -> float:
21
+ """Evaluate the loss for the supplied blade row."""
22
+ raise NotImplementedError
20
23
 
21
24
 
22
25
 
23
26
  @property
24
27
  def loss_type(self):
25
- return self._loss_type
28
+ return self._loss_type
29
+
30
+
31
+ class CompositeLossModel(LossBaseClass):
32
+ """Combines multiple loss models of the same type."""
33
+
34
+ def __init__(self, models: Iterable[LossBaseClass]):
35
+ models_tuple: Tuple[LossBaseClass, ...] = tuple(models)
36
+ if not models_tuple:
37
+ raise ValueError("CompositeLossModel requires at least one loss model.")
38
+
39
+ loss_type = models_tuple[0].loss_type
40
+ for model in models_tuple[1:]:
41
+ if model.loss_type != loss_type:
42
+ raise ValueError("All loss models must share the same LossType.")
43
+
44
+ super().__init__(loss_type)
45
+ self._models: Tuple[LossBaseClass, ...] = models_tuple
46
+
47
+ def __call__(self, row: Any, upstream: Any) -> float:
48
+ total_loss = 0.0
49
+ for model in self._models:
50
+ total_loss += float(model(row, upstream))
51
+ return total_loss
52
+
53
+ @property
54
+ def models(self) -> Tuple[LossBaseClass, ...]:
55
+ return self._models
@@ -69,7 +69,7 @@ class AinleyMathieson(LossBaseClass):
69
69
  beta1 = -np.abs(np.radians(row.beta1_metal))
70
70
  alpha2 = np.abs(row.alpha2)
71
71
  if row.M<0.5:
72
- alpha2 = self.data['Fig05'](np.degrees(np.cos(row.throat/row.pitch)))
72
+ alpha2 = self.data['Fig05'](float(np.degrees(np.cos(row.throat/row.pitch))))
73
73
  elif row.M<0.95:
74
74
  X = 0.7
75
75
  alpha2 = np.arctan(
@@ -88,7 +88,7 @@ class AinleyMathieson(LossBaseClass):
88
88
  beta1 = np.abs(np.radians(row.beta1_metal))
89
89
  alpha2 = -np.abs(row.beta2)
90
90
  if row.M_rel<0.5:
91
- alpha2 = self.data['Fig05'](np.degrees(np.cos(row.throat/row.pitch)))
91
+ alpha2 = self.data['Fig05'](float(np.degrees(np.cos(row.throat/row.pitch))))
92
92
  elif row.M_rel<0.95:
93
93
  X = 1.35 # Shrouded Blade
94
94
  alpha2 = np.arctan(
@@ -106,18 +106,18 @@ class AinleyMathieson(LossBaseClass):
106
106
  ID = (upstream.r[-1]+row.r[-1])/2; OD = (upstream.r[-1]+row.r[-1])/2
107
107
  A1 = np.pi*(upstream.r[-1]**2 - upstream.r[0]**2)*np.cos(beta1)
108
108
  A2 = np.pi*(row.r[-1]**2 - row.r[0]**2)*np.cos(alpha2)
109
- lam = self.data['Fig08']((A2/A1)**2/(1+ID/OD)) # Eqn but using Figure 8
109
+ lam = self.data['Fig08'](float((A2/A1)**2/(1+ID/OD))) # Eqn but using Figure 8
110
110
  alpha_m = np.arctan((np.tan(alpha1) - np.tan(alpha2))/2)
111
111
  Cl_s_c = 2*(np.tan(alpha1)-np.tan(alpha2))*np.cos(alpha_m)
112
112
  # calculated but not used
113
113
  Y_secondary_clearance = (lam + B * k/h) * (Cl_s_c)**2 * (np.cos(alpha2)/np.cos(alpha_m)) # Eqn 6
114
114
 
115
115
 
116
- Yp_beta0 = self.data['Fig04a'](s_c, np.degrees(alpha2))
117
- Yp_beta1_eq_alpha2 = self.data['Fig04b'](s_c, np.degrees(alpha2))
116
+ Yp_beta0 = self.data['Fig04a'](float(s_c), float(np.degrees(alpha2)))
117
+ Yp_beta1_eq_alpha2 = self.data['Fig04b'](float(s_c), float(np.degrees(alpha2)))
118
118
 
119
119
  Yp_i0 = (Yp_beta0 + (beta1/alpha2)**2 *(Yp_beta1_eq_alpha2 - Yp_beta0)) *(t_c/0.2)**(-beta1/alpha2) # Fig 4 and Eqn 5
120
120
 
121
121
  Yt = Yp_i0 + Y_secondary_clearance
122
122
  return Yt # Profile loss at zero incidence
123
-
123
+
@@ -116,58 +116,58 @@ class CraigCox(LossBaseClass):
116
116
 
117
117
  asin_os = np.degrees(np.arcsin(currentRow.throat/currentRow.pitch))
118
118
 
119
- N_pr = self.data['Fig03'](Re,0.05) # use a good finish for the geometry
119
+ N_pr = self.data['Fig03'](float(Re), 0.05) # use a good finish for the geometry
120
120
  if (inlet_flow_angle-imin < 10):
121
121
  Fl = 13
122
122
  else:
123
- Fl = self.data['Fig04'](outlet_flow_angle,inlet_flow_angle-imin)
123
+ Fl = self.data['Fig04'](float(outlet_flow_angle), float(inlet_flow_angle-imin))
124
124
 
125
125
  x = 1-np.sin(np.radians(outlet_flow_angle))/np.sin(np.radians(inlet_flow_angle))
126
- contraction_ratio = self.data['Fig07'](x,s_b) # contraction ratio
126
+ contraction_ratio = self.data['Fig07'](float(x), float(s_b)) # contraction ratio
127
127
 
128
- X_pb = self.data['Fig05'](Fl*s_b,contraction_ratio)
129
- delta_X_pt = self.data['Fig06_delta_Xpt'](currentRow.te_pitch)
130
- N_pt = self.data['Fig06_Npt'](currentRow.te_pitch,outlet_flow_angle)
131
- delta_Xpm = self.data['Fig08'](M_out,np.degrees(np.arcsin((currentRow.throat+te)/currentRow.pitch)))
132
- delta_Xp_se = self.data['Fig09'](e_s,M_out)
128
+ X_pb = self.data['Fig05'](float(Fl*s_b), float(contraction_ratio))
129
+ delta_X_pt = self.data['Fig06_delta_Xpt'](float(currentRow.te_pitch))
130
+ N_pt = self.data['Fig06_Npt'](float(currentRow.te_pitch), float(outlet_flow_angle))
131
+ delta_Xpm = self.data['Fig08'](float(M_out), float(np.degrees(np.arcsin((currentRow.throat+te)/currentRow.pitch))))
132
+ delta_Xp_se = self.data['Fig09'](float(e_s), float(M_out))
133
133
 
134
- Fi = self.data['Fig15'](blade_inlet_angle,s_b)
134
+ Fi = self.data['Fig15'](float(blade_inlet_angle), float(s_b))
135
135
  # Incidence Effects
136
136
  if currentRow.beta1_fixed:
137
137
  if incidence_angle>0: # Positive incidence
138
- stall_incidence_angle = self.data['Fig11'](currentRow.beta1.mean(),asin_os)
138
+ stall_incidence_angle = self.data['Fig11'](float(currentRow.beta1.mean()), float(asin_os))
139
139
 
140
140
  incidence_ratio = (incidence_angle - imin)/(stall_incidence_angle-imin)
141
141
 
142
- i_plus_istall_sb = self.data['Fig12_sb'](s_b,asin_os)
143
- i_plus_istall_cor = self.data['Fig12_cr'](contraction_ratio,asin_os)
142
+ i_plus_istall_sb = self.data['Fig12_sb'](float(s_b), float(asin_os))
143
+ i_plus_istall_cor = self.data['Fig12_cr'](float(contraction_ratio), float(asin_os))
144
144
 
145
145
  if blade_inlet_angle<=90:
146
- i_plus_istall_basic = self.data['Fig11'](inlet_flow_angle,asin_os)
146
+ i_plus_istall_basic = self.data['Fig11'](float(inlet_flow_angle), float(asin_os))
147
147
  i_plus_istall = i_plus_istall_basic + i_plus_istall_sb + i_plus_istall_cor # Eqn 5
148
148
  else:
149
- i_plus_istall_basic = self.data["Fig14_i+istall"](blade_inlet_angle,asin_os)
149
+ i_plus_istall_basic = self.data["Fig14_i+istall"](float(blade_inlet_angle), float(asin_os))
150
150
  i_plus_istall = i_plus_istall_basic + (1-(blade_inlet_angle-90)/(90-asin_os))*(i_plus_istall_sb + i_plus_istall_cor) # Eqn 7
151
151
  else:
152
- i_minus_istall_sb = self.data['Fig13'](s_b,asin_os)
152
+ i_minus_istall_sb = self.data['Fig13'](float(s_b), float(asin_os))
153
153
 
154
154
  if blade_inlet_angle<=90:
155
- i_minus_istall_basic = self.data['Fig13_alpha1'](s_b,asin_os)
155
+ i_minus_istall_basic = self.data['Fig13_alpha1'](float(s_b), float(asin_os))
156
156
  i_minus_istall = i_minus_istall_basic + i_minus_istall_sb # Eqn 6
157
157
  else:
158
- i_minus_istall_basic = self.data["Fig14_i-istall"](blade_inlet_angle,asin_os)
158
+ i_minus_istall_basic = self.data["Fig14_i-istall"](float(blade_inlet_angle), float(asin_os))
159
159
  i_minus_istall = i_minus_istall_basic + (1-(blade_inlet_angle - 90)/(90-asin_os)) * i_minus_istall_sb # Eqn 8
160
160
 
161
161
  imin = (i_plus_istall + Fi * (i_minus_istall))/(1+Fi) # type: ignore # Eqn 9
162
- N_pi = self.data['Fig10'](imin,incidence_ratio)
162
+ N_pi = self.data['Fig10'](float(imin), float(incidence_ratio))
163
163
  else:
164
164
  N_pi = 1 # No effect
165
165
 
166
166
  Xp = X_pb*N_pr*N_pi*N_pt + delta_X_pt + delta_Xp_se + delta_Xpm # Eqn 10
167
167
 
168
168
  # Secondary Loss
169
- Ns_hb = self.data['Fig17'](1/currentRow.aspect_ratio)
170
- x_sb = self.data['Fig18']((V_inlet/V)**2,s_b*Fl)
169
+ Ns_hb = self.data['Fig17'](float(1/currentRow.aspect_ratio))
170
+ x_sb = self.data['Fig18'](float((V_inlet/V)**2), float(s_b*Fl))
171
171
 
172
172
  Nsr = 1 # N_pr # I have no clue about this. Craig Cox doesn't describe. setting it to 1 for now.
173
173
  Xs = Nsr*Ns_hb*x_sb
@@ -192,4 +192,4 @@ class CraigCox(LossBaseClass):
192
192
  # According to Equation 3, Group 1 loss is an enthalpy loss Cp*T0. Need to convert to Pressure Loss
193
193
  eta_total = (upstream.T0.mean() - row.T0.mean())/(upstream.T0.mean()-(row.T0.mean()-T0_Loss))
194
194
  return eta_total
195
-
195
+
@@ -8,14 +8,22 @@ import pathlib
8
8
  from ..losstype import LossBaseClass
9
9
  import requests
10
10
 
11
- class KackerOkapuu(LossBaseClass):
12
11
 
13
- def __init__(self):
12
+ def _mean_value(value):
13
+ """Return the mean of an array-like as a Python float."""
14
+ return float(np.asarray(value).mean())
15
+
16
+ class KackerOkapuu(LossBaseClass):
17
+ UseCFM:bool = False
18
+ def __init__(self,UseCFM:bool=False):
14
19
  """KackerOkapuu model is an improvement to the Ainley Mathieson model.
15
20
 
16
21
  Limitations:
17
22
  - Doesn't factor incidence loss
18
23
  - For steam turbines and impulse turbines
24
+
25
+ Args:
26
+ UseCFM (bool): Factor in supersonic drag rise. Authors state in AMDC loss that this is not accurate. It's a multplier to pressure loss.
19
27
 
20
28
  Reference:
21
29
  Kacker, S. C., and U. Okapuu. "A mean line prediction method for axial flow turbine efficiency." (1982): 111-119.
@@ -33,7 +41,7 @@ class KackerOkapuu(LossBaseClass):
33
41
 
34
42
  with open(path.absolute(),'rb') as f:
35
43
  self.data = pickle.load(f) # type: ignore
36
-
44
+ self.UseCFM = UseCFM
37
45
 
38
46
 
39
47
  def __call__(self,row:BladeRow, upstream:BladeRow) -> float:
@@ -52,71 +60,100 @@ class KackerOkapuu(LossBaseClass):
52
60
  Returns:
53
61
  float: Pressure Loss
54
62
  """
55
- if upstream.row_type == RowType.Stator:
56
- M1 = upstream.M
57
- else:
58
- M1 = upstream.M_rel
59
-
63
+ # Get the Inlet incoming mach number relative to the blade
60
64
  c = row.chord
61
65
  b = row.axial_chord
62
66
  if row.row_type == RowType.Stator:
63
- alpha1 = np.degrees(row.alpha1)
64
- beta1 = np.degrees(row.beta1_metal)
65
- alpha2 = np.degrees(row.alpha2)
66
- M2 = row.M
67
+ beta1_rad = np.abs(_mean_value(np.radians(row.beta1_metal))) # Metal angle from fig 3
68
+ alpha1_rad = np.abs(_mean_value(row.alpha1)) # Flow angle
69
+ alpha2_rad = np.abs(_mean_value(row.alpha2)) # Flow angle at exit which is metal angle
70
+ beta2_rad = alpha2_rad
71
+ alpham_rad = (alpha1_rad + alpha2_rad)*0.5
72
+ M1 = _mean_value(upstream.M)
73
+ M2 = _mean_value(row.M)
67
74
  h = 0
68
- Rec = row.V*row.rho*row.chord / sutherland(row.T)
75
+ Rec = _mean_value(row.V*row.rho*row.chord / sutherland(row.T))
76
+
77
+ beta1_deg = np.abs(np.degrees(beta1_rad))
78
+ alpha2_deg = np.abs(np.degrees(alpha2_rad))
79
+ Yp_beta0 = self.data['Fig01_beta0'](float(row.pitch_to_chord), alpha2_deg) # when beta1 = 0
80
+ Yp_beta1_alpha2 = self.data['Fig02'](float(row.pitch_to_chord), alpha2_deg) # When beta1 = alpha2
81
+ t_max_c = self.data['Fig04'](beta1_deg+alpha2_deg)
69
82
  else:
70
83
  h = row.tip_clearance * (row.r[-1]-row.r[0])
71
- alpha1 = np.degrees(row.beta1)
72
- beta1 = row.beta1_metal
73
- alpha2 = np.degrees(row.beta2)
74
- M2 = row.M_rel
75
- Rec = row.W*row.rho*row.chord / sutherland(row.T)
76
-
77
- Yp_beta0 = self.data['Fig01'](row.pitch_to_chord, alpha2)
78
- Yp_beta1_alpha2 = self.data['Fig02'](row.pitch_to_chord, alpha2)
79
- t_max_c = self.data['Fig04'](np.abs(beta1)+np.abs(alpha2))
84
+ alpha1_rad = np.abs(_mean_value(row.beta1))
85
+ beta1_rad = np.abs(_mean_value(np.radians(row.beta1_metal))) # metal angles are stored as degrees
86
+ beta2_rad = np.abs(_mean_value(np.radians(row.beta2_metal)))
87
+ alpha2_rad = np.abs(_mean_value(row.beta2))
88
+ alpham_rad = (beta1_rad + beta2_rad)*0.5
89
+ M1 = _mean_value(upstream.M_rel)
90
+ M2 = _mean_value(row.M_rel)
91
+ Rec = _mean_value(row.W*row.rho*row.chord / sutherland(row.T))
92
+
93
+ beta1_deg = np.abs(np.degrees(beta1_rad))
94
+ alpha2_deg = np.abs(np.degrees(beta2_rad))
95
+ Yp_beta0 = self.data['Fig01_beta0'](float(row.pitch_to_chord), alpha2_deg) # when beta1 = 0
96
+ Yp_beta1_alpha2 = self.data['Fig02'](float(row.pitch_to_chord), alpha2_deg) # When beta1 = alpha2
97
+ t_max_c = self.data['Fig04'](beta1_deg+alpha2_deg)
80
98
 
81
- Yp_amdc = (Yp_beta0 + np.abs(beta1/alpha2) *beta1/alpha2 * (Yp_beta1_alpha2-Yp_beta0)) * ((t_max_c)/0.2)**(beta1/alpha2) # Eqn 2, AMDC = Ainley Mathieson Dunham Came
99
+ ratio = beta1_rad / alpha2_rad
100
+ Yp_amdc = (Yp_beta0 + np.abs(ratio) * ratio * (Yp_beta1_alpha2-Yp_beta0)) * ((t_max_c)/0.2)**(ratio) # Eqn 2, AMDC = Ainley Mathieson Dunham Came
82
101
 
83
102
  # Shock Loss
84
- dP_q1_hub = 0.75*(M1-0.4)**1.75 # Eqn 4, this is at the hub
85
- dP_q1_shock = row.r[-1]/row.r[0] * dP_q1_hub # Eqn 5
86
- Y_shock = dP_q1_shock * upstream.P/row.P * (1-(1+(upstream.gamma-1)/2*M1**2))/(1-(1+(row.gamma-1)/2*M2**2)) # Eqn 6
103
+ if M1>=0.4: # You'll have imaginary numbers if M1<0.4
104
+ dP_q1_hub = 0.75*(M1-0.4)**1.75 # Eqn 4, this is at the hub
105
+ dP_q1_shock = row.r[-1]/row.r[0] * dP_q1_hub # Eqn 5
106
+ Y_shock = dP_q1_shock * upstream.P/row.P * (1-(1+(upstream.gamma-1)/2*M1**2))/(1-(1+(row.gamma-1)/2*M2**2)) # Eqn 6
107
+ Y_shock = _mean_value(Y_shock)
108
+ else:
109
+ Y_shock = 0
87
110
 
88
- K1 = self.data['Fig08_K1'](M2)
111
+ if M2 <= 0.2:
112
+ K1 = 1
113
+ else:
114
+ K1 = 1-1.25*(M2-0.2)
89
115
  K2 = (M1/M2)**2
90
116
  Kp = 1-K2*(1-K1)
91
117
 
92
- CFM = 1+60*(M2-1)**2 # Eqn 9
118
+ if (M2>1) and (self.UseCFM is True):
119
+ CFM = 1+60*(M2-1)**2 # Eqn 9
120
+ else:
121
+ CFM = 1
93
122
 
94
- Yp = 0.914 * (2/3*Yp_amdc *Kp + Y_shock) # Eqn 8 Subsonic regime
123
+ Yp = 0.914 * (2/3*Yp_amdc*Kp + Y_shock) # Eqn 8 Subsonic regime
95
124
  if M2>1:
96
125
  Yp = Yp*CFM
97
126
 
98
127
  f_ar = (1-0.25*np.sqrt(2-h/c)) / (h/c) if h/c<=2 else 1/(h/c)
99
- alpham = np.arctan(0.5*(np.tan(alpha1) - np.tan(alpha2)))
100
- Cl_sc = 2*(np.tan(alpha1)+np.tan(alpha2))*np.cos(alpham)
101
- Ys_amdc = 0.0334 *f_ar *np.cos(alpha2)/np.cos(beta1) * (Cl_sc)**2 * np.cos(alpha2)**2 / np.cos(alpham)**3
102
- # Secondary Loss
103
- K3 = 1/(h/(b))**2 # Fig 13, it's actually bx in the picture which is the axial chord
104
- Ks = 1-K3*(1-Kp) # Eqn 15
105
- Ys = 1.2*Ys_amdc*Ks # Eqn 16
128
+ alpham = np.arctan(0.5*(np.tan(alpha1_rad) - np.tan(alpha2_rad)))
129
+ Cl_sc = 2*(np.tan(alpha1_rad)+np.tan(alpha2_rad))*np.cos(alpham)
130
+ Ys_amdc = 0.0334 *f_ar *np.cos(alpha2_rad)/np.cos(beta1_rad) * (Cl_sc)**2 * np.cos(alpha2_rad)**2 / np.cos(alpham)**3
131
+ # Secondary Loss
132
+ if h>0: # h is calculated from tip clearance. When h is 0 there is no tip clearance
133
+ K3 = 1/(h/(b))**2 # Fig 13, it's actually bx in the picture which is the axial chord; h is 0 this causes nan
134
+ Ks = 1-K3*(1-Kp) # Eqn 15
135
+ Ys = 1.2*Ys_amdc*Ks # Eqn 16
136
+ else:
137
+ K3 = 0
138
+ Ks = 0
139
+ Ys = 0
106
140
 
107
141
  # Trailing Edge
108
- if np.abs(alpha1-alpha2)<5:
109
- delta_phi2 = self.data['Fig14_Impulse'](row.te_pitch*row.pitch / row.throat)
142
+ if np.abs(beta1_deg-np.degrees(beta2_rad))<5: # impulse turbine the inlet and exit angles are the same
143
+ delta_phi2 = self.data['Fig14_Impulse'](float(row.te_pitch*row.pitch / row.throat))
110
144
  else:
111
- delta_phi2 = self.data['Fig14_Axial_Entry'](row.te_pitch*row.pitch / row.throat)
145
+ delta_phi2 = self.data['Fig14_Axial_Entry'](float(row.te_pitch*row.pitch / row.throat))
112
146
 
113
- Ytet = (1-(row.gamma-1)/2 - M2**2 * (1/(1-delta_phi2)-1))**(-row.gamma/(row.gamma-1))-1
147
+ Ytet = (1-(row.gamma-1)/2 * M2**2 * (1/(1-delta_phi2)-1)) **(-row.gamma/(row.gamma-1)) - 1 # Equation 18
114
148
  Ytet = Ytet / (1-(1+(row.gamma-1)/2*M2**2)**(-row.gamma/(row.gamma-1)))
115
149
 
116
150
  # Tip Clearance
117
- kprime = row.tip_clearance/(3)**0.42 # Number of seals
118
- Ytc = 0.37*c/h * (kprime/c)**0.78 * Cl_sc**2 * np.cos(alpha2)**2 / np.cos(alpham)**3
119
-
151
+ if h > 0:
152
+ kprime = row.tip_clearance/(3)**0.42 # Number of seals
153
+ Ytc = 0.37*c/h * (kprime/c)**0.78 * Cl_sc**2 * np.cos(alpha2_rad)**2 / np.cos(alpham)**3
154
+ else:
155
+ Ytc = 0
156
+
120
157
  if Rec <= 2E5:
121
158
  f_re = (Rec/2E5)**-0.4
122
159
  elif Rec<1E6:
@@ -126,4 +163,4 @@ class KackerOkapuu(LossBaseClass):
126
163
 
127
164
  Yt = Yp*f_re + Ys + Ytet + Ytc
128
165
  return Yt
129
-
166
+
@@ -45,21 +45,21 @@ class Traupel(LossBaseClass):
45
45
 
46
46
  if row.row_type == RowType.Rotor:
47
47
  turning = np.abs(np.degrees(upstream.beta2-row.beta2).mean())
48
- F = self.data['Fig06']((upstream.W/row.W).mean(),turning) # Inlet velocity
48
+ F = self.data['Fig06'](float((upstream.W/row.W).mean()), float(turning)) # Inlet velocity
49
49
  else:
50
50
  turning = np.abs(np.degrees(upstream.alpha2-row.alpha2).mean())
51
- F = self.data['Fig06']((upstream.V/row.V).mean(),turning) # Inlet velocity
51
+ F = self.data['Fig06'](float((upstream.V/row.V).mean()), float(turning)) # Inlet velocity
52
52
 
53
- H = self.data['Fig07'](alpha1-beta2,alpha2-beta3)
53
+ H = self.data['Fig07'](float(alpha1-beta2), float(alpha2-beta3))
54
54
 
55
55
  zeta_s = F*g/h_stator # (h1-h1s)/(0.5*c1s**2) # no idea what h1s or h2s is
56
56
  zeta_r = F*g/h_rotor # (h2-h2s)/(0.5*w2s**2)
57
- x_p_stator = self.data['Fig01'](alpha1,alpha2) # not sure if this is the right figure
58
- x_p_rotor = self.data['Fig01'](beta2,beta3) # not sure if this is the right figure
59
- zeta_p_stator = self.data['Fig02'](alpha1,alpha2)
60
- x_m_stator = self.data['Fig03_0'](upstream.M)
61
- zeta_p_rotor = self.data['Fig02'](beta2,beta3)
62
- x_m_rotor = self.data['Fig03_0'](row.M_rel)
57
+ x_p_stator = self.data['Fig01'](float(alpha1), float(alpha2)) # not sure if this is the right figure
58
+ x_p_rotor = self.data['Fig01'](float(beta2), float(beta3)) # not sure if this is the right figure
59
+ zeta_p_stator = self.data['Fig02'](float(alpha1), float(alpha2))
60
+ x_m_stator = self.data['Fig03_0'](float(np.mean(upstream.M)))
61
+ zeta_p_rotor = self.data['Fig02'](float(beta2), float(beta3))
62
+ x_m_rotor = self.data['Fig03_0'](float(np.mean(row.M_rel)))
63
63
 
64
64
 
65
65
  e_te = upstream.te_pitch * g
@@ -67,10 +67,10 @@ class Traupel(LossBaseClass):
67
67
  ssen_alpha2 = e_te/o # Thickness of Trailing edge divide by throat
68
68
  ssen_beta2 = row.te_pitch*g / row.throat
69
69
 
70
- x_delta_stator = self.data['Fig05'](ssen_alpha2,alpha2)
71
- zeta_delta_stator = self.data['Fig04'](ssen_alpha2,alpha2)
72
- x_delta_rotor = self.data['Fig05'](ssen_beta2,beta3)
73
- zeta_delta_rotor = self.data['Fig04'](ssen_beta2,beta3)
70
+ x_delta_stator = self.data['Fig05'](float(ssen_alpha2), float(alpha2))
71
+ zeta_delta_stator = self.data['Fig04'](float(ssen_alpha2), float(alpha2))
72
+ x_delta_rotor = self.data['Fig05'](float(ssen_beta2), float(beta3))
73
+ zeta_delta_rotor = self.data['Fig04'](float(ssen_beta2), float(beta3))
74
74
 
75
75
  Dm = 2* (upstream.r[-1] + upstream.r[0])/2 # Is this the mean diameter? I dont know
76
76
  zeta_f = 0.5 * (h_stator/Dm)**2
@@ -85,7 +85,7 @@ class Traupel(LossBaseClass):
85
85
  if row.row_type == RowType.Stator:
86
86
  zeta_cl = 0
87
87
  else:
88
- zeta_cl = self.data['Fig08'](row.tip_clearance) # For simplicity assume unshrouded blade
88
+ zeta_cl = self.data['Fig08'](float(row.tip_clearance)) # For simplicity assume unshrouded blade
89
89
 
90
90
  zeta_z = 0 # Do not factor this in, a bit complicated
91
91
  # 1 - (internal) - (external)
@@ -97,4 +97,4 @@ class Traupel(LossBaseClass):
97
97
 
98
98
 
99
99
 
100
-
100
+
turbodesign/radeq.py CHANGED
@@ -70,12 +70,13 @@ def radeq(row:BladeRow,upstream:BladeRow,downstream:Optional[BladeRow]=None) ->
70
70
  down_Vm = interp1d(row_radius, downstream.Vm)(r)
71
71
  else:
72
72
  down_Vm = Vm
73
+
74
+ row_m = interp1d(row_radius, row.m)(r)
73
75
  up_m = interp1d(row_radius, upstream.m)(r)
74
76
 
75
- # Get a rough guess of dVm/dm
77
+ # Get a rough guess of dVm/dmv
76
78
  if downstream!=None:
77
79
  down_m = interp1d(row_radius, downstream.m)(r)
78
- row_m = interp1d(row_radius, row.m)(r)
79
80
  if down_m != row_m:
80
81
  func_Vm_m = PchipInterpolator([up_m, row_m, down_m],[up_Vm, Vm, down_Vm])
81
82
  else:
turbodesign/td_math.py CHANGED
@@ -1,6 +1,5 @@
1
- from typing import List, Optional, Tuple
1
+ from typing import List, Optional
2
2
  import numpy as np
3
- import math
4
3
  import numpy.typing as npt
5
4
  from .bladerow import BladeRow, compute_gas_constants
6
5
  from .enums import RowType, LossType
@@ -271,6 +270,26 @@ def rotor_calc(row:BladeRow,upstream:BladeRow,calculate_vm:bool=True):
271
270
  row (BladeRow): Rotor Row
272
271
  upstream (BladeRow): Stator Row or Rotor Row
273
272
  """
273
+ def _log_rotor_failure(reason:str):
274
+ def _fmt(val):
275
+ try:
276
+ return np.array2string(np.asarray(val), precision=5)
277
+ except Exception:
278
+ return str(val)
279
+
280
+ print(f"[RotorCalc] Failure detected: {reason}")
281
+ print(f" row.T0R: {_fmt(row.T0R)}")
282
+ print(f" row.T: {_fmt(row.T)}")
283
+ print(f" row.W: {_fmt(row.W)}")
284
+ print(f" row.M: {_fmt(getattr(row,'M', np.nan))}")
285
+ print(f" row.M_rel: {_fmt(getattr(row,'M_rel', np.nan))}")
286
+ print(f" row.Yp: {_fmt(getattr(row,'Yp', np.nan))}")
287
+ if np.any(row.T >= row.T0R):
288
+ print(" Note: T should be less than T0R.")
289
+ yp_val = getattr(row,'Yp', None)
290
+ if yp_val is not None and np.any(yp_val > 0.3):
291
+ print(" Note: row.Yp exceeded 0.3 which may indicate an issue with the design or loss model.")
292
+
274
293
  row.P0_stator_inlet = upstream.P0_stator_inlet
275
294
  ## P0_P is assumed
276
295
  # row.P = row.P0_stator_inlet*1/row.P0_P
@@ -303,9 +322,18 @@ def rotor_calc(row:BladeRow,upstream:BladeRow,calculate_vm:bool=True):
303
322
  row.T = (row.T0R/T0R_T) # Exit static temperature
304
323
  if calculate_vm: # Calculates the T0 at the exit
305
324
  row.W = np.sqrt(2*row.Cp*(row.T0R-row.T)) #! nan popups here a lot for radial machines
306
- if np.isnan(np.sum(row.W)):
325
+ nan_in_velocity = np.isnan(np.sum(row.W))
326
+ temp_issue = np.any(row.T >= row.T0R)
327
+ high_loss = np.any(getattr(row,'Yp',0) > 0.3)
328
+ if nan_in_velocity:
307
329
  # Need to adjust T
308
- raise ValueError(f'nan detected: check flow path. Turbine inlet cut should be horizontal')
330
+ reason = "nan detected in relative velocity"
331
+ if temp_issue:
332
+ reason += "; T >= T0R shouldn't happen because of T-s diagram'"
333
+ if high_loss:
334
+ reason += "; Yp > 0.3 This could be a problem with the loss model;"
335
+ _log_rotor_failure(reason)
336
+ raise ValueError(f'nan detected')
309
337
  row.Vr = row.W*np.sin(row.phi)
310
338
  row.Vm = row.W*np.cos(row.beta2)
311
339
  row.Wt = row.W*np.sin(row.beta2)
@@ -378,4 +406,4 @@ def inlet_calc(row:BladeRow):
378
406
  raise ValueError(f"High inlet mach can lead to errors iter:{iter} Mach:{avg_mach}")
379
407
 
380
408
  if np.mean(row.M)<0.01:
381
- print(f"Unusually slow flow:{iter} Mach:{avg_mach}")
409
+ print(f"Unusually slow flow:{iter} Mach:{avg_mach}")
@@ -449,9 +449,9 @@ def calculate_massflows(blade_rows:List[BladeRow],calculate_vm:bool=False,fluid:
449
449
  row.Yp = 0
450
450
  stator_calc(row,upstream,downstream,calculate_vm=True)
451
451
  row = radeq(row,upstream)
452
- row = compute_gas_constants(row,fluid)
452
+ compute_gas_constants(row,fluid)
453
453
  stator_calc(row,upstream,downstream,calculate_vm=False)
454
- row = compute_gas_constants(row,fluid)
454
+ compute_gas_constants(row,fluid)
455
455
  compute_massflow(row)
456
456
  compute_power(row,upstream)
457
457