turbo-design 1.0.0__py2.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.
@@ -0,0 +1,189 @@
1
+ import pickle, os
2
+ from typing import Dict
3
+ from ...bladerow import BladeRow, sutherland
4
+ from ...lossinterp import LossInterp
5
+ from ...enums import RowType, LossType
6
+ import numpy as np
7
+ import pathlib
8
+ from ..losstype import LossBaseClass
9
+
10
+ class CraigCox(LossBaseClass):
11
+
12
+ def __init__(self):
13
+ """Craig and Cox used to estimate loss for subsonic higher mach number flows in axial turbines. Assumes suction side has low convex surface close to throat.
14
+
15
+ Reference:
16
+ Craig, H. R. M., and H. J. A. Cox. "Performance estimation of axial flow turbines." Proceedings of the Institution of Mechanical Engineers 185.1 (1970): 407-424.
17
+
18
+ """
19
+ super().__init__(LossType.Enthalpy)
20
+ path = pathlib.Path(os.path.join(os.environ['TD3_HOME'],"craigcox"+".pkl"))
21
+
22
+ if not path.exists():
23
+ print('Download file if doesn\'t exist')
24
+
25
+ with open(path.absolute(),'rb') as f:
26
+ self.data = pickle.load(f) # type: ignore
27
+
28
+ self.C = 1/(200*32.2*778.16) # https://www.sciencedirect.com/science/article/pii/S2666202721000574
29
+
30
+
31
+ def __call__(self,row:BladeRow, upstream:BladeRow) -> float:
32
+ """Craig and Cox uses the enthalpy definition of loss to calculate the loss of a turbine stage.
33
+
34
+ Note:
35
+ Losses are organized as Group 1 which include profile losses and secondary flows.
36
+ Group 2 losses include rotor tip leakage, balance losses, guide gland losses, lacing wire losses.
37
+
38
+ All equation numbers are from the craig cox paper
39
+ Craig, H. R. M., and H. J. A. Cox. "Performance estimation of axial flow turbines." Proceedings of the Institution of Mechanical Engineers 185.1 (1970): 407-424.
40
+
41
+ Equations:
42
+ Eta_t = (Work done - Group 2 losses) / (Work done + Group 1 Losses)
43
+
44
+ Group1 Losses = (Xp + Xs + Xa)*C1**2/(200gJ) + (Xp + Xs + Xa)*W2**2/(200gJ)
45
+ Group1 Losses = Stator Component + Rotor Component where C1 and W2 are exit velocities for stator and rotor
46
+
47
+ i+i_stall = (i+i_stall)_basic + (\delta i + stall)_sb + (\delta i + stall)_cb
48
+
49
+ (i+i_stall)_basic from Figure 11
50
+ (delta incidence + stall)_sb + (delta incidence + stall)_cb from Figure 12
51
+
52
+ Profile Loss
53
+ Xp = x_pb N_pr N_pi N_pt + (\delta x_p)_t + (\delta x_p)_s/e + (\delta x_p)_m
54
+
55
+ - x_pb from Figure 5 but use Figure 4 to calculate Fl. Fl*x*s/b is the x axis for Figure 5
56
+ - N_pr from figure 3
57
+ - N_pi from Figure 10
58
+ - N_pt from Figure 6
59
+ - (\delta x_p)_t
60
+ - (\delta x_p)_s/e from Figure 9
61
+ - (\delta x_p)_m from Figure 8
62
+
63
+ Args:
64
+ upstream (BladeRow): Upstream blade row
65
+ row (BladeRow): downstream blade row
66
+
67
+ Returns:
68
+ float: Stage Efficiency
69
+ """
70
+ if row.row_type == RowType.Stator:
71
+ return 0
72
+ else:
73
+ V_inlet = upstream.V.mean()
74
+ V = row.W.mean()
75
+
76
+ def lookup_constants(currentRow:BladeRow):
77
+ # Contraction Ratio find s/b and use Fig07
78
+ s_b = currentRow.pitch/currentRow.camber
79
+ if currentRow.row_type == RowType.Stator:
80
+ V = currentRow.V.mean()
81
+ # See weird velocity triangle in Figure 1
82
+ inlet_flow_angle = 90-np.abs(np.degrees(currentRow.alpha1).mean())
83
+ outlet_flow_angle =90-np.abs(np.degrees(currentRow.alpha2).mean())
84
+ incidence_angle = np.degrees(currentRow.alpha1 - currentRow.beta1_metal).mean()
85
+ M_out = currentRow.M.mean()
86
+ else:
87
+ V = currentRow.W.mean()
88
+ inlet_flow_angle = 90-np.abs(np.degrees(currentRow.beta1).mean())
89
+ outlet_flow_angle =90-np.abs(np.degrees(currentRow.beta2).mean())
90
+ incidence_angle = np.degrees(currentRow.beta1 - currentRow.beta1_metal).mean()
91
+ M_out = currentRow.M_rel.mean()
92
+
93
+ Re = V * currentRow.rho*currentRow.chord / sutherland(currentRow.T.mean())
94
+ Re = Re.mean()
95
+
96
+ if currentRow.beta1_fixed:
97
+ blade_inlet_angle = 90-np.abs(np.degrees(currentRow.beta1_metal.mean())) # Alpha
98
+ else:
99
+ if currentRow.row_type == RowType.Stator:
100
+ blade_inlet_angle = 90-np.abs(np.degrees(currentRow.alpha1.mean())) # Alpha1
101
+ else:
102
+ blade_inlet_angle = 90-np.abs(np.degrees(currentRow.beta1.mean())) # beta1
103
+
104
+ te = currentRow.te_pitch * currentRow.pitch
105
+ e_s = 0.3 # Pitch to back radius ratio, assumed. lower value = less loss
106
+ if currentRow.beta1_fixed:
107
+ imin = 90-currentRow.beta1_metal.mean()
108
+ else:
109
+ imin = 90-currentRow.beta1.mean() # Incidence required for minimum loss
110
+
111
+ asin_os = np.degrees(np.arcsin(currentRow.throat/currentRow.pitch))
112
+
113
+ N_pr = self.data['Fig03'](Re,0.05) # use a good finish for the geometry
114
+ if (inlet_flow_angle-imin < 10):
115
+ Fl = 13
116
+ else:
117
+ Fl = self.data['Fig04'](outlet_flow_angle,inlet_flow_angle-imin)
118
+
119
+ x = 1-np.sin(np.radians(outlet_flow_angle))/np.sin(np.radians(inlet_flow_angle))
120
+ contraction_ratio = self.data['Fig07'](x,s_b) # contraction ratio
121
+
122
+ X_pb = self.data['Fig05'](Fl*s_b,contraction_ratio)
123
+ delta_X_pt = self.data['Fig06_delta_Xpt'](currentRow.te_pitch)
124
+ N_pt = self.data['Fig06_Npt'](currentRow.te_pitch,outlet_flow_angle)
125
+ delta_Xpm = self.data['Fig08'](M_out,np.degrees(np.arcsin((currentRow.throat+te)/currentRow.pitch)))
126
+ delta_Xp_se = self.data['Fig09'](e_s,M_out)
127
+
128
+ Fi = self.data['Fig15'](blade_inlet_angle,s_b)
129
+ # Incidence Effects
130
+ if currentRow.beta1_fixed:
131
+ if incidence_angle>0: # Positive incidence
132
+ stall_incidence_angle = self.data['Fig11'](currentRow.beta1.mean(),asin_os)
133
+
134
+ incidence_ratio = (incidence_angle - imin)/(stall_incidence_angle-imin)
135
+
136
+ i_plus_istall_sb = self.data['Fig12_sb'](s_b,asin_os)
137
+ i_plus_istall_cor = self.data['Fig12_cr'](contraction_ratio,asin_os)
138
+
139
+ if blade_inlet_angle<=90:
140
+ i_plus_istall_basic = self.data['Fig11'](inlet_flow_angle,asin_os)
141
+ i_plus_istall = i_plus_istall_basic + i_plus_istall_sb + i_plus_istall_cor # Eqn 5
142
+ else:
143
+ i_plus_istall_basic = self.data["Fig14_i+istall"](blade_inlet_angle,asin_os)
144
+ i_plus_istall = i_plus_istall_basic + (1-(blade_inlet_angle-90)/(90-asin_os))*(i_plus_istall_sb + i_plus_istall_cor) # Eqn 7
145
+ else:
146
+ i_minus_istall_sb = self.data['Fig13'](s_b,asin_os)
147
+
148
+ if blade_inlet_angle<=90:
149
+ i_minus_istall_basic = self.data['Fig13_alpha1'](s_b,asin_os)
150
+ i_minus_istall = i_minus_istall_basic + i_minus_istall_sb # Eqn 6
151
+ else:
152
+ i_minus_istall_basic = self.data["Fig14_i-istall"](blade_inlet_angle,asin_os)
153
+ i_minus_istall = i_minus_istall_basic + (1-(blade_inlet_angle - 90)/(90-asin_os)) * i_minus_istall_sb # Eqn 8
154
+
155
+ imin = (i_plus_istall + Fi * (i_minus_istall))/(1+Fi) # type: ignore # Eqn 9
156
+ N_pi = self.data['Fig10'](imin,incidence_ratio)
157
+ else:
158
+ N_pi = 1 # No effect
159
+
160
+ Xp = X_pb*N_pr*N_pi*N_pt + delta_X_pt + delta_Xp_se + delta_Xpm # Eqn 10
161
+
162
+ # Secondary Loss
163
+ Ns_hb = self.data['Fig17'](1/currentRow.aspect_ratio)
164
+ x_sb = self.data['Fig18']((V_inlet/V)**2,s_b*Fl)
165
+
166
+ Nsr = 1 # N_pr # I have no clue about this. Craig Cox doesn't describe. setting it to 1 for now.
167
+ Xs = Nsr*Ns_hb*x_sb
168
+ # Annulus Loss Factor
169
+ Xa = 0
170
+ return Xp, Xs, Xa
171
+
172
+
173
+ Xp1,Xs1,Xa1 = lookup_constants(upstream)
174
+ Xp2,Xs2,Xa2 = lookup_constants(row)
175
+ Group1_Loss = (Xp1 + Xs1 + Xa1) * V_inlet**2 *3.28**2 * self.C + (Xp2 + Xs2 + Xa2*V_inlet**2/V**2) * V**2 *3.28**2 * self.C # type: ignore
176
+ # Eqn 4, convert V from m^2/s^2to ft^2/s^2
177
+
178
+ # According to Equation 3, Group 1 loss is an enthalpy loss Cp*T0 Btu/lb. Need to convert to Pressure Loss
179
+
180
+ # Btu/lbf to J/Kg
181
+
182
+ T0_Loss = Group1_Loss * 2326 / row.Cp # Eqn 3 in Kelvin
183
+ T0_T = (row.T0/row.T).mean()
184
+ T02 = row.T0.mean()-T0_Loss # P02 Changes
185
+
186
+ # According to Equation 3, Group 1 loss is an enthalpy loss Cp*T0. Need to convert to Pressure Loss
187
+ eta_total = (upstream.T0.mean() - row.T0.mean())/(upstream.T0.mean()-(row.T0.mean()-T0_Loss))
188
+ return eta_total
189
+
@@ -0,0 +1,29 @@
1
+ from ...bladerow import BladeRow, sutherland
2
+ from ...enums import RowType, LossType
3
+ from ..losstype import LossBaseClass
4
+
5
+ class FixedEfficiency(LossBaseClass):
6
+ efficiency:float
7
+
8
+ def __init__(self,efficiency:float):
9
+ """Fixed Efficiency Loss
10
+ """
11
+ super().__init__(LossType.Enthalpy)
12
+ self.efficiency = efficiency
13
+
14
+
15
+ def __call__(self,row:BladeRow, upstream:BladeRow) -> float:
16
+ """Fixed efficiency loss
17
+
18
+ Args:
19
+ upstream (BladeRow): Upstream blade row
20
+ row (BladeRow): downstream blade row
21
+
22
+ Returns:
23
+ float: Stage Efficiency
24
+ """
25
+ if row.row_type == RowType.Stator:
26
+ return 0
27
+ else:
28
+ return self.efficiency
29
+
@@ -0,0 +1,25 @@
1
+ from ...bladerow import BladeRow
2
+ from ..losstype import LossBaseClass
3
+ from ...enums import LossType
4
+
5
+ class FixedPressureLoss(LossBaseClass):
6
+ pressure_loss:float
7
+
8
+ def __init__(self,pressure_loss:float):
9
+ """Fixed Pressure Loss
10
+ """
11
+ super().__init__(LossType.Pressure)
12
+ self.pressure_loss = pressure_loss
13
+
14
+
15
+ def __call__(self,row:BladeRow, upstream:BladeRow) -> float:
16
+ """Outputs the fixed Pressure Loss
17
+
18
+ Args:
19
+ upstream (BladeRow): Upstream blade row
20
+ row (BladeRow): downstream blade row
21
+
22
+ Returns:
23
+ float: Pressure Loss
24
+ """
25
+ return self.pressure_loss
@@ -0,0 +1,124 @@
1
+ import pickle, os
2
+ from typing import Dict
3
+ from ...bladerow import BladeRow, sutherland
4
+ from ...lossinterp import LossInterp
5
+ from ...enums import RowType, LossType
6
+ import numpy as np
7
+ import pathlib
8
+ from ..losstype import LossBaseClass
9
+
10
+ class KrackerOkapuu(LossBaseClass):
11
+
12
+ def __init__(self):
13
+ """KackerOkapuu model is an improvement to the Ainley Mathieson model.
14
+
15
+ Limitations:
16
+ - Doesn't factor incidence loss
17
+ - For steam turbines and impulse turbines
18
+
19
+ Reference:
20
+ Kacker, S. C., and U. Okapuu. "A mean line prediction method for axial flow turbine efficiency." (1982): 111-119.
21
+
22
+ """
23
+ super().__init__(LossType.Pressure)
24
+ path = pathlib.Path(os.path.join(os.environ['TD3_HOME'],"kackerokapuu"+".pkl"))
25
+
26
+ if not path.exists():
27
+ print('Download file if doesn\'t exist')
28
+
29
+ with open(path.absolute(),'rb') as f:
30
+ self.data = pickle.load(f) # type: ignore
31
+
32
+
33
+
34
+ def __call__(self,row:BladeRow, upstream:BladeRow) -> float:
35
+ """Kacker Okapuu is an updated version of Ainley Mathieson and Dunham Came. This tool uses the pressure loss definition.
36
+
37
+ Note:
38
+ All equation numbers are from the Kacker Okapuu paper
39
+
40
+ Reference:
41
+ Kacker, S. C., and U. Okapuu. "A mean line prediction method for axial flow turbine efficiency." (1982): 111-119.
42
+
43
+ Args:
44
+ upstream (BladeRow): Upstream blade row
45
+ row (BladeRow): downstream blade row
46
+
47
+ Returns:
48
+ float: Pressure Loss
49
+ """
50
+ if upstream.row_type == RowType.Stator:
51
+ M1 = upstream.M
52
+ else:
53
+ M1 = upstream.M_rel
54
+
55
+ c = row.chord
56
+ b = row.axial_chord
57
+ if row.row_type == RowType.Stator:
58
+ alpha1 = np.degrees(row.alpha1)
59
+ beta1 = np.degrees(row.beta1_metal)
60
+ alpha2 = np.degrees(row.alpha2)
61
+ M2 = row.M
62
+ h = 0
63
+ Rec = row.V*row.rho*row.chord / sutherland(row.T)
64
+ else:
65
+ h = row.tip_clearance * (row.r[-1]-row.r[0])
66
+ alpha1 = np.degrees(row.beta1)
67
+ beta1 = row.beta1_metal
68
+ alpha2 = np.degrees(row.beta2)
69
+ M2 = row.M_rel
70
+ Rec = row.W*row.rho*row.chord / sutherland(row.T)
71
+
72
+ Yp_beta0 = self.data['Fig01'](row.pitch_to_chord, alpha2)
73
+ Yp_beta1_alpha2 = self.data['Fig02'](row.pitch_to_chord, alpha2)
74
+ t_max_c = self.data['Fig04'](np.abs(beta1)+np.abs(alpha2))
75
+
76
+ Yp_amdc = (Yp_beta0 + np.abs(beta1/alpha2) *beta1/alpha2 * (Yp_beta1_alpha2-Yp_beta0)) * ((t_max_c)/0.2)**(beta1/alpha2) # Eqn 2, AMDC = Ainley Mathieson Dunham Came
77
+
78
+ # Shock Loss
79
+ dP_q1_hub = 0.75*(M1-0.4)**1.75 # Eqn 4, this is at the hub
80
+ dP_q1_shock = row.r[-1]/row.r[0] * dP_q1_hub # Eqn 5
81
+ Y_shock = dP_q1_shock * upstream.P/row.P * (1-(1+(upstream.gamma-1)/2*M1**2))/(1-(1+(row.gamma-1)/2*M2**2)) # Eqn 6
82
+
83
+ K1 = self.data['Fig08_K1'](M2)
84
+ K2 = (M1/M2)**2
85
+ Kp = 1-K2*(1-K1)
86
+
87
+ CFM = 1+60*(M2-1)**2 # Eqn 9
88
+
89
+ Yp = 0.914 * (2/3*Yp_amdc *Kp + Y_shock) # Eqn 8 Subsonic regime
90
+ if M2>1:
91
+ Yp = Yp*CFM
92
+
93
+ f_ar = (1-0.25*np.sqrt(2-h/c)) / (h/c) if h/c<=2 else 1/(h/c)
94
+ alpham = np.arctan(0.5*(np.tan(alpha1) - np.tan(alpha2)))
95
+ Cl_sc = 2*(np.tan(alpha1)+np.tan(alpha2))*np.cos(alpham)
96
+ Ys_amdc = 0.0334 *f_ar *np.cos(alpha2)/np.cos(beta1) * (Cl_sc)**2 * np.cos(alpha2)**2 / np.cos(alpham)**3
97
+ # Secondary Loss
98
+ K3 = 1/(h/(b))**2 # Fig 13, it's actually bx in the picture which is the axial chord
99
+ Ks = 1-K3*(1-Kp) # Eqn 15
100
+ Ys = 1.2*Ys_amdc*Ks # Eqn 16
101
+
102
+ # Trailing Edge
103
+ if np.abs(alpha1-alpha2)<5:
104
+ delta_phi2 = self.data['Fig14_Impulse'](row.te_pitch*row.pitch / row.throat)
105
+ else:
106
+ delta_phi2 = self.data['Fig14_Axial_Entry'](row.te_pitch*row.pitch / row.throat)
107
+
108
+ Ytet = (1-(row.gamma-1)/2 - M2**2 * (1/(1-delta_phi2)-1))**(-row.gamma/(row.gamma-1))-1
109
+ Ytet = Ytet / (1-(1+(row.gamma-1)/2*M2**2)**(-row.gamma/(row.gamma-1)))
110
+
111
+ # Tip Clearance
112
+ kprime = row.tip_clearance/(3)**0.42 # Number of seals
113
+ Ytc = 0.37*c/h * (kprime/c)**0.78 * Cl_sc**2 * np.cos(alpha2)**2 / np.cos(alpham)**3
114
+
115
+ if Rec <= 2E5:
116
+ f_re = (Rec/2E5)**-0.4
117
+ elif Rec<1E6:
118
+ f_re = 1
119
+ else:
120
+ f_re = (Rec/1E6)**-0.2
121
+
122
+ Yt = Yp*f_re + Ys + Ytet + Ytc
123
+ return Yt
124
+
@@ -0,0 +1,95 @@
1
+ import pickle, os
2
+ from typing import Dict
3
+ from ...bladerow import BladeRow, sutherland
4
+ from ...lossinterp import LossInterp
5
+ from ...enums import RowType, LossType
6
+ import numpy as np
7
+ import pathlib
8
+ from ..losstype import LossBaseClass
9
+
10
+ class Traupel(LossBaseClass):
11
+ def __init__(self):
12
+ super().__init__(LossType.Enthalpy)
13
+ path = pathlib.Path(os.path.join(os.environ['TD3_HOME'],"traupel"+".pkl"))
14
+
15
+ if not path.exists():
16
+ print('Download file if doesn\'t exist')
17
+
18
+ with open(path.absolute(),'rb') as f:
19
+ self.data = pickle.load(f) # type: ignore
20
+
21
+ def __call__(self,row:BladeRow, upstream:BladeRow) -> float:
22
+ """Enthalpy loss is computed for the entire stage.
23
+
24
+ Args:
25
+ upstream (BladeRow): Stator Row
26
+ row (BladeRow): Rotor Row
27
+
28
+ Returns:
29
+ float: Efficiency
30
+ """
31
+
32
+ alpha1 = 90-np.degrees(upstream.alpha1.mean())
33
+ alpha2 = 90-np.degrees(upstream.alpha2.mean())
34
+ beta2 = 90 - np.degrees(row.beta1.mean())
35
+ beta3 = 90 - np.degrees(row.beta2.mean())
36
+
37
+ g = upstream.pitch # G is the pitch
38
+ h_stator = upstream.r[-1] - upstream.r[0]
39
+ h_rotor = row.r[-1] - row.r[0]
40
+
41
+ if row.row_type == RowType.Rotor:
42
+ turning = np.abs(np.degrees(upstream.beta2-row.beta2).mean())
43
+ F = self.data['Fig06']((upstream.W/row.W).mean(),turning) # Inlet velocity
44
+ else:
45
+ turning = np.abs(np.degrees(upstream.alpha2-row.alpha2).mean())
46
+ F = self.data['Fig06']((upstream.V/row.V).mean(),turning) # Inlet velocity
47
+
48
+ H = self.data['Fig07'](alpha1-beta2,alpha2-beta3)
49
+
50
+ zeta_s = F*g/h_stator # (h1-h1s)/(0.5*c1s**2) # no idea what h1s or h2s is
51
+ zeta_r = F*g/h_rotor # (h2-h2s)/(0.5*w2s**2)
52
+ x_p_stator = self.data['Fig01'](alpha1,alpha2) # not sure if this is the right figure
53
+ x_p_rotor = self.data['Fig01'](beta2,beta3) # not sure if this is the right figure
54
+ zeta_p_stator = self.data['Fig02'](alpha1,alpha2)
55
+ x_m_stator = self.data['Fig03_0'](upstream.M)
56
+ zeta_p_rotor = self.data['Fig02'](beta2,beta3)
57
+ x_m_rotor = self.data['Fig03_0'](row.M_rel)
58
+
59
+
60
+ e_te = upstream.te_pitch * g
61
+ o = upstream.throat
62
+ ssen_alpha2 = e_te/o # Thickness of Trailing edge divide by throat
63
+ ssen_beta2 = row.te_pitch*g / row.throat
64
+
65
+ x_delta_stator = self.data['Fig05'](ssen_alpha2,alpha2)
66
+ zeta_delta_stator = self.data['Fig04'](ssen_alpha2,alpha2)
67
+ x_delta_rotor = self.data['Fig05'](ssen_beta2,beta3)
68
+ zeta_delta_rotor = self.data['Fig04'](ssen_beta2,beta3)
69
+
70
+ Dm = 2* (upstream.r[-1] + upstream.r[0])/2 # Is this the mean diameter? I dont know
71
+ zeta_f = 0.5 * (h_stator/Dm)**2
72
+
73
+ zeta_pr_stator = zeta_p_stator * x_p_stator * x_m_stator * x_delta_stator + zeta_delta_stator + zeta_f
74
+
75
+ Dm = 2* (row.r[-1] + row.r[0])/2 # Is this the mean diameter? I dont know
76
+ zeta_f = 0.5 * (h_rotor/Dm)**2
77
+
78
+ zeta_pr_rotor = zeta_p_rotor * x_p_rotor * x_m_rotor * x_delta_rotor + zeta_delta_rotor + zeta_f
79
+
80
+ if row.row_type == RowType.Stator:
81
+ zeta_cl = 0
82
+ else:
83
+ zeta_cl = self.data['Fig08'](row.tip_clearance) # For simplicity assume unshrouded blade
84
+
85
+ zeta_z = 0 # Do not factor this in, a bit complicated
86
+ # 1 - (internal) - (external)
87
+ zeta_v = 0
88
+ zeta_off = 0
89
+ eta_stator = 1- (zeta_pr_stator + zeta_s + 0 + zeta_z) - (zeta_r+zeta_v) - zeta_off # Presentation slide 9
90
+ eta_rotor = 1 - (zeta_pr_rotor + zeta_r + zeta_cl + zeta_z) - (zeta_r+zeta_v) - zeta_off
91
+ return eta_stator+eta_rotor
92
+
93
+
94
+
95
+
@@ -0,0 +1,178 @@
1
+ from typing import List, Tuple, Union
2
+ import pandas as pd
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+ from scipy.interpolate import bisplrep, bisplev, interp1d, LSQBivariateSpline
6
+ import matplotlib.pyplot as plt
7
+
8
+
9
+ class LossInterp:
10
+ """Interpret the loss of an XY Graph with an additional 3rd variable
11
+ """
12
+ df: pd.DataFrame
13
+ x:np.ndarray
14
+ y:np.ndarray
15
+ c:np.ndarray
16
+
17
+ x_max_c:np.ndarray
18
+ x_min_c:np.ndarray
19
+
20
+ c_max:float
21
+ c_min:float
22
+
23
+ fxc_max: interp1d
24
+ fxc_min: interp1d
25
+
26
+ weights: np.ndarray
27
+ xlabel:str
28
+ ylabel:str
29
+ clabel:str
30
+ _name:str
31
+
32
+ is_xy:bool
33
+ func: interp1d
34
+
35
+ xlogscale:bool
36
+
37
+ @property
38
+ def name(self):
39
+ return self._name
40
+
41
+ @name.setter
42
+ def name(self,val:str):
43
+ self._name = val
44
+
45
+ def __init__(self,csv_filename:str,xlabel:str="",ylabel:str="",clabel:str="",logx10:bool=False):
46
+ """Initialize the loss interpolation with data
47
+
48
+ Note:
49
+ The data contains x,y, and c. Y is predicted from x and c
50
+ Args:
51
+ csv_filename (str): csv data with 3 columns, x,y,c.
52
+ xlabel (str, optional): xlabel. Defaults to "".
53
+ ylabel (str, optional): ylabel. Defaults to "".
54
+ clabel (str, optional): clabel. Defaults to "".
55
+ logx10 (bool, optional): apply log base 10 to x axis. Defaults to False
56
+ """
57
+ self.df = pd.read_csv(csv_filename)
58
+ self.df = self.df.sort_values(self.df.columns[0],ascending=True)
59
+ self.name = csv_filename
60
+ data = self.df.to_numpy()
61
+ self.xlabel = xlabel
62
+ self.ylabel = ylabel
63
+ self.logX10 = logx10
64
+ if data.shape[1] == 3:
65
+ self.x = data[:,0]
66
+ self.y = data[:,1]
67
+ self.c = data[:,2]
68
+
69
+ self.clabel = clabel
70
+
71
+ # Find the minimum x value for every item in row
72
+ self.x_max_c = np.zeros(shape=(len(self.df.iloc[:,2].unique()),2))
73
+ self.x_min_c = np.zeros(shape=(len(self.df.iloc[:,2].unique()),2))
74
+
75
+ i = 0
76
+ for inlet_flow in self.df.iloc[:,2].unique():
77
+ df_slice = self.df[self.df.iloc[:,2] == inlet_flow]
78
+ df_slice = df_slice.sort_values(df_slice.columns[0],ascending=True)
79
+ self.x_max_c[i,0] = inlet_flow
80
+ self.x_max_c[i,1] = df_slice.iloc[:,0].max()
81
+
82
+ self.x_min_c[i,0] = inlet_flow
83
+ self.x_min_c[i,1] = df_slice.iloc[:,0].min()
84
+ i+=1
85
+ self.fxc_max = interp1d(self.x_max_c[:,0],self.x_max_c[:,1]) # xmax as a function of c
86
+ self.fxc_min = interp1d(self.x_min_c[:,0],self.x_min_c[:,1]) # xmin as a function of c
87
+
88
+ # self.weights = bisplrep(self.x,self.c,self.y,kx=5, ky=5)
89
+ if self.logX10:
90
+ self.func = LSQBivariateSpline(np.log10(self.x),self.c,self.y,np.log10(np.unique(self.x[:4])),np.unique(self.c[:4]))
91
+ else:
92
+ self.func = LSQBivariateSpline(self.x,self.c,self.y,np.unique(self.x[:3]),np.unique(self.c[:3]))
93
+ self.is_xy = False # 3 columns
94
+ self.c_max = self.df.to_numpy()[:,-1].max()
95
+ self.c_min = self.df.to_numpy()[:,-1].min()
96
+ else:
97
+ self.is_xy = True
98
+ self.x = data[:,0]
99
+ self.y = data[:,1]
100
+ if self.logX10:
101
+ self.func = interp1d(np.log10(self.x),self.y)
102
+ else:
103
+ self.func = interp1d(self.x,self.y)
104
+
105
+ def __call__(self,x:Union[npt.NDArray,float],c:Union[npt.NDArray,float,None]=None) -> float:
106
+ """Pass in an array of x and c values
107
+
108
+ Args:
109
+ x (Union[npt.NDArray,float]): value on x axis
110
+ c (Union[npt.NDArray,float,None]): Third axis value
111
+
112
+ Returns:
113
+ float: y
114
+ """
115
+
116
+ if self.is_xy or c==None:
117
+ if self.logX10:
118
+ x = np.log10(x)
119
+ if isinstance(x, np.ndarray):
120
+ return self.func(x)
121
+ elif isinstance(x, float):
122
+ return float(self.func(x))
123
+ else:
124
+ if isinstance(x, np.ndarray):
125
+ xmax = self.fxc_max(c)
126
+ xmin = self.fxc_min(c)
127
+ x[x>xmax] = xmax
128
+ x[x<xmin] = xmin
129
+ if self.logX10:
130
+ y = self.func(np.log10(x),c)
131
+ else:
132
+ y = self.func(x,c)
133
+ elif isinstance(x, float):
134
+ if (c>self.c_max):
135
+ c = self.c_max
136
+ elif c<self.c_min:
137
+ c = self.c_min
138
+ xmax = float(self.fxc_max(c))
139
+ xmin = float(self.fxc_min(c))
140
+ x = xmin if x<xmin else x
141
+ x = xmax if x>xmax else x
142
+ # y[j] = bisplev(x[j],cc,self.weights)
143
+ if self.logX10:
144
+ y = self.func(np.log10(x),c)[0][0] # type: ignore
145
+ else:
146
+ y = self.func(x,c)[0][0] # type: ignore
147
+ return y
148
+
149
+ def plot(self):
150
+ """Plot the data with predicted values
151
+ """
152
+ plt.figure(num=1,figsize=(10,6))
153
+ if not self.is_xy:
154
+ c_unique = np.unique(self.c)
155
+ graycolors = plt.cm.gray(np.linspace(0,1,len(c_unique)))
156
+ coolcolors = plt.cm.cool(np.linspace(0,1,len(c_unique)))
157
+
158
+
159
+ # Plot the actual data
160
+ i = 0
161
+ for c in c_unique:
162
+ df2 = self.df[self.df.iloc[:,2] == c]
163
+ x = df2.iloc[:,0].to_numpy()
164
+ plt.plot(x,df2.iloc[:,1],'.-',label=f'{c}',color=graycolors[i])
165
+ y = self(x,c)
166
+ plt.plot(x,y,'o-',label=f'predicted-{c}',color=coolcolors[i])
167
+ i+=1
168
+ else:
169
+ x = self.df.iloc[:,0].to_numpy()
170
+ plt.plot(x,self.df.iloc[:,1],'ro',linewidth=2,label='actual')
171
+ y = self(x)
172
+ plt.plot(x,y,'b-',label='predicted')
173
+
174
+ plt.ylabel(self.ylabel)
175
+ plt.xlabel(self.xlabel)
176
+ plt.title(self.name)
177
+ plt.legend()
178
+ plt.show()