-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtransp_dists.py
432 lines (295 loc) · 12.7 KB
/
transp_dists.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# Tools for working with TRANSP ion distributions.
# ------------------------------------------------------
import os
from netCDF4 import Dataset
import numpy as np
from scipy.interpolate import griddata, RectBivariateSpline, interp1d
import sampler
class Thermal:
def __init__(self, transp_output, ion='D'):
self.transp_output = transp_output
self.step = transp_output.step
self.ion = ion.upper()
self.pop = 'BULK' # type of population
# Time window
self.t0 = transp_output.t0
self.t1 = transp_output.t1
# Volume elements
dV = transp_output.get_variable('DVOL')
dV[1] = dV[1]/1e6 # m^-3
self.dV = dV
self.V = dV[1].sum()
# Temperature and density profiles
T = transp_output.get_variable('TI')
if self.ion == 'D':
n = transp_output.get_variable('ND')
if self.ion == 'T':
n = transp_output.get_variable('NT')
if self.ion == '3HE':
n = transp_output.get_variable('NHE3')
T[1] = T[1] / 1e3 # keV
n[1] = n[1] * 1e6 # m^-3
self.n_max = n[1].max()
self.T = T
self.n = n
self.n_interp = interp1d(n[0], n[1], fill_value='extrapolate')
# Rotation
omega = transp_output.get_variable('OMEGA')
if omega != -1:
self.omega = omega
else:
self.omega = None
# Numer of particles in each volume element
self.N_vol = self.n[1]*self.dV[1]
self.N_vol_interp = interp1d(n[0], self.N_vol, fill_value='extrapolate')
self.N_vol_max = self.N_vol.max()
# Total number of particles in the distribution
self.N = np.sum(self.N_vol)
def __repr__(self):
return 'TRANSP thermal {} dist from {}'.format(self.ion, self.transp_output.out_file)
def get_density(self, R, Z):
X = self.transp_output.get_rho(R, Z)
#n = np.interp(X, self.n[0], self.n[1], right=0)
n = self.n_interp(X)
n[X>1.0] = 0
return n
def get_N_vol(self, R, Z):
X = self.transp_output.get_rho(R, Z)
N_vol = self.N_vol_interp(X)
N_vol[X>1.0] = 0.0
return N_vol
def get_temperature(self, R, Z):
X = self.transp_output.get_rho(R, Z)
T = np.interp(X, self.T[0], self.T[1], right=0)
return T
def get_rotation(self, R, Z):
if self.omega is None:
vrot = np.zeros_like(R)
else:
X = self.transp_output.get_rho(R, Z)
omega = np.interp(X, self.omega[0], self.omega[1], right=0) # rad/s
vrot = omega * R # m/s
return vrot
def sample(self, n_samples=None, R=None, Z=None):
"""
Sample from the distribution. Must input either 'n_samples',
(in which case the positions are sampled), or both 'R' and 'Z',
(in which case energy and pitch are sampled at the correspoinding positions).
"""
if (R is None) and (Z is None):
sample_pos = True
# Draw random positions
Rmin = self.transp_output.R.min()
Rmax = self.transp_output.R.max()
Zmin = self.transp_output.Z.min()
Zmax = self.transp_output.Z.max()
lims = ([Rmin, Zmin], [Rmax, Zmax])
s = sampler.sample_acc_rej(self.get_N_vol, lims, self.N_vol_max, n_samples)
R = s[:,0]
Z = s[:,1]
else:
sample_pos = False
n_samples = len(R)
# The (kinetic) energy is distributed as a chi2 variable with 3 degrees of freedom
T = self.get_temperature(R, Z)
E = np.random.chisquare(3, size=n_samples) * 0.5 * T
# Pitch distribution is isotropic
p = sampler.sample_uniform([-1,1], n_samples)
if sample_pos:
return R, Z, E, p
else:
return E, p
def __call__(self, R, Z, E, p):
""" Evaluate the distribution at the given point(s). """
T = self.get_temperature(R,Z)
n = self.get_density(R,Z)
f = n*np.sqrt(E/np.pi)*T**(-1.5)*np.exp(-E/T)
return f
class FBM:
def __init__(self, transp_output, ion='D', pop='NBI', jdotb=None):
self.transp_output = transp_output
step = transp_output.step
if step is None:
raise ValueError('Invalid time step!')
self.step = step
self.ion = ion.upper()
self.pop = pop.upper()
suffix = self.ion + '_' + self.pop
# Set default sampling method for EP distributions
self.EP_sampling_method = 'acc_rej'
# Open the FBM netCDF file
fbm_cdf = transp_output._openfbm()
# Time window
self.t0 = transp_output.t0
self.t1 = transp_output.t1
# Extract the distribution data
self.X = fbm_cdf.variables['X2D'][:]
self.TH = fbm_cdf.variables['TH2D'][:]
self.R = fbm_cdf.variables['R2D'][:] / 100
self.Z = fbm_cdf.variables['Z2D'][:] / 100
self.index = np.arange(len(self.X))
self.dV = fbm_cdf.variables['BMVOL'][:] / 1e6 # m^-3
self.V = np.sum(self.dV)
# Read fast ion distribution
self.F = fbm_cdf.variables['F_{}'.format(suffix)][:] / 2.0 * 1000.0 * 1e6
self.E = fbm_cdf.variables['E_{}'.format(suffix)][:] / 1000.0
self.A = fbm_cdf.variables['A_{}'.format(suffix)][:]
self.EB = fbm_cdf.variables['EB_{}'.format(suffix)][:] / 1000.0
self.AB = fbm_cdf.variables['AB_{}'.format(suffix)][:]
self.dE = np.diff(self.EB)
self.dA = np.diff(self.AB)
if jdotb is None:
self.jdotb = int(fbm_cdf.variables['nsjdotb'][:])
else:
self.jdotb = int(jdotb)
if self.jdotb == -1:
# Negate and flip pitch axis in order to always have
# pitch values given relative to the B-field.
self.F = self.F[:, ::-1, :]
self.A = -self.A[::-1]
# Make interpolants for the (E,p) distributions in each spatial zone
self.F_Ep = []
self.F_Ep_max = np.zeros_like(self.X)
for i in range(len(self.X)):
F = RectBivariateSpline(self.E, self.A, self.F[i].T, kx=1, ky=1)
self.F_Ep.append(F)
# Also store maximum distribution value at each point,
# in order to know suitable sampling regions later.
self.F_Ep_max[i] = self.F[i].max()
self.Emin = self.E[0]
self.Emax = self.E[-1]
self.pmin = -1
self.pmax = 1
# Calculate fast ion density at each grid point
dE = self.dE
dA = self.dA
self.n = np.sum( np.sum(self.F*dE, axis=-1) *dA, axis=-1 ) # m^-3
self.n_max = self.n.max()
self.Rmin = self.R.min()
self.Rmax = self.R.max()
self.Zmin = self.Z.min()
self.Zmax = self.Z.max()
# Number of particles in each volume element
self.N_vol = self.n * self.dV
self.N_vol_max = self.N_vol.max()
# Total number of particles in the distribution
self.N = np.sum(self.n * self.dV)
# Integrate distribution over RZ plane
# (this factor can be handy for normalizing MC calculations)
self.C_RZ = np.sum(self.n * self.dV/(2*np.pi*self.R))
# Prepare for inverse CDF sampling
dAdE = dA[None,:,None] * dE[None,None,:] # phase space volume elements
Ptab = self.F * dAdE
self.Ptab = Ptab.reshape(len(self.X), len(self.E)*len(self.A)) # 1D tables of probabilities
self.Ctab = np.cumsum(self.Ptab, axis=-1)
Egrid, Agrid = np.meshgrid(self.E, self.A)
self.Etab = Egrid.flatten()
self.Atab = Agrid.flatten()
self.i_tab = np.arange(len(self.Etab))
def __repr__(self):
return 'TRANSP FBM {} {} dist from {}'.format(self.ion, self.pop, self.transp_output.fbm_files[self.step-1])
def get_density(self, R, Z):
X = self.transp_output.get_rho(R, Z)
n = griddata((self.R, self.Z), self.n, (R,Z), method='nearest', fill_value=0)
return np.where(X<1.0, n, 0.0)
def get_N_vol(self, R, Z):
X = self.transp_output.get_rho(R, Z)
N_vol = griddata((self.R, self.Z), self.N_vol, (R,Z), method='nearest', fill_value=0)
return np.where(X<1.0, N_vol, 0.0)
def get_F_Ep(self, i_vol, E, p):
F = np.zeros_like(E, dtype='d')
valid_E = (E < self.Emax) & (E > self.Emin)
valid_p = (p < self.pmax) & (p > self.pmin)
valid = valid_E & valid_p
F[valid] = self.F_Ep[i_vol](E[valid], p[valid], grid=False)
return F
def get_spatial_index(self, R, Z):
# Map each R,Z value to the closest spatial grid point.
i_spatial = griddata((self.R, self.Z), self.index, (R,Z), method='nearest')
# Points outside the plasma should not get a valid index
X = self.transp_output.get_rho(R, Z)
i_spatial[X>1] = self.index[-1] + 1
# Map each spatial grid point to the corresponding R,Z values
in_vol = np.empty(len(self.R), dtype='object')
for i in range(len(in_vol)):
in_vol[i] = []
for i, i_s in enumerate(i_spatial):
if i_s > self.index[-1]:
continue # don't include points outside the plasma
else:
in_vol[i_s].append(i)
return i_spatial, in_vol
def sample(self, n_samples=None, R=None, Z=None):
"""
Sample from the distribution. Must input either 'n_samples',
(in which case the positions are sampled), or both 'R' and 'Z',
(in which case energy and pitch are sampled at the corresponding positions).
"""
if (R is None) and (Z is None):
print('Sampling positions...')
sample_pos = True
# Draw random positions
def fun(R,Z): return self.get_density(R,Z)*R
lims = ([self.Rmin,self.Zmin], [self.Rmax,self.Zmax])
max_val = self.n_max * 3.2 # a bit of a hack...
s = sampler.sample_acc_rej(fun, lims, max_val, n_samples)
R = s[:,0]
Z = s[:,1]
else:
R,Z = np.atleast_1d(R,Z)
sample_pos = False
n_samples = len(R)
# Check in which volumes the sampled/requested positions reside,
# and sample E and p from the appropriate distributions.
print('Sampling energy and pitch...')
i_spatial, in_vol = self.get_spatial_index(R, Z)
E = np.zeros(n_samples)
p = np.zeros(n_samples)
for i,ip in enumerate(in_vol):
if len(ip) == 0:
continue # no points here, moving on...
# Draw samples
if self.F_Ep_max[i] == 0.0:
# If the distribution is zero in this volume element
# we return samples with zero energy.
E[ip] = 0.0
p[ip] = 0.0
else:
if self.EP_sampling_method == 'acc_rej':
def fun(E,p): return self.get_F_Ep(i,E,p) # function to sample from
lims = ([self.Emin, self.pmin], [self.Emax,self.pmax]) # sampling region
s = sampler.sample_acc_rej(fun, lims, self.F_Ep_max[i], len(ip))
E[ip] = s[:,0]
p[ip] = s[:,1]
elif self.EP_sampling_method == 'inv_cdf':
# Make 1D table of probabilities
r = np.random.rand(len(ip)) * self.Ctab[i][-1]
s = np.interp(r, self.Ctab[i], self.i_tab)
E[ip] = np.interp(s, self.i_tab, self.Etab)
p[ip] = np.interp(s, self.i_tab, self.Atab)
if self.EP_sampling_method == 'inv_cdf':
# Add jitter to pitch samples
dp = np.interp(p, self.A, self.dA) / 2.0
p = np.random.uniform(low=p-dp, high=p+dp)
print('Done!')
if sample_pos:
return R, Z, E, p
else:
return E, p
def __call__(self, R, Z, E, p):
""" Evaluate the distribution at the given point(s). """
R, Z, E, p = np.atleast_1d(R, Z, E, p)
F = np.zeros_like(R, dtype='d')
# Only consider points inside the last closed flux surface
X = self.transp_output.get_rho(R, Z)
valid = X < 1
R, Z, E, p = R[valid], Z[valid], E[valid], p[valid]
F_valid = F[valid]
# Find the closest spatial grid point
i_spatial, in_vol = self.get_spatial_index(R, Z)
# Loop over all volumes that contain requested points
for i in np.unique(i_spatial):
ip = in_vol[i]
F_valid[ip] = self.get_F_Ep(i, E[ip], p[ip])
F[valid] = F_valid
return F