2
2
3
3
from __future__ import annotations
4
4
5
+ from collections .abc import Iterable
5
6
from enum import Enum
6
7
from math import isclose
7
8
from typing import Any , Optional , Union
8
9
9
10
import numpy as np
10
- import pydantic
11
+ import pydantic .v1 as pydantic
12
+ from shapely .geometry import (
13
+ GeometryCollection ,
14
+ MultiLineString ,
15
+ MultiPoint ,
16
+ MultiPolygon ,
17
+ Polygon ,
18
+ )
19
+ from shapely .geometry .base import BaseGeometry
11
20
12
21
from tidy3d .components .base import Tidy3dBaseModel
13
22
from tidy3d .components .geometry .base import Box
38
47
]
39
48
40
49
50
+ def flatten_shapely_geometries (
51
+ geoms : Union [Shapely , Iterable [Shapely ]], keep_types : tuple [type , ...] = (Polygon ,)
52
+ ) -> list [Shapely ]:
53
+ """
54
+ Flatten nested geometries into a flat list, while only keeping the specified types.
55
+
56
+ Recursively extracts and returns non-empty geometries of the given types from input geometries,
57
+ expanding any GeometryCollections or Multi* types.
58
+
59
+ Parameters
60
+ ----------
61
+ geoms : Union[Shapely, Iterable[Shapely]]
62
+ Input geometries to flatten.
63
+
64
+ keep_types : tuple[type, ...]
65
+ Geometry types to keep (e.g., (Polygon, LineString)). Default is
66
+ (Polygon).
67
+
68
+ Returns
69
+ -------
70
+ list[Shapely]
71
+ Flat list of non-empty geometries matching the specified types.
72
+ """
73
+ # Handle single Shapely object by wrapping it in a list
74
+ if isinstance (geoms , Shapely ):
75
+ geoms = [geoms ]
76
+
77
+ flat = []
78
+ for geom in geoms :
79
+ if geom .is_empty :
80
+ continue
81
+ if isinstance (geom , keep_types ):
82
+ flat .append (geom )
83
+ elif isinstance (geom , (MultiPolygon , MultiLineString , MultiPoint , GeometryCollection )):
84
+ flat .extend (flatten_shapely_geometries (geom .geoms , keep_types ))
85
+ elif isinstance (geom , BaseGeometry ) and hasattr (geom , "geoms" ):
86
+ flat .extend (flatten_shapely_geometries (geom .geoms , keep_types ))
87
+ return flat
88
+
89
+
41
90
def merging_geometries_on_plane (
42
91
geometries : list [GeometryType ],
43
92
plane : Box ,
@@ -373,6 +422,15 @@ class SnappingSpec(Tidy3dBaseModel):
373
422
description = "Describes how snapping positions will be chosen." ,
374
423
)
375
424
425
+ margin : Optional [
426
+ tuple [pydantic .NonNegativeInt , pydantic .NonNegativeInt , pydantic .NonNegativeInt ]
427
+ ] = pydantic .Field (
428
+ (0 , 0 , 0 ),
429
+ title = "Margin" ,
430
+ description = "Number of additional grid points to consider when expanding or contracting "
431
+ "during snapping. Only applies when ``SnapBehavior`` is ``Expand`` or ``Contract``." ,
432
+ )
433
+
376
434
377
435
def get_closest_value (test : float , coords : np .ArrayLike , upper_bound_idx : int ) -> float :
378
436
"""Helper to choose the closest value in an array to a given test value,
@@ -404,6 +462,8 @@ def get_lower_bound(
404
462
using the index of the upper bound. If the test value is close to the upper
405
463
bound, it assumes they are equal, and in that case the upper bound is returned.
406
464
"""
465
+ upper_bound_idx = min (upper_bound_idx , len (coords ))
466
+ upper_bound_idx = max (upper_bound_idx , 0 )
407
467
if upper_bound_idx == len (coords ):
408
468
return coords [upper_bound_idx - 1 ]
409
469
if upper_bound_idx == 0 or isclose (coords [upper_bound_idx ], test , rel_tol = rel_tol ):
@@ -417,14 +477,20 @@ def get_upper_bound(
417
477
using the index of the upper bound. If the test value is close to the lower
418
478
bound, it assumes they are equal, and in that case the lower bound is returned.
419
479
"""
480
+ upper_bound_idx = min (upper_bound_idx , len (coords ))
481
+ upper_bound_idx = max (upper_bound_idx , 0 )
420
482
if upper_bound_idx == len (coords ):
421
483
return coords [upper_bound_idx - 1 ]
422
484
if upper_bound_idx > 0 and isclose (coords [upper_bound_idx - 1 ], test , rel_tol = rel_tol ):
423
485
return coords [upper_bound_idx - 1 ]
424
486
return coords [upper_bound_idx ]
425
487
426
488
def find_snapping_locations (
427
- interval_min : float , interval_max : float , coords : np .ndarray , snap_type : SnapBehavior
489
+ interval_min : float ,
490
+ interval_max : float ,
491
+ coords : np .ndarray ,
492
+ snap_type : SnapBehavior ,
493
+ snap_margin : pydantic .NonNegativeInt ,
428
494
) -> tuple [float , float ]:
429
495
"""Helper that snaps a supplied interval [interval_min, interval_max] to a
430
496
sorted array representing coordinate values.
@@ -436,9 +502,15 @@ def find_snapping_locations(
436
502
min_snap = get_closest_value (interval_min , coords , min_upper_bound_idx )
437
503
max_snap = get_closest_value (interval_max , coords , max_upper_bound_idx )
438
504
elif snap_type == SnapBehavior .Expand :
505
+ min_upper_bound_idx -= snap_margin
506
+ max_upper_bound_idx += snap_margin
439
507
min_snap = get_lower_bound (interval_min , coords , min_upper_bound_idx , rel_tol = rtol )
440
508
max_snap = get_upper_bound (interval_max , coords , max_upper_bound_idx , rel_tol = rtol )
441
509
else : # SnapType.Contract
510
+ min_upper_bound_idx += snap_margin
511
+ max_upper_bound_idx -= snap_margin
512
+ if max_upper_bound_idx < min_upper_bound_idx :
513
+ raise SetupError ("The supplied 'snap_buffer' is too large for this contraction." )
442
514
min_snap = get_upper_bound (interval_min , coords , min_upper_bound_idx , rel_tol = rtol )
443
515
max_snap = get_lower_bound (interval_max , coords , max_upper_bound_idx , rel_tol = rtol )
444
516
return (min_snap , max_snap )
@@ -450,6 +522,7 @@ def find_snapping_locations(
450
522
for axis in range (3 ):
451
523
snap_location = snap_spec .location [axis ]
452
524
snap_type = snap_spec .behavior [axis ]
525
+ snap_margin = snap_spec .margin [axis ]
453
526
if snap_type == SnapBehavior .Off :
454
527
continue
455
528
if snap_location == SnapLocation .Boundary :
@@ -460,7 +533,9 @@ def find_snapping_locations(
460
533
box_min = min_b [axis ]
461
534
box_max = max_b [axis ]
462
535
463
- (new_min , new_max ) = find_snapping_locations (box_min , box_max , snap_coords , snap_type )
536
+ (new_min , new_max ) = find_snapping_locations (
537
+ box_min , box_max , snap_coords , snap_type , snap_margin
538
+ )
464
539
min_b [axis ] = new_min
465
540
max_b [axis ] = new_max
466
541
return Box .from_bounds (min_b , max_b )
0 commit comments