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