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.
33 current = (sys.version_info[0], sys.version_info[1])
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(
".")
43 path_version = (int(parts[0]), int(parts[1]))
44 except (IndexError, ValueError):
47 if path_version != current:
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.
89 km, jm, im, _ = nodes.shape
90 cells = (km - 1, jm - 1, im - 1)
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 "
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)
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.
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)
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
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`.
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
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`.
171 kind = filter_spec.get(
"type",
"none")
172 kx, ky, kz = continuum_symbols
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)
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}.")
186def fourier_resample(values, target_cells_kji, lengths_xyz, source_first_xyz, target_first_xyz,
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
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]`.
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)
212 coeff = numpy.fft.fftn(out, axes=(0, 1, 2))
213 out = numpy.fft.ifftn(coeff * transfer[...,
None], axes=(0, 1, 2)).real
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.
_resample_axis(values, storage_axis, target_count, length, shift)
Move one periodic axis of an array of samples to another uniform resolution.
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.