@@ -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}")
0 commit comments