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/coolant.py CHANGED
@@ -1,10 +1,22 @@
1
- from dataclasses import dataclass, field
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
2
6
  from cantera import Solution
3
7
 
8
+
4
9
  @dataclass
5
10
  class Coolant:
6
- T0:float = field(default=900) # Kelvin
7
- P0:float = field(default=50*101325) # Pascal
8
- massflow_percentage:float = field(default=0.03) # Fraction of total massflow going through compressor
9
- Cp:float = field(default=1000) # J/K
10
-
11
+ """Simple container for coolant properties used in spool calculations."""
12
+
13
+ fluid: Optional[Solution] = None
14
+ T0: float = 300.0
15
+ P0: float = 101325.0
16
+ massflow_percentage: float = 0.0
17
+ Cp: Optional[float] = None
18
+
19
+ def __post_init__(self) -> None:
20
+ # Default Cp to the provided fluid value when available.
21
+ if self.Cp is None:
22
+ self.Cp = float(self.fluid.cp) if self.fluid is not None else 0.0
@@ -0,0 +1,5 @@
1
+ from .deviation_base import DeviationBaseClass
2
+ from .fixed_deviation import FixedDeviation
3
+ from .carter_deviation import CarterDeviation
4
+
5
+ __all__ = ["DeviationBaseClass", "FixedDeviation", "CarterDeviation"]
@@ -0,0 +1,3 @@
1
+ from .carter_deviation import CarterDeviation
2
+
3
+ __all__ = ["CarterDeviation"]
@@ -0,0 +1,79 @@
1
+ from typing import Optional, TYPE_CHECKING
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+
6
+ from ..arrayfuncs import convert_to_ndarray
7
+ from ..enums import RowType
8
+ from .deviation_base import DeviationBaseClass
9
+
10
+ if TYPE_CHECKING:
11
+ from ..bladerow import BladeRow # for type hints only
12
+
13
+
14
+ class CarterDeviation(DeviationBaseClass):
15
+ """Carter-style exit deviation for axial compressor blades.
16
+
17
+ Uses Mattingly's form:
18
+ gamma_e = (4 * alpha_exit * sqrt(sigma) - gamma_i) / (4 * sqrt(sigma) - 1)
19
+ deviation = alpha_exit - gamma_e
20
+
21
+ where gamma_i/gamma_e are inlet/exit metal (or flow) angles and sigma is solidity.
22
+ """
23
+
24
+ def __init__(self, gamma_inlet: Optional[float | npt.ArrayLike] = None, gamma_exit: Optional[float | npt.ArrayLike] = None, alpha_exit: Optional[float | npt.ArrayLike] = None):
25
+ """
26
+ Args:
27
+ gamma_inlet: Optional inlet metal/camber angle array (radians). If not
28
+ provided, defaults to ``row.alpha1`` (stator) or ``row.beta1`` (rotor).
29
+ gamma_exit: Optional exit metal/camber angle array (radians). If not
30
+ provided, defaults to ``row.alpha2`` (stator) or ``row.beta2`` (rotor).
31
+ alpha_exit: Optional flow exit angle array (radians). If not provided,
32
+ defaults to ``row.alpha2`` for stators and ``row.beta2`` for rotors.
33
+ """
34
+ super().__init__()
35
+ self.gamma_inlet = gamma_inlet
36
+ self.gamma_exit = gamma_exit
37
+ self.alpha_exit = alpha_exit
38
+
39
+ def _spanwise(self, value: float | npt.ArrayLike, row: "BladeRow") -> npt.NDArray:
40
+ """Convert scalar/array input to spanwise distribution on row grid."""
41
+ arr = convert_to_ndarray(value)
42
+ if arr.size == 1:
43
+ return arr * np.ones_like(row.percent_hub_shroud, dtype=float)
44
+ t_src = np.linspace(0, 1, arr.size)
45
+ return np.interp(row.percent_hub_shroud, t_src, arr)
46
+
47
+ def __call__(self, row: "BladeRow", upstream: "BladeRow") -> npt.NDArray: # noqa: ARG002
48
+ """Compute deviation (radians) along the span for the supplied row."""
49
+ gamma_i = self.gamma_inlet
50
+ gamma_e = self.gamma_exit
51
+ alpha_exit = self.alpha_exit
52
+
53
+ if gamma_i is None:
54
+ gamma_i = getattr(row, "gamma_inlet", None)
55
+ if gamma_e is None:
56
+ gamma_e = getattr(row, "gamma_exit", None)
57
+ if alpha_exit is None:
58
+ alpha_exit = row.alpha2 if row.row_type == RowType.Stator else row.beta2
59
+
60
+ # Default to blade flow angles if no explicit camber/metal angles are provided.
61
+ if gamma_i is None:
62
+ gamma_i = row.alpha1 if row.row_type == RowType.Stator else row.beta1
63
+ if gamma_e is None:
64
+ gamma_e = row.alpha2 if row.row_type == RowType.Stator else row.beta2
65
+
66
+ gamma_i_span = self._spanwise(gamma_i, row)
67
+ alpha_exit_span = self._spanwise(alpha_exit, row)
68
+
69
+ sigma = getattr(row, "solidity", 0.0) or 0.0
70
+ sigma = max(float(sigma), 1e-6) # avoid divide-by-zero
71
+ k = np.sqrt(sigma)
72
+
73
+ if gamma_e is not None:
74
+ gamma_e_span = self._spanwise(gamma_e, row)
75
+ else:
76
+ gamma_e_span = (4.0 * alpha_exit_span * k - gamma_i_span) / np.maximum(4.0 * k - 1.0, 1e-6)
77
+
78
+ deviation = alpha_exit_span - gamma_e_span
79
+ return deviation
@@ -0,0 +1,20 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict, Iterable, Tuple
3
+ import numpy.typing as npt
4
+ import os
5
+
6
+ class DeviationBaseClass(ABC):
7
+ """All deviation models should inherit from this abstract base class
8
+ """
9
+ def __init__(self):
10
+ # Make the environment directory
11
+ default_home = os.path.join(os.path.expanduser("~"), ".cache")
12
+ os.environ['TD3_HOME'] = os.path.join(default_home,'TD3_LossModels')
13
+ os.makedirs(os.environ['TD3_HOME'],exist_ok=True)
14
+
15
+ @abstractmethod
16
+ def __call__(self, row:Any, upstream:Any) -> npt.NDArray:
17
+ """Evaluate the loss for the supplied blade row."""
18
+ raise NotImplementedError
19
+
20
+
@@ -0,0 +1,42 @@
1
+ from typing import Optional, TYPE_CHECKING
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+ from scipy.interpolate import interp1d
6
+
7
+ from ..arrayfuncs import convert_to_ndarray
8
+ from .deviation_base import DeviationBaseClass
9
+
10
+ if TYPE_CHECKING:
11
+ from ..bladerow import BladeRow # for type hints only
12
+
13
+
14
+ class FixedDeviation(DeviationBaseClass):
15
+ """Fixed deviation angle (scalar or spanwise distribution)."""
16
+
17
+ t: Optional[npt.ArrayLike]
18
+ deviation_angles: npt.NDArray
19
+
20
+ def __init__(self, deviation_angles: float | npt.ArrayLike, t: Optional[npt.ArrayLike] = None):
21
+ """
22
+ Args:
23
+ deviation_angles: deviation angle in degrees; either scalar or an array
24
+ along the span (0 at hub to 1 at shroud).
25
+ t: optional spanwise coordinates corresponding to ``deviation_angles``.
26
+ If not provided and ``deviation_angles`` is array-like, a uniform
27
+ spanwise distribution is assumed.
28
+ """
29
+ super().__init__()
30
+ self.deviation_angles = convert_to_ndarray(deviation_angles)
31
+ self.t = t
32
+
33
+ def __call__(self, row: "BladeRow", upstream: "BladeRow") -> npt.NDArray:
34
+ """Outputs the fixed deviation distribution."""
35
+ deviation_angles = self.deviation_angles
36
+ if deviation_angles.size == 1:
37
+ deviation = deviation_angles * np.ones_like(row.r)
38
+ else:
39
+ if self.t is None:
40
+ self.t = np.linspace(0, 1, len(self.deviation_angles))
41
+ deviation = interp1d(self.t, self.deviation_angles, bounds_error=True)(row.percent_hub_shroud)
42
+ return deviation
turbodesign/enums.py CHANGED
@@ -4,18 +4,17 @@ class LossType(Enum):
4
4
  Pressure = 1
5
5
  Enthalpy = 2
6
6
  Entropy = 3
7
+ Polytropic = 4
7
8
 
8
9
  class RowType(Enum):
10
+ # Similar to a stator but for compressor calculations there is no Total Pressure rise
11
+ IGV = 0
9
12
  Stator = 1
10
- Rotor = 2
13
+ Rotor = 2
11
14
  CounterRotating = 2
12
15
  Inlet = 3
13
16
  Outlet = 4
14
17
 
15
- class MassflowConstraint(Enum):
16
- MatchMassFlow = 1 # Changes the exit angles to match the massflow
17
- BalanceMassFlow = 2 # Keeps the exit angle but balances the massflow between the stages as best it can. This will affect the static pressure at the stage exit
18
-
19
18
  class PowerType(Enum):
20
19
  """The code for BladeRow will assume a PowerType automatically depending on what you specify. If you specify the blade row to have P0_P which is the stator inlet total pressure to rotor exit static pressure then that will be used to calculate all the quantities.
21
20
 
@@ -33,4 +32,4 @@ class PowerType(Enum):
33
32
 
34
33
  class PassageType(Enum):
35
34
  Centrifugal=0
36
- Axial=1
35
+ Axial=1
@@ -0,0 +1,158 @@
1
+ from typing import Tuple
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+
6
+ from .bladerow import BladeRow
7
+ from .enums import RowType
8
+
9
+ def compute_streamline_areas(row: BladeRow) -> Tuple[float, npt.NDArray]:
10
+ """Compute total annulus area and individual streamline cross-sectional areas.
11
+
12
+ Calculates the total annulus area and the cross-sectional area for each streamtube
13
+ based on the radial (r) and axial (x) coordinates of the blade row. Handles both
14
+ axial machines (constant x) and radial machines (varying x).
15
+
16
+ Args:
17
+ row: BladeRow object containing percent_hub_shroud, x, r coordinates
18
+
19
+ Returns:
20
+ tuple: (total_area, streamline_area) where
21
+ - total_area (float): Total annulus cross-sectional area [m²]
22
+ - streamline_area (ndarray): Array of streamtube areas [m²] matching row.r shape
23
+ """
24
+ total_area = 0.0
25
+ streamline_area = np.zeros(len(row.percent_hub_shroud))
26
+ if len(row.percent_hub_shroud) <= 1:
27
+ if hasattr(row, "total_area") and row.total_area:
28
+ total_area = float(row.total_area)
29
+ streamline_area = np.array([total_area])
30
+ return total_area, streamline_area
31
+ for j in range(1, len(row.percent_hub_shroud)):
32
+ if np.abs((row.x[j] - row.x[j - 1])) < 1e-5: # Axial machines
33
+ delta = np.pi * (row.r[j] ** 2 - row.r[j - 1] ** 2)
34
+ streamline_area[j] = delta
35
+ total_area += delta
36
+ else: # Radial machines
37
+ dx = row.x[j] - row.x[j - 1]
38
+ S = row.r[j] - row.r[j - 1]
39
+ C = np.sqrt(1 + ((row.r[j] - row.r[j - 1]) / dx) ** 2)
40
+ streamline_area[j] = 2 * np.pi * C * (S / 2 * dx ** 2 + row.r[j - 1] * dx)
41
+ total_area += streamline_area[j]
42
+ return total_area, streamline_area
43
+
44
+
45
+ def compute_massflow(row: BladeRow) -> None:
46
+ """Calculate massflow distribution across streamlines and populate row attributes.
47
+
48
+ Computes the cumulative massflow through each streamtube based on density, meridional
49
+ velocity, and streamtube cross-sectional areas. Accounts for blockage and optional
50
+ coolant injection. Updates row attributes in-place.
51
+
52
+ Args:
53
+ row: BladeRow object with Vm, rho, percent_hub_shroud, blockage defined
54
+
55
+ Returns:
56
+ None. Updates the following row attributes in-place:
57
+ - row.massflow: Cumulative massflow array [kg/s]
58
+ - row.total_massflow: Total massflow including coolant [kg/s]
59
+ - row.total_massflow_no_coolant: Massflow without coolant [kg/s]
60
+ - row.calculated_massflow: Final massflow value [kg/s]
61
+ - row.total_area: Total annulus area [m²]
62
+ - row.area: Streamtube areas array [m²]
63
+ """
64
+ n = len(row.percent_hub_shroud)
65
+ massflow_fraction = np.linspace(0, 1, n)
66
+ total_area, streamline_area = compute_streamline_areas(row)
67
+
68
+ if n <= 1:
69
+ Vm = float(row.Vm[0]) if len(row.Vm) else 0.0
70
+ rho = float(row.rho[0]) if len(row.rho) else 0.0
71
+ mass = Vm * rho * (total_area if total_area else 0.0) * (1 - row.blockage)
72
+ massflow = np.array([mass])
73
+ row.total_massflow_no_coolant = mass
74
+ if row.coolant is not None:
75
+ massflow += row.coolant.massflow_percentage * mass
76
+ row.massflow = massflow
77
+ row.calculated_massflow = massflow[-1]
78
+ row.total_massflow = massflow[-1]
79
+ row.total_area = total_area
80
+ row.area = streamline_area
81
+ return
82
+
83
+ massflow = np.zeros_like(row.percent_hub_shroud, dtype=float)
84
+ for j in range(1, n):
85
+ Vm = (row.Vm[j] + row.Vm[j - 1]) / 2
86
+ rho = (row.rho[j] + row.rho[j - 1]) / 2
87
+ massflow[j] = Vm * rho * streamline_area[j] * (1 - row.blockage) + massflow[j - 1]
88
+
89
+ row.total_massflow_no_coolant = massflow[-1]
90
+ if row.coolant is not None:
91
+ # account for coolant as a fraction of inlet flow
92
+ massflow += massflow_fraction * row.coolant.massflow_percentage * row.total_massflow_no_coolant
93
+ row.massflow = massflow
94
+ row.calculated_massflow = massflow[-1]
95
+ row.total_massflow = massflow[-1]
96
+ row.total_area = total_area
97
+ row.area = streamline_area
98
+
99
+
100
+ def compute_power(row: BladeRow, upstream: BladeRow | None = None, downstream: BladeRow | None = None, is_compressor: bool | None = None) -> None:
101
+ """Calculate power and efficiencies for a blade row (compressor or turbine).
102
+
103
+ Args:
104
+ row: The blade row being evaluated.
105
+ upstream: Upstream reference row (default for turbine-style calculations).
106
+ downstream: Downstream reference row (optional; useful for compressor-style staging).
107
+ is_compressor: Force compressor sign convention when True (power added to flow);
108
+ when False, assumes turbine (power extracted). If None, infers from P0 gain.
109
+ """
110
+ ref = upstream if upstream is not None else downstream
111
+ if ref is None:
112
+ return
113
+
114
+ mdot = row.massflow[-1] if getattr(row, "massflow", np.array([])).size else getattr(row, "total_massflow", 0.0)
115
+
116
+ if row.row_type == RowType.Stator:
117
+ row.power = 0.0
118
+ row.eta_static = 0.0
119
+ row.eta_total = 0.0
120
+ row.stage_loading = 0.0
121
+ row.euler_power = 0.0
122
+ row.T_is = 0 * row.T0
123
+ row.T0_is = 0 * row.T0 # Make it an array
124
+ else:
125
+ # Preserve any user-configured target ratio. compute_power historically overwrote row.P0_ratio,
126
+ # which makes it hard to treat P0_ratio as a design input elsewhere.
127
+ if getattr(row, "P0_ratio_target", 0.0) == 0 and getattr(row, "P0_ratio", 0.0) != 0:
128
+ row.P0_ratio_target = row.P0_ratio
129
+
130
+ P0_P = (ref.P0 / row.P).mean()
131
+ P0_ratio_actual = (row.P0 / ref.P0).mean()
132
+ row.P0_ratio = P0_ratio_actual
133
+ setattr(row, "P0_ratio_actual", float(P0_ratio_actual))
134
+ row.T_is = ref.T0 * (1 / P0_P) ** ((row.gamma - 1) / row.gamma)
135
+ row.T0_is = ref.T0 * (row.P0 / ref.P0) ** ((row.gamma - 1) / row.gamma)
136
+
137
+ comp_mode = is_compressor
138
+ if comp_mode is None:
139
+ comp_mode = bool(np.mean(row.P0) > np.mean(ref.P0))
140
+
141
+ if comp_mode:
142
+ deltaT = row.T0.mean() - ref.T0.mean()
143
+ row.power = mdot * row.Cp * deltaT
144
+ denom_static = max(row.T.mean() - ref.T0.mean(), 1e-9)
145
+ denom_total = max(row.T0.mean() - ref.T0.mean(), 1e-9)
146
+ row.eta_static = (row.T_is.mean() - ref.T0.mean()) / denom_static
147
+ row.eta_total = (row.T0_is.mean() - ref.T0.mean()) / denom_total
148
+ else:
149
+ deltaT = ref.T0.mean() - row.T0.mean()
150
+ row.power = mdot * row.Cp * deltaT
151
+ row.eta_static = row.power / (mdot * row.Cp * (ref.T0.mean() - row.T_is.mean()))
152
+ row.eta_total = (ref.T0.mean() - row.T0.mean()) / (ref.T0.mean() - row.T0_is.mean())
153
+
154
+ row.stage_loading = row.Cp * (ref.T0.mean() - row.T0.mean()) / max(row.U.mean() ** 2, 1e-9)
155
+ if is_compressor:
156
+ row.stage_loading *= -1 # Stage_loading will be negative
157
+ row.euler_power = mdot * (ref.U * ref.Vt - row.U * row.Vt).mean()
158
+ row.flow_coefficient = abs(float(np.mean(row.Vm / row.U)))
turbodesign/inlet.py CHANGED
@@ -1,8 +1,10 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import List, Union
2
+ from optparse import Option
3
+ from typing import List, Optional, Union
4
+
3
5
  from .enums import RowType
4
- from .bladerow import BladeRow, compute_gas_constants, interpolate_quantities
5
- from .arrayfuncs import convert_to_ndarray
6
+ from .bladerow import BladeRow, compute_gas_constants
7
+ from .arrayfuncs import convert_to_ndarray, safe_interpolate
6
8
  import numpy as np
7
9
  from cantera import Solution
8
10
  from .passage import Passage
@@ -16,41 +18,88 @@ class Inlet(BladeRow):
16
18
  (BladeRow): Defines the properties of the blade row
17
19
  """
18
20
  fun: interp1d
19
-
20
- def __init__(self,M:float,T0:Union[float,List[float]],
21
- P0:Union[float,List[float]],
22
- location:float=0,
23
- beta:Union[float,List[float]]=[0],
24
- percent_radii:Union[float,List[float]]=[0.5]):
21
+ static_defined: bool
22
+ def __init__(self,
23
+ hub_location:float=0,
24
+ shroud_location:Optional[float]=None,
25
+ alpha:Union[float,List[float]]=[0]):
25
26
  """Initializes the inlet station.
26
27
  Uses the beta and exit mach number to predict a value for Vm
27
28
 
28
29
  Args:
29
- M (float): Mach number at the inlet plane
30
- T0 (Union[float,List[float]]): Total Temperature Array
31
- P0 (Union[float,List[float]]): Total Pressure Array
32
- percent_radii (Union[float,List[float]]): Radius where total pressure and temperature are defined
33
30
  location (float): Location as a percentage of hub length
34
31
  beta (Union[float,List[float]], optional): Inlet flow angle in relative direction. Defaults to [].
35
32
 
36
33
  """
37
- super().__init__(row_type=RowType.Inlet,location=location,stage_id=-1)
38
- self.beta1 = convert_to_ndarray(beta)
34
+ super().__init__(row_type=RowType.Inlet,hub_location=hub_location,shroud_location=shroud_location,stage_id=-1)
35
+ self.beta1 = convert_to_ndarray([0.0])
36
+ # Default absolute angles to zero to avoid attribute errors during interpolation
37
+ self.alpha1 = convert_to_ndarray([0.0])
38
+ self.alpha2 = convert_to_ndarray(alpha)
39
+ self.beta2 = convert_to_ndarray([0.0])
40
+
41
+
42
+ def init_static(self,P:Union[float,List[float]],T:Union[float,List[float]],M:Union[float,List[float]],percent_radii:Union[float,List[float]]=[0.5]):
43
+ """Initializes the inlet with static quantities at the inlet
44
+
45
+ Args:
46
+ P (Union[float,List[float]]): Static Pressure either as a float or array
47
+ T (Union[float,List[float]]): Static Temperature either as a float or array
48
+ M (Union[float,List[float]]): Mach Number either as a float or array
49
+ percent_radii (Union[float,List[float]], optional): Percent radii where P,T, and M are defined. Defaults to [0.5].
50
+ """
51
+ self.P = convert_to_ndarray(P)
39
52
  self.M = convert_to_ndarray(M)
40
- self.T0 = convert_to_ndarray(T0)
41
- self.P0 = convert_to_ndarray(P0)
53
+ self.T = convert_to_ndarray(T)
54
+ self.static_defined = True
42
55
  self.percent_hub_shroud = convert_to_ndarray(percent_radii)
43
-
44
- def initialize_inputs(self,num_streamlines:int=5):
45
- self.M = interpolate_quantities(self.M, self.percent_hub_shroud, np.linspace(0,1,num_streamlines))
46
- self.P0 = interpolate_quantities(self.P0,self.percent_hub_shroud, np.linspace(0,1,num_streamlines))
47
- self.T0 = interpolate_quantities(self.T0,self.percent_hub_shroud, np.linspace(0,1,num_streamlines))
48
- # if it's inlet alpha and beta are the same, relative flow angle = absolute.
49
- self.beta1 = interpolate_quantities(self.beta1,self.percent_hub_shroud, np.linspace(0,1,num_streamlines))
50
- self.beta2 = np.radians(convert_to_ndarray(self.beta1))
51
- self.alpha1 = np.radians(convert_to_ndarray(self.beta1))
52
56
 
53
- def initialize_fluid(self,fluid:Solution=None,R:float=287.15,gamma:float=1.4,Cp:float=1024):
57
+ def init_total(
58
+ self,
59
+ P0:Union[float,List[float]],
60
+ T0:Union[float,List[float]],
61
+ M:Union[float,List[float]],
62
+ percent_radii:Optional[Union[float,List[float]]]=None
63
+ ):
64
+ """Initializes the inlet with total quantities at the inlet
65
+
66
+ Args:
67
+ P0 (Union[float,List[float]]): Total Pressure either as a float or array
68
+ T0 (Union[float,List[float]]): Total Temperature either as a float or array
69
+ M (Union[float,List[float]]): Mach Number either as a float or array
70
+ percent_radii (Optional[Union[float,List[float]]], optional): Percent radii where P0, T0, and M are defined. Defaults to `None`, which uses evenly spaced radii when multiple values exist or `[0.5]` otherwise.
71
+ """
72
+ self.P0 = convert_to_ndarray(P0)
73
+ self.T0 = convert_to_ndarray(T0)
74
+ self.M = convert_to_ndarray(M)
75
+ if percent_radii is None:
76
+ percent_radii = convert_to_ndarray([0.5]) # type: ignore
77
+ if len(self.M)>1:
78
+ percent_radii = np.linspace(0,1,len(self.M)) # type: ignore
79
+ self.static_defined = False
80
+ self.percent_hub_shroud = percent_radii
81
+
82
+ def __interpolate_quantities__(self,num_streamlines:int=5):
83
+ """Initializes the inputs
84
+
85
+ Args:
86
+ num_streamlines (int, optional): _description_. Defaults to 5.
87
+ IsCompressor (bool, optional): This is if static pressure is defined at the inlet and total pressure at the outlet. Defaults to False.
88
+ """
89
+ dst = np.array([0.5]) if num_streamlines <= 1 else np.linspace(0,1,num_streamlines)
90
+ self.M = safe_interpolate(self.M, self.percent_hub_shroud, dst)
91
+ if self.static_defined: # This comes from the initialization
92
+ self.P = safe_interpolate(self.P, self.percent_hub_shroud, dst)
93
+ else:
94
+ self.P0 = safe_interpolate(self.P0, self.percent_hub_shroud, dst)
95
+ self.T0 = safe_interpolate(self.T0, self.percent_hub_shroud, dst)
96
+ # Angles: default to 0 if unspecified
97
+ self.beta1 = safe_interpolate(self.beta1, self.percent_hub_shroud, dst, radians=True)
98
+ self.beta2 = safe_interpolate(self.beta2, self.percent_hub_shroud, dst, radians=True)
99
+ self.alpha1 = safe_interpolate(self.alpha1, self.percent_hub_shroud, dst, radians=True)
100
+ self.alpha2 = safe_interpolate(self.alpha2, self.percent_hub_shroud, dst, radians=True)
101
+
102
+ def __initialize_fluid__(self,fluid:Optional[Solution]=None,R:float=287.15,gamma:float=1.4,Cp:float=1024):
54
103
  """Initialize the inlet using the fluid. This function should be called by a class that inherits from spool
55
104
 
56
105
  Args:
@@ -58,15 +107,18 @@ class Inlet(BladeRow):
58
107
  R (float, optional): Ideal Gas Constant. Defaults to 287.15 J/(Kg K) for air
59
108
  gamma (float, optional): _description_. Defaults to 1.4.
60
109
  Cp (float, optional): _description_. Defaults to 1024 J/(Kg K).
61
-
62
110
  """
63
111
  self.loss_function = None
64
112
 
65
113
  if fluid:
66
114
  fluid.TP = self.T0.mean(),self.P0.mean()
67
115
  self.gamma = fluid.cp/fluid.cv
116
+ if self.static_defined:
117
+ self.P0 = self.P * (1+(self.gamma-1)/2 * self.M**2) ** (self.gamma/(self.gamma-1))
118
+ self.T0 = self.T * (1+(self.gamma-1)/2 * self.M**2)
119
+ else:
120
+ self.P = self.P0 * 1/(1 + (self.gamma-1) * self.M**2)**(self.gamma/(self.gamma-1))
68
121
  self.T = self.T0 * 1/(1 + (self.gamma-1) * self.M**2)
69
- self.P = self.P0 * 1/(1 + (self.gamma-1) * self.M**2)**(self.gamma/(self.gamma-1))
70
122
  fluid.TP = self.T.mean(),self.P.mean()
71
123
  self.rho = convert_to_ndarray([fluid.density])
72
124
  else:
@@ -74,10 +126,13 @@ class Inlet(BladeRow):
74
126
  self.gamma = gamma
75
127
  self.R = R
76
128
  self.T = self.T0 * 1/(1 + (self.gamma-1) * self.M**2)
77
- self.P = self.P0 * 1/(1 + (self.gamma-1) * self.M**2)**(self.gamma/(self.gamma-1))
129
+ if self.static_defined:
130
+ self.P0 = self.P * (1+(self.gamma-1)/2 * self.M**2) ** (self.gamma/(self.gamma-1))
131
+ self.T0 = self.T * (1+(self.gamma-1)/2 * self.M**2)
132
+ else:
133
+ self.P = self.P0 * 1/(1 + (self.gamma-1) * self.M**2)**(self.gamma/(self.gamma-1))
78
134
  self.rho = self.P/(self.R*self.T)
79
135
 
80
- self.rpm = 0
81
136
  self.beta1_metal = [0]
82
137
  self.beta2_metal = [0]
83
138
  if len(self.percent_hub_shroud) == 1:
@@ -86,9 +141,9 @@ class Inlet(BladeRow):
86
141
  self.T0 = self.percent_hub_shroud*0+self.T0[0]
87
142
  self.P0_fun = interp1d(self.percent_hub_shroud,self.P0)
88
143
  self.T0_fun = interp1d(self.percent_hub_shroud,self.T0)
89
- self.mprime = [0]
144
+ self.mprime = [0] # type: ignore
90
145
 
91
- def initialize_velocity(self,passage:Passage,num_streamlines:int):
146
+ def __initialize_velocity__(self,passage:Passage,num_streamlines:int):
92
147
  """Initialize velocity calculations. Assumes streamlines and inclination angles have been calculated
93
148
  Call this before performing calculations
94
149
 
@@ -100,13 +155,14 @@ class Inlet(BladeRow):
100
155
  # Perform Calculations on Velocity
101
156
  Vm_prev = 0; Vm_err = 0
102
157
 
103
- cutline,_,_ = passage.get_cutting_line(self.location)
104
- self.x,self.r = cutline.get_point(np.linspace(0,1,num_streamlines))
105
- for _ in range(10):
158
+ cutline,_,_ = passage.get_cutting_line(t_hub=self.location,t_shroud=self.shroud_location)
159
+ t_span = np.array([0.5]) if num_streamlines <= 1 else np.linspace(0,1,num_streamlines)
160
+ self.x,self.r = cutline.get_point(t_span)
161
+ for _ in range(2):
106
162
  T0_T = (1+(self.gamma-1)/2 * self.M**2)
107
-
163
+
108
164
  self.Vm = self.M**2 * self.gamma*self.R*self.T0/T0_T \
109
- / (1+np.cos(self.phi)**2 * np.tan(self.alpha1)**2)
165
+ / (1+np.cos(self.phi)**2 * np.tan(self.alpha2)**2)
110
166
 
111
167
  self.Vm = np.sqrt(self.Vm)
112
168
  self.T = self.T0/T0_T
@@ -114,40 +170,54 @@ class Inlet(BladeRow):
114
170
  self.rho = self.P/(self.R*self.T)
115
171
 
116
172
  self.Vx = self.Vm * np.cos(self.phi)
117
- self.Vt = self.Vm * np.cos(self.phi) * np.tan(self.beta1)
173
+ self.Vt = self.Vm * np.cos(self.phi) * np.tan(self.alpha2)
118
174
  self.V = np.sqrt(self.Vm**2 + self.Vt**2)
119
175
  self.Vr = self.Vm * np.sin(self.phi)
120
176
 
121
177
  compute_gas_constants(self)
122
178
  rho_mean = self.rho.mean()
123
- for i in range(len(self.massflow)-1):
124
- tube_massflow = self.massflow[i+1]-self.massflow[i]
179
+ Vm_tube = np.zeros(max(len(self.massflow)-1, 1))
180
+ if len(self.massflow) <= 1:
181
+ Vm_tube[0] = float(self.Vm.mean())
182
+ # Compute tube-averaged Vm from massflow differences
183
+ for i in range(1, len(self.massflow)):
184
+ tube_massflow = self.massflow[i]-self.massflow[i-1]
185
+ rho_bar = rho_mean if len(self.rho) == 1 else 0.5 * (self.rho[i] + self.rho[i-1])
125
186
  if np.abs((self.x[-1]-self.x[0]))<1E-5: # Axial Machines
126
- self.Vm[i+1] = tube_massflow/(rho_mean*np.pi*(self.r[i+1]**2-self.r[i]**2))
187
+ area = np.pi*(self.r[i]**2-self.r[i-1]**2)
127
188
  else: # Radial Machines
128
189
  dx = self.x[i]-self.x[i-1]
129
190
  S = (self.r[i]-self.r[i-1])
130
191
  C = np.sqrt(1+((self.r[i]-self.r[i-1])/dx)**2)
131
192
  area = 2*np.pi*C*(S/2*dx**2+self.r[i-1]*dx)
132
- self.Vm[i+1] = tube_massflow/(rho_mean*area)
133
- self.Vm[0] = 1/(len(self.Vm)-1)*self.Vm[1:].sum()
193
+ Vm_tube[i-1] = tube_massflow/(rho_bar*area + 1e-12)
194
+
195
+ # Recover per-streamline Vm; handle single-streamline as meanline
196
+ if len(self.Vm) <= 1:
197
+ self.Vm = np.array([Vm_tube[0] if len(Vm_tube) else rho_mean*0])
198
+ else:
199
+ self.Vm[0] = Vm_tube[0]
200
+ for i in range(1, len(self.Vm)):
201
+ self.Vm[i] = 2 * Vm_tube[i-1] - self.Vm[i-1]
134
202
 
135
- self.M = self.Vm /np.sqrt(self.gamma*self.R*self.T)
203
+ self.M = self.V /np.sqrt(self.gamma*self.R*self.T)
136
204
  Vm_err = np.max(abs(self.Vm-Vm_prev)/self.Vm)
137
205
  Vm_prev = self.Vm
138
206
  if Vm_err < 1E-4:
139
207
  break
140
208
 
141
- Area = 0
142
- for j in range(1,num_streamlines):
143
- if np.abs((self.x[j]-self.x[j-1]))<1E-12: # Axial Machines
144
- Area += np.pi*(self.r[j]**2-self.r[j-1]**2)
145
- else: # Radial Machines
146
- dx = self.x[j]-self.x[j-1]
147
- S = (self.r[j]-self.r[j-1])
148
- C = np.sqrt(1+((self.r[j]-self.r[j-1])/dx)**2)
149
- Area += 2*np.pi*C*(S/2*dx**2+self.r[j-1]*dx)
150
-
209
+ if num_streamlines <= 1:
210
+ Area = passage.get_area(self.location)
211
+ else:
212
+ Area = 0
213
+ for j in range(1,num_streamlines):
214
+ if np.abs((self.x[j]-self.x[j-1]))<1E-12: # Axial Machines
215
+ Area += np.pi*(self.r[j]**2-self.r[j-1]**2)
216
+ else: # Radial Machines
217
+ dx = self.x[j]-self.x[j-1]
218
+ S = (self.r[j]-self.r[j-1])
219
+ C = np.sqrt(1+((self.r[j]-self.r[j-1])/dx)**2)
220
+ Area += 2*np.pi*C*(S/2*dx**2+self.r[j-1]*dx)
151
221
  self.calculated_massflow = self.rho.mean()*self.Vm.mean() * Area
152
222
 
153
223
 
@@ -177,4 +247,4 @@ class Inlet(BladeRow):
177
247
  if type(percent_hub_shroud) == float:
178
248
  return float(self.T0_fun(percent_hub_shroud))
179
249
  else:
180
- return self.T0_fun(percent_hub_shroud)
250
+ return self.T0_fun(percent_hub_shroud)