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.
@@ -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