turbx 1.0.2__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.
- turbx/__init__.py +52 -0
- turbx/bl.py +620 -0
- turbx/blasius.py +64 -0
- turbx/cli.py +19 -0
- turbx/composite_profile.py +243 -0
- turbx/confidence_interval.py +64 -0
- turbx/eas3.py +420 -0
- turbx/eas4.py +567 -0
- turbx/fig_ax_constructor.py +52 -0
- turbx/freestream_parameters.py +268 -0
- turbx/gradient.py +391 -0
- turbx/grid_metric.py +272 -0
- turbx/h5.py +236 -0
- turbx/mvp.py +385 -0
- turbx/rgd.py +2693 -0
- turbx/rgd_mean.py +523 -0
- turbx/rgd_testing.py +354 -0
- turbx/rgd_xpln_ccor.py +701 -0
- turbx/rgd_xpln_coh.py +992 -0
- turbx/rgd_xpln_mean_dim.py +336 -0
- turbx/rgd_xpln_spectrum.py +940 -0
- turbx/rgd_xpln_stats.py +738 -0
- turbx/rgd_xpln_turb_budget.py +1193 -0
- turbx/set_mpl_env.py +85 -0
- turbx/signal.py +277 -0
- turbx/spd.py +1206 -0
- turbx/spd_wall_ccor.py +629 -0
- turbx/spd_wall_ci.py +406 -0
- turbx/spd_wall_import.py +676 -0
- turbx/spd_wall_spectrum.py +638 -0
- turbx/spd_wall_stats.py +618 -0
- turbx/utils.py +84 -0
- turbx/ztmd.py +2224 -0
- turbx/ztmd_analysis.py +2337 -0
- turbx/ztmd_loader.py +56 -0
- turbx-1.0.2.dist-info/LICENSE +21 -0
- turbx-1.0.2.dist-info/METADATA +120 -0
- turbx-1.0.2.dist-info/RECORD +41 -0
- turbx-1.0.2.dist-info/WHEEL +5 -0
- turbx-1.0.2.dist-info/entry_points.txt +2 -0
- turbx-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from .utils import even_print
|
|
4
|
+
|
|
5
|
+
# ======================================================================
|
|
6
|
+
|
|
7
|
+
class freestream_parameters(object):
|
|
8
|
+
'''
|
|
9
|
+
calculate freestream parameters & characteristic scales for dry air (21% O2 / 78% N2) and standard conditions
|
|
10
|
+
'''
|
|
11
|
+
|
|
12
|
+
def __init__(self, Re=None, M_inf=None, lchar=None, U_inf=None, T_inf=None, p_inf=None, rho_inf=None, Pr=None, compressible=True, HTfac=1.0):
|
|
13
|
+
|
|
14
|
+
if not isinstance(compressible, bool):
|
|
15
|
+
raise ValueError
|
|
16
|
+
|
|
17
|
+
self.compressible = compressible
|
|
18
|
+
|
|
19
|
+
## dry air (21% O2 / 78% N2)
|
|
20
|
+
M_molar = 28.9647e-3 ## molar mass [kg/mol]
|
|
21
|
+
R_molar = 8.31446261815324 ## molar gas constant [J/(K·mol)]
|
|
22
|
+
R = R_molar / M_molar ## specific / individual gas constant [J/(kg·K)]
|
|
23
|
+
cp = (7/2)*R ## isobaric specific heat (ideal gas) [J/(kg·K)]
|
|
24
|
+
cv = (5/2)*R ## isochoric specific heat (ideal gas) [J/(kg·K)]
|
|
25
|
+
kappa = cp/cv
|
|
26
|
+
|
|
27
|
+
# === get freestream static T_inf, p_inf, ρ_inf
|
|
28
|
+
|
|
29
|
+
# T_inf = 273.15 + 15 ## freestream static temperature [K]
|
|
30
|
+
# p_inf = 101325. ## freestream static pressure [Pa]
|
|
31
|
+
# rho_inf = 1.2249908312142817 ## freestream mass density [kg/m³]
|
|
32
|
+
|
|
33
|
+
if all([(T_inf is None),(p_inf is None),(rho_inf is None)]): ## NONE of T,p,ρ provided --> use standard atmosphere
|
|
34
|
+
T_inf = 273.15 + 15
|
|
35
|
+
p_inf = 101325.
|
|
36
|
+
rho_inf = p_inf/(R*T_inf)
|
|
37
|
+
elif all([(T_inf is not None),(p_inf is not None),(rho_inf is not None)]): ## ALL T,p,ρ provided --> check
|
|
38
|
+
np.testing.assert_allclose(rho_inf, p_inf/(R*T_inf), rtol=1e-12, atol=1e-12)
|
|
39
|
+
elif (T_inf is not None) and (p_inf is None) and (rho_inf is None): ## only T provided
|
|
40
|
+
p_inf = 101325.
|
|
41
|
+
rho_inf = p_inf/(R*T_inf)
|
|
42
|
+
elif (T_inf is None) and (p_inf is not None) and (rho_inf is None): ## only p provided
|
|
43
|
+
T_inf = 273.15 + 15
|
|
44
|
+
rho_inf = p_inf/(R*T_inf)
|
|
45
|
+
elif (T_inf is None) and (p_inf is None) and (rho_inf is not None): ## only ρ provided
|
|
46
|
+
T_inf = 273.15 + 15
|
|
47
|
+
p_inf = rho_inf*R*T_inf
|
|
48
|
+
elif (T_inf is not None) and (p_inf is not None) and (rho_inf is None): ## T & p provided
|
|
49
|
+
rho_inf = p_inf/(R*T_inf)
|
|
50
|
+
elif (T_inf is not None) and (p_inf is None) and (rho_inf is not None): ## T & ρ provided
|
|
51
|
+
p_inf = rho_inf*R*T_inf
|
|
52
|
+
elif (T_inf is None) and (p_inf is not None) and (rho_inf is not None): ## p & ρ provided
|
|
53
|
+
T_inf = p_inf/(rho_inf*R)
|
|
54
|
+
else:
|
|
55
|
+
raise ValueError('this should never happen.')
|
|
56
|
+
|
|
57
|
+
np.testing.assert_allclose(rho_inf, p_inf/(R*T_inf), rtol=1e-12, atol=1e-12)
|
|
58
|
+
|
|
59
|
+
# === get freestream dynamic & kinematic viscosities using Sutherland's Law
|
|
60
|
+
|
|
61
|
+
## Sutherland's Law : dynamic viscosity : μ(T)
|
|
62
|
+
## https://www.cfd-online.com/Wiki/Sutherland%27s_law
|
|
63
|
+
## White 2006 'Viscous Fluid Flow' 3rd Edition, p. 28-31
|
|
64
|
+
S_Suth = 110.4 ## [K] --> Sutherland temperature
|
|
65
|
+
mu_Suth_ref = 1.716e-5 ## [kg/(m·s)] --> μ of air at T_Suth_ref = 273.15 [K]
|
|
66
|
+
T_Suth_ref = 273.15 ## [K]
|
|
67
|
+
C_Suth = mu_Suth_ref/(T_Suth_ref**(3/2))*(T_Suth_ref + S_Suth) ## [kg/(m·s·√K)]
|
|
68
|
+
|
|
69
|
+
mu_inf_1 = C_Suth*T_inf**(3/2)/(T_inf+S_Suth) ## [Pa·s] | [N·s/m²]
|
|
70
|
+
mu_inf_2 = mu_Suth_ref*(T_inf/T_Suth_ref)**(3/2) * ((T_Suth_ref+S_Suth)/(T_inf+S_Suth)) ## [Pa·s] | [N·s/m²]
|
|
71
|
+
np.testing.assert_allclose(mu_inf_1, mu_inf_2, rtol=1e-12, atol=1e-12)
|
|
72
|
+
mu_inf = mu_inf_1
|
|
73
|
+
|
|
74
|
+
nu_inf = mu_inf / rho_inf ## kinematic viscosity [m²/s] --> momentum diffusivity
|
|
75
|
+
|
|
76
|
+
# === get Prandtl number, thermal conductivity, thermal diffusivity
|
|
77
|
+
|
|
78
|
+
if (Pr is None):
|
|
79
|
+
|
|
80
|
+
## Sutherland's Law : thermal conductivity : k(T)
|
|
81
|
+
## White 2006 'Viscous Fluid Flow' 3rd Edition, p. 28-31
|
|
82
|
+
k_Suth_ref = 0.0241 ## [W/(m·K)] reference thermal conductivity
|
|
83
|
+
Sk_Suth = 194.0 ## [K]
|
|
84
|
+
k_inf = k_Suth_ref*(T_inf/T_Suth_ref)**(3/2) * ((T_Suth_ref+Sk_Suth)/(T_inf+Sk_Suth)) ## thermal conductivity [W/(m·K)]
|
|
85
|
+
|
|
86
|
+
alpha = k_inf/(rho_inf*cp) ## thermal diffusivity [m²/s]
|
|
87
|
+
Pr = nu_inf/alpha ## [-] ==cp·mu/k_inf
|
|
88
|
+
|
|
89
|
+
else:
|
|
90
|
+
|
|
91
|
+
k_inf = cp*mu_inf/Pr ## thermal conductivity [W/(m·K)]
|
|
92
|
+
alpha = k_inf/(rho_inf*cp) ## thermal diffusivity [m²/s]
|
|
93
|
+
|
|
94
|
+
np.testing.assert_allclose(nu_inf/alpha, cp*mu_inf/k_inf, rtol=1e-12, atol=1e-12)
|
|
95
|
+
np.testing.assert_allclose(Pr, nu_inf/alpha, rtol=1e-12, atol=1e-12)
|
|
96
|
+
np.testing.assert_allclose(Pr, cp*mu_inf/k_inf, rtol=1e-12, atol=1e-12)
|
|
97
|
+
|
|
98
|
+
# === get freestream velocity magnitude U_inf, freestream speed of sound a_inf, and Mach number M_inf
|
|
99
|
+
|
|
100
|
+
a_inf = np.sqrt(kappa*R*T_inf) ## speed of sound [m/s]
|
|
101
|
+
|
|
102
|
+
if (not self.compressible): ## incompressible
|
|
103
|
+
|
|
104
|
+
if (U_inf is None):
|
|
105
|
+
raise ValueError('if compressible=False, then providing U_inf is required')
|
|
106
|
+
if (M_inf is not None) and (M_inf != 0.):
|
|
107
|
+
raise ValueError('if compressible=False, then providing a non-zero M_inf makes no sense')
|
|
108
|
+
|
|
109
|
+
M_inf = 0.
|
|
110
|
+
a_inf = np.inf
|
|
111
|
+
|
|
112
|
+
else: ## compressible
|
|
113
|
+
|
|
114
|
+
if (U_inf is None) and (M_inf is None):
|
|
115
|
+
raise ValueError('provide either U_inf or M_inf')
|
|
116
|
+
elif (U_inf is not None) and (M_inf is not None):
|
|
117
|
+
np.testing.assert_allclose(M_inf, U_inf/a_inf, rtol=1e-12, atol=1e-12)
|
|
118
|
+
elif (U_inf is None) and (M_inf is not None):
|
|
119
|
+
U_inf = a_inf * M_inf ## velocity freestream [m/s]
|
|
120
|
+
elif (U_inf is not None) and (M_inf is None):
|
|
121
|
+
M_inf = U_inf / a_inf ## Mach number
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError('this should never happen.')
|
|
124
|
+
|
|
125
|
+
np.testing.assert_allclose(M_inf, U_inf/a_inf, rtol=1e-12, atol=1e-12)
|
|
126
|
+
|
|
127
|
+
# === get characteristic freestream scales: length, time, velocity
|
|
128
|
+
|
|
129
|
+
if (lchar is None) and (Re is None):
|
|
130
|
+
raise ValueError('provide either lchar or Re')
|
|
131
|
+
elif (lchar is not None) and (Re is not None):
|
|
132
|
+
np.testing.assert_allclose(Re, lchar * U_inf / nu_inf, rtol=1e-12, atol=1e-12)
|
|
133
|
+
elif (lchar is None) and (Re is not None):
|
|
134
|
+
lchar = Re * nu_inf / U_inf
|
|
135
|
+
elif (lchar is not None) and (Re is None):
|
|
136
|
+
Re = lchar * U_inf / nu_inf
|
|
137
|
+
else:
|
|
138
|
+
raise ValueError('this should never happen.')
|
|
139
|
+
|
|
140
|
+
np.testing.assert_allclose(Re, lchar*U_inf/nu_inf, rtol=1e-12, atol=1e-12)
|
|
141
|
+
|
|
142
|
+
tchar = lchar / U_inf
|
|
143
|
+
uchar = lchar / tchar
|
|
144
|
+
if not np.isclose(uchar, U_inf, rtol=1e-12):
|
|
145
|
+
raise AssertionError('U_inf!=uchar')
|
|
146
|
+
|
|
147
|
+
np.testing.assert_allclose(lchar, uchar*tchar, rtol=1e-12, atol=1e-12)
|
|
148
|
+
|
|
149
|
+
# === get isentropic total state
|
|
150
|
+
|
|
151
|
+
if self.compressible:
|
|
152
|
+
T_tot = T_inf * (1 + (kappa-1)/2 * M_inf**2)
|
|
153
|
+
p_tot = p_inf * (1 + (kappa-1)/2 * M_inf**2)**(kappa/(kappa-1))
|
|
154
|
+
rho_tot = rho_inf * (1 + (kappa-1)/2 * M_inf**2)**(1/(kappa-1))
|
|
155
|
+
else:
|
|
156
|
+
T_tot = T_inf + U_inf**2 / (2 * cp)
|
|
157
|
+
p_tot = p_inf + 0.5*rho_inf*U_inf**2 ## Bernoulli
|
|
158
|
+
rho_tot = p_tot/(R*T_tot)
|
|
159
|
+
|
|
160
|
+
np.testing.assert_allclose(T_tot , T_inf + U_inf**2/(2*cp) , rtol=1e-12, atol=1e-12)
|
|
161
|
+
np.testing.assert_allclose(p_tot , rho_tot*R*T_tot , rtol=1e-12, atol=1e-12)
|
|
162
|
+
|
|
163
|
+
# === get wall temperatures for a turbulent boundary layer
|
|
164
|
+
|
|
165
|
+
recov_fac = pow(Pr,1/3) ## recovery factor (turbulent flat plate boundary layer)
|
|
166
|
+
Taw = T_inf + recov_fac * U_inf**2/(2*cp) ## adiabatic wall temperature
|
|
167
|
+
Tw = T_inf + recov_fac * U_inf**2/(2*cp) * HTfac ## wall temperature (with heat transfer factor)
|
|
168
|
+
|
|
169
|
+
# === attach to self
|
|
170
|
+
|
|
171
|
+
self.R_molar = R_molar
|
|
172
|
+
self.M_molar = M_molar
|
|
173
|
+
self.R = R
|
|
174
|
+
self.cp = cp
|
|
175
|
+
self.cv = cv
|
|
176
|
+
self.kappa = kappa
|
|
177
|
+
|
|
178
|
+
self.T_inf = T_inf
|
|
179
|
+
self.p_inf = p_inf
|
|
180
|
+
self.rho_inf = rho_inf
|
|
181
|
+
|
|
182
|
+
self.S_Suth = S_Suth
|
|
183
|
+
self.mu_Suth_ref = mu_Suth_ref
|
|
184
|
+
self.T_Suth_ref = T_Suth_ref
|
|
185
|
+
self.C_Suth = C_Suth
|
|
186
|
+
self.mu_inf = mu_inf
|
|
187
|
+
self.nu_inf = nu_inf
|
|
188
|
+
|
|
189
|
+
#self.k_Suth_ref = k_Suth_ref
|
|
190
|
+
#self.Sk_Suth = Sk_Suth
|
|
191
|
+
self.k_inf = k_inf
|
|
192
|
+
|
|
193
|
+
self.alpha = alpha
|
|
194
|
+
self.Pr = Pr
|
|
195
|
+
|
|
196
|
+
self.M_inf = M_inf
|
|
197
|
+
self.a_inf = a_inf
|
|
198
|
+
self.U_inf = U_inf
|
|
199
|
+
|
|
200
|
+
self.Re = Re
|
|
201
|
+
self.lchar = lchar
|
|
202
|
+
self.tchar = tchar
|
|
203
|
+
self.uchar = uchar
|
|
204
|
+
|
|
205
|
+
self.T_tot = T_tot
|
|
206
|
+
self.p_tot = p_tot
|
|
207
|
+
self.rho_tot = rho_tot
|
|
208
|
+
|
|
209
|
+
self.recov_fac = recov_fac
|
|
210
|
+
self.HTfac = HTfac
|
|
211
|
+
self.Taw = Taw
|
|
212
|
+
self.Tw = Tw
|
|
213
|
+
|
|
214
|
+
def set_Re(self, Re):
|
|
215
|
+
'''
|
|
216
|
+
set reference Reynolds Number, which determines lchar & tchar
|
|
217
|
+
'''
|
|
218
|
+
self.Re = Re
|
|
219
|
+
self.lchar = Re * self.nu_inf / self.U_inf
|
|
220
|
+
self.tchar = self.lchar / self.U_inf
|
|
221
|
+
self.uchar = self.lchar / self.tchar
|
|
222
|
+
if not np.isclose(self.uchar, self.U_inf, rtol=1e-12):
|
|
223
|
+
raise AssertionError('U_inf!=uchar')
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
def print(self,):
|
|
227
|
+
|
|
228
|
+
even_print('R_molar' , '%0.6f [J/(K·mol)]' % self.R_molar )
|
|
229
|
+
even_print('M_molar' , '%0.5e [kg/mol]' % self.M_molar )
|
|
230
|
+
even_print('R' , '%0.3f [J/(kg·K)]' % self.R )
|
|
231
|
+
even_print('cp' , '%0.3f [J/(kg·K)]' % self.cp )
|
|
232
|
+
even_print('cv' , '%0.3f [J/(kg·K)]' % self.cv )
|
|
233
|
+
even_print('kappa' , '%0.3f [-]' % self.kappa )
|
|
234
|
+
|
|
235
|
+
print(72*'-')
|
|
236
|
+
even_print('compressible' , '%s' % self.compressible )
|
|
237
|
+
even_print('M_inf' , '%0.4f [-]' % self.M_inf )
|
|
238
|
+
even_print('Re' , '%0.4f [-]' % self.Re )
|
|
239
|
+
|
|
240
|
+
even_print('T_inf' , '%0.4f [K]' % self.T_inf )
|
|
241
|
+
even_print('T_tot' , '%0.4f [K]' % self.T_tot )
|
|
242
|
+
#even_print('Tw' , '%0.4f [K]' % self.Tw )
|
|
243
|
+
#even_print('Taw' , '%0.4f [K]' % self.Taw )
|
|
244
|
+
even_print('p_inf' , '%0.1f [Pa]' % self.p_inf )
|
|
245
|
+
even_print('p_tot' , '%0.1f [Pa]' % self.p_tot )
|
|
246
|
+
even_print('rho_inf' , '%0.4f [kg/m³]' % self.rho_inf )
|
|
247
|
+
even_print('rho_tot' , '%0.4f [kg/m³]' % self.rho_tot )
|
|
248
|
+
|
|
249
|
+
even_print('T_Suth_ref' , '%0.2f [K]' % self.T_Suth_ref )
|
|
250
|
+
even_print('C_Suth' , '%0.3e [kg/(m·s·√K)]' % self.C_Suth )
|
|
251
|
+
even_print('S_Suth' , '%0.2f [K]' % self.S_Suth )
|
|
252
|
+
|
|
253
|
+
even_print('mu_inf' , '%0.5e [kg/(m·s)]' % self.mu_inf )
|
|
254
|
+
even_print('nu_inf' , '%0.5e [m²/s]' % self.nu_inf )
|
|
255
|
+
even_print('k_inf' , '%0.5e [W/(m·K)]' % self.k_inf )
|
|
256
|
+
even_print('alpha' , '%0.5e [m²/s]' % self.alpha )
|
|
257
|
+
|
|
258
|
+
even_print('Pr' , '%0.5f [-]' % self.Pr )
|
|
259
|
+
#even_print('recovery factor', '%0.5f [-]' % self.recov_fac )
|
|
260
|
+
|
|
261
|
+
even_print('a_inf' , '%0.5f [m/s]' % self.a_inf )
|
|
262
|
+
even_print('U_inf' , '%0.5f [m/s]' % self.U_inf )
|
|
263
|
+
even_print('uchar' , '%0.5f [m/s]' % self.uchar )
|
|
264
|
+
even_print('lchar' , '%0.5e [m]' % self.lchar )
|
|
265
|
+
even_print('tchar' , '%0.5e [s]' % self.tchar )
|
|
266
|
+
print(72*'-')
|
|
267
|
+
|
|
268
|
+
return
|
turbx/gradient.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy as sp
|
|
3
|
+
|
|
4
|
+
# ======================================================================
|
|
5
|
+
|
|
6
|
+
def gradient(u, x=None, d=1, axis=0, acc=6, edge_stencil='full', return_coeffs=False, no_warn=True):
|
|
7
|
+
'''
|
|
8
|
+
Numerical Gradient Approximation Using Finite Differences
|
|
9
|
+
-----
|
|
10
|
+
- calculates stencil given arbitrary accuracy & derivative order
|
|
11
|
+
- handles non-uniform grids
|
|
12
|
+
- accuracy order is only mathematically valid for:
|
|
13
|
+
- uniform coordinate array
|
|
14
|
+
- inner points which have full central stencil
|
|
15
|
+
- handles N-D numpy arrays (gradient performed over axis denoted by axis arg)
|
|
16
|
+
-----
|
|
17
|
+
u : input array to perform differentiation upon
|
|
18
|
+
x : coordinate vector (np.ndarray) OR dx (float) in the case of a uniform grid
|
|
19
|
+
d : derivative order
|
|
20
|
+
axis : axis along which to perform gradient
|
|
21
|
+
acc : accuracy order (only fully valid for inner points with central stencil on uniform grid)
|
|
22
|
+
-----
|
|
23
|
+
edge_stencil : type of edge stencil to use ('half','full')
|
|
24
|
+
return_coeffs : if True, then return stencil & coefficient information
|
|
25
|
+
-----
|
|
26
|
+
# stencil_npts : number of index pts in (central) stencil
|
|
27
|
+
# --> no longer an input
|
|
28
|
+
# --> using 'acc' (accuracy order) instead and calculating npts from formula
|
|
29
|
+
# - stencil_npts=3 : stencil=[ -1,0,+1 ]
|
|
30
|
+
# - stencil_npts=5 : stencil=[ -2,-1,0,+1,+2 ]
|
|
31
|
+
# - stencil_npts=7 : stencil=[-3,-2,-1,0,+1,+2,+3]
|
|
32
|
+
# - edges are filled out with appropriate clipping of central stencil
|
|
33
|
+
-----
|
|
34
|
+
turbx.gradient( u , x , d=1 , acc=2 , edge_stencil='half' , axis=0 )
|
|
35
|
+
...reproduces...
|
|
36
|
+
np.gradient(u, x, edge_order=1, axis=0)
|
|
37
|
+
|
|
38
|
+
turbx.gradient( u , x , d=1 , acc=2 , edge_stencil='full' , axis=0 )
|
|
39
|
+
...reproduces...
|
|
40
|
+
np.gradient(u, x, edge_order=2, axis=0)
|
|
41
|
+
-----
|
|
42
|
+
https://en.wikipedia.org/wiki/Finite_difference_coefficient
|
|
43
|
+
https://web.media.mit.edu/~crtaylor/calculator.html
|
|
44
|
+
-----
|
|
45
|
+
Fornberg B. (1988) Generation of Finite Difference Formulas on
|
|
46
|
+
Arbitrarily Spaced Grids, Mathematics of Computation 51, no. 184 : 699-706.
|
|
47
|
+
http://www.ams.org/journals/mcom/1988-51-184/S0025-5718-1988-0935077-0/S0025-5718-1988-0935077-0.pdf
|
|
48
|
+
'''
|
|
49
|
+
|
|
50
|
+
u = np.asanyarray(u)
|
|
51
|
+
nd = u.ndim
|
|
52
|
+
|
|
53
|
+
# print('contiguous : %s'%str( u.data.contiguous ) )
|
|
54
|
+
# print('C-contiguous : %s'%str( u.data.c_contiguous ) )
|
|
55
|
+
# print('F-contiguous : %s'%str( u.data.f_contiguous ) )
|
|
56
|
+
|
|
57
|
+
if (nd==0):
|
|
58
|
+
raise ValueError('turbx.gradient() requires input that is at least 1D')
|
|
59
|
+
|
|
60
|
+
axes = tuple(range(nd))
|
|
61
|
+
|
|
62
|
+
if not isinstance(axis, int):
|
|
63
|
+
raise ValueError('axis should be of type int')
|
|
64
|
+
if (axis not in axes):
|
|
65
|
+
raise ValueError('axis=%i is not valid for array with u.ndim=%s'%(axis,str(u.ndim)))
|
|
66
|
+
|
|
67
|
+
nx = u.shape[axis] ## size of axis over which gradient will be performed
|
|
68
|
+
|
|
69
|
+
if (nx<3):
|
|
70
|
+
raise ValueError('nx<3')
|
|
71
|
+
|
|
72
|
+
if (x is not None):
|
|
73
|
+
if isinstance(x, float):
|
|
74
|
+
if (x<=0.):
|
|
75
|
+
raise ValueError('if x is a float it should be >0.')
|
|
76
|
+
elif isinstance(x, int):
|
|
77
|
+
x = float(x)
|
|
78
|
+
elif isinstance(x, np.ndarray):
|
|
79
|
+
if (x.ndim!=1):
|
|
80
|
+
raise ValueError('x should be 1D if it is of type np.ndarray')
|
|
81
|
+
if (x.shape[0]!=nx):
|
|
82
|
+
raise ValueError('size of x does not match data axis specified')
|
|
83
|
+
if (not np.all(np.diff(x) > 0.)) and (not np.all(np.diff(x) < 0.)):
|
|
84
|
+
raise AssertionError('x is not monotonically increasing/decreasing')
|
|
85
|
+
|
|
86
|
+
## optimization: check if x is actually uniformly spaced, in which case x=Δx
|
|
87
|
+
dx0 = x[1]-x[0]
|
|
88
|
+
if np.all(np.isclose(np.diff(x), dx0, rtol=1e-12)):
|
|
89
|
+
#print('turbx.gradient() : x arr with x.shape=%s seems like it is actually uniformly spaced. applying x=%0.8e'%(str(x.shape),dx0))
|
|
90
|
+
x = dx0
|
|
91
|
+
|
|
92
|
+
else:
|
|
93
|
+
raise ValueError('x should be a 1D np.ndarray or float')
|
|
94
|
+
else:
|
|
95
|
+
x = 1. ## if x not provided, assume uniform unit coordinate vector
|
|
96
|
+
|
|
97
|
+
if isinstance(x, float):
|
|
98
|
+
uniform_grid = True
|
|
99
|
+
elif isinstance(x, np.ndarray):
|
|
100
|
+
uniform_grid = False
|
|
101
|
+
else:
|
|
102
|
+
raise ValueError('turbx.gradient() : this should never happen... check!')
|
|
103
|
+
|
|
104
|
+
if not isinstance(d, int):
|
|
105
|
+
raise ValueError('d (derivative order) should be of type int')
|
|
106
|
+
if not (d>0):
|
|
107
|
+
raise ValueError('d (derivative order) should be >0')
|
|
108
|
+
|
|
109
|
+
if not isinstance(acc, int):
|
|
110
|
+
raise ValueError('acc (accuracy order) should be of type int')
|
|
111
|
+
if not (acc>=2):
|
|
112
|
+
raise ValueError('acc (accuracy order) should be >=2')
|
|
113
|
+
if (acc%2!=0):
|
|
114
|
+
raise ValueError('acc (accuracy order) should be an integer multiple of 2')
|
|
115
|
+
|
|
116
|
+
## for the d'th derivative with accuracy=acc, the following formula gives the n pts of the (central) stencil
|
|
117
|
+
stencil_npts = 2*int(np.floor((d+1)/2)) - 1 + acc
|
|
118
|
+
|
|
119
|
+
if not isinstance(stencil_npts, int):
|
|
120
|
+
raise ValueError('stencil_npts must be of type \'int\'')
|
|
121
|
+
if (stencil_npts<3):
|
|
122
|
+
raise ValueError('stencil_npts should be >=3')
|
|
123
|
+
if ((stencil_npts-1)%2 != 0):
|
|
124
|
+
raise ValueError('(stencil_npts-1) should be divisible by 2 (for central stencil)')
|
|
125
|
+
if (stencil_npts > nx):
|
|
126
|
+
raise ValueError('stencil_npts > nx')
|
|
127
|
+
|
|
128
|
+
if all([ (edge_stencil!='half') , (edge_stencil!='full') ]):
|
|
129
|
+
raise ValueError('edge_stencil=%s not valid. options are: \'full\', \'half\''%str(edge_stencil))
|
|
130
|
+
|
|
131
|
+
# ===
|
|
132
|
+
|
|
133
|
+
n_full_central_stencils = nx - stencil_npts + 1
|
|
134
|
+
|
|
135
|
+
if ( n_full_central_stencils < 5 ) and not no_warn:
|
|
136
|
+
print('\nWARNING\n'+72*'-')
|
|
137
|
+
print('n pts with full central stencils = %i (<5)'%n_full_central_stencils)
|
|
138
|
+
#print('nx//3=%i'%(nx//3))
|
|
139
|
+
print('--> consider reducing acc arg (accuracy order)')
|
|
140
|
+
print(72*'-'+'\n')
|
|
141
|
+
|
|
142
|
+
stencil_width = stencil_npts-1
|
|
143
|
+
sw2 = stencil_width//2
|
|
144
|
+
|
|
145
|
+
# === build up stencil & coefficients vector
|
|
146
|
+
|
|
147
|
+
fdc_vec = [] ## vector of finite difference coefficient information
|
|
148
|
+
|
|
149
|
+
## left side
|
|
150
|
+
for i in range(0,sw2):
|
|
151
|
+
|
|
152
|
+
## full
|
|
153
|
+
stencil_L = np.arange(-i,stencil_width+1-i)
|
|
154
|
+
|
|
155
|
+
## half
|
|
156
|
+
if (edge_stencil=='half') and not all([(acc==2),(d>=2)]):
|
|
157
|
+
stencil_L = np.arange(-i,sw2+1)
|
|
158
|
+
|
|
159
|
+
i_range = np.arange( 0 , stencil_L.shape[0] )
|
|
160
|
+
|
|
161
|
+
if uniform_grid:
|
|
162
|
+
fdc = fd_coeff_calculator( stencil_L , d=d , dx=x )
|
|
163
|
+
else:
|
|
164
|
+
fdc = fd_coeff_calculator( stencil_L , d=d , x=x[i_range] )
|
|
165
|
+
|
|
166
|
+
fdc_vec.append( [ fdc , i_range , stencil_L ] )
|
|
167
|
+
|
|
168
|
+
## inner pts
|
|
169
|
+
stencil = np.arange(stencil_npts) - sw2
|
|
170
|
+
if uniform_grid:
|
|
171
|
+
fdc_inner = fd_coeff_calculator( stencil , d=d , dx=x )
|
|
172
|
+
for i in range(sw2,nx-sw2):
|
|
173
|
+
|
|
174
|
+
i_range = np.arange(i-sw2,i+sw2+1)
|
|
175
|
+
|
|
176
|
+
if uniform_grid:
|
|
177
|
+
fdc = fdc_inner
|
|
178
|
+
else:
|
|
179
|
+
fdc = fd_coeff_calculator( stencil , d=d , x=x[i_range] )
|
|
180
|
+
|
|
181
|
+
fdc_vec.append( [ fdc , i_range , stencil ] )
|
|
182
|
+
|
|
183
|
+
## right side
|
|
184
|
+
for i in range(nx-sw2,nx):
|
|
185
|
+
|
|
186
|
+
## full
|
|
187
|
+
stencil_R = np.arange(-stencil_width+(nx-i-1),nx-i)
|
|
188
|
+
|
|
189
|
+
## half
|
|
190
|
+
if (edge_stencil=='half') and not all([(acc==2),(d>=2)]):
|
|
191
|
+
stencil_R = np.arange(-sw2,nx-i)
|
|
192
|
+
|
|
193
|
+
i_range = np.arange( nx-stencil_R.shape[0] , nx )
|
|
194
|
+
|
|
195
|
+
if uniform_grid:
|
|
196
|
+
fdc = fd_coeff_calculator( stencil_R , d=d , dx=x )
|
|
197
|
+
else:
|
|
198
|
+
fdc = fd_coeff_calculator( stencil_R , d=d , x=x[i_range] )
|
|
199
|
+
|
|
200
|
+
fdc_vec.append( [ fdc , i_range , stencil_R ] )
|
|
201
|
+
|
|
202
|
+
# === debug
|
|
203
|
+
|
|
204
|
+
# print('i_range')
|
|
205
|
+
# for fdc_vec_ in fdc_vec:
|
|
206
|
+
# print(fdc_vec_[1])
|
|
207
|
+
# print('')
|
|
208
|
+
# print('stencil')
|
|
209
|
+
# for fdc_vec_ in fdc_vec:
|
|
210
|
+
# print(fdc_vec_[2])
|
|
211
|
+
|
|
212
|
+
# === evaluate gradient
|
|
213
|
+
|
|
214
|
+
if (nd==1): ## 1D
|
|
215
|
+
|
|
216
|
+
u_ddx = np.zeros_like(u)
|
|
217
|
+
for i in range(len(fdc_vec)):
|
|
218
|
+
fdc, i_range, stencil = fdc_vec[i]
|
|
219
|
+
u_ddx[i] = np.dot( fdc , u[i_range] )
|
|
220
|
+
|
|
221
|
+
else: ## N-D
|
|
222
|
+
|
|
223
|
+
## the order in memory of the incoming data
|
|
224
|
+
if u.data.contiguous:
|
|
225
|
+
if u.data.c_contiguous:
|
|
226
|
+
order='C'
|
|
227
|
+
elif u.data.f_contiguous:
|
|
228
|
+
order='F'
|
|
229
|
+
else:
|
|
230
|
+
raise ValueError
|
|
231
|
+
else:
|
|
232
|
+
order='C'
|
|
233
|
+
|
|
234
|
+
## if the array is C-ordered, use axis=0 as the axis to shift to
|
|
235
|
+
## if the array is F-ordered, use axis=-1 as the axis to shift to
|
|
236
|
+
if order=='C':
|
|
237
|
+
shift_pos=0 ## 0th axis
|
|
238
|
+
elif order=='F':
|
|
239
|
+
shift_pos=nd-1 ## last axis
|
|
240
|
+
|
|
241
|
+
## shift gradient axis
|
|
242
|
+
u = np.swapaxes(u, axis, shift_pos)
|
|
243
|
+
shape_new = u.shape
|
|
244
|
+
if (shift_pos==0):
|
|
245
|
+
size_all_but_ax = np.prod(np.array(shape_new)[1:])
|
|
246
|
+
elif (shift_pos==nd-1):
|
|
247
|
+
size_all_but_ax = np.prod(np.array(shape_new)[:-1])
|
|
248
|
+
else:
|
|
249
|
+
raise ValueError
|
|
250
|
+
|
|
251
|
+
## reshape N-D to 2D
|
|
252
|
+
## --> shift_pos=0 : gradient axis is 0, all other axes are flattened on axis=1)
|
|
253
|
+
## --> shift_pos=last : ...
|
|
254
|
+
if (shift_pos==0):
|
|
255
|
+
u = np.reshape(u, (nx, size_all_but_ax), order=order)
|
|
256
|
+
elif (shift_pos==nd-1):
|
|
257
|
+
u = np.reshape(u, (size_all_but_ax, nx), order=order)
|
|
258
|
+
else:
|
|
259
|
+
raise ValueError
|
|
260
|
+
|
|
261
|
+
u_ddx = np.zeros_like(u,order=order)
|
|
262
|
+
for i in range(nx):
|
|
263
|
+
fdc, i_range, stencil = fdc_vec[i]
|
|
264
|
+
ia=min(i_range)
|
|
265
|
+
ib=max(i_range)+1
|
|
266
|
+
if (shift_pos==0):
|
|
267
|
+
u_ddx[i,:] = np.einsum('ij,i->j', u[ia:ib,:], fdc)
|
|
268
|
+
else:
|
|
269
|
+
u_ddx[:,i] = np.einsum('ji,i->j', u[:,ia:ib], fdc)
|
|
270
|
+
|
|
271
|
+
## reshape 2D back to original N-D
|
|
272
|
+
u_ddx = np.reshape(u_ddx, shape_new, order=order)
|
|
273
|
+
|
|
274
|
+
## shift gradient axis back to original position
|
|
275
|
+
u_ddx = np.swapaxes(u_ddx, shift_pos, axis)
|
|
276
|
+
|
|
277
|
+
## the original data array should have been de-referenced during this func
|
|
278
|
+
u = None; del u
|
|
279
|
+
|
|
280
|
+
if return_coeffs:
|
|
281
|
+
return u_ddx, fdc_vec
|
|
282
|
+
else:
|
|
283
|
+
return u_ddx
|
|
284
|
+
|
|
285
|
+
def fd_coeff_calculator(stencil, d=1, x=None, dx=None):
|
|
286
|
+
'''
|
|
287
|
+
Calculate Finite Difference Coefficients for Arbitrary Stencil
|
|
288
|
+
-----
|
|
289
|
+
stencil : indices of stencil pts e.g. np.array([-2,-1,0,1,2])
|
|
290
|
+
d : derivative order
|
|
291
|
+
x : locations of grid points corresponding to stencil indices
|
|
292
|
+
dx : spacing of grid points in the case of uniform grid
|
|
293
|
+
-----
|
|
294
|
+
https://en.wikipedia.org/wiki/Finite_difference_coefficient
|
|
295
|
+
https://web.media.mit.edu/~crtaylor/calculator.html
|
|
296
|
+
-----
|
|
297
|
+
Fornberg B. (1988) Generation of Finite Difference Formulas on
|
|
298
|
+
Arbitrarily Spaced Grids, Mathematics of Computation 51, no. 184 : 699-706.
|
|
299
|
+
http://www.ams.org/journals/mcom/1988-51-184/S0025-5718-1988-0935077-0/S0025-5718-1988-0935077-0.pdf
|
|
300
|
+
'''
|
|
301
|
+
|
|
302
|
+
stencil = np.asanyarray(stencil)
|
|
303
|
+
|
|
304
|
+
if not isinstance(stencil, np.ndarray):
|
|
305
|
+
raise ValueError('stencil should be of type np.ndarray')
|
|
306
|
+
if (stencil.ndim!=1):
|
|
307
|
+
raise ValueError('stencil should be 1D')
|
|
308
|
+
if (stencil.shape[0]<2):
|
|
309
|
+
raise ValueError('stencil size should be >=2')
|
|
310
|
+
if (0 not in stencil):
|
|
311
|
+
raise ValueError('stencil does not contain 0')
|
|
312
|
+
if not np.issubdtype(stencil.dtype, np.integer):
|
|
313
|
+
raise ValueError('stencil.dtype not a subdtype of np.integer')
|
|
314
|
+
|
|
315
|
+
if not isinstance(d, int):
|
|
316
|
+
raise ValueError('d (derivative order) should be of type int')
|
|
317
|
+
if not (d>0):
|
|
318
|
+
raise ValueError('d (derivative order) should be >0')
|
|
319
|
+
|
|
320
|
+
if (dx is None) and (x is None):
|
|
321
|
+
raise ValueError('one of args \'dx\' or \'x\' should be defined')
|
|
322
|
+
if (dx is not None) and (x is not None):
|
|
323
|
+
raise ValueError('only one of args \'dx\' or \'x\' should be defined')
|
|
324
|
+
if (dx is not None):
|
|
325
|
+
if not isinstance(dx, float):
|
|
326
|
+
raise ValueError('dx should be of type float')
|
|
327
|
+
|
|
328
|
+
if (x is not None):
|
|
329
|
+
if not isinstance(x, np.ndarray):
|
|
330
|
+
raise ValueError('x should be of type np.ndarray')
|
|
331
|
+
if (x.shape[0] != stencil.shape[0]):
|
|
332
|
+
raise ValueError('x, stencil should have same shape')
|
|
333
|
+
if (not np.all(np.diff(x) > 0.)) and (not np.all(np.diff(x) < 0.)):
|
|
334
|
+
raise AssertionError('x is not monotonically increasing/decreasing')
|
|
335
|
+
|
|
336
|
+
## overwrite stencil (int index) to be coordinate array (delta from 0 position)
|
|
337
|
+
|
|
338
|
+
i0 = np.where(stencil==0)[0][0]
|
|
339
|
+
|
|
340
|
+
if (x is not None):
|
|
341
|
+
stencil = x - x[i0]
|
|
342
|
+
|
|
343
|
+
if (dx is not None):
|
|
344
|
+
stencil = dx * stencil.astype(np.float64)
|
|
345
|
+
|
|
346
|
+
nn = stencil.shape[0]
|
|
347
|
+
|
|
348
|
+
dvec = np.zeros( (nn,) , dtype=np.float64 )
|
|
349
|
+
#dvec = np.zeros( (nn,) , dtype=np.longdouble )
|
|
350
|
+
dfac=1
|
|
351
|
+
for i in range(d):
|
|
352
|
+
dfac *= (i+1)
|
|
353
|
+
dvec[d] = dfac
|
|
354
|
+
|
|
355
|
+
## increase precision
|
|
356
|
+
#stencil = np.copy(stencil).astype(np.longdouble)
|
|
357
|
+
|
|
358
|
+
#stencil_abs_max = np.abs(stencil).max()
|
|
359
|
+
stencil_abs_min_nonzero = np.abs(stencil[[ i for i in range(stencil.size) if i!=i0 ]]).min()
|
|
360
|
+
|
|
361
|
+
'''
|
|
362
|
+
scale/normalize the coordinate stencil (to avoid ill-conditioning)
|
|
363
|
+
- if coordinates are already small/large, the Vandermonde matrix becomes
|
|
364
|
+
HIGHLY ill-conditioned due to row exponents
|
|
365
|
+
- coordinates are normalized here so that smallest absolute non-zero delta coord. is =1
|
|
366
|
+
- RHS vector (dvec) gets normalized too
|
|
367
|
+
- FD coefficients are (theoretically) unaffected
|
|
368
|
+
'''
|
|
369
|
+
normalize_stencil = True
|
|
370
|
+
|
|
371
|
+
if normalize_stencil:
|
|
372
|
+
stencil /= stencil_abs_min_nonzero
|
|
373
|
+
|
|
374
|
+
mat = np.zeros( (nn,nn) , dtype=np.float64)
|
|
375
|
+
#mat = np.zeros( (nn,nn) , dtype=np.longdouble)
|
|
376
|
+
for i in range(nn):
|
|
377
|
+
mat[i,:] = np.power( stencil , i )
|
|
378
|
+
|
|
379
|
+
## condition_number = np.linalg.cond(mat, p=-2)
|
|
380
|
+
|
|
381
|
+
# mat_inv = np.linalg.inv( mat )
|
|
382
|
+
# coeffv = np.dot( mat_inv , dvec )
|
|
383
|
+
|
|
384
|
+
if normalize_stencil:
|
|
385
|
+
for i in range(nn):
|
|
386
|
+
dvec[i] /= np.power( stencil_abs_min_nonzero , i )
|
|
387
|
+
|
|
388
|
+
#coeffv = np.linalg.solve(mat, dvec)
|
|
389
|
+
coeffv = sp.linalg.solve(mat, dvec)
|
|
390
|
+
|
|
391
|
+
return coeffv
|