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.
- {turbo_design-1.3.8.dist-info → turbo_design-1.3.10.dist-info}/METADATA +2 -1
- turbo_design-1.3.10.dist-info/RECORD +46 -0
- {turbo_design-1.3.8.dist-info → turbo_design-1.3.10.dist-info}/WHEEL +1 -1
- turbodesign/__init__.py +57 -4
- turbodesign/agf.py +346 -0
- turbodesign/arrayfuncs.py +31 -1
- turbodesign/bladerow.py +238 -155
- turbodesign/compressor_math.py +386 -0
- turbodesign/compressor_spool.py +941 -0
- turbodesign/coolant.py +18 -6
- turbodesign/deviation/__init__.py +5 -0
- turbodesign/deviation/axial_compressor.py +3 -0
- turbodesign/deviation/carter_deviation.py +79 -0
- turbodesign/deviation/deviation_base.py +20 -0
- turbodesign/deviation/fixed_deviation.py +42 -0
- turbodesign/enums.py +5 -6
- turbodesign/flow_math.py +158 -0
- turbodesign/inlet.py +126 -56
- turbodesign/isentropic.py +59 -15
- turbodesign/loss/__init__.py +3 -1
- turbodesign/loss/compressor/OTAC_README.md +39 -0
- turbodesign/loss/compressor/__init__.py +54 -0
- turbodesign/loss/compressor/diffusion.py +61 -0
- turbodesign/loss/compressor/lieblein.py +1 -0
- turbodesign/loss/compressor/otac.py +799 -0
- turbodesign/loss/compressor/references/schobeiri-2012-shock-loss-model-for-transonic-and-supersonic-axial-compressors-with-curved-blades.pdf +0 -0
- turbodesign/loss/fixedpolytropic.py +27 -0
- turbodesign/loss/fixedpressureloss.py +30 -0
- turbodesign/loss/losstype.py +2 -30
- turbodesign/loss/turbine/TD2.py +25 -29
- turbodesign/loss/turbine/__init__.py +0 -1
- turbodesign/loss/turbine/ainleymathieson.py +6 -5
- turbodesign/loss/turbine/craigcox.py +6 -5
- turbodesign/loss/turbine/fixedefficiency.py +8 -7
- turbodesign/loss/turbine/kackerokapuu.py +7 -5
- turbodesign/loss/turbine/traupel.py +17 -16
- turbodesign/outlet.py +81 -22
- turbodesign/passage.py +98 -63
- turbodesign/row_factory.py +129 -0
- turbodesign/solve_radeq.py +9 -10
- turbodesign/{td_math.py → turbine_math.py} +144 -185
- turbodesign/turbine_spool.py +1219 -0
- turbo_design-1.3.8.dist-info/RECORD +0 -33
- turbodesign/compressorspool.py +0 -60
- turbodesign/loss/turbine/fixedpressureloss.py +0 -25
- turbodesign/rotor.py +0 -38
- turbodesign/spool.py +0 -317
- turbodesign/turbinespool.py +0 -543
turbodesign/coolant.py
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
from
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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,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
|
turbodesign/flow_math.py
ADDED
|
@@ -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
|
|
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
|
|
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,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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,
|
|
38
|
-
self.beta1 = convert_to_ndarray(
|
|
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.
|
|
41
|
-
self.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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.
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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)
|