vectorwaves 1.0.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.
- vectorwaves/__init__.py +88 -0
- vectorwaves/backends/__init__.py +0 -0
- vectorwaves/backends/cupy_backend.py +197 -0
- vectorwaves/backends/numba_backend.py +271 -0
- vectorwaves/backends/numpy_backend.py +193 -0
- vectorwaves/beam_stuff.py +623 -0
- vectorwaves/config_stuff.py +747 -0
- vectorwaves/engine_stuff.py +379 -0
- vectorwaves/py.typed +0 -0
- vectorwaves/singularities.py +617 -0
- vectorwaves/spectra.py +341 -0
- vectorwaves/utils.py +130 -0
- vectorwaves/version.py +5 -0
- vectorwaves-1.0.0.dist-info/METADATA +114 -0
- vectorwaves-1.0.0.dist-info/RECORD +18 -0
- vectorwaves-1.0.0.dist-info/WHEEL +5 -0
- vectorwaves-1.0.0.dist-info/licenses/LICENSE +21 -0
- vectorwaves-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Field Engine & Results
|
|
3
|
+
======================
|
|
4
|
+
|
|
5
|
+
The final stage of the VectorWaves pipeline. This module provides the `FieldEngine`,
|
|
6
|
+
which orchestrates the spatial evaluation of electromagnetic fields by routing
|
|
7
|
+
precomputed `Beam` objects to hardware backends (NumPy, Numba or CuPy).
|
|
8
|
+
|
|
9
|
+
All computations are encapsulated in the `FieldResult` container, which provides
|
|
10
|
+
a unified interface for Electric field components, vector calculus operations (Curl,
|
|
11
|
+
Divergence), and the Jacobian tensor as well as the Magnetic field components.
|
|
12
|
+
|
|
13
|
+
Pipeline Context:
|
|
14
|
+
1. Config (config_stuff.py) -> Define parameters.
|
|
15
|
+
2. Beam (beam_stuff.py) -> Precompute spectrum/weights via BeamMaker.
|
|
16
|
+
3. Engine (engine_stuff.py) -> Evaluate fields on points/grids via FieldEngine.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import time
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Optional, TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
from .config_stuff import Config
|
|
26
|
+
from .beam_stuff import Beam
|
|
27
|
+
|
|
28
|
+
# Import backends
|
|
29
|
+
from .backends.numpy_backend import NumpyMethods
|
|
30
|
+
try:
|
|
31
|
+
from .backends.numba_backend import NumbaMethods, has_numba
|
|
32
|
+
except ImportError:
|
|
33
|
+
NumbaMethods = None
|
|
34
|
+
has_numba = False
|
|
35
|
+
try:
|
|
36
|
+
from .backends.cupy_backend import CupyMethods, has_cupy
|
|
37
|
+
except ImportError:
|
|
38
|
+
CupyMethods = None
|
|
39
|
+
has_cupy = False
|
|
40
|
+
|
|
41
|
+
# tqdm
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from tqdm import tqdm
|
|
44
|
+
else:
|
|
45
|
+
try:
|
|
46
|
+
from tqdm import tqdm
|
|
47
|
+
except ImportError:
|
|
48
|
+
class tqdm:
|
|
49
|
+
def __init__(self, *args, **kwargs): pass
|
|
50
|
+
def update(self, n): pass
|
|
51
|
+
def close(self): pass
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class FieldResult:
|
|
55
|
+
"""
|
|
56
|
+
Structured container for electromagnetic field data and its derivatives.
|
|
57
|
+
|
|
58
|
+
This class provides a unified interface for accessing field components,
|
|
59
|
+
vector calculus operations (Divergence, Curl), and the Jacobian tensor
|
|
60
|
+
regardless of whether the evaluation was at a point, a cloud, or a grid.
|
|
61
|
+
|
|
62
|
+
Shape Conventions
|
|
63
|
+
-----------------
|
|
64
|
+
Let `domain_shape` be the spatial dimensions of the evaluation:
|
|
65
|
+
- compute_point: ()
|
|
66
|
+
- compute_cloud: (N,)
|
|
67
|
+
- compute_grid: (Ny, Nx)
|
|
68
|
+
|
|
69
|
+
- E, B: (3, *domain_shape)
|
|
70
|
+
- jacobian_E: (3, 3, *domain_shape)
|
|
71
|
+
- div_E: (*domain_shape)
|
|
72
|
+
- curl_E: (3, *domain_shape)
|
|
73
|
+
|
|
74
|
+
Attributes
|
|
75
|
+
----------
|
|
76
|
+
E : np.ndarray
|
|
77
|
+
The Electric field vector.
|
|
78
|
+
E[0, ...] is Ex, E[1, ...] is Ey, E[2, ...] is Ez.
|
|
79
|
+
"""
|
|
80
|
+
E: np.ndarray
|
|
81
|
+
_B: Optional[np.ndarray] = None
|
|
82
|
+
_jacobian_E: Optional[np.ndarray] = None
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
shape = self.E.shape[1:] if self.E.ndim > 1 else "Point"
|
|
86
|
+
has_b = self._B is not None
|
|
87
|
+
has_jac = self._jacobian_E is not None
|
|
88
|
+
return f"<FieldResult: Grid={shape}, B_computed={has_b}, Derivs_computed={has_jac}>"
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def B(self) -> np.ndarray:
|
|
92
|
+
"""
|
|
93
|
+
The Magnetic field vector (3, *domain_shape).
|
|
94
|
+
Raises RuntimeError if 'need_b' was False during computation.
|
|
95
|
+
"""
|
|
96
|
+
if self._B is None:
|
|
97
|
+
raise RuntimeError("Magnetic field (B) was not computed. Set need_b=True.")
|
|
98
|
+
return self._B
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def jacobian_E(self) -> np.ndarray:
|
|
102
|
+
"""
|
|
103
|
+
The Jacobian matrix of the Electric field (3, 3, *domain_shape).
|
|
104
|
+
Raises RuntimeError if 'need_derivs' was False during computation.
|
|
105
|
+
|
|
106
|
+
Layout (Numerator Convention):
|
|
107
|
+
The first index (i) corresponds to the E-field component (Ex, Ey, Ez).
|
|
108
|
+
The second index (j) corresponds to the spatial derivative(d/dx, d/dy, d/dz).
|
|
109
|
+
|
|
110
|
+
result[i, j, ...] = dE_i / dx_j
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
>>> # Get dEy / dz
|
|
114
|
+
>>> dEy_dz = field.jacobian_E[1, 2]
|
|
115
|
+
"""
|
|
116
|
+
if self._jacobian_E is None:
|
|
117
|
+
raise RuntimeError("Derivatives were not computed. Set 'need_derivs=True'.")
|
|
118
|
+
return self._jacobian_E
|
|
119
|
+
|
|
120
|
+
# --- Convenience Slice Accessors ---
|
|
121
|
+
@property
|
|
122
|
+
def dE_dx(self) -> np.ndarray: return self.jacobian_E[:, 0, ...]
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def dE_dy(self) -> np.ndarray: return self.jacobian_E[:, 1, ...]
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def dE_dz(self) -> np.ndarray: return self.jacobian_E[:, 2, ...]
|
|
129
|
+
|
|
130
|
+
# --- Derived Physical Quantities ---
|
|
131
|
+
@property
|
|
132
|
+
def div_E(self) -> np.ndarray:
|
|
133
|
+
"""
|
|
134
|
+
Divergence: div(E)
|
|
135
|
+
Computed as the trace of the Electric field Jacobian.
|
|
136
|
+
"""
|
|
137
|
+
return np.trace(self.jacobian_E, axis1=0, axis2=1)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def curl_E(self) -> np.ndarray:
|
|
141
|
+
"""
|
|
142
|
+
Curl: curl(E)
|
|
143
|
+
Computed from the anti-symmetric components of the Electric field Jacobian.
|
|
144
|
+
"""
|
|
145
|
+
j = self.jacobian_E
|
|
146
|
+
return np.stack([
|
|
147
|
+
j[2, 1, ...] - j[1, 2, ...],
|
|
148
|
+
j[0, 2, ...] - j[2, 0, ...],
|
|
149
|
+
j[1, 0, ...] - j[0, 1, ...]
|
|
150
|
+
], axis=0)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def intensity_E(self) -> np.ndarray:
|
|
154
|
+
"""Calculates intensity = |E|^2 over domain."""
|
|
155
|
+
return np.sum(np.abs(self.E)**2, axis=0)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class FieldEngine:
|
|
159
|
+
"""
|
|
160
|
+
Main engine for computing spatial electromagnetic fields from a Beam.
|
|
161
|
+
"""
|
|
162
|
+
def __init__(self, beam: Beam, config: Config):
|
|
163
|
+
self.beam = beam
|
|
164
|
+
self.config = config
|
|
165
|
+
|
|
166
|
+
self.backend_name = self.selector(self.config.backend)
|
|
167
|
+
if self.config.verbose:
|
|
168
|
+
print(f"--- FieldEngine Initialized (Backend: {self.backend_name}) ---")
|
|
169
|
+
|
|
170
|
+
center_x, center_y = self.config.op.center
|
|
171
|
+
w, h = self.config.op.size
|
|
172
|
+
dx = self.config.op.spacing
|
|
173
|
+
|
|
174
|
+
nx = int(w / dx)
|
|
175
|
+
ny = int(h / dx)
|
|
176
|
+
|
|
177
|
+
self.x = np.linspace(center_x - w/2, center_x + w/2, nx)
|
|
178
|
+
self.y = np.linspace(center_y - h/2, center_y + h/2, ny)
|
|
179
|
+
self.X, self.Y = np.meshgrid(self.x, self.y)
|
|
180
|
+
|
|
181
|
+
self.op_extent = [min(self.x), max(self.x), min(self.y), max(self.y)]
|
|
182
|
+
|
|
183
|
+
def selector(self, choice: str) -> str:
|
|
184
|
+
choice = choice.lower()
|
|
185
|
+
if choice == "auto":
|
|
186
|
+
if has_cupy: return "cupy64"
|
|
187
|
+
return "numba" if has_numba else "numpy"
|
|
188
|
+
|
|
189
|
+
# Define supported strings
|
|
190
|
+
valid_backends = ["numpy", "numba", "cupy32", "cupy64"]
|
|
191
|
+
if choice not in valid_backends:
|
|
192
|
+
raise ValueError(f"Backend '{choice}' not supported. Choose from {valid_backends}")
|
|
193
|
+
|
|
194
|
+
# Check availability
|
|
195
|
+
if choice.startswith("cupy") and not has_cupy:
|
|
196
|
+
raise ValueError("CuPy backend requested but cupy is not installed or no GPU found.")
|
|
197
|
+
if choice == "numba" and not has_numba:
|
|
198
|
+
raise ValueError("Numba backend requested but numba is not installed.")
|
|
199
|
+
|
|
200
|
+
return choice
|
|
201
|
+
|
|
202
|
+
def get_backend(self, backend_name: Optional[str] = None):
|
|
203
|
+
if backend_name is None:
|
|
204
|
+
return self.get_backend(self.backend_name)
|
|
205
|
+
name = self.selector(backend_name)
|
|
206
|
+
|
|
207
|
+
if name == "numpy":
|
|
208
|
+
return NumpyMethods(self.beam)
|
|
209
|
+
|
|
210
|
+
elif name == "numba" and NumbaMethods:
|
|
211
|
+
return NumbaMethods(self.beam)
|
|
212
|
+
|
|
213
|
+
elif name == "cupy32" and CupyMethods:
|
|
214
|
+
return CupyMethods(self.beam, use_single_precision=True)
|
|
215
|
+
|
|
216
|
+
elif name == "cupy64" and CupyMethods:
|
|
217
|
+
return CupyMethods(self.beam, use_single_precision=False)
|
|
218
|
+
|
|
219
|
+
raise RuntimeError(f"Backend '{name}' could not be constructed.")
|
|
220
|
+
|
|
221
|
+
def _wrap_results(self, E: np.ndarray, D: Optional[tuple], B: Optional[np.ndarray]) -> FieldResult:
|
|
222
|
+
"""Helper to package raw backend output into FieldResult."""
|
|
223
|
+
E.flags.writeable = False
|
|
224
|
+
if B is not None: B.flags.writeable = False
|
|
225
|
+
|
|
226
|
+
jacobian_E = None
|
|
227
|
+
if D is not None and all(d is not None for d in D):
|
|
228
|
+
jacobian_E = np.stack(D, axis=1)
|
|
229
|
+
jacobian_E.flags.writeable = False
|
|
230
|
+
|
|
231
|
+
return FieldResult(E=E, _B=B, _jacobian_E=jacobian_E)
|
|
232
|
+
|
|
233
|
+
def compute_on_op(
|
|
234
|
+
self, z: float = 0.0, t: float = 0.0,
|
|
235
|
+
need_b: bool = True, need_derivs: bool = True,
|
|
236
|
+
backend_name: Optional[str] = None, progress_bar: bool = False
|
|
237
|
+
) -> FieldResult:
|
|
238
|
+
"""
|
|
239
|
+
Computes the electromagnetic field arrays on the Observation Plane (OP).
|
|
240
|
+
|
|
241
|
+
The observation plane dimensions and resolution are determined by the
|
|
242
|
+
`Config.op` settings passed during initialization.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
z : float
|
|
247
|
+
The longitudinal z-coordinate of the observation plane.
|
|
248
|
+
t : float
|
|
249
|
+
The time step for the field evaluation.
|
|
250
|
+
need_b : bool
|
|
251
|
+
If True, calculates and returns the Magnetic field (B).
|
|
252
|
+
need_derivs : bool
|
|
253
|
+
If True, calculates and returns spatial derivatives (Jacobian).
|
|
254
|
+
backend_name : str, optional
|
|
255
|
+
Override the default backend for this specific call.
|
|
256
|
+
progress_bar : bool, optional
|
|
257
|
+
Provides a tqdm progress bar if available.
|
|
258
|
+
|
|
259
|
+
Returns
|
|
260
|
+
-------
|
|
261
|
+
FieldResult
|
|
262
|
+
Object containing E (3, Ny, Nx) and optionally B and Jacobian.
|
|
263
|
+
"""
|
|
264
|
+
if np.ndim(z) != 0 or np.ndim(t) != 0:
|
|
265
|
+
raise ValueError("compute_on_op requires 'z' and 't' to be scalars.")
|
|
266
|
+
backend = self.get_backend(backend_name)
|
|
267
|
+
|
|
268
|
+
if self.config.verbose:
|
|
269
|
+
print(f"OP Grid: {len(self.x)}x{len(self.y)} points | Spacing: {self.config.op.spacing} | Z: {z}")
|
|
270
|
+
t0 = time.time()
|
|
271
|
+
|
|
272
|
+
callback = None
|
|
273
|
+
if progress_bar:
|
|
274
|
+
pbar = tqdm(total=len(self.y), desc="OP Grid", unit="rows")
|
|
275
|
+
callback = lambda n: pbar.update(n)
|
|
276
|
+
|
|
277
|
+
E, D, B = backend.compute_grid(
|
|
278
|
+
self.x, self.y, z, t,
|
|
279
|
+
need_b=need_b, need_derivs=need_derivs,
|
|
280
|
+
progress_callback=callback,
|
|
281
|
+
)
|
|
282
|
+
if progress_bar:
|
|
283
|
+
pbar.close()
|
|
284
|
+
|
|
285
|
+
if self.config.verbose:
|
|
286
|
+
print(f"OP Computation complete in {time.time() - t0:.4f}s")
|
|
287
|
+
|
|
288
|
+
return self._wrap_results(E, D, B)
|
|
289
|
+
|
|
290
|
+
def compute_cloud(
|
|
291
|
+
self, x_arr: np.ndarray, y_arr: np.ndarray, z_arr: np.ndarray, t: float = 0.0,
|
|
292
|
+
need_b: bool = True, need_derivs: bool = True, backend_name: Optional[str] = None,
|
|
293
|
+
progress_bar: bool = False
|
|
294
|
+
|
|
295
|
+
) -> FieldResult:
|
|
296
|
+
"""
|
|
297
|
+
Field calculator for N arbitrary points (cloud).
|
|
298
|
+
|
|
299
|
+
Parameters
|
|
300
|
+
----------
|
|
301
|
+
x_arr, y_arr, z_arr : np.ndarray
|
|
302
|
+
1D arrays of shape (N,) specifying evaluation points.
|
|
303
|
+
t : float
|
|
304
|
+
Time coordinate.
|
|
305
|
+
need_b : bool
|
|
306
|
+
If True, includes Magnetic field.
|
|
307
|
+
need_derivs : bool
|
|
308
|
+
If True, includes spatial derivatives.
|
|
309
|
+
progress_bar : bool, optional
|
|
310
|
+
Provides a tqdm progress bar if available.
|
|
311
|
+
|
|
312
|
+
Returns
|
|
313
|
+
-------
|
|
314
|
+
FieldResult
|
|
315
|
+
Object containing E (3, N) and optional fields.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
x_arr = np.atleast_1d(x_arr)
|
|
319
|
+
y_arr = np.atleast_1d(y_arr)
|
|
320
|
+
z_arr = np.atleast_1d(z_arr)
|
|
321
|
+
|
|
322
|
+
if x_arr.ndim != 1 or y_arr.ndim != 1 or z_arr.ndim != 1:
|
|
323
|
+
raise ValueError(
|
|
324
|
+
f"compute_cloud requires 1D arrays. Got ndims: "
|
|
325
|
+
f"x:{x_arr.ndim}, y:{y_arr.ndim}, z:{z_arr.ndim}"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if not (len(x_arr) == len(y_arr) == len(z_arr)):
|
|
329
|
+
raise ValueError(
|
|
330
|
+
f"compute_cloud requires arrays of identical length. "
|
|
331
|
+
f"Got lengths - x:{len(x_arr)}, y:{len(y_arr)}, z:{len(z_arr)}"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if np.ndim(t) != 0:
|
|
335
|
+
raise ValueError(f"Time 't' must be a scalar, got ndim={np.ndim(t)}")
|
|
336
|
+
|
|
337
|
+
callback = None
|
|
338
|
+
if progress_bar:
|
|
339
|
+
total = len(x_arr)
|
|
340
|
+
pbar = tqdm(total=total, desc="Point Cloud", unit="pts")
|
|
341
|
+
callback = lambda n: pbar.update(n)
|
|
342
|
+
|
|
343
|
+
backend = self.get_backend(backend_name)
|
|
344
|
+
|
|
345
|
+
E,D,B = backend.compute_cloud(
|
|
346
|
+
x_arr, y_arr, z_arr, t,
|
|
347
|
+
need_b=need_b, need_derivs=need_derivs,
|
|
348
|
+
progress_callback=callback,
|
|
349
|
+
)
|
|
350
|
+
if progress_bar:
|
|
351
|
+
pbar.close()
|
|
352
|
+
|
|
353
|
+
return self._wrap_results(E,D,B)
|
|
354
|
+
|
|
355
|
+
def compute_point(
|
|
356
|
+
self, x: float, y: float, z: float, t: float = 0.0,
|
|
357
|
+
need_b: bool = True, need_derivs: bool = True, backend_name: Optional[str] = None,
|
|
358
|
+
) -> FieldResult:
|
|
359
|
+
"""
|
|
360
|
+
Compute fields at a single exact spacetime point.
|
|
361
|
+
|
|
362
|
+
Returns
|
|
363
|
+
-------
|
|
364
|
+
FieldResult
|
|
365
|
+
Object containing E (3,) and optional fields.
|
|
366
|
+
"""
|
|
367
|
+
if np.ndim(x) != 0 or np.ndim(y) != 0 or np.ndim(z) != 0 or np.ndim(t) != 0:
|
|
368
|
+
raise ValueError(
|
|
369
|
+
"compute_point requires pure scalars for x, y, z, and t. "
|
|
370
|
+
"If you want to compute multiple points, use compute_cloud."
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
backend = self.get_backend(backend_name)
|
|
374
|
+
|
|
375
|
+
E,D,B = backend.compute_point(
|
|
376
|
+
x, y, z, t, need_b=need_b,
|
|
377
|
+
need_derivs=need_derivs
|
|
378
|
+
)
|
|
379
|
+
return self._wrap_results(E,D,B)
|
vectorwaves/py.typed
ADDED
|
File without changes
|