PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
Functions | Variables
periodic_spectral Namespace Reference

Functions

 _drop_imported_package (str package_name)
 Remove a failed/partial import package tree from sys.modules.
 
 _prune_incompatible_python_site_paths (paths)
 Remove site-package paths for a different Python major/minor version.
 
 require_numpy ()
 Import NumPy with the same compatibility retry used by the conductor.
 
 validate_spectral_grid (nodes, transform_axes=(0, 1, 2), subject="spectra require")
 Validate the uniform axis-aligned Cartesian grid an FFT on periodic axes requires.
 
 spectral_symbols (cells_kji, lengths_xyz)
 Build continuum and centered-discrete Fourier symbols.
 
 _resample_axis (values, storage_axis, target_count, length, shift)
 Move one periodic axis of an array of samples to another uniform resolution.
 
 filter_transfer (continuum_symbols, filter_spec)
 Transfer function of a resampling filter on the target grid's wavenumbers.
 
 fourier_resample (values, target_cells_kji, lengths_xyz, source_first_xyz, target_first_xyz, filter_spec=None)
 Resample a triply periodic vector field onto another uniform grid, optionally filtered.
 

Variables

tuple RESAMPLE_FILTERS = ("none", "sharp", "gaussian", "box")
 

Function Documentation

◆ _drop_imported_package()

periodic_spectral._drop_imported_package ( str  package_name)
protected

Remove a failed/partial import package tree from sys.modules.

Parameters
[in]package_nameTop-level package name.

Definition at line 16 of file periodic_spectral.py.

16def _drop_imported_package(package_name: str):
17 """!
18 @brief Remove a failed/partial import package tree from sys.modules.
19 @param[in] package_name Top-level package name.
20 """
21 prefix = package_name + "."
22 for module_name in list(sys.modules):
23 if module_name == package_name or module_name.startswith(prefix):
24 sys.modules.pop(module_name, None)
25
26
Head of a generic C-style linked list.
Definition variables.h:476
Here is the caller graph for this function:

◆ _prune_incompatible_python_site_paths()

periodic_spectral._prune_incompatible_python_site_paths (   paths)
protected

Remove site-package paths for a different Python major/minor version.

Parameters
[in]pathsCandidate sys.path entries.
Returns
Filtered path list.

Definition at line 27 of file periodic_spectral.py.

27def _prune_incompatible_python_site_paths(paths):
28 """!
29 @brief Remove site-package paths for a different Python major/minor version.
30 @param[in] paths Candidate sys.path entries.
31 @return Filtered path list.
32 """
33 current = (sys.version_info[0], sys.version_info[1])
34 filtered = []
35 for path in paths:
36 text = str(path)
37 marker = "python"
38 idx = text.lower().find(marker)
39 if idx >= 0 and ("site-packages" in text or "dist-packages" in text):
40 version_text = text[idx + len(marker):idx + len(marker) + 4].strip("-/")
41 parts = version_text.split(".")
42 try:
43 path_version = (int(parts[0]), int(parts[1]))
44 except (IndexError, ValueError):
45 filtered.append(path)
46 continue
47 if path_version != current:
48 continue
49 filtered.append(path)
50 return filtered
51
52
Here is the caller graph for this function:

◆ require_numpy()

periodic_spectral.require_numpy ( )

Import NumPy with the same compatibility retry used by the conductor.

Returns
Imported NumPy module.

Definition at line 53 of file periodic_spectral.py.

53def require_numpy():
54 """!
55 @brief Import NumPy with the same compatibility retry used by the conductor.
56 @return Imported NumPy module.
57 """
58 try:
59 import numpy
60 return numpy
61 except Exception as exc:
62 first_error = exc
63 original_path = list(sys.path)
64 try:
65 _drop_imported_package("numpy")
66 sys.path = _prune_incompatible_python_site_paths(original_path)
67 import numpy
68 return numpy
69 except Exception as retry_exc:
70 raise RuntimeError(
71 "NumPy is required for PICurv spectral generators, but no compatible "
72 "NumPy could be imported. "
73 f"First error: {first_error}. Retry error: {retry_exc}"
74 ) from retry_exc
75 finally:
76 sys.path = original_path
77
78
Here is the call graph for this function:
Here is the caller graph for this function:

◆ validate_spectral_grid()

periodic_spectral.validate_spectral_grid (   nodes,
  transform_axes = (0, 1, 2),
  subject = "spectra require" 
)

Validate the uniform axis-aligned Cartesian grid an FFT on periodic axes requires.

Parameters
[in]nodesPICGRID node-coordinate array shaped (KM, JM, IM, 3).
[in]transform_axesCartesian axes that are transformed and so must be uniform; the remaining axes may stretch.
[in]subjectWhat needs the grid and its verb, opening each error message.
Returns
Cell counts in storage order and physical lengths in Cartesian order.

Definition at line 79 of file periodic_spectral.py.

79def validate_spectral_grid(nodes, transform_axes=(0, 1, 2), subject="spectra require"):
80 """!
81 @brief Validate the uniform axis-aligned Cartesian grid an FFT on periodic axes requires.
82 @param[in] nodes PICGRID node-coordinate array shaped `(KM, JM, IM, 3)`.
83 @param[in] transform_axes Cartesian axes that are transformed and so must be uniform;
84 the remaining axes may stretch.
85 @param[in] subject What needs the grid and its verb, opening each error message.
86 @return Cell counts in storage order and physical lengths in Cartesian order.
87 """
88 numpy = require_numpy()
89 km, jm, im, _ = nodes.shape
90 cells = (km - 1, jm - 1, im - 1)
91 if min(cells) < 4:
92 raise ValueError(f"{subject} at least four cells per axis; found {cells}.")
93 axes = (nodes[0, 0, :, 0], nodes[0, :, 0, 1], nodes[:, 0, 0, 2])
94 for axis, (physical_axis, values) in enumerate(zip("xyz", axes)):
95 delta = numpy.diff(values)
96 if not numpy.all(numpy.isfinite(delta)) or not numpy.all(delta > 0) or (
97 axis in transform_axes and not numpy.allclose(delta, delta[0], rtol=1e-10, atol=1e-12)):
98 raise ValueError(f"{subject} uniform positive {physical_axis} spacing on every "
99 "transformed axis.")
100 if not (numpy.allclose(nodes[..., 0], axes[0][None, None, :]) and
101 numpy.allclose(nodes[..., 1], axes[1][None, :, None]) and
102 numpy.allclose(nodes[..., 2], axes[2][:, None, None])):
103 raise ValueError(f"{subject} an axis-aligned Cartesian grid (x->i, y->j, z->k).")
104 return cells, tuple(float(values[-1] - values[0]) for values in axes)
105
106
Here is the call graph for this function:

◆ spectral_symbols()

periodic_spectral.spectral_symbols (   cells_kji,
  lengths_xyz 
)

Build continuum and centered-discrete Fourier symbols.

Parameters
[in]cells_kjiCell counts in storage order.
[in]lengths_xyzPhysical box lengths in Cartesian order.
Returns
Continuum symbols, centered-discrete symbols, and Cartesian spacings.

Definition at line 107 of file periodic_spectral.py.

107def spectral_symbols(cells_kji, lengths_xyz):
108 """!
109 @brief Build continuum and centered-discrete Fourier symbols.
110 @param[in] cells_kji Cell counts in storage order.
111 @param[in] lengths_xyz Physical box lengths in Cartesian order.
112 @return Continuum symbols, centered-discrete symbols, and Cartesian spacings.
113 """
114 numpy = require_numpy()
115 nk, nj, ni = cells_kji
116 lx, ly, lz = lengths_xyz
117 dx, dy, dz = lx / ni, ly / nj, lz / nk
118 kz = 2*numpy.pi*numpy.fft.fftfreq(nk, d=dz)[:, None, None]
119 ky = 2*numpy.pi*numpy.fft.fftfreq(nj, d=dy)[None, :, None]
120 kx = 2*numpy.pi*numpy.fft.fftfreq(ni, d=dx)[None, None, :]
121 discrete = (numpy.sin(kx*dx)/dx, numpy.sin(ky*dy)/dy, numpy.sin(kz*dz)/dz)
122 return (kx, ky, kz), discrete, (dx, dy, dz)
123
124
125#: Filter kernels `fourier_resample` applies, by name.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _resample_axis()

periodic_spectral._resample_axis (   values,
  storage_axis,
  target_count,
  length,
  shift 
)
protected

Move one periodic axis of an array of samples to another uniform resolution.

The samples are read as a truncated Fourier series, which is re-evaluated at the target points. Modes are kept only where both resolutions represent them unambiguously, |m| < min(N, M)/2, so each Nyquist mode is dropped and the result stays real.

Parameters
[in]valuesReal samples; storage_axis is the axis being moved.
[in]storage_axisArray axis to resample.
[in]target_countNumber of target samples along that axis.
[in]lengthPhysical period of the axis.
[in]shiftTarget first-sample position minus source first-sample position.
Returns
Real samples with target_count points along storage_axis.

Definition at line 129 of file periodic_spectral.py.

129def _resample_axis(values, storage_axis, target_count, length, shift):
130 """!
131 @brief Move one periodic axis of an array of samples to another uniform resolution.
132 @details The samples are read as a truncated Fourier series, which is re-evaluated at
133 the target points. Modes are kept only where both resolutions represent them
134 unambiguously, |m| < min(N, M)/2, so each Nyquist mode is dropped and the
135 result stays real.
136 @param[in] values Real samples; `storage_axis` is the axis being moved.
137 @param[in] storage_axis Array axis to resample.
138 @param[in] target_count Number of target samples along that axis.
139 @param[in] length Physical period of the axis.
140 @param[in] shift Target first-sample position minus source first-sample position.
141 @return Real samples with `target_count` points along `storage_axis`.
142 """
143 numpy = require_numpy()
144 source_count = values.shape[storage_axis]
145 coeff = numpy.fft.fft(values, axis=storage_axis) / source_count
146 modes = numpy.rint(numpy.fft.fftfreq(source_count) * source_count).astype(int)
147 keep = numpy.abs(modes) < min(source_count, target_count) / 2.0
148 phase = numpy.exp(1j * 2.0 * numpy.pi * modes[keep] * shift / length)
149 shape = [1] * values.ndim
150 shape[storage_axis] = int(keep.sum())
151 kept = numpy.compress(keep, coeff, axis=storage_axis) * phase.reshape(shape)
152 out_shape = list(values.shape)
153 out_shape[storage_axis] = target_count
154 target = numpy.zeros(out_shape, dtype=complex)
155 index = [slice(None)] * values.ndim
156 index[storage_axis] = modes[keep] % target_count
157 target[tuple(index)] = kept
158 return (numpy.fft.ifft(target, axis=storage_axis) * target_count).real
159
160
Here is the call graph for this function:
Here is the caller graph for this function:

◆ filter_transfer()

periodic_spectral.filter_transfer (   continuum_symbols,
  filter_spec 
)

Transfer function of a resampling filter on the target grid's wavenumbers.

Parameters
[in]continuum_symbolsContinuum (kx, ky, kz) from spectral_symbols().
[in]filter_specMapping with type in RESAMPLE_FILTERS; sharp takes cutoff (a wavenumber), gaussian and box take width (the filter width Delta, a length).
Returns
Broadcastable transfer array, or None for none.

Definition at line 161 of file periodic_spectral.py.

161def filter_transfer(continuum_symbols, filter_spec):
162 """!
163 @brief Transfer function of a resampling filter on the target grid's wavenumbers.
164 @param[in] continuum_symbols Continuum `(kx, ky, kz)` from `spectral_symbols()`.
165 @param[in] filter_spec Mapping with `type` in `RESAMPLE_FILTERS`; `sharp` takes
166 `cutoff` (a wavenumber), `gaussian` and `box` take `width`
167 (the filter width Delta, a length).
168 @return Broadcastable transfer array, or None for `none`.
169 """
170 numpy = require_numpy()
171 kind = filter_spec.get("type", "none")
172 kx, ky, kz = continuum_symbols
173 if kind == "none":
174 return None
175 if kind == "sharp":
176 return (kx*kx + ky*ky + kz*kz <= float(filter_spec["cutoff"])**2).astype(float)
177 width = float(filter_spec["width"])
178 if kind == "gaussian":
179 return numpy.exp(-(kx*kx + ky*ky + kz*kz) * width * width / 24.0)
180 if kind == "box":
181 return numpy.sinc(kx*width/(2*numpy.pi)) * numpy.sinc(ky*width/(2*numpy.pi)) * \
182 numpy.sinc(kz*width/(2*numpy.pi))
183 raise ValueError(f"filter type must be one of {RESAMPLE_FILTERS}; got {kind!r}.")
184
185
Here is the call graph for this function:
Here is the caller graph for this function:

◆ fourier_resample()

periodic_spectral.fourier_resample (   values,
  target_cells_kji,
  lengths_xyz,
  source_first_xyz,
  target_first_xyz,
  filter_spec = None 
)

Resample a triply periodic vector field onto another uniform grid, optionally filtered.

Exact for band-limited data: every mode both grids resolve is carried over with its amplitude and phase, then the filter multiplies its transfer function. Each axis is moved separately, so memory stays at one array of the larger size.

Parameters
[in]valuesReal samples [k, j, i, component] of the source field.
[in]target_cells_kjiTarget sample counts in storage order.
[in]lengths_xyzPhysical periods, shared by source and target, in Cartesian order.
[in]source_first_xyzPosition of the first source sample on each axis, relative to the box origin.
[in]target_first_xyzPosition of the first target sample on each axis.
[in]filter_specOptional filter mapping for filter_transfer().
Returns
Real target samples [k, j, i, component].

Definition at line 186 of file periodic_spectral.py.

187 filter_spec=None):
188 """!
189 @brief Resample a triply periodic vector field onto another uniform grid, optionally filtered.
190 @details Exact for band-limited data: every mode both grids resolve is carried over with
191 its amplitude and phase, then the filter multiplies its transfer function. Each
192 axis is moved separately, so memory stays at one array of the larger size.
193 @param[in] values Real samples `[k, j, i, component]` of the source field.
194 @param[in] target_cells_kji Target sample counts in storage order.
195 @param[in] lengths_xyz Physical periods, shared by source and target, in Cartesian order.
196 @param[in] source_first_xyz Position of the first source sample on each axis, relative
197 to the box origin.
198 @param[in] target_first_xyz Position of the first target sample on each axis.
199 @param[in] filter_spec Optional filter mapping for `filter_transfer()`.
200 @return Real target samples `[k, j, i, component]`.
201 """
202 numpy = require_numpy()
203 out = numpy.asarray(values, dtype=float)
204 for axis in range(3):
205 storage_axis = 2 - axis
206 out = _resample_axis(out, storage_axis, int(target_cells_kji[storage_axis]),
207 float(lengths_xyz[axis]),
208 float(target_first_xyz[axis]) - float(source_first_xyz[axis]))
209 if filter_spec and filter_spec.get("type", "none") != "none":
210 continuum, _discrete, _spacing = spectral_symbols(tuple(target_cells_kji), lengths_xyz)
211 transfer = filter_transfer(continuum, filter_spec)
212 coeff = numpy.fft.fftn(out, axes=(0, 1, 2))
213 out = numpy.fft.ifftn(coeff * transfer[..., None], axes=(0, 1, 2)).real
214 return out
Here is the call graph for this function:

Variable Documentation

◆ RESAMPLE_FILTERS

tuple periodic_spectral.RESAMPLE_FILTERS = ("none", "sharp", "gaussian", "box")

Definition at line 126 of file periodic_spectral.py.