Skip to content

Commit 505416e

Browse files
authored
Merge pull request #37 from QuentinWach/bench
Fixing Mode Source in 3D & Example
2 parents f3a8380 + bc8275e commit 505416e

File tree

13 files changed

+1263
-544
lines changed

13 files changed

+1263
-544
lines changed

beamz/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from beamz.design.materials import Material, CustomMaterial
1414
from beamz.design.structures import (
1515
Rectangle, Circle, Ring,
16-
CircularBend, Polygon, Taper
16+
CircularBend, Polygon, Taper, Sphere
1717
)
1818
from beamz.devices.sources import ModeSource, GaussianSource
1919
from beamz.devices.monitors import Monitor
@@ -64,6 +64,7 @@
6464
'CircularBend': CircularBend,
6565
'Polygon': Polygon,
6666
'Taper': Taper,
67+
'Sphere': Sphere,
6768

6869
# Sources
6970
'ModeSource': ModeSource,

beamz/design/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from shapely.ops import unary_union
55
from beamz.const import µm
66
from beamz.design.materials import Material
7-
from beamz.design.structures import Polygon, Rectangle, Circle, Ring, CircularBend, Taper
7+
from beamz.design.structures import Polygon, Rectangle, Circle, Ring, CircularBend, Taper, Sphere
88

99

1010
class Design:

beamz/design/meshing.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -716,20 +716,25 @@ def __rasterize_3d__(self):
716716

717717
def _process_3d_pml(self, permittivity, permeability, conductivity,
718718
x_centers, y_centers, z_centers, dt_estimate):
719-
"""Process 3D PML boundaries."""
719+
"""Process 3D PML boundaries and add conductivity to the grid."""
720720
if not hasattr(self.design, 'boundaries') or not self.design.boundaries: return
721721

722722
with create_rich_progress() as progress:
723723
task = progress.add_task("Processing 3D PML boundaries...", total=len(self.design.boundaries))
724724

725725
for boundary in self.design.boundaries:
726-
# For now, apply 2D PML to all z-layers (can be enhanced for true 3D PML)
726+
# Add PML conductivity to the global 3D conductivity grid for all 6 faces
727727
for k, z in enumerate(z_centers):
728728
for i, y in enumerate(y_centers):
729729
for j, x in enumerate(x_centers):
730-
# Add PML conductivity
731-
pml_conductivity = boundary.get_conductivity(x, y, dx=self.resolution_xy, dt=dt_estimate,
732-
eps_avg=permittivity[k, i, j])
730+
# Calculate PML conductivity at this point (x,y,z)
731+
pml_conductivity = boundary.get_conductivity(x, y, z,
732+
dx=self.resolution_xy,
733+
dt=dt_estimate,
734+
eps_avg=permittivity[k, i, j],
735+
width=self.design.width,
736+
height=self.design.height,
737+
depth=self.design.depth)
733738
if pml_conductivity > 0: conductivity[k, i, j] += pml_conductivity
734739

735740
progress.update(task, advance=1)

beamz/design/structures.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,15 +204,13 @@ def point_in_polygon(self, x, y, z=None):
204204
if z is not None and hasattr(self, 'depth') and self.depth > 0:
205205
if not (self.z <= z <= self.z + self.depth):
206206
return False
207-
207+
208208
exterior_path = self.vertices
209209
interior_paths = self.interiors
210210
if not exterior_path: return False
211-
if not self._point_in_polygon_single_path(x, y, exterior_path):
212-
return False
211+
if not self._point_in_polygon_single_path(x, y, exterior_path): return False
213212
for interior_path_pts in interior_paths:
214-
if interior_path_pts and self._point_in_polygon_single_path(x, y, interior_path_pts):
215-
return False
213+
if interior_path_pts and self._point_in_polygon_single_path(x, y, interior_path_pts): return False
216214
return True
217215

218216
class Rectangle(Polygon):
@@ -460,4 +458,24 @@ def copy(self):
460458
new_taper = Taper(self.position, self.input_width, self.output_width,
461459
self.length, self.material, self.color, self.optimize, self.depth, self.z)
462460
new_taper.vertices = [(x, y, z) for x, y, z in self.vertices]
463-
return new_taper
461+
return new_taper
462+
463+
class Sphere(Polygon):
464+
def __init__(self, position=(0,0,0), radius=1, material=None, color=None, optimize=False):
465+
"""Create a 3D sphere at position (x,y,z) with specified radius."""
466+
if len(position) == 2: position = (position[0], position[1], 0.0)
467+
super().__init__(vertices=[], material=material, color=color, optimize=optimize, depth=2*radius, z=position[2]-radius)
468+
self.position = position
469+
self.radius = radius
470+
471+
def get_bounding_box(self):
472+
x, y, z = self.position
473+
r = self.radius
474+
return (x - r, y - r, z - r, x + r, y + r, z + r)
475+
476+
def point_in_polygon(self, x, y, z=0):
477+
cx, cy, cz = self.position
478+
return (x - cx)**2 + (y - cy)**2 + (z - cz)**2 <= self.radius**2
479+
480+
def copy(self):
481+
return Sphere(self.position, self.radius, self.material, self.color, self.optimize)

beamz/devices/monitors/monitors.py

Lines changed: 75 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -112,19 +112,52 @@ def _init_3d_monitor(self, start, end, plane_normal, plane_position, size):
112112
end = (end[0], end[1], start[2]) # Same z as start
113113
self.end = end
114114

115-
# Calculate plane properties from the two points
116-
self.size = (abs(end[0] - start[0]), abs(end[1] - start[1]))
117-
self.plane_position = start[2] # Use z-coordinate of start point
118-
self.plane_normal = 'z' # Default to xy plane
119-
120-
# Ensure start is the bottom-left corner
121-
self.start = (min(start[0], end[0]), min(start[1], end[1]), start[2])
115+
# Auto-detect plane normal if not explicitly provided
116+
if plane_normal is None:
117+
dx = abs(end[0] - start[0])
118+
dy = abs(end[1] - start[1])
119+
dz = abs(end[2] - start[2])
120+
121+
# The normal is the axis with the smallest (ideally zero) extent
122+
dims = [dx, dy, dz]
123+
min_dim_idx = np.argmin(dims)
124+
if min_dim_idx == 0: self.plane_normal = 'x'
125+
elif min_dim_idx == 1: self.plane_normal = 'y'
126+
else: self.plane_normal = 'z'
127+
else:
128+
self.plane_normal = plane_normal
129+
130+
# Set size and position based on detected/provided normal
131+
if self.plane_normal == 'x':
132+
self.size = (abs(end[1] - start[1]), abs(end[2] - start[2]))
133+
self.plane_position = start[0]
134+
elif self.plane_normal == 'y':
135+
self.size = (abs(end[0] - start[0]), abs(end[2] - start[2]))
136+
self.plane_position = start[1]
137+
else: # z
138+
self.size = (abs(end[0] - start[0]), abs(end[1] - start[1]))
139+
self.plane_position = start[2]
140+
141+
# Ensure start is the bottom-left corner of the ROI
142+
self.start = (min(start[0], end[0]), min(start[1], end[1]), min(start[2], end[2]))
122143

123144
else:
124145
# Monitor defined by plane normal and position (legacy mode)
125146
self.end = None
126147
self.plane_normal = plane_normal or 'z' # Default to xy plane
127-
self.plane_position = plane_position
148+
149+
# Extract plane_position from start if not explicitly provided
150+
if plane_position == 0 and start is not None and len(start) >= 3:
151+
if self.plane_normal == 'z':
152+
self.plane_position = start[2]
153+
elif self.plane_normal == 'y':
154+
self.plane_position = start[1]
155+
elif self.plane_normal == 'x':
156+
self.plane_position = start[0]
157+
else:
158+
self.plane_position = plane_position
159+
else:
160+
self.plane_position = plane_position
128161

129162
# Determine plane dimensions
130163
if size is None:
@@ -221,59 +254,42 @@ def get_grid_points_2d(self, dx, dy):
221254

222255
def get_grid_slice_3d(self, dx, dy, dz, field_shape):
223256
"""Get grid slice for 3D plane monitor.
224-
Assumes simulation arrays are ordered as (z, y, x).
225-
Returns indices in (y_slice, x_slice, fixed_index) order, where fixed_index corresponds to the
226-
axis normal to the plane.
257+
Returns (z_idx, y_idx, x_idx) consistent with simulation array order (z, y, x).
258+
One of these will be an integer, the other two will be slice objects.
227259
"""
228260
# Derive base grid counts from either design or field_shape
229261
if self.design:
230262
base_nx = max(1, int(round((getattr(self.design, 'width', 0.0)) / dx)))
231263
base_ny = max(1, int(round((getattr(self.design, 'height', 0.0)) / dy)))
232264
base_nz = max(1, int(round((getattr(self.design, 'depth', 0.0) or 0.0) / dz)))
233265
else:
234-
# Fallback to field_shape if design is not available
235266
base_nz, base_ny, base_nx = field_shape
236267

237268
if self.plane_normal == 'z':
238269
# xy plane at fixed z
239270
z_idx = int(round(self.plane_position / dz))
240-
z_idx = max(0, min(z_idx, base_nz - 1))
241271
x_start = int(round(self.start[0] / dx))
242272
x_end = int(round((self.start[0] + self.size[0]) / dx))
243273
y_start = int(round(self.start[1] / dy))
244274
y_end = int(round((self.start[1] + self.size[1]) / dy))
245-
x_start = max(0, min(x_start, base_nx - 1))
246-
x_end = max(0, min(x_end, base_nx))
247-
y_start = max(0, min(y_start, base_ny - 1))
248-
y_end = max(0, min(y_end, base_ny))
249-
return slice(y_start, y_end), slice(x_start, x_end), z_idx
275+
return z_idx, slice(y_start, y_end), slice(x_start, x_end)
250276

251277
elif self.plane_normal == 'y':
252278
# xz plane at fixed y
253279
y_idx = int(round(self.plane_position / dy))
254-
y_idx = max(0, min(y_idx, base_ny - 1))
255280
x_start = int(round(self.start[0] / dx))
256281
x_end = int(round((self.start[0] + self.size[0]) / dx))
257282
z_start = int(round(self.start[2] / dz))
258283
z_end = int(round((self.start[2] + self.size[1]) / dz))
259-
x_start = max(0, min(x_start, base_nx - 1))
260-
x_end = max(0, min(x_end, base_nx))
261-
z_start = max(0, min(z_start, base_nz - 1))
262-
z_end = max(0, min(z_end, base_nz))
263-
return y_idx, slice(x_start, x_end), slice(z_start, z_end)
284+
return slice(z_start, z_end), y_idx, slice(x_start, x_end)
264285
else: # x normal
265286
# yz plane at fixed x
266287
x_idx = int(round(self.plane_position / dx))
267-
x_idx = max(0, min(x_idx, base_nx - 1))
268288
y_start = int(round(self.start[1] / dy))
269289
y_end = int(round((self.start[1] + self.size[0]) / dy))
270290
z_start = int(round(self.start[2] / dz))
271291
z_end = int(round((self.start[2] + self.size[1]) / dz))
272-
y_start = max(0, min(y_start, base_ny - 1))
273-
y_end = max(0, min(y_end, base_ny))
274-
z_start = max(0, min(z_start, base_nz - 1))
275-
z_end = max(0, min(z_end, base_nz))
276-
return slice(y_start, y_end), x_idx, slice(z_start, z_end)
292+
return slice(z_start, z_end), slice(y_start, y_end), x_idx
277293

278294
def should_record(self, step):
279295
"""Check if this step should be recorded based on interval."""
@@ -320,47 +336,24 @@ def record_fields_2d(self, Ez, Hx, Hy, t, dx, dy, step=0):
320336
def record_fields_3d(self, Ex, Ey, Ez, Hx, Hy, Hz, t, dx, dy, dz, step=0):
321337
"""Record 3D field data from plane slice."""
322338
if not self.should_record(step): return
323-
# Get the appropriate slice for this monitor
324-
field_shape = Ex.shape # (nz, ny, nx or nx-1)
325-
slice_indices = self.get_grid_slice_3d(dx, dy, dz, field_shape)
326-
# Compute effective indices per field to respect individual shapes (z,y,x order)
339+
327340
def slice_field(arr):
328-
# Arrays are ordered (z, y, x)
329-
if isinstance(slice_indices[2], int):
330-
# Plane normal is z: slice fixed z index, take y/x ranges
331-
z_idx = min(max(0, slice_indices[2]), arr.shape[0]-1)
332-
y_start = slice_indices[0].start or 0
333-
y_stop = slice_indices[0].stop or arr.shape[1]
334-
x_start = slice_indices[1].start or 0
335-
x_stop = slice_indices[1].stop or arr.shape[2]
336-
y_start = max(0, min(y_start, arr.shape[1]-1))
337-
y_stop = max(y_start, min(y_stop, arr.shape[1]))
338-
x_start = max(0, min(x_start, arr.shape[2]-1))
339-
x_stop = max(x_start, min(x_stop, arr.shape[2]))
340-
return arr[z_idx, y_start:y_stop, x_start:x_stop].copy()
341-
else:
342-
# Other planes: (y_slice, x_slice, fixed_index) with fixed along y or x
343-
a0, a1, a2 = arr.shape
344-
s0, s1, fixed = slice_indices
345-
if isinstance(fixed, int):
346-
if self.plane_normal == 'y':
347-
# fixed y index
348-
fixed_eff = max(0, min(fixed, a1-1))
349-
x_start = s1.start or 0; x_stop = s1.stop or a2
350-
z_start = s0.start or 0; z_stop = s0.stop or a0
351-
x_start = max(0, min(x_start, a2-1)); x_stop = max(x_start, min(x_stop, a2))
352-
z_start = max(0, min(z_start, a0-1)); z_stop = max(z_start, min(z_stop, a0))
353-
return arr[z_start:z_stop, fixed_eff, x_start:x_stop].copy()
354-
else:
355-
# fixed x index
356-
fixed_eff = max(0, min(fixed, a2-1))
357-
y_start = s0.start or 0; y_stop = s0.stop or a1
358-
z_start = s1.start or 0; z_stop = s1.stop or a0
359-
y_start = max(0, min(y_start, a1-1)); y_stop = max(y_start, min(y_stop, a1))
360-
z_start = max(0, min(z_start, a0-1)); z_stop = max(z_start, min(z_stop, a0))
361-
return arr[z_start:z_stop, y_start:y_stop, fixed_eff].copy()
362-
# Fallback: return a copy
363-
return arr.copy()
341+
# Returns (z_slice, y_slice, x_slice) consistent with (z, y, x) order
342+
z_idx, y_idx, x_idx = self.get_grid_slice_3d(dx, dy, dz, arr.shape)
343+
344+
nz, ny, nx = arr.shape
345+
346+
# Helper to clamp indices to the actual array shape (handles Yee staggering)
347+
def clamp(idx, limit):
348+
if isinstance(idx, int):
349+
return min(max(0, idx), limit - 1)
350+
else:
351+
start = max(0, min(idx.start if idx.start is not None else 0, limit - 1))
352+
stop = max(start, min(idx.stop if idx.stop is not None else limit, limit))
353+
return slice(start, stop)
354+
355+
# Extract and copy the 2D slice
356+
return arr[clamp(z_idx, nz), clamp(y_idx, ny), clamp(x_idx, nx)].copy()
364357

365358
Ex_slice = slice_field(Ex)
366359
Ey_slice = slice_field(Ey)
@@ -369,19 +362,18 @@ def slice_field(arr):
369362
Hy_slice = slice_field(Hy)
370363
Hz_slice = slice_field(Hz)
371364

372-
#print(f"● Monitor shapes: Ex={Ex_slice.shape}, Ey={Ey_slice.shape}, Ez={Ez_slice.shape}, Hx={Hx_slice.shape}, Hy={Hy_slice.shape}, Hz={Hz_slice.shape}")
373-
374-
# Align to common overlapping region to account for Yee staggering
375-
min_y = min(Ex_slice.shape[0], Ey_slice.shape[0], Ez_slice.shape[0],
376-
Hx_slice.shape[0], Hy_slice.shape[0], Hz_slice.shape[0])
377-
min_x = min(Ex_slice.shape[1], Ey_slice.shape[1], Ez_slice.shape[1],
378-
Hx_slice.shape[1], Hy_slice.shape[1], Hz_slice.shape[1])
379-
Ex_slice = Ex_slice[:min_y, :min_x]
380-
Ey_slice = Ey_slice[:min_y, :min_x]
381-
Ez_slice = Ez_slice[:min_y, :min_x]
382-
Hx_slice = Hx_slice[:min_y, :min_x]
383-
Hy_slice = Hy_slice[:min_y, :min_x]
384-
Hz_slice = Hz_slice[:min_y, :min_x]
365+
# Align to common overlapping region to account for Yee staggering differences between components
366+
min_dim0 = min(Ex_slice.shape[0], Ey_slice.shape[0], Ez_slice.shape[0],
367+
Hx_slice.shape[0], Hy_slice.shape[0], Hz_slice.shape[0])
368+
min_dim1 = min(Ex_slice.shape[1], Ey_slice.shape[1], Ez_slice.shape[1],
369+
Hx_slice.shape[1], Hy_slice.shape[1], Hz_slice.shape[1])
370+
371+
Ex_slice = Ex_slice[:min_dim0, :min_dim1]
372+
Ey_slice = Ey_slice[:min_dim0, :min_dim1]
373+
Ez_slice = Ez_slice[:min_dim0, :min_dim1]
374+
Hx_slice = Hx_slice[:min_dim0, :min_dim1]
375+
Hy_slice = Hy_slice[:min_dim0, :min_dim1]
376+
Hz_slice = Hz_slice[:min_dim0, :min_dim1]
385377

386378
# print(f"● Monitor record step {step}: Ez_slice max={np.max(np.abs(Ez_slice)):.2e}")
387379
#print(f"● Monitor record step {step}: Ez_slice max={np.max(np.abs(Ez_slice)):.2e}")

beamz/devices/sources/gaussian.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,44 @@ def _inject_3d(self, fields, t, dt, resolution):
160160

161161
signal_val = self._get_signal_value(t + 0.5 * dt, dt)
162162
eps_region = fields.permittivity[self._grid_indices]
163-
163+
164164
term = self._spatial_profile_ez * signal_val
165165
injection = -term * dt / (EPS_0 * eps_region)
166-
166+
167167
fields.Ez[self._grid_indices] += injection
168+
169+
def add_to_plot(self, ax, facecolor="none", edgecolor="orange", alpha=0.8, linestyle="-"):
170+
"""Add source visualization to 2D matplotlib plot.
171+
172+
Draws a circle at the source position with radius proportional to the width.
173+
"""
174+
from matplotlib.patches import Circle
175+
176+
# Get position (2D or 3D)
177+
if len(self.position) >= 2:
178+
x_pos, y_pos = self.position[0], self.position[1]
179+
else:
180+
x_pos, y_pos = self.position[0], 0
181+
182+
# Draw circle with radius = width (standard deviation)
183+
circle = Circle(
184+
(x_pos, y_pos),
185+
radius=self.width,
186+
facecolor='none',
187+
edgecolor=edgecolor,
188+
linewidth=2,
189+
alpha=alpha,
190+
linestyle=linestyle,
191+
label='GaussianSource'
192+
)
193+
ax.add_patch(circle)
194+
195+
# Draw a small filled circle at center
196+
center_dot = Circle(
197+
(x_pos, y_pos),
198+
radius=self.width * 0.1,
199+
facecolor=edgecolor,
200+
edgecolor='none',
201+
alpha=alpha
202+
)
203+
ax.add_patch(center_dot)

0 commit comments

Comments
 (0)