plasmasds-fields 0.1.0__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.
- plasmasds_fields/__init__.py +17 -0
- plasmasds_fields/models/__init__.py +0 -0
- plasmasds_fields/models/hesel.py +278 -0
- plasmasds_fields/models/motion.py +359 -0
- plasmasds_fields/models/profilegen.py +67 -0
- plasmasds_fields/tools/__init__.py +0 -0
- plasmasds_fields/tools/interpolate.py +209 -0
- plasmasds_fields/tools/plot.py +777 -0
- plasmasds_fields/w7x/__init__.py +0 -0
- plasmasds_fields/w7x/fields.py +343 -0
- plasmasds_fields/w7x/poincare.py +1055 -0
- plasmasds_fields-0.1.0.dist-info/METADATA +138 -0
- plasmasds_fields-0.1.0.dist-info/RECORD +15 -0
- plasmasds_fields-0.1.0.dist-info/WHEEL +4 -0
- plasmasds_fields-0.1.0.dist-info/licenses/LICENSE +504 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plasma field and magnetic geometry inputs for the plasma-sds synthetic
|
|
3
|
+
diagnostics.
|
|
4
|
+
|
|
5
|
+
Subpackages:
|
|
6
|
+
|
|
7
|
+
* ``plasmasds_fields.w7x``: W7-X Poincaré flux surfaces and density
|
|
8
|
+
allocation on them.
|
|
9
|
+
* ``plasmasds_fields.tools``: profile interpolation and plotting of fields.
|
|
10
|
+
* ``plasmasds_fields.models``: HESEL output, background profiles and
|
|
11
|
+
time-dependent perturbations.
|
|
12
|
+
|
|
13
|
+
Import what you need from the subpackages, for example
|
|
14
|
+
``from plasmasds_fields.w7x.poincare import load_w7x_flux_surfaces``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
File without changes
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reading and extending HESEL simulation output.
|
|
3
|
+
"""
|
|
4
|
+
import h5py as h5
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HESEL:
|
|
9
|
+
"""
|
|
10
|
+
Data handler for HESEL simulation output stored in large HDF5 files.
|
|
11
|
+
|
|
12
|
+
On construction the R, Z and time axes are loaded from the
|
|
13
|
+
``data/xanimation`` group and their grid spacings are derived. The HDF5
|
|
14
|
+
file stays open as ``self.file``, so field datasets can be read lazily,
|
|
15
|
+
section by section, with :meth:`extract_field`.
|
|
16
|
+
|
|
17
|
+
The handler can be used as a context manager::
|
|
18
|
+
|
|
19
|
+
with HESEL("run.h5") as hesel:
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
or closed explicitly with :meth:`close`.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
path : str or path-like
|
|
27
|
+
Path to the HESEL HDF5 output file.
|
|
28
|
+
|
|
29
|
+
Attributes
|
|
30
|
+
----------
|
|
31
|
+
path : str
|
|
32
|
+
Path of the opened file.
|
|
33
|
+
file : h5py.File or None
|
|
34
|
+
Open HDF5 file (read-only); None after :meth:`close`.
|
|
35
|
+
R_axis, Z_axis, time_axis : numpy.ndarray
|
|
36
|
+
1-D R [m], Z [m] and time [s] axes, read from
|
|
37
|
+
``data/xanimation/xgrid[0, :]``, ``data/xanimation/ygrid[:, 0]`` and
|
|
38
|
+
``data/xanimation/time``.
|
|
39
|
+
dR, dZ, dt : float
|
|
40
|
+
Step between the first two samples of each axis (the grid spacing for
|
|
41
|
+
uniform axes).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, path):
|
|
45
|
+
self.path = str(path)
|
|
46
|
+
self.file = h5.File(self.path, "r")
|
|
47
|
+
|
|
48
|
+
self.R_axis = self.file["data/xanimation/xgrid"][0, :]
|
|
49
|
+
self.Z_axis = self.file["data/xanimation/ygrid"][:, 0]
|
|
50
|
+
self.time_axis = self.file["data/xanimation/time"][:, 0]
|
|
51
|
+
|
|
52
|
+
self.dR = self._axis_spacing(self.R_axis)
|
|
53
|
+
self.dZ = self._axis_spacing(self.Z_axis)
|
|
54
|
+
self.dt = self._axis_spacing(self.time_axis)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _axis_spacing(axis):
|
|
58
|
+
"""
|
|
59
|
+
Return the step between the first two samples of a 1-D axis.
|
|
60
|
+
"""
|
|
61
|
+
return float(axis[1] - axis[0])
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _range_to_selector(axis, rng):
|
|
65
|
+
"""
|
|
66
|
+
Translate a range into an h5py selector for the 1-D ``axis``.
|
|
67
|
+
|
|
68
|
+
* None: ``slice(None)``, the full axis.
|
|
69
|
+
* Scalar: index of the nearest sample (collapses the axis).
|
|
70
|
+
* ``[min, max]``: ``slice(i_lo, i_hi + 1)`` covering the samples
|
|
71
|
+
nearest to ``min`` and ``max``.
|
|
72
|
+
"""
|
|
73
|
+
if rng is None:
|
|
74
|
+
return slice(None)
|
|
75
|
+
|
|
76
|
+
arr = np.atleast_1d(rng)
|
|
77
|
+
if arr.size == 1:
|
|
78
|
+
return int(np.argmin(np.abs(axis - arr[0])))
|
|
79
|
+
if arr.size == 2:
|
|
80
|
+
lo, hi = float(arr[0]), float(arr[1])
|
|
81
|
+
if lo > hi:
|
|
82
|
+
lo, hi = hi, lo
|
|
83
|
+
i_lo = int(np.argmin(np.abs(axis - lo)))
|
|
84
|
+
i_hi = int(np.argmin(np.abs(axis - hi)))
|
|
85
|
+
if i_lo > i_hi:
|
|
86
|
+
i_lo, i_hi = i_hi, i_lo
|
|
87
|
+
return slice(i_lo, i_hi + 1)
|
|
88
|
+
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"Range must be None, a scalar, or a two-element [min, max]; "
|
|
91
|
+
f"got an array of size {arr.size}."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def extract_field(self, field, r_range=None, z_range=None, t_range=None):
|
|
95
|
+
"""
|
|
96
|
+
Read a section of a HESEL field from ``data/xanimation/<field>``.
|
|
97
|
+
|
|
98
|
+
The dataset is 3-D with shape (time, Z, R). Only the requested section
|
|
99
|
+
is read from disk.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
field : {"density", "electron_temperature", "ion_temperature"}
|
|
104
|
+
Name of the dataset to read.
|
|
105
|
+
r_range, z_range, t_range : float or sequence of float, default None
|
|
106
|
+
Selection along R [m], Z [m] and time [s]:
|
|
107
|
+
|
|
108
|
+
* None: keep the whole axis.
|
|
109
|
+
* Scalar: keep only the nearest sample; the axis is dropped from
|
|
110
|
+
the result.
|
|
111
|
+
* Two-element ``[min, max]``: keep the contiguous block of samples
|
|
112
|
+
between the samples nearest to the endpoints.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
numpy.ndarray
|
|
117
|
+
The selected section, with axes in the order (time, Z, R), minus
|
|
118
|
+
the axes selected by a scalar.
|
|
119
|
+
|
|
120
|
+
Raises
|
|
121
|
+
------
|
|
122
|
+
ValueError
|
|
123
|
+
If ``field`` is not a valid dataset name, or if a range has more
|
|
124
|
+
than two elements.
|
|
125
|
+
"""
|
|
126
|
+
valid = ("density", "electron_temperature", "ion_temperature")
|
|
127
|
+
if field not in valid:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"field must be one of {valid}; got {field!r}."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
dataset = self.file[f"data/xanimation/{field}"]
|
|
133
|
+
|
|
134
|
+
t_sel = self._range_to_selector(self.time_axis, t_range)
|
|
135
|
+
z_sel = self._range_to_selector(self.Z_axis, z_range)
|
|
136
|
+
r_sel = self._range_to_selector(self.R_axis, r_range)
|
|
137
|
+
|
|
138
|
+
return dataset[t_sel, z_sel, r_sel]
|
|
139
|
+
|
|
140
|
+
def close(self):
|
|
141
|
+
"""
|
|
142
|
+
Close the HDF5 file. Safe to call more than once.
|
|
143
|
+
"""
|
|
144
|
+
if self.file is not None:
|
|
145
|
+
self.file.close()
|
|
146
|
+
self.file = None
|
|
147
|
+
|
|
148
|
+
def __enter__(self):
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
def __exit__(self, exc_type, exc, tb):
|
|
152
|
+
self.close()
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
def __del__(self):
|
|
156
|
+
try:
|
|
157
|
+
self.close()
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
def __repr__(self):
|
|
162
|
+
nR = getattr(self.R_axis, "size", 0)
|
|
163
|
+
nZ = getattr(self.Z_axis, "size", 0)
|
|
164
|
+
nT = getattr(self.time_axis, "size", 0)
|
|
165
|
+
return f"HESEL(path={self.path!r}, R={nR}, Z={nZ}, time={nT})"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def expand_hesel(field, R_axis, Z_axis, n_z=1, n_sol=0, n_edge=0,
|
|
169
|
+
r_axis=-1, z_axis=-2):
|
|
170
|
+
"""
|
|
171
|
+
Expand a HESEL field along R and/or Z.
|
|
172
|
+
|
|
173
|
+
Three independent expansions can be combined in one call:
|
|
174
|
+
|
|
175
|
+
* **Edge** (``n_edge > 0``): prepend ``n_edge`` R samples below
|
|
176
|
+
``R_axis[0]``, filled per frame with the Z-average of the innermost R
|
|
177
|
+
column of ``field``.
|
|
178
|
+
* **SOL** (``n_sol > 0``): append ``n_sol`` R samples above ``R_axis[-1]``,
|
|
179
|
+
filled per frame with the Z-average of the outermost R column.
|
|
180
|
+
* **Periodic Z stacking** (``n_z > 1``): concatenate ``n_z`` copies of the
|
|
181
|
+
field along Z, assuming periodic boundary conditions in Z.
|
|
182
|
+
|
|
183
|
+
The extended axes keep the original grid spacings. With the defaults
|
|
184
|
+
``n_z=1, n_sol=0, n_edge=0`` the inputs are returned unchanged (as arrays).
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
field : array_like
|
|
189
|
+
Field to expand, e.g. (time, Z, R) from :meth:`HESEL.extract_field` or
|
|
190
|
+
a 2-D (Z, R) snapshot.
|
|
191
|
+
R_axis, Z_axis : array_like
|
|
192
|
+
1-D, uniformly spaced R and Z axes [m] matching the ``r_axis`` and
|
|
193
|
+
``z_axis`` dimensions of ``field``.
|
|
194
|
+
n_z : int, default 1
|
|
195
|
+
Number of periodic copies along Z; 1 leaves Z unchanged.
|
|
196
|
+
n_sol : int, default 0
|
|
197
|
+
Number of SOL samples appended on the high-R side.
|
|
198
|
+
n_edge : int, default 0
|
|
199
|
+
Number of edge samples prepended on the low-R side.
|
|
200
|
+
r_axis : int, default -1
|
|
201
|
+
Axis of ``field`` corresponding to R.
|
|
202
|
+
z_axis : int, default -2
|
|
203
|
+
Axis of ``field`` corresponding to Z.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
field_expanded : numpy.ndarray
|
|
208
|
+
The expanded field, with the R axis longer by ``n_edge + n_sol`` and
|
|
209
|
+
the Z axis ``n_z`` times longer.
|
|
210
|
+
R_expanded : numpy.ndarray
|
|
211
|
+
Extended R axis (ascending), length ``R_axis.size + n_edge + n_sol``
|
|
212
|
+
[m].
|
|
213
|
+
Z_expanded : numpy.ndarray
|
|
214
|
+
Extended Z axis, length ``n_z * Z_axis.size`` [m].
|
|
215
|
+
|
|
216
|
+
Raises
|
|
217
|
+
------
|
|
218
|
+
ValueError
|
|
219
|
+
If ``n_z``, ``n_sol`` or ``n_edge`` is not an integer, if ``n_z`` < 1,
|
|
220
|
+
or if ``n_sol`` or ``n_edge`` is negative.
|
|
221
|
+
"""
|
|
222
|
+
for name, n in (("n_z", n_z), ("n_sol", n_sol), ("n_edge", n_edge)):
|
|
223
|
+
if not isinstance(n, (int, np.integer)):
|
|
224
|
+
raise ValueError(f"{name} must be an integer; got {n!r}.")
|
|
225
|
+
if n_z < 1:
|
|
226
|
+
raise ValueError(f"n_z must be >= 1; got {n_z!r}.")
|
|
227
|
+
if n_sol < 0 or n_edge < 0:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"n_sol and n_edge must be >= 0; got n_sol={n_sol}, n_edge={n_edge}."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
field = np.asarray(field)
|
|
233
|
+
R_axis = np.asarray(R_axis)
|
|
234
|
+
Z_axis = np.asarray(Z_axis)
|
|
235
|
+
|
|
236
|
+
def _radial_extend(field, r_index, count):
|
|
237
|
+
"""
|
|
238
|
+
Return a constant R extension of ``count`` samples.
|
|
239
|
+
|
|
240
|
+
It is filled with the Z-average of the R column at index ``r_index``.
|
|
241
|
+
"""
|
|
242
|
+
slc = [slice(None)] * field.ndim
|
|
243
|
+
slc[r_axis] = slice(r_index, r_index + 1) if r_index >= 0 else slice(r_index, None)
|
|
244
|
+
column = field[tuple(slc)]
|
|
245
|
+
fill = column.mean(axis=z_axis, keepdims=True)
|
|
246
|
+
ext_shape = list(field.shape)
|
|
247
|
+
ext_shape[r_axis] = count
|
|
248
|
+
return np.broadcast_to(fill, ext_shape)
|
|
249
|
+
|
|
250
|
+
if n_edge > 0:
|
|
251
|
+
edge_ext = _radial_extend(field, 0, n_edge)
|
|
252
|
+
field = np.concatenate([edge_ext, field], axis=r_axis)
|
|
253
|
+
|
|
254
|
+
if n_sol > 0:
|
|
255
|
+
sol_ext = _radial_extend(field, -1, n_sol)
|
|
256
|
+
field = np.concatenate([field, sol_ext], axis=r_axis)
|
|
257
|
+
|
|
258
|
+
if n_z > 1:
|
|
259
|
+
field = np.concatenate([field] * n_z, axis=z_axis)
|
|
260
|
+
|
|
261
|
+
dR = R_axis[1] - R_axis[0]
|
|
262
|
+
R_expanded = R_axis
|
|
263
|
+
if n_edge > 0:
|
|
264
|
+
R_expanded = np.concatenate(
|
|
265
|
+
[R_axis[0] - dR * np.arange(n_edge, 0, -1), R_expanded]
|
|
266
|
+
)
|
|
267
|
+
if n_sol > 0:
|
|
268
|
+
R_expanded = np.concatenate(
|
|
269
|
+
[R_expanded, R_axis[-1] + dR * np.arange(1, n_sol + 1)]
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if n_z > 1:
|
|
273
|
+
L = Z_axis.size * (Z_axis[1] - Z_axis[0])
|
|
274
|
+
Z_expanded = np.concatenate([Z_axis + i * L for i in range(n_z)])
|
|
275
|
+
else:
|
|
276
|
+
Z_expanded = Z_axis
|
|
277
|
+
|
|
278
|
+
return field, R_expanded, Z_expanded
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Time-dependent density perturbations on 2-D background fields: Gaussian
|
|
3
|
+
filaments and sine waves, with HDF5 input and output.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
import h5py
|
|
7
|
+
|
|
8
|
+
def _field_rz(R, Z, field):
|
|
9
|
+
"""
|
|
10
|
+
Return ``field`` in (NR, NZ) layout.
|
|
11
|
+
|
|
12
|
+
Accepts (NR, NZ), and (NZ, NR) as produced by
|
|
13
|
+
plasmasds_fields.w7x.fields.make_regular_density_field or HESEL snapshots.
|
|
14
|
+
A square field (NR == NZ) is taken as (NR, NZ).
|
|
15
|
+
"""
|
|
16
|
+
field = np.asarray(field)
|
|
17
|
+
if field.shape == (len(R), len(Z)):
|
|
18
|
+
return field
|
|
19
|
+
if field.shape == (len(Z), len(R)):
|
|
20
|
+
return field.T
|
|
21
|
+
raise ValueError(
|
|
22
|
+
f"field shape {field.shape} does not match R ({len(R)}) and "
|
|
23
|
+
f"Z ({len(Z)}); expected ({len(R)}, {len(Z)}) or "
|
|
24
|
+
f"({len(Z)}, {len(R)}).")
|
|
25
|
+
|
|
26
|
+
def perturbation_gaussian(R, Z, field,
|
|
27
|
+
dt, r_trajectory , z_trajectory ,
|
|
28
|
+
perturbation_fwhm, perturbation_ampl,
|
|
29
|
+
file=None, return_array=True):
|
|
30
|
+
"""
|
|
31
|
+
Simulate travelling Gaussian filaments on a 2-D background density field.
|
|
32
|
+
|
|
33
|
+
At each time step, every filament is added to the background field as a
|
|
34
|
+
round 2-D Gaussian, evaluated within ±3 sigma of its centre. The result can
|
|
35
|
+
be written to an HDF5 file and/or returned in memory.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
R : numpy.ndarray, shape (NR,)
|
|
40
|
+
R coordinates of the grid [m].
|
|
41
|
+
Z : numpy.ndarray, shape (NZ,)
|
|
42
|
+
Z coordinates of the grid [m].
|
|
43
|
+
field : numpy.ndarray, shape (NR, NZ) or (NZ, NR)
|
|
44
|
+
Background density field [m^-3]. The (NZ, NR) layout of
|
|
45
|
+
:func:`plasmasds_fields.w7x.fields.make_regular_density_field` is
|
|
46
|
+
transposed automatically; a square field is taken as (NR, NZ).
|
|
47
|
+
dt : float
|
|
48
|
+
Time step [s].
|
|
49
|
+
r_trajectory : list of array_like, length n_filaments
|
|
50
|
+
R position of each filament at each time step [m]:
|
|
51
|
+
``r_trajectory[f_i][t_i]`` is the R of filament ``f_i`` at step
|
|
52
|
+
``t_i``. None or NaN values mean the filament is absent.
|
|
53
|
+
z_trajectory : list of array_like, length n_filaments
|
|
54
|
+
Z position of each filament at each time step [m], indexed like
|
|
55
|
+
``r_trajectory``. None or NaN values mean the filament is absent.
|
|
56
|
+
|
|
57
|
+
Trajectories (and per-step ``perturbation_fwhm`` /
|
|
58
|
+
``perturbation_ampl`` lists) may have different lengths: the number of
|
|
59
|
+
frames is set by the longest trajectory, and a filament is absent in
|
|
60
|
+
the frames beyond the end of its own data.
|
|
61
|
+
perturbation_fwhm : float or list of array_like, length n_filaments
|
|
62
|
+
Full width at half maximum of the filaments [m]. A scalar applies to
|
|
63
|
+
all filaments and time steps; a list is indexed like ``r_trajectory``.
|
|
64
|
+
The Gaussian sigma is FWHM / (2 sqrt(2 ln 2)) ≈ FWHM / 2.3548.
|
|
65
|
+
perturbation_ampl : float or list of array_like, length n_filaments
|
|
66
|
+
Peak amplitude of the filaments [m^-3]. A scalar applies to all
|
|
67
|
+
filaments and time steps; a list is indexed like ``r_trajectory``.
|
|
68
|
+
file : str or path-like, default None
|
|
69
|
+
If given, the result is written to an HDF5 file at this path, with
|
|
70
|
+
datasets ``"R"``, ``"Z"``, ``"t"`` and one dataset per time step named
|
|
71
|
+
``"field_TTTTT"`` (zero-padded frame number). The file is closed even
|
|
72
|
+
if an error occurs.
|
|
73
|
+
return_array : bool, default True
|
|
74
|
+
If True, the time-resolved field is kept in memory and returned. Set to
|
|
75
|
+
False to save memory when only the file output is needed.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
dict or None
|
|
80
|
+
If ``return_array`` is True, a dictionary with:
|
|
81
|
+
|
|
82
|
+
* ``"R"``: R coordinates, shape (NR,) [m].
|
|
83
|
+
* ``"Z"``: Z coordinates, shape (NZ,) [m].
|
|
84
|
+
* ``"t"``: time axis, shape (Nt,) [s].
|
|
85
|
+
* ``"field"``: density field at each time step, shape (Nt, NR, NZ)
|
|
86
|
+
[m^-3].
|
|
87
|
+
|
|
88
|
+
None if ``return_array`` is False.
|
|
89
|
+
|
|
90
|
+
Raises
|
|
91
|
+
------
|
|
92
|
+
ValueError
|
|
93
|
+
If ``field`` does not match the lengths of ``R`` and ``Z``, or if
|
|
94
|
+
``z_trajectory``, ``perturbation_fwhm`` or ``perturbation_ampl`` (when
|
|
95
|
+
given per filament) has a different number of filaments than
|
|
96
|
+
``r_trajectory``.
|
|
97
|
+
"""
|
|
98
|
+
field = _field_rz(R, Z, field)
|
|
99
|
+
|
|
100
|
+
_scalar_types = (int, float, np.integer, np.floating)
|
|
101
|
+
fwhm_is_scalar = isinstance(perturbation_fwhm, _scalar_types)
|
|
102
|
+
ampl_is_scalar = isinstance(perturbation_ampl, _scalar_types)
|
|
103
|
+
n_f = len(r_trajectory)
|
|
104
|
+
|
|
105
|
+
# every per-filament input must describe the same filaments
|
|
106
|
+
per_filament = [("z_trajectory", z_trajectory)]
|
|
107
|
+
if not fwhm_is_scalar: per_filament.append(("perturbation_fwhm",
|
|
108
|
+
perturbation_fwhm))
|
|
109
|
+
if not ampl_is_scalar: per_filament.append(("perturbation_ampl",
|
|
110
|
+
perturbation_ampl))
|
|
111
|
+
for name, values in per_filament:
|
|
112
|
+
if len(values) != n_f:
|
|
113
|
+
raise ValueError(f"{name} has {len(values)} filaments, "
|
|
114
|
+
f"r_trajectory has {n_f}.")
|
|
115
|
+
|
|
116
|
+
# the longest trajectory sets the number of frames
|
|
117
|
+
n_t = max(len(traj) for traj in list(r_trajectory) + list(z_trajectory))
|
|
118
|
+
t = np.arange(n_t) * dt
|
|
119
|
+
|
|
120
|
+
if return_array:
|
|
121
|
+
data = {"R": R, "Z": Z, "t": t,
|
|
122
|
+
"field": np.zeros((len(t), len(R), len(Z)))}
|
|
123
|
+
|
|
124
|
+
# 2-D Gaussian, evaluated within a ±3 sigma window around each filament
|
|
125
|
+
def gaussian_2d(R, Z, r0=0, z0=0, sigma_r=1, sigma_z=1, A=1):
|
|
126
|
+
"""
|
|
127
|
+
Return a 2-D Gaussian of amplitude ``A`` centred at (r0, z0).
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
return A * np.exp(-(((R - r0) ** 2) / (2 * sigma_r ** 2)
|
|
131
|
+
+ ((Z - z0) ** 2) / (2 * sigma_z ** 2)))
|
|
132
|
+
|
|
133
|
+
fwhm_to_sigma = 2 * np.sqrt(2 * np.log(2)) # FWHM = 2.3548 sigma
|
|
134
|
+
|
|
135
|
+
def step(values, f_i, t_i):
|
|
136
|
+
"""Value of filament f_i at step t_i, None beyond its data."""
|
|
137
|
+
return values[f_i][t_i] if t_i < len(values[f_i]) else None
|
|
138
|
+
|
|
139
|
+
f = None
|
|
140
|
+
try:
|
|
141
|
+
if file is not None:
|
|
142
|
+
# create the output file, fill it after
|
|
143
|
+
f = h5py.File(file, "w")
|
|
144
|
+
f.create_dataset("R", data = R, compression="gzip")
|
|
145
|
+
f.create_dataset("Z", data = Z, compression="gzip")
|
|
146
|
+
f.create_dataset("t", data = t, compression="gzip")
|
|
147
|
+
|
|
148
|
+
# iterate through all the frames and the filaments
|
|
149
|
+
for t_i in range(len(t)):
|
|
150
|
+
den_act = field.copy()
|
|
151
|
+
for f_i in range(n_f):
|
|
152
|
+
posR = step(r_trajectory, f_i, t_i)
|
|
153
|
+
posZ = step(z_trajectory, f_i, t_i)
|
|
154
|
+
fwhm_val = (perturbation_fwhm if fwhm_is_scalar
|
|
155
|
+
else step(perturbation_fwhm, f_i, t_i))
|
|
156
|
+
ampl = (perturbation_ampl if ampl_is_scalar
|
|
157
|
+
else step(perturbation_ampl, f_i, t_i))
|
|
158
|
+
# missing data (None or NaN): no filament in this frame
|
|
159
|
+
if any(v is None or np.isnan(v)
|
|
160
|
+
for v in (posR, posZ, fwhm_val, ampl)):
|
|
161
|
+
continue
|
|
162
|
+
sigR = sigZ = fwhm_val / fwhm_to_sigma
|
|
163
|
+
effR = effZ = 3 * sigR
|
|
164
|
+
# grid indices within the ±3 sigma window
|
|
165
|
+
G_ind_r = np.where((R >= posR - effR) &
|
|
166
|
+
(R <= posR + effR))[0]
|
|
167
|
+
G_ind_z = np.where((Z >= posZ - effZ) &
|
|
168
|
+
(Z <= posZ + effZ))[0]
|
|
169
|
+
# filament window entirely outside the grid: nothing to add
|
|
170
|
+
if G_ind_r.size == 0 or G_ind_z.size == 0:
|
|
171
|
+
continue
|
|
172
|
+
GR, GZ = np.meshgrid(R[G_ind_r], Z[G_ind_z], indexing="ij")
|
|
173
|
+
G_field = gaussian_2d(GR, GZ, r0 = posR, z0 = posZ,
|
|
174
|
+
sigma_r = sigR, sigma_z = sigZ,
|
|
175
|
+
A = ampl)
|
|
176
|
+
# adding the Gaussian influence to the original den. field
|
|
177
|
+
den_act[G_ind_r[0] : G_ind_r[-1] +1,
|
|
178
|
+
G_ind_z[0] : G_ind_z[-1] +1] += G_field
|
|
179
|
+
|
|
180
|
+
if return_array: data["field"][t_i, :, :] = den_act
|
|
181
|
+
|
|
182
|
+
if f is not None:
|
|
183
|
+
# Save arrays to an HDF5 file
|
|
184
|
+
f.create_dataset("field_" + str(t_i).zfill(5),
|
|
185
|
+
data = den_act, compression="gzip")
|
|
186
|
+
finally:
|
|
187
|
+
if f is not None: f.close()
|
|
188
|
+
|
|
189
|
+
if return_array: return data
|
|
190
|
+
else: return None
|
|
191
|
+
|
|
192
|
+
def perturbation_sine(R, Z, field, dt, nt, r_trajectory, perturbation_speed,
|
|
193
|
+
perturbation_wavelength, perturbation_ampl,
|
|
194
|
+
file=None, return_array=True):
|
|
195
|
+
"""
|
|
196
|
+
Simulate a travelling sine perturbation of a 2-D background density field.
|
|
197
|
+
|
|
198
|
+
The perturbation is a sine wave around a vertical centre line,
|
|
199
|
+
``r_trajectory``, travelling along Z. At each time step and Z grid point,
|
|
200
|
+
all densities between the centre line and the displaced wave are set to the
|
|
201
|
+
background density at the centre line, i.e. the density profile is
|
|
202
|
+
flattened over the displacement. The result can be written to an HDF5 file
|
|
203
|
+
and/or returned in memory.
|
|
204
|
+
|
|
205
|
+
Parameters
|
|
206
|
+
----------
|
|
207
|
+
R : numpy.ndarray, shape (NR,)
|
|
208
|
+
R coordinates of the grid [m].
|
|
209
|
+
Z : numpy.ndarray, shape (NZ,)
|
|
210
|
+
Z coordinates of the grid [m].
|
|
211
|
+
field : numpy.ndarray, shape (NR, NZ) or (NZ, NR)
|
|
212
|
+
Background density field [m^-3]. The (NZ, NR) layout of
|
|
213
|
+
:func:`plasmasds_fields.w7x.fields.make_regular_density_field` is
|
|
214
|
+
transposed automatically; a square field is taken as (NR, NZ).
|
|
215
|
+
dt : float
|
|
216
|
+
Time step [s].
|
|
217
|
+
nt : int
|
|
218
|
+
Number of time steps (total time nt * dt).
|
|
219
|
+
r_trajectory : float or numpy.ndarray, shape (NZ,)
|
|
220
|
+
R position of the centre line of the wave at each Z grid point [m],
|
|
221
|
+
usually a parabolic line along Z. A scalar gives a straight vertical
|
|
222
|
+
centre line.
|
|
223
|
+
perturbation_speed : float
|
|
224
|
+
Propagation speed of the wave along Z [m/s]; positive values travel
|
|
225
|
+
towards larger Z.
|
|
226
|
+
perturbation_wavelength : float
|
|
227
|
+
Wavelength of the wave along Z [m].
|
|
228
|
+
perturbation_ampl : float
|
|
229
|
+
Amplitude of the radial displacement of the wave [m].
|
|
230
|
+
file : str or path-like, default None
|
|
231
|
+
If given, the result is written to an HDF5 file at this path, with
|
|
232
|
+
datasets ``"R"``, ``"Z"``, ``"t"`` and one dataset per time step named
|
|
233
|
+
``"field_TTTTT"`` (zero-padded frame number). The file is closed even
|
|
234
|
+
if an error occurs.
|
|
235
|
+
return_array : bool, default True
|
|
236
|
+
If True, the time-resolved field is kept in memory and returned. Set to
|
|
237
|
+
False to save memory when only the file output is needed.
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
-------
|
|
241
|
+
dict or None
|
|
242
|
+
If ``return_array`` is True, a dictionary with:
|
|
243
|
+
|
|
244
|
+
* ``"R"``: R coordinates, shape (NR,) [m].
|
|
245
|
+
* ``"Z"``: Z coordinates, shape (NZ,) [m].
|
|
246
|
+
* ``"t"``: time axis, shape (Nt,) [s].
|
|
247
|
+
* ``"field"``: density field at each time step, shape (Nt, NR, NZ)
|
|
248
|
+
[m^-3].
|
|
249
|
+
|
|
250
|
+
None if ``return_array`` is False.
|
|
251
|
+
|
|
252
|
+
Raises
|
|
253
|
+
------
|
|
254
|
+
ValueError
|
|
255
|
+
If ``field`` does not match the lengths of ``R`` and ``Z``, or if
|
|
256
|
+
``r_trajectory`` is neither a scalar nor one value per Z grid point.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
field = _field_rz(R, Z, field)
|
|
260
|
+
|
|
261
|
+
# centre line: one R value per Z grid point; a scalar gives a straight line
|
|
262
|
+
r_trajectory = np.asarray(r_trajectory, dtype=float)
|
|
263
|
+
if r_trajectory.ndim == 0:
|
|
264
|
+
r_trajectory = np.full(len(Z), float(r_trajectory))
|
|
265
|
+
if r_trajectory.shape != (len(Z),):
|
|
266
|
+
raise ValueError(
|
|
267
|
+
f"r_trajectory must be a scalar or have one value per Z grid "
|
|
268
|
+
f"point ({len(Z)}), got shape {r_trajectory.shape}.")
|
|
269
|
+
|
|
270
|
+
t = np.arange(nt) * dt
|
|
271
|
+
|
|
272
|
+
if return_array:
|
|
273
|
+
data = {"R": R, "Z": Z, "t": t,
|
|
274
|
+
"field": np.zeros((len(t), len(R), len(Z)))}
|
|
275
|
+
|
|
276
|
+
f = None
|
|
277
|
+
try:
|
|
278
|
+
if file is not None:
|
|
279
|
+
# create the output file, fill it after
|
|
280
|
+
f = h5py.File(file, "w")
|
|
281
|
+
f.create_dataset("R", data = R, compression="gzip")
|
|
282
|
+
f.create_dataset("Z", data = Z, compression="gzip")
|
|
283
|
+
f.create_dataset("t", data = t, compression="gzip")
|
|
284
|
+
|
|
285
|
+
# iterate through all the frames and the filaments
|
|
286
|
+
mask = np.zeros(len(R), dtype=bool)
|
|
287
|
+
for t_i in range(len(t)):
|
|
288
|
+
den_act = field.copy()
|
|
289
|
+
|
|
290
|
+
for z_i in range(len(Z)):
|
|
291
|
+
r0_center = r_trajectory[z_i]
|
|
292
|
+
|
|
293
|
+
k = 2 * np.pi / perturbation_wavelength
|
|
294
|
+
omega = k * perturbation_speed
|
|
295
|
+
r0_act = (r0_center + perturbation_ampl
|
|
296
|
+
* np.cos(k * Z[z_i] - omega * t[t_i]))
|
|
297
|
+
|
|
298
|
+
peak = np.interp(r0_center, R, den_act[:, z_i])
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
if r0_act < r0_center: r_min, r_max = r0_act, r0_center
|
|
302
|
+
else: r_min, r_max = r0_center, r0_act
|
|
303
|
+
mask[:] = (R < r_max) & (R > r_min)
|
|
304
|
+
|
|
305
|
+
den_act[mask, z_i] = peak
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
if return_array: data["field"][t_i, :, :] = den_act
|
|
309
|
+
|
|
310
|
+
if f is not None:
|
|
311
|
+
# Save arrays to an HDF5 file
|
|
312
|
+
f.create_dataset("field_" + str(t_i).zfill(5),
|
|
313
|
+
data = den_act, compression="gzip")
|
|
314
|
+
finally:
|
|
315
|
+
if f is not None: f.close()
|
|
316
|
+
|
|
317
|
+
if return_array: return data
|
|
318
|
+
else: return None
|
|
319
|
+
|
|
320
|
+
def import_density_field(file):
|
|
321
|
+
"""
|
|
322
|
+
Read a density field simulation from an HDF5 file.
|
|
323
|
+
|
|
324
|
+
Reads files written by :func:`perturbation_gaussian` or
|
|
325
|
+
:func:`perturbation_sine`, with datasets ``"R"``, ``"Z"``, ``"t"`` and one
|
|
326
|
+
2-D snapshot per time step named ``"field_TTTTT"`` (zero-padded frame
|
|
327
|
+
number).
|
|
328
|
+
|
|
329
|
+
Parameters
|
|
330
|
+
----------
|
|
331
|
+
file : str or path-like
|
|
332
|
+
Path to the HDF5 file.
|
|
333
|
+
|
|
334
|
+
Returns
|
|
335
|
+
-------
|
|
336
|
+
dict
|
|
337
|
+
Dictionary with:
|
|
338
|
+
|
|
339
|
+
* ``"R"``: R coordinates, shape (NR,) [m].
|
|
340
|
+
* ``"Z"``: Z coordinates, shape (NZ,) [m].
|
|
341
|
+
* ``"t"``: time axis, shape (Nt,) [s].
|
|
342
|
+
* ``"field"``: density field at each time step, shape (Nt, NR, NZ)
|
|
343
|
+
[m^-3], in order of the frame number in the dataset names.
|
|
344
|
+
|
|
345
|
+
Raises
|
|
346
|
+
------
|
|
347
|
+
KeyError
|
|
348
|
+
If the file has no ``"R"``, ``"Z"`` or ``"t"`` dataset.
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
with h5py.File(file, "r") as f:
|
|
352
|
+
Z, R = np.array(f["Z"]), np.array(f["R"])
|
|
353
|
+
# sort by frame number, not alphabetically (robust beyond 5 digits)
|
|
354
|
+
keys = sorted((key for key in f.keys() if key.startswith("field_")),
|
|
355
|
+
key=lambda key: int(key[len("field_"):]))
|
|
356
|
+
field = np.asarray([f[key] for key in keys])
|
|
357
|
+
t = np.array(f["t"])
|
|
358
|
+
return {"R": R, "Z": Z, "field": field, "t": t}
|
|
359
|
+
|