witwin-maxwell 0.3.0__py3-none-win_amd64.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.
Files changed (107) hide show
  1. witwin/maxwell/__init__.py +164 -0
  2. witwin/maxwell/adapters/__init__.py +1 -0
  3. witwin/maxwell/adapters/gds.py +203 -0
  4. witwin/maxwell/adapters/tidy3d.py +1669 -0
  5. witwin/maxwell/adjoint_inputs.py +70 -0
  6. witwin/maxwell/compiler/__init__.py +11 -0
  7. witwin/maxwell/compiler/materials.py +1355 -0
  8. witwin/maxwell/compiler/monitors.py +164 -0
  9. witwin/maxwell/compiler/sources.py +222 -0
  10. witwin/maxwell/fdfd/__init__.py +3 -0
  11. witwin/maxwell/fdfd/adjoint/__init__.py +3 -0
  12. witwin/maxwell/fdfd/adjoint/bridge.py +180 -0
  13. witwin/maxwell/fdfd/ams.py +300 -0
  14. witwin/maxwell/fdfd/krylov.py +280 -0
  15. witwin/maxwell/fdfd/plotting.py +110 -0
  16. witwin/maxwell/fdfd/postprocess.py +33 -0
  17. witwin/maxwell/fdfd/solver.py +1057 -0
  18. witwin/maxwell/fdtd/__init__.py +4 -0
  19. witwin/maxwell/fdtd/adjoint/__init__.py +21 -0
  20. witwin/maxwell/fdtd/adjoint/bridge.py +746 -0
  21. witwin/maxwell/fdtd/adjoint/capabilities.py +29 -0
  22. witwin/maxwell/fdtd/adjoint/core.py +3614 -0
  23. witwin/maxwell/fdtd/adjoint/dispatch.py +684 -0
  24. witwin/maxwell/fdtd/adjoint/native.py +2561 -0
  25. witwin/maxwell/fdtd/adjoint/profiler.py +195 -0
  26. witwin/maxwell/fdtd/adjoint/reverse_common.py +207 -0
  27. witwin/maxwell/fdtd/adjoint/seeds.py +524 -0
  28. witwin/maxwell/fdtd/boundary/__init__.py +37 -0
  29. witwin/maxwell/fdtd/boundary/bloch.py +90 -0
  30. witwin/maxwell/fdtd/boundary/common.py +41 -0
  31. witwin/maxwell/fdtd/boundary/cpml.py +527 -0
  32. witwin/maxwell/fdtd/boundary/runtime.py +209 -0
  33. witwin/maxwell/fdtd/checkpoint.py +227 -0
  34. witwin/maxwell/fdtd/coords.py +32 -0
  35. witwin/maxwell/fdtd/cuda/__init__.py +11 -0
  36. witwin/maxwell/fdtd/cuda/backend.py +3532 -0
  37. witwin/maxwell/fdtd/cuda/build.py +292 -0
  38. witwin/maxwell/fdtd/cuda/extension.cpp +2053 -0
  39. witwin/maxwell/fdtd/cuda/kernels/adjoint.cu +5290 -0
  40. witwin/maxwell/fdtd/cuda/kernels/aniso.cu +1704 -0
  41. witwin/maxwell/fdtd/cuda/kernels/boundary.cu +424 -0
  42. witwin/maxwell/fdtd/cuda/kernels/common.cu +74 -0
  43. witwin/maxwell/fdtd/cuda/kernels/common.cuh +82 -0
  44. witwin/maxwell/fdtd/cuda/kernels/dispersive.cu +782 -0
  45. witwin/maxwell/fdtd/cuda/kernels/electric.cu +3613 -0
  46. witwin/maxwell/fdtd/cuda/kernels/magnetic.cu +781 -0
  47. witwin/maxwell/fdtd/cuda/kernels/observers.cu +347 -0
  48. witwin/maxwell/fdtd/cuda/kernels/projection.cu +211 -0
  49. witwin/maxwell/fdtd/cuda/kernels/sources.cu +1482 -0
  50. witwin/maxwell/fdtd/cuda/kernels/spectral.cu +297 -0
  51. witwin/maxwell/fdtd/cuda/launch.h +23 -0
  52. witwin/maxwell/fdtd/cuda/prebuilt/witwin_maxwell_fdtd_cuda.pyd +0 -0
  53. witwin/maxwell/fdtd/cuda/runtime/__init__.py +6 -0
  54. witwin/maxwell/fdtd/cuda/runtime/dispatcher.py +6 -0
  55. witwin/maxwell/fdtd/cuda/runtime/graph.py +33 -0
  56. witwin/maxwell/fdtd/cuda/tensors.h +35 -0
  57. witwin/maxwell/fdtd/dispersion.py +48 -0
  58. witwin/maxwell/fdtd/evaluation.py +290 -0
  59. witwin/maxwell/fdtd/excitation/__init__.py +27 -0
  60. witwin/maxwell/fdtd/excitation/injection.py +1102 -0
  61. witwin/maxwell/fdtd/excitation/modes.py +1675 -0
  62. witwin/maxwell/fdtd/excitation/spatial.py +469 -0
  63. witwin/maxwell/fdtd/excitation/temporal.py +510 -0
  64. witwin/maxwell/fdtd/excitation/tfsf_apply.py +264 -0
  65. witwin/maxwell/fdtd/excitation/tfsf_common.py +421 -0
  66. witwin/maxwell/fdtd/excitation/tfsf_specs.py +490 -0
  67. witwin/maxwell/fdtd/excitation/tfsf_state.py +778 -0
  68. witwin/maxwell/fdtd/io.py +31 -0
  69. witwin/maxwell/fdtd/material_pullback.py +371 -0
  70. witwin/maxwell/fdtd/meshing/__init__.py +3 -0
  71. witwin/maxwell/fdtd/meshing/autogrid.py +335 -0
  72. witwin/maxwell/fdtd/observers.py +1453 -0
  73. witwin/maxwell/fdtd/plotting.py +121 -0
  74. witwin/maxwell/fdtd/postprocess.py +205 -0
  75. witwin/maxwell/fdtd/runtime/__init__.py +119 -0
  76. witwin/maxwell/fdtd/runtime/initialization.py +236 -0
  77. witwin/maxwell/fdtd/runtime/materials.py +1563 -0
  78. witwin/maxwell/fdtd/runtime/module_cache.py +31 -0
  79. witwin/maxwell/fdtd/runtime/spectral.py +581 -0
  80. witwin/maxwell/fdtd/runtime/stepping.py +1987 -0
  81. witwin/maxwell/fdtd/solver.py +502 -0
  82. witwin/maxwell/geometry/__init__.py +5 -0
  83. witwin/maxwell/geometry/polyslab.py +299 -0
  84. witwin/maxwell/materials.py +3 -0
  85. witwin/maxwell/media.py +1467 -0
  86. witwin/maxwell/monitors.py +743 -0
  87. witwin/maxwell/ports.py +113 -0
  88. witwin/maxwell/postprocess/RCS.py +139 -0
  89. witwin/maxwell/postprocess/__init__.py +42 -0
  90. witwin/maxwell/postprocess/diffraction.py +219 -0
  91. witwin/maxwell/postprocess/directivity.py +152 -0
  92. witwin/maxwell/postprocess/emission.py +67 -0
  93. witwin/maxwell/postprocess/modal.py +324 -0
  94. witwin/maxwell/postprocess/nfft.py +266 -0
  95. witwin/maxwell/postprocess/scattering_parameters.py +358 -0
  96. witwin/maxwell/postprocess/stratton_chu.py +1193 -0
  97. witwin/maxwell/result.py +967 -0
  98. witwin/maxwell/scene.py +1338 -0
  99. witwin/maxwell/simulation.py +659 -0
  100. witwin/maxwell/sources.py +917 -0
  101. witwin/maxwell/visualization/__init__.py +28 -0
  102. witwin/maxwell/visualization/interactive.py +484 -0
  103. witwin/maxwell/visualization/plots.py +568 -0
  104. witwin_maxwell-0.3.0.dist-info/METADATA +184 -0
  105. witwin_maxwell-0.3.0.dist-info/RECORD +107 -0
  106. witwin_maxwell-0.3.0.dist-info/WHEEL +4 -0
  107. witwin_maxwell-0.3.0.dist-info/licenses/LICENSE +13 -0
@@ -0,0 +1,299 @@
1
+ """Maxwell-specific extruded polygon geometries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import torch
7
+
8
+ from witwin.core import GeometryBase
9
+ from witwin.core.geometry.polygon import polygon_loops_signed_distance_2d
10
+
11
+
12
+ def _as_scalar(value, *, device=None) -> torch.Tensor:
13
+ if isinstance(value, torch.Tensor):
14
+ return value.to(device=device, dtype=torch.float32).reshape(())
15
+ return torch.tensor(float(value), dtype=torch.float32, device=device)
16
+
17
+
18
+ def _axial_split(dx, dy, dz, axis: str):
19
+ if axis == "z":
20
+ return dz, dx, dy
21
+ if axis == "y":
22
+ return dy, dx, dz
23
+ return dx, dy, dz
24
+
25
+
26
+ def _constant_tensor(data, *, device, dtype=torch.float32) -> torch.Tensor:
27
+ return torch.tensor(data, dtype=dtype, device=device)
28
+
29
+
30
+ def _faces_tensor(data, *, device) -> torch.Tensor:
31
+ return _constant_tensor(data, device=device, dtype=torch.int64)
32
+
33
+
34
+ def _as_polygon_vertices(value, *, device=None) -> torch.Tensor:
35
+ if isinstance(value, torch.Tensor):
36
+ vertices = value.to(device=device, dtype=torch.float32)
37
+ else:
38
+ vertices = torch.tensor(
39
+ [[float(u), float(v)] for u, v in value], dtype=torch.float32, device=device
40
+ )
41
+ if vertices.ndim != 2 or vertices.shape[0] < 3 or vertices.shape[1] != 2:
42
+ raise ValueError("vertices must have shape (N, 2) with N >= 3.")
43
+ return vertices
44
+
45
+
46
+ def _as_axis_bounds(value, *, device=None) -> torch.Tensor:
47
+ if isinstance(value, torch.Tensor):
48
+ bounds = value.to(device=device, dtype=torch.float32)
49
+ else:
50
+ bounds = torch.tensor(
51
+ [float(value[0]), float(value[1])], dtype=torch.float32, device=device
52
+ )
53
+ if bounds.shape != (2,):
54
+ raise ValueError("bounds must contain (min, max) along the extrusion axis.")
55
+ return bounds
56
+
57
+
58
+ def _cross_2d_scalar(origin, first, second) -> float:
59
+ return float(
60
+ (first[0] - origin[0]) * (second[1] - origin[1])
61
+ - (first[1] - origin[1]) * (second[0] - origin[0])
62
+ )
63
+
64
+
65
+ def _point_in_triangle_2d(point, a, b, c, eps=1.0e-12) -> bool:
66
+ """Inclusive point-in-triangle test for a CCW triangle (a, b, c)."""
67
+ return (
68
+ _cross_2d_scalar(a, b, point) >= -eps
69
+ and _cross_2d_scalar(b, c, point) >= -eps
70
+ and _cross_2d_scalar(c, a, point) >= -eps
71
+ )
72
+
73
+
74
+ def _ear_clip_faces_2d(polygon: np.ndarray) -> list[list[int]]:
75
+ """Triangulate a simple CCW (N, 2) polygon into CCW index triangles via ear clipping."""
76
+ remaining = list(range(polygon.shape[0]))
77
+ faces: list[list[int]] = []
78
+ while len(remaining) > 3:
79
+ clipped = False
80
+ for slot in range(len(remaining)):
81
+ i0 = remaining[slot - 1]
82
+ i1 = remaining[slot]
83
+ i2 = remaining[(slot + 1) % len(remaining)]
84
+ a, b, c = polygon[i0], polygon[i1], polygon[i2]
85
+ if _cross_2d_scalar(a, b, c) <= 1.0e-12:
86
+ continue
87
+ others = (polygon[j] for j in remaining if j not in (i0, i1, i2))
88
+ if any(_point_in_triangle_2d(p, a, b, c) for p in others):
89
+ continue
90
+ faces.append([i0, i1, i2])
91
+ remaining.pop(slot)
92
+ clipped = True
93
+ break
94
+ if not clipped:
95
+ raise ValueError(
96
+ "Failed to triangulate polygon; vertices must describe a simple polygon."
97
+ )
98
+ faces.append(list(remaining))
99
+ return faces
100
+
101
+
102
+ def _offset_polygon_2d(polygon: np.ndarray, offset: float) -> np.ndarray:
103
+ """Approximate outward miter offset of a CCW (N, 2) polygon (negative offset shrinks)."""
104
+ if offset == 0.0:
105
+ return polygon
106
+ previous_edges = polygon - np.roll(polygon, 1, axis=0)
107
+ next_edges = np.roll(polygon, -1, axis=0) - polygon
108
+
109
+ def _outward_normals(edges: np.ndarray) -> np.ndarray:
110
+ normals = np.stack([edges[:, 1], -edges[:, 0]], axis=1)
111
+ return normals / np.maximum(
112
+ np.linalg.norm(normals, axis=1, keepdims=True), 1.0e-12
113
+ )
114
+
115
+ normal_previous = _outward_normals(previous_edges)
116
+ normal_next = _outward_normals(next_edges)
117
+ denom = np.maximum(
118
+ 1.0 + np.sum(normal_previous * normal_next, axis=1, keepdims=True), 1.0e-3
119
+ )
120
+ return polygon + offset * (normal_previous + normal_next) / denom
121
+
122
+
123
+ class PolySlab(GeometryBase):
124
+ """Extruded 2D polygon with optional linear sidewall taper (Tidy3D semantics).
125
+
126
+ ``vertices`` is the (N, 2) cross-section in the plane perpendicular to ``axis``
127
+ (plane coordinates follow ``_axial_split``: axis z -> (x, y), y -> (x, z),
128
+ x -> (y, z)), ``bounds`` is (min, max) along the axis, and a positive
129
+ ``sidewall_angle`` (radians) shrinks the cross-section as the axis coordinate
130
+ increases. ``reference_plane`` selects the axis position where the drawn
131
+ polygon is exact. The interior uses the even-odd rule, so non-convex polygons
132
+ are supported and vertex winding is irrelevant.
133
+ """
134
+
135
+ kind = "poly_slab"
136
+
137
+ def __init__(
138
+ self,
139
+ vertices,
140
+ bounds,
141
+ axis="z",
142
+ sidewall_angle=0.0,
143
+ reference_plane="middle",
144
+ position=(0, 0, 0),
145
+ rotation=None,
146
+ *,
147
+ device=None,
148
+ ):
149
+ super().__init__(position=position, rotation=rotation, device=device)
150
+ self.vertices: torch.Tensor = _as_polygon_vertices(vertices, device=device)
151
+ self.bounds: torch.Tensor = _as_axis_bounds(bounds, device=device)
152
+ if isinstance(axis, int) and not isinstance(axis, bool):
153
+ if axis not in (0, 1, 2):
154
+ raise ValueError("integer axis must be 0, 1, or 2.")
155
+ axis = ("x", "y", "z")[axis]
156
+ self.axis: str = self._validate_axis(axis)
157
+ self.sidewall_angle: torch.Tensor = _as_scalar(sidewall_angle, device=device)
158
+ if reference_plane not in ("bottom", "middle", "top"):
159
+ raise ValueError("reference_plane must be 'bottom', 'middle', or 'top'.")
160
+ self.reference_plane: str = reference_plane
161
+ self.loop_sizes: tuple[int, ...] = (int(self.vertices.shape[0]),)
162
+
163
+ def _reference_position(self, bounds: torch.Tensor) -> torch.Tensor:
164
+ if self.reference_plane == "bottom":
165
+ return bounds[0]
166
+ if self.reference_plane == "top":
167
+ return bounds[1]
168
+ return 0.5 * (bounds[0] + bounds[1])
169
+
170
+ def signed_distance(self, xx, yy, zz):
171
+ dx, dy, dz = self._local_coords(xx, yy, zz)
172
+ axial, pu, pv = _axial_split(dx, dy, dz, self.axis)
173
+ vertices = self.vertices.to(device=pu.device, dtype=pu.dtype)
174
+ loops = torch.split(vertices, list(self.loop_sizes), dim=0)
175
+ points = torch.stack([pu, pv], dim=-1).reshape(-1, 2)
176
+ polygon_distance = polygon_loops_signed_distance_2d(points, loops).reshape(
177
+ pu.shape
178
+ )
179
+ bounds = self.bounds.to(device=axial.device, dtype=axial.dtype)
180
+ tan_angle = torch.tan(
181
+ self.sidewall_angle.to(device=axial.device, dtype=axial.dtype)
182
+ )
183
+ # Signed distance minus an offset is exact morphological erosion/dilation, so
184
+ # the taper is a z-dependent inward offset of the cross-section.
185
+ tapered_distance = polygon_distance + tan_angle * (
186
+ axial - self._reference_position(bounds)
187
+ )
188
+ axial_distance = torch.abs(axial - 0.5 * (bounds[0] + bounds[1])) - 0.5 * (
189
+ bounds[1] - bounds[0]
190
+ )
191
+ outside = torch.sqrt(
192
+ torch.clamp(tapered_distance, min=0.0) ** 2
193
+ + torch.clamp(axial_distance, min=0.0) ** 2
194
+ )
195
+ inside = torch.clamp(torch.maximum(tapered_distance, axial_distance), max=0.0)
196
+ return outside + inside
197
+
198
+ def to_mesh(self, segments=16):
199
+ del segments
200
+ device = self.device
201
+ polygon = self.vertices.detach().cpu().to(torch.float64).numpy()
202
+ # Drop consecutive duplicate vertices (including a closed-ring wrap-around
203
+ # repeat); coincident points would block every candidate ear below.
204
+ keep = np.linalg.norm(polygon - np.roll(polygon, 1, axis=0), axis=1) > 1.0e-12
205
+ polygon = polygon[keep]
206
+ if polygon.shape[0] < 3:
207
+ raise ValueError("vertices must contain at least 3 distinct points.")
208
+ signed_area = 0.5 * float(
209
+ np.sum(
210
+ polygon[:, 0] * np.roll(polygon[:, 1], -1)
211
+ - np.roll(polygon[:, 0], -1) * polygon[:, 1]
212
+ )
213
+ )
214
+ if signed_area < 0.0:
215
+ polygon = polygon[::-1].copy()
216
+ bounds = self.bounds.detach().cpu().to(torch.float64).numpy()
217
+ reference = {
218
+ "bottom": bounds[0],
219
+ "top": bounds[1],
220
+ "middle": 0.5 * (bounds[0] + bounds[1]),
221
+ }[self.reference_plane]
222
+ tan_angle = float(
223
+ np.tan(self.sidewall_angle.detach().cpu().to(torch.float64).item())
224
+ )
225
+ bottom_ring = _offset_polygon_2d(polygon, -tan_angle * (bounds[0] - reference))
226
+ top_ring = _offset_polygon_2d(polygon, -tan_angle * (bounds[1] - reference))
227
+ count = polygon.shape[0]
228
+ bottom = np.concatenate([bottom_ring, np.full((count, 1), bounds[0])], axis=1)
229
+ top = np.concatenate([top_ring, np.full((count, 1), bounds[1])], axis=1)
230
+ vertices = _constant_tensor(
231
+ np.concatenate([bottom, top], axis=0), device=device
232
+ )
233
+ faces = []
234
+ for index in range(count):
235
+ next_index = (index + 1) % count
236
+ faces.extend(
237
+ [
238
+ [index, next_index, count + next_index],
239
+ [index, count + next_index, count + index],
240
+ ]
241
+ )
242
+ for i0, i1, i2 in _ear_clip_faces_2d(polygon):
243
+ faces.append([i0, i2, i1])
244
+ faces.append([count + i0, count + i1, count + i2])
245
+ # Map local (u, v, axial) columns to world axes following _axial_split
246
+ # (axis z -> (x, y), y -> (x, z), x -> (y, z)); _remap_axis_torch would
247
+ # mirror u/v for axis "x" and disagree with signed_distance/to_mask.
248
+ vertices = vertices[
249
+ :, {"z": [0, 1, 2], "y": [0, 2, 1], "x": [2, 0, 1]}[self.axis]
250
+ ]
251
+ faces_tensor = _faces_tensor(faces, device=device)
252
+ if self.axis == "y":
253
+ # Odd column permutation reflects orientation; flip winding to keep
254
+ # face normals outward.
255
+ faces_tensor = torch.flip(faces_tensor, dims=(1,))
256
+ vertices = self._transform_mesh_verts(vertices)
257
+ return vertices, faces_tensor
258
+
259
+
260
+ class ComplexPolySlab(PolySlab):
261
+ """PolySlab variant for non-simple cross-sections.
262
+
263
+ Accepts a list of vertex loops; self-intersecting loops (e.g. bowties) and
264
+ multiple loops are allowed. The interior is the even-odd rule over the combined
265
+ crossing count of all loops, so loop winding is irrelevant and extra loops carve
266
+ holes. The distance magnitude is the minimum distance over all edges of all loops.
267
+ """
268
+
269
+ kind = "complex_poly_slab"
270
+
271
+ def __init__(
272
+ self,
273
+ loops,
274
+ bounds,
275
+ axis="z",
276
+ sidewall_angle=0.0,
277
+ reference_plane="middle",
278
+ position=(0, 0, 0),
279
+ rotation=None,
280
+ *,
281
+ device=None,
282
+ ):
283
+ loop_tensors = [_as_polygon_vertices(loop, device=device) for loop in loops]
284
+ if not loop_tensors:
285
+ raise ValueError("loops must contain at least one polygon loop.")
286
+ super().__init__(
287
+ torch.cat(loop_tensors, dim=0),
288
+ bounds,
289
+ axis=axis,
290
+ sidewall_angle=sidewall_angle,
291
+ reference_plane=reference_plane,
292
+ position=position,
293
+ rotation=rotation,
294
+ device=device,
295
+ )
296
+ self.loop_sizes = tuple(int(loop.shape[0]) for loop in loop_tensors)
297
+
298
+ def to_mesh(self, segments=16):
299
+ raise NotImplementedError("ComplexPolySlab does not support mesh export.")
@@ -0,0 +1,3 @@
1
+ from .compiler.materials import compile_material_tensors
2
+
3
+ __all__ = ["compile_material_tensors"]