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
turbodesign/bladerow.py CHANGED
@@ -1,4 +1,4 @@
1
- from dataclasses import field, Field
1
+ from dataclasses import dataclass, field, Field
2
2
  from typing import Any, Callable, List, Optional, Sequence, Tuple, Union
3
3
  from .enums import RowType, PowerType
4
4
  import numpy as np
@@ -9,45 +9,54 @@ 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, CompositeLossModel
12
+ from .loss import LossBaseClass
13
+ from .deviation.deviation_base import DeviationBaseClass
13
14
  from .passage import Passage
15
+ from .arrayfuncs import safe_interpolate
14
16
 
15
17
 
18
+ @dataclass(eq=False)
16
19
  class BladeRow:
17
- id:int = 0
18
- stage_id:int = 0
20
+ id: int = 0
21
+ stage_id: int = 0
19
22
  row_type: RowType = RowType.Stator
20
- loss_function:LossBaseClass
21
- cutting_line:line2D # Line perpendicular to the streamline
22
- rp:float = 0.4 # Degree of Reaction
23
-
23
+ loss_function: Optional[LossBaseClass] = None
24
+ deviation_function: Optional[DeviationBaseClass] = None
25
+ cutting_line: Optional[line2D] = None # Line perpendicular to the streamline
26
+ rp: float = 0.4 # Degree of Reaction
27
+ hub_location: float = 0.0
28
+ shroud_location: float = 0.0
29
+
24
30
  # Fluid
25
31
  R: float = 287.15 # Ideal Gas constant J/(Kg K)
26
32
  gamma: float = 1.33 # Ratio of Cp/Cv
27
33
  Cp: float = 1019 # Cp J/(Kg*K)
28
34
  Cv: float = 1019/1.14 # Cv J/(Kg*K)
29
- _coolant:Coolant = None # type: ignore # Coolant Fluid
30
- mu:float = 0
31
-
32
- total_massflow:float = 0 # Massflow spool + all upstream cooling flow [kg/s]
33
- massflow:npt.NDArray = field(default_factory=lambda: np.array([0])) # Massflow per radii
34
- total_massflow_no_coolant:float = 0 # Inlet massflow
35
+ _coolant: Optional[Coolant] = None # Coolant Fluid
36
+ mu: float = 0
37
+
38
+ total_massflow: float = 0 # Massflow spool + all upstream cooling flow [kg/s]
39
+ massflow: npt.NDArray = field(default_factory=lambda: np.array([0])) # Massflow per radii
40
+ total_massflow_no_coolant: float = 0 # Inlet massflow
41
+ massflow_target: Optional[npt.NDArray] = None # Custom massflow distribution for angle matching [kg/s]
35
42
  # ----------------------------------
36
43
 
37
44
  # Streamline Properties
38
- percent_hub:float = 0 # Where blade row is defined along the hub.
45
+ percent_hub: float = 0 # Where blade row is defined along the hub.
39
46
  percent_hub_shroud: npt.NDArray = field(default_factory=lambda: np.array([0])) # Percent streamline length from hub to shroud.
40
47
  x: npt.NDArray = field(default_factory=lambda: np.array([0])) # x - coordinates (useful for computing axial chord)
41
48
  r: npt.NDArray = field(default_factory=lambda: np.array([0])) # Radius - coordinates
42
49
  m: npt.NDArray = field(default_factory=lambda: np.array([0])) # meridional
43
- area:float = 0
50
+ total_area: float = 0
51
+ area: npt.NDArray = field(default_factory=lambda: np.array([0]))
44
52
  # Calculated massflow is the massflow computed after radial eq solver
45
53
  calculated_massflow: float = 0
46
-
54
+
47
55
  # Row Efficiency (calculated or specified)
48
- eta_total:float = 0 # Total to Total
49
- eta_static:float = 0 # Total to static
50
- stage_loading:float = 0 # stage loading how much work done per stage
56
+ eta_total: float = 0 # Total to Total
57
+ eta_static: float = 0 # Total to static
58
+ eta_poly: float = 0 # Polytropic efficiency (per row if applicable)
59
+ stage_loading: float = 0 # stage loading how much work done per stage
51
60
 
52
61
  alpha1: npt.NDArray = field(default_factory=lambda: np.array([0])) # Blade inlet absolute flow angle
53
62
  alpha2: npt.NDArray = field(default_factory=lambda: np.array([0])) # Blade exit absolute flow angle
@@ -55,20 +64,21 @@ class BladeRow:
55
64
  beta1: npt.NDArray = field(default_factory=lambda: np.array([0])) # Blade inlet relative flow angle
56
65
  beta2: npt.NDArray = field(default_factory=lambda: np.array([0])) # Blade exit relative flow angle
57
66
 
58
- _beta1_metal:npt.NDArray = field(default_factory=lambda: np.array([0])) # blade inlet metal angle
59
- beta1_metal_radii:npt.NDArray = field(default_factory=lambda: np.array([0])) # radii where metal angle is defined
67
+ deviation: npt.NDArray = field(default_factory=lambda: np.array([0]))
68
+ _beta1_metal: npt.NDArray = field(default_factory=lambda: np.array([0])) # blade inlet metal angle
69
+ beta1_metal_radii: npt.NDArray = field(default_factory=lambda: np.array([0])) # radii where metal angle is defined
60
70
 
61
- _beta2_metal:npt.NDArray = field(default_factory=lambda: np.array([0])) # blade exit metal angle
62
- beta2_metal_radii:npt.NDArray = field(default_factory=lambda: np.array([0])) # radii where metal angle is defined
71
+ _beta2_metal: npt.NDArray = field(default_factory=lambda: np.array([0])) # blade exit metal angle
72
+ beta2_metal_radii: npt.NDArray = field(default_factory=lambda: np.array([0])) # radii where metal angle is defined
63
73
 
64
- beta1_fixed:bool = False # Geometry already defined. This affects the inlet flow angle
65
- beta2_fixed:bool = False # Geometry already defined. This affects the exit flow angle
74
+ beta1_fixed: bool = False # Geometry already defined. This affects the inlet flow angle
75
+ beta2_fixed: bool = False # Geometry already defined. This affects the exit flow angle
66
76
 
67
77
  # Velocities
68
78
  Vm: npt.NDArray = field(default_factory=lambda: np.array([0])) # Meridional velocity
69
79
  Vx: npt.NDArray = field(default_factory=lambda: np.array([0])) # Axial Velocity
70
80
  Vt: npt.NDArray = field(default_factory=lambda: np.array([0])) # Tangential Velocity
71
- Vr:npt.NDArray = field(default_factory=lambda: np.array([0])) # Radial velocity
81
+ Vr: npt.NDArray = field(default_factory=lambda: np.array([0])) # Radial velocity
72
82
  V: npt.NDArray = field(default_factory=lambda: np.array([0])) # Absolute Velocity in 3D coordinate system
73
83
  V2: npt.NDArray = field(default_factory=lambda: np.array([0])) # Absolute Velocity in Theta-Axial plane
74
84
  M: npt.NDArray = field(default_factory=lambda: np.array([0])) # Mach Number
@@ -77,15 +87,17 @@ class BladeRow:
77
87
  W: npt.NDArray = field(default_factory=lambda: np.array([0])) # Relative Velocity in Theta-Axial plane
78
88
  Wt: npt.NDArray = field(default_factory=lambda: np.array([0])) # Relative Tangential Velocity
79
89
 
80
- _rpm: float = 0
81
- omega:float = 0 # angular velocity rad/s
90
+ _rpm: float = field(default=0, init=False, repr=False)
91
+ omega: float = 0 # angular velocity rad/s
82
92
 
83
- P0_stator_inlet = field(default_factory=lambda: np.array([0])) # Every quantity is an exit quantity, This is used for efficiency calcs
84
- T0_stator_inlet = field(default_factory=lambda: np.array([0])) # Every quantity is an exit quantity, This is used for efficiency calcs
93
+ P0_stator_inlet: npt.NDArray = field(default_factory=lambda: np.array([0])) # Every quantity is an exit quantity, This is used for efficiency calcs
94
+ T0_stator_inlet: npt.NDArray = field(default_factory=lambda: np.array([0])) # Every quantity is an exit quantity, This is used for efficiency calcs
85
95
  P0: npt.NDArray = field(default_factory=lambda: np.array([0])) # Total Quantities
96
+ P0_is: npt.NDArray = field(default_factory=lambda: np.array([0]))
86
97
  T0: npt.NDArray = field(default_factory=lambda: np.array([0]))
87
- T0_is:npt.NDArray = field(default_factory=lambda: np.array([0]))
98
+ T0_is: npt.NDArray = field(default_factory=lambda: np.array([0]))
88
99
  P0R: npt.NDArray = field(default_factory=lambda: np.array([0])) # Relative Total Pressure (Pa)
100
+ P0R_is: npt.NDArray = field(default_factory=lambda: np.array([0]))
89
101
  T0R: npt.NDArray = field(default_factory=lambda: np.array([0]))
90
102
 
91
103
  # Static Quantities
@@ -93,36 +105,49 @@ class BladeRow:
93
105
  T: npt.NDArray = field(default_factory=lambda: np.array([0]))
94
106
  T_is: npt.NDArray = field(default_factory=lambda: np.array([0]))
95
107
  rho: npt.NDArray = field(default_factory=lambda: np.array([0]))
96
-
108
+ entropy_rise: npt.NDArray = field(default_factory=lambda: np.array([0]))
109
+
97
110
  # Related to streamline curvature
98
- phi:npt.NDArray = field(default_factory=lambda: np.array([0])) # Inclination angle x,r plane. AY td2.f
111
+ phi: npt.NDArray = field(default_factory=lambda: np.array([0])) # Inclination angle x,r plane. AY td2.f
99
112
  rm: npt.NDArray = field(default_factory=lambda: np.array([0])) # Curvature
100
113
  incli_curve_radii: npt.NDArray = field(default_factory=lambda: np.array([0])) # radius at which curvature was evaluated
101
- mprime:npt.NDArray = field(default_factory=lambda: np.array([0])) # Mprime distance
102
-
103
- Yp: float = 0 # Pressure loss
104
- power:float = 0 # Watts
105
- power_mean:float = 0
106
- power_distribution:npt.NDArray # How power is divided by radius. Example: Equal distribution [0.33 0.33 0.33]. More at Tip [0.2,0.3,0.5]. More at Hub [0.6 0.5 ]
107
- P0_P:float = 0 # Total to Static Pressure Ratio
108
- Power_Type:PowerType
109
- euler_power:float = 0
110
- Reynolds:float = 0
114
+ mprime: npt.NDArray = field(default_factory=lambda: np.array([0])) # Mprime distance
115
+
116
+ Yp: npt.NDArray = field(default_factory=lambda: np.array([0])) # Pressure loss
117
+ blockage: float = 0
118
+ flow_coefficient: float = 0 # Vm/U or similar nondimensional flow coefficient
119
+ power: float = 0 # Watts
120
+ power_mean: float = 0
121
+ power_distribution: npt.NDArray = field(default_factory=lambda: np.array([0])) # How power is divided by radius.
122
+ P0_P: float = 0 # Total to Static Pressure Ratio
123
+ P0_ratio: float = 0 # Total-to-total pressure ratio target (design input; may be overwritten in legacy diagnostics)
124
+ P0_ratio_target: float = 0 # Frozen design target for P0_ratio (never overwritten; used for initial guesses)
125
+ Power_Type: PowerType = PowerType.P0_P
126
+ euler_power: float = 0
127
+ Reynolds: float = 0
128
+ eta_poly: float = 0.0 # Optional per-row polytropic efficiency target
129
+ num_blades: int = 0
111
130
 
112
131
  # Used for loss calculations
113
- _blade_to_blade_gap:float = 0.025 # Gap between blade in terms of percent chord.
132
+ _blade_to_blade_gap: float = 0.025 # Gap between blade in terms of percent chord.
114
133
 
115
- _aspect_ratio:float = 0.9 #
116
- _pitch_to_chord:float = 0.7 # Pitch to chord ratio, used to determine number of blades and compute loss
134
+ _aspect_ratio: float = 0.9 #
135
+ _pitch_to_chord: npt.NDArray = field(default_factory=lambda: np.array([0.7])) # Pitch to chord ratio, used to determine number of blades and compute loss
117
136
 
118
- _axial_chord:float = -1
119
- _chord:float = -1
120
- _stagger:float = 42
121
- _te_s:float = 0.08
122
- _tip_clearance:float = 0 # Clearance as a percentage of span or blade height
137
+ _axial_chord: float = -1
138
+ _chord: npt.NDArray = field(default_factory=lambda: np.array([-1.0]))
139
+ _stagger: npt.NDArray = field(default_factory=lambda: np.array([42.0]))
140
+ _te_s: float = 0.08
141
+ _tip_clearance: float = 0 # Clearance as a percentage of span or blade height
142
+
143
+ _inlet_to_outlet_pratio: list = field(default_factory=lambda: [0.06,0.95])
123
144
 
124
- _inlet_to_outlet_pratio = [0.06,0.95]
125
- location:float = 0 # Percent along hub where bladerow is defined
145
+ def __post_init__(self):
146
+ if self.shroud_location == 0:
147
+ self.shroud_location = self.hub_location
148
+ # Preserve any user-specified target ratio so later diagnostics can safely overwrite P0_ratio.
149
+ if self.P0_ratio_target == 0 and self.P0_ratio != 0:
150
+ self.P0_ratio_target = self.P0_ratio
126
151
 
127
152
  @property
128
153
  def inlet_to_outlet_pratio(self) -> Tuple[float,float]:
@@ -192,7 +217,7 @@ class BladeRow:
192
217
 
193
218
 
194
219
  @property
195
- def pitch_to_chord(self) -> float:
220
+ def pitch_to_chord(self) -> npt.NDArray:
196
221
  """Gets the pitch to chord ratio
197
222
 
198
223
  Returns:
@@ -207,10 +232,10 @@ class BladeRow:
207
232
  Args:
208
233
  val (float): new pitch to chord ratio. Typically stators are 0.8 to 0.95. Rotors 0.7 to 0.8
209
234
  """
210
- self._pitch_to_chord = val
235
+ self._pitch_to_chord = convert_to_ndarray(val)
211
236
 
212
237
  @property
213
- def solidity(self) -> float:
238
+ def solidity(self) -> npt.NDArray:
214
239
  """Inverse of pitch to chord ratio
215
240
 
216
241
  Returns:
@@ -228,16 +253,28 @@ class BladeRow:
228
253
  Returns:
229
254
  float: solidity
230
255
  """
231
- self._pitch_to_chord = 1/val
256
+ self._pitch_to_chord = 1/convert_to_ndarray(val)
232
257
 
233
258
  @property
234
- def beta1_metal(self) -> npt.NDArray:
259
+ def metal_inlet_angle(self) -> npt.NDArray:
260
+ """Blade metal inlet angle (degrees)."""
235
261
  return np.degrees(self._beta1_metal)
236
262
 
237
263
  @property
238
- def beta2_metal(self) -> npt.NDArray:
264
+ def beta1_metal(self) -> npt.NDArray:
265
+ """Backward-compatible alias for metal_inlet_angle."""
266
+ return self.metal_inlet_angle
267
+
268
+ @property
269
+ def metal_exit_angle(self) -> npt.NDArray:
270
+ """Blade metal exit angle (degrees)."""
239
271
  return np.degrees(self._beta2_metal)
240
272
 
273
+ @property
274
+ def beta2_metal(self) -> npt.NDArray:
275
+ """Backward-compatible alias for metal_exit_angle."""
276
+ return self.metal_exit_angle
277
+
241
278
  @property
242
279
  def stagger(self) -> float:
243
280
  """Average stagger angle
@@ -257,7 +294,7 @@ class BladeRow:
257
294
  self._stagger = val
258
295
 
259
296
  @property
260
- def chord(self) -> float:
297
+ def chord(self) -> npt.NDArray:
261
298
  """Chord defined at mean radius
262
299
 
263
300
  Returns:
@@ -286,14 +323,16 @@ class BladeRow:
286
323
  else:
287
324
  return self.pitch*np.sin(np.pi/2-self.beta2.mean())
288
325
 
326
+ _num_blades: float = 0
327
+
289
328
  @property
290
- def num_blades(self) ->float:
291
- """returns the number of blades
329
+ def num_blades(self) -> float:
330
+ """Configured number of blades (set during design/initialization)."""
331
+ return self._num_blades
292
332
 
293
- Returns:
294
- float: number of blades
295
- """
296
- return int(2*np.pi*self.r.mean() / self.pitch)
333
+ @num_blades.setter
334
+ def num_blades(self, val: float) -> None:
335
+ self._num_blades = val
297
336
 
298
337
  @property
299
338
  def camber(self) -> float:
@@ -330,54 +369,63 @@ class BladeRow:
330
369
  """
331
370
  self._tip_clearance = val
332
371
 
333
- def __init__(self,location:float,row_type:RowType=RowType.Stator,stage_id:int = 0):
334
- """Initializes the blade row to be a particular type
335
-
336
- Args:
337
- location (float): Location of the blade row as a percentage of hub length
338
- row_type (RowType): Specifies the Type. Defaults to RowType.Stator
339
- power (float, optional): power . Defaults to 0.
340
- P0_P (float, optional): Total to Static Pressure Ratio
341
- stage_id (int, optional): ID of the stage so if you have 9 stages, the id could be 9. It's used to separate the stages. Each stage will have it's own unique degree of reaction
342
- """
343
- self.row_type = row_type
344
- self.location = location
345
- self.Yp = 0 # Loss
346
- self.stage_id = stage_id
347
-
348
- @beta1_metal.setter
349
- def beta1_metal(self,beta1_metal:List[float],percent:List[float]=[]):
350
- """Sets the leading edge metal angle for the blade
351
-
352
- Args:
353
- beta1_metal (List[float]): blade leading edge angle
354
- percent (List[float]): percent location of metal angles from hub to shroud.
355
- """
356
- self._beta1_metal = np.radians(convert_to_ndarray(beta1_metal))
357
- if len(percent) != len(beta1_metal):
358
- percent = np.linspace(0,1,len(self._beta1_metal)).tolist() # type: ignore
372
+ # Backwards-compatible alias
373
+ @property
374
+ def location(self) -> float:
375
+ return self.hub_location
376
+
377
+ @location.setter
378
+ def location(self, val: float) -> None:
379
+ self.hub_location = val
380
+
381
+ @metal_inlet_angle.setter
382
+ def metal_inlet_angle(self, metal_inlet_angle: List[float], percent: List[float] = []):
383
+ """Sets the leading edge metal angle for the blade (degrees)."""
384
+ arr = np.radians(convert_to_ndarray(metal_inlet_angle))
385
+ if len(percent) != len(metal_inlet_angle):
386
+ percent = np.linspace(0, 1, len(arr)).tolist() # type: ignore
387
+ self._beta1_metal = arr
359
388
  self.beta1_metal_radii = convert_to_ndarray(percent)
360
389
  self.beta1_fixed = True
361
- self.beta1 = self.beta1_metal.copy()
390
+ self.beta1 = self.metal_inlet_angle.copy()
391
+
392
+ @beta1_metal.setter
393
+ def beta1_metal(self, beta1_metal: List[float], percent: List[float] = []):
394
+ """Backward-compatible alias for metal_inlet_angle setter."""
395
+ self.metal_inlet_angle = beta1_metal
362
396
 
363
- @beta2_metal.setter
364
- def beta2_metal(self,beta2_metal:List[float],percent:List[float]=[]):
365
- """Sets the trailing edge metal angle for the blade
366
-
367
- Args:
368
- beta2_metal (List[float]): Blade exit metal angle
369
- percent (List[float]): percent location of metal angles from hub to shroud.
370
-
371
- """
372
- self._beta2_metal = np.radians(convert_to_ndarray(beta2_metal))
373
- if len(percent) != len(beta2_metal):
374
- percent = np.linspace(0,1,len(self._beta2_metal)).tolist() # type: ignore
397
+ @metal_exit_angle.setter
398
+ def metal_exit_angle(self, metal_exit_angle: List[float], percent: List[float] = []):
399
+ """Sets the trailing edge metal angle for the blade (degrees)."""
400
+ arr = np.radians(convert_to_ndarray(metal_exit_angle))
401
+ if len(percent) != len(metal_exit_angle):
402
+ percent = np.linspace(0, 1, len(arr)).tolist() # type: ignore
403
+ self._beta2_metal = arr
375
404
  self.beta2_metal_radii = convert_to_ndarray(percent)
376
405
  self.beta2_fixed = True
377
- self.beta2 = self._beta2_metal.copy()
378
-
406
+
407
+ # Apply deviation if defined; deviation_function returns degrees
408
+ deviation_func = getattr(self, "deviation_function", None)
409
+ deviation_rad = 0.0
410
+ if callable(deviation_func):
411
+ try:
412
+ deviation_val = deviation_func(self, None)
413
+ deviation_rad = np.radians(deviation_val)
414
+ except Exception:
415
+ deviation_rad = 0.0
416
+
417
+ beta2_effective = self._beta2_metal + deviation_rad
379
418
  if self.row_type == RowType.Stator:
380
- self.alpha2 = self._beta2_metal.copy()
419
+ self.alpha2 = beta2_effective.copy()
420
+ self.beta2 = beta2_effective.copy()
421
+ else:
422
+ self.beta2 = beta2_effective.copy()
423
+ self.deviation = np.full_like(self.beta2, deviation_rad)
424
+
425
+ @beta2_metal.setter
426
+ def beta2_metal(self, beta2_metal: List[float], percent: List[float] = []):
427
+ """Backward-compatible alias for metal_exit_angle setter."""
428
+ self.metal_exit_angle = beta2_metal
381
429
 
382
430
  @property
383
431
  def rpm(self):
@@ -416,14 +464,6 @@ class BladeRow:
416
464
  self.loss_function = model
417
465
  return
418
466
 
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
467
  raise TypeError("Loss models must inherit LossBaseClass.")
428
468
 
429
469
  @property
@@ -446,6 +486,39 @@ class BladeRow:
446
486
 
447
487
  def __repr__(self):
448
488
  return f"{self.row_type.name} P0:{np.mean(self.P0):0.2f} T0:{np.mean(self.T0):0.2f} P:{np.mean(self.P):0.2f} massflow:{np.mean(self.total_massflow_no_coolant):0.3f}"
489
+
490
+ def synchronize_blade_geometry(self) -> None:
491
+ """Couple num_blades, pitch-to-chord/solidity, chord, and stagger.
492
+
493
+ Uses mean radius from interpolated streamlines to derive pitch, chord,
494
+ and stagger (axial chord / chord).
495
+ """
496
+ if self.num_blades <= 0 or self.r.size == 0:
497
+ return
498
+
499
+ # Pitch from blade count and local radius
500
+ pitch = 2 * np.pi * self.r / self.num_blades
501
+
502
+ # Pitch-to-chord (or 1/solidity) may be scalar or spanwise; broadcast it
503
+ ptc = convert_to_ndarray(self.pitch_to_chord)
504
+ if ptc.size == 1:
505
+ ptc = ptc * np.ones_like(self.r, dtype=float)
506
+ else:
507
+ t_src = np.linspace(0, 1, ptc.size)
508
+ ptc = np.interp(self.percent_hub_shroud, t_src, ptc)
509
+
510
+ chord = pitch / np.maximum(ptc, 1e-9)
511
+ self._chord = chord
512
+ self._pitch_to_chord = ptc
513
+
514
+ axial = self.axial_chord if self.axial_chord > 0 else float(np.mean(chord))
515
+ if self.axial_chord <= 0:
516
+ self.axial_chord = axial
517
+
518
+ ratio = np.clip(axial / np.maximum(chord, 1e-9), -1.0, 1.0)
519
+ stagger_rad = np.arccos(ratio)
520
+ # Store stagger distribution in degrees
521
+ self._stagger = np.degrees(stagger_rad)
449
522
 
450
523
  def to_dict(self):
451
524
 
@@ -487,15 +560,18 @@ class BladeRow:
487
560
  "rho":self.rho.tolist(),
488
561
  "mu":self.mu,
489
562
  "Yp":self.Yp,
563
+ "flow_coefficient": self.flow_coefficient,
490
564
  "Power":self.power,
491
565
  "P0_P": self.P0_P,
492
566
  "eta_total":self.eta_total,
493
567
  "eta_static":self.eta_static,
568
+ "eta_poly": self.eta_poly,
494
569
  "euler_power":self.euler_power,
495
570
  "axial_chord":self.axial_chord,
496
571
  "aspect_ratio":self.aspect_ratio,
497
572
  "num_blades":self.num_blades,
498
- "area": self.area,
573
+ "total_area": self.total_area,
574
+ "area": self.area.tolist(),
499
575
  "radius":self.r.tolist(),
500
576
  "x":self.x.tolist(),
501
577
  "dx":self.x[-1]-self.x[0],
@@ -508,7 +584,7 @@ class BladeRow:
508
584
  return data
509
585
 
510
586
  #* Some functions related to blade row
511
- def interpolate_streamline_radii(row:BladeRow,passage:Passage,num_streamlines:int=3):
587
+ def interpolate_streamline_quantities(row:BladeRow,passage:Passage,num_streamlines:int=3):
512
588
  """Interpolate all quantities onto the streamline and allocates variables.
513
589
  Run this after setting some initial conditions
514
590
 
@@ -520,9 +596,17 @@ def interpolate_streamline_radii(row:BladeRow,passage:Passage,num_streamlines:in
520
596
  Returns:
521
597
  (BladeRow): new row object with quantities interpolated
522
598
  """
599
+ src_percent = convert_to_ndarray(row.percent_hub_shroud)
600
+
523
601
  row.cutting_line,_,_ = passage.get_cutting_line(row.location)
524
- row.x,row.r = row.cutting_line.get_point(np.linspace(0,1,num_streamlines))
525
- streamline_percent_length = np.sqrt((row.r-row.r[0])**2+(row.x-row.x[0])**2)/row.cutting_line.length
602
+ t_span = np.array([0.5]) if num_streamlines <= 1 else np.linspace(0, 1, num_streamlines)
603
+ row.x, row.r = row.cutting_line.get_point(t_span)
604
+ if num_streamlines <= 1:
605
+ streamline_percent_length = np.array([0.5])
606
+ row.total_area = passage.get_area(row.location)
607
+ row.area = np.array([row.total_area])
608
+ else:
609
+ streamline_percent_length = np.sqrt((row.r-row.r[0])**2+(row.x-row.x[0])**2)/row.cutting_line.length
526
610
 
527
611
  # Flow angles
528
612
  row._beta1_metal = row._beta1_metal.default_factory() if type(row._beta1_metal) == Field else row._beta1_metal
@@ -534,51 +618,48 @@ def interpolate_streamline_radii(row:BladeRow,passage:Passage,num_streamlines:in
534
618
  row._beta2_metal = interpolate_quantities(row._beta2_metal,row.beta2_metal_radii,streamline_percent_length)
535
619
  row.beta1_metal_radii = streamline_percent_length
536
620
  row.beta2_metal_radii = streamline_percent_length
621
+ row.deviation = streamline_percent_length * 0
537
622
 
538
- if type(row.percent_hub_shroud) == Field:
539
- row.percent_hub_shroud = streamline_percent_length
540
- else:
541
- row.percent_hub_shroud = streamline_percent_length # Reset the radii to streamline radii
623
+ row.mprime = interpolate_quantities(row.mprime, src_percent, streamline_percent_length)
542
624
 
543
- row.alpha1 = interpolate_quantities(row.alpha1,row.percent_hub_shroud,streamline_percent_length)
544
- row.alpha2 = interpolate_quantities(row.alpha2,row.percent_hub_shroud,streamline_percent_length)
545
- row.beta1 = interpolate_quantities(row.beta1,row.percent_hub_shroud,streamline_percent_length)
546
- row.beta2 = interpolate_quantities(row.beta2,row.percent_hub_shroud,streamline_percent_length)
625
+ row.alpha1 = safe_interpolate(row.alpha1, src_percent, streamline_percent_length, radians=False)
626
+ row.alpha2 = safe_interpolate(row.alpha2, src_percent, streamline_percent_length, radians=False)
627
+ row.beta1 = safe_interpolate(row.beta1, src_percent, streamline_percent_length, radians=False)
628
+ row.beta2 = safe_interpolate(row.beta2, src_percent, streamline_percent_length, radians=False)
547
629
 
548
630
  # Velocities
549
- row.Vm = interpolate_quantities(row.Vm, row.percent_hub_shroud, streamline_percent_length)
550
- row.Vx = interpolate_quantities(row.Vx, row.percent_hub_shroud, streamline_percent_length)
551
- row.Vt = interpolate_quantities(row.Vt, row.percent_hub_shroud, streamline_percent_length)
552
- row.Vr = interpolate_quantities(row.Vr, row.percent_hub_shroud, streamline_percent_length)
553
- row.V = interpolate_quantities(row.V, row.percent_hub_shroud, streamline_percent_length)
554
- row.V2 = interpolate_quantities(row.V2, row.percent_hub_shroud, streamline_percent_length)
555
- row.M = interpolate_quantities(row.M, row.percent_hub_shroud, streamline_percent_length)
556
- row.M_rel = interpolate_quantities(row.M_rel, row.percent_hub_shroud, streamline_percent_length)
557
- row.U = interpolate_quantities(row.U, row.percent_hub_shroud, streamline_percent_length)
558
- row.W = interpolate_quantities(row.W, row.percent_hub_shroud, streamline_percent_length)
559
- row.Wt = interpolate_quantities(row.Wt, row.percent_hub_shroud, streamline_percent_length)
631
+ row.Vm = interpolate_quantities(row.Vm, src_percent, streamline_percent_length)
632
+ row.Vx = interpolate_quantities(row.Vx, src_percent, streamline_percent_length)
633
+ row.Vt = interpolate_quantities(row.Vt, src_percent, streamline_percent_length)
634
+ row.Vr = interpolate_quantities(row.Vr, src_percent, streamline_percent_length)
635
+ row.V = interpolate_quantities(row.V, src_percent, streamline_percent_length)
636
+ row.V2 = interpolate_quantities(row.V2, src_percent, streamline_percent_length)
637
+ row.M = interpolate_quantities(row.M, src_percent, streamline_percent_length)
638
+ row.M_rel = interpolate_quantities(row.M_rel, src_percent, streamline_percent_length)
639
+ row.U = interpolate_quantities(row.U, src_percent, streamline_percent_length)
640
+ row.W = interpolate_quantities(row.W, src_percent, streamline_percent_length)
641
+ row.Wt = interpolate_quantities(row.Wt, src_percent, streamline_percent_length)
560
642
 
561
643
  # Total Quantities
562
- row.T0 = interpolate_quantities(row.T0,row.percent_hub_shroud,streamline_percent_length)
563
- row.T0_is = interpolate_quantities(row.T0,row.percent_hub_shroud,streamline_percent_length)
564
- row.P0 = interpolate_quantities(row.P0,row.percent_hub_shroud,streamline_percent_length)
565
- row.P0_stator_inlet = interpolate_quantities(row.P0_stator_inlet,row.percent_hub_shroud,streamline_percent_length)
644
+ row.T0 = interpolate_quantities(row.T0, src_percent, streamline_percent_length)
645
+ row.T0_is = interpolate_quantities(row.T0, src_percent, streamline_percent_length) # For Turbines
646
+ row.P0 = interpolate_quantities(row.P0, src_percent, streamline_percent_length)
647
+ row.P0_is = interpolate_quantities(row.P0, src_percent, streamline_percent_length) # For Compressors
648
+ row.P0_stator_inlet = interpolate_quantities(row.P0_stator_inlet, src_percent, streamline_percent_length)
566
649
 
567
650
  # Relative Quantities
568
- row.P0R = interpolate_quantities(row.P0R,row.percent_hub_shroud,streamline_percent_length)
569
- row.T0R = interpolate_quantities(row.T0R,row.percent_hub_shroud,streamline_percent_length)
651
+ row.P0R = interpolate_quantities(row.P0R, src_percent, streamline_percent_length)
652
+ row.P0R_is = interpolate_quantities(row.P0, src_percent, streamline_percent_length)
653
+ row.T0R = interpolate_quantities(row.T0R, src_percent, streamline_percent_length)
570
654
 
571
655
  # Static Quantities
572
- row.P = interpolate_quantities(row.P,row.percent_hub_shroud,streamline_percent_length)
573
- row.T = interpolate_quantities(row.T,row.percent_hub_shroud,streamline_percent_length)
574
- row.T_is = interpolate_quantities(row.T_is,row.percent_hub_shroud,streamline_percent_length)
575
- row.rho = interpolate_quantities(row.rho,row.percent_hub_shroud,streamline_percent_length)
656
+ row.P = interpolate_quantities(row.P, src_percent, streamline_percent_length)
657
+ row.T = interpolate_quantities(row.T, src_percent, streamline_percent_length)
658
+ row.T_is = interpolate_quantities(row.T_is, src_percent, streamline_percent_length)
659
+ row.rho = interpolate_quantities(row.rho, src_percent, streamline_percent_length)
660
+ row.entropy_rise = interpolate_quantities(row.entropy_rise, src_percent, streamline_percent_length)
576
661
 
577
- # if row.row_type == RowType.Inlet:
578
- # row.P0_fun = interp1d(row.percent_hub_shroud,row.P0)
579
- # row.T0_fun = interp1d(row.percent_hub_shroud,row.T0)
580
- # elif row.row_type == RowType.Outlet:
581
- # row.P_fun = interp1d(row.percent_hub_shroud,row.P)
662
+ row.percent_hub_shroud = streamline_percent_length
582
663
 
583
664
  return row
584
665
 
@@ -615,6 +696,8 @@ def interpolate_quantities(q:npt.NDArray,r:npt.NDArray,r2:npt.NDArray):
615
696
  q2 = np.zeros(shape=r2.shape)
616
697
  return q[0]+q2
617
698
  else:
699
+ if len(r) != len(q):
700
+ r = np.linspace(0, 1, len(q))
618
701
  return interp1d(r,q,kind='linear')(r2)
619
702
 
620
703
  def compute_gas_constants(row:BladeRow,fluid:Optional[Solution]=None) -> None: