11# type: ignore
22from collections .abc import Callable
3- from functools import partial
43
54import gdsfactory as gf
65import kfactory as kf
109import shapely .ops as ops
1110from gdsfactory import logger
1211from klayout .db import DPoint , Polygon
13- from scipy .signal import savgol_filter
14- from scipy .spatial import distance
12+ from scipy .spatial import Voronoi , distance , voronoi_plot_2d
13+
14+ from gplugins .path_length_analysis .utils import (
15+ filter_points_by_std_distance ,
16+ resample_polygon_points_w_interpolator ,
17+ sort_points_nearest_neighbor ,
18+ )
1519
16- filter_savgol_filter = partial (savgol_filter , window_length = 11 , polyorder = 3 , axis = 0 )
1720fix_values = [0 , 1 , - 1 , 2 , - 2 , 3 , - 3 , 4 , - 4 , 5 , - 5 , 6 , - 6 , 7 , - 7 , 8 , - 8 ]
1821
1922
@@ -55,6 +58,118 @@ def _check_midpoint_found(inner_points, outer_points, port_list) -> bool:
5558 return False
5659
5760
61+ def centerline_voronoi_2_ports (
62+ poly : Polygon ,
63+ port_list : list [gf .Port ],
64+ plot : bool = False ,
65+ ) -> np .ndarray :
66+ """Returns the centerline for a single polygon that has 2 ports.
67+
68+ This function uses Voronoi tessellation to find the centerline of a polygon.
69+
70+ Args:
71+ poly: The KLayout Polygon object representing the waveguide or structure.
72+ port_list: List of two ports (gf.Port objects) corresponding to the start and end ports of the polygon.
73+ plot: If True, plots the Voronoi diagram and the centerline.
74+
75+ Returns:
76+ np.ndarray: An array of (x, y) points representing the calculated centerline in microns.
77+ """
78+ if len (port_list ) != 2 :
79+ raise ValueError (
80+ "port_list must be a list of 2 ports, got: "
81+ f"{ port_list } with length { len (port_list )} "
82+ )
83+ # Simplify points that are too close to each other
84+ r = gf .kdb .Region (poly )
85+ r = r .smoothed (0.05 , True )
86+
87+ # Get polygon points from klayout DPolygon and resample
88+ points = np .array ([(pt .x , pt .y ) for pt in r [0 ].each_point_hull ()])
89+ if len (points ) < 3 :
90+ raise ValueError (
91+ "The polygon must have at least 3 points to compute a centerline."
92+ )
93+ # Ensure the polygon is closed by appending the first point to the end
94+ # This ensures resampling for all edges later on
95+ points = np .vstack ((points , points [0 ]))
96+ shapely_poly_original = sh .Polygon (points )
97+
98+ # Infer port widths to be 2x the distance between the port center and the nearest point on the polygon
99+ port_widths = []
100+ for port in port_list :
101+ # Compute distances from each point to port center
102+ distances = distance .cdist ([port .center ], points )
103+ # Find the minimum distance
104+ min_distance = np .min (distances )
105+ port_widths .append (min_distance * 2 )
106+
107+ # Interpolate between the points to ensure enough sampling using Pchip interpolation
108+ interpolated_points = resample_polygon_points_w_interpolator (
109+ points , n_samples_coeff = 2
110+ )
111+
112+ # Remove interpolated points that are too close to the ports
113+ for port , width in zip (port_list , port_widths ):
114+ distances = distance .cdist ([port .center ], interpolated_points )
115+ # Keep points that are at least half a width away from the port center
116+ mask = distances [0 ] > (width - 1 ) / 2
117+ interpolated_points = interpolated_points [mask ]
118+
119+ # Use Voronoi to find the centerline
120+ voronoi = Voronoi (interpolated_points )
121+
122+ if plot :
123+ voronoi_plot_2d (voronoi )
124+ plt .gca ().set_aspect ("equal" , adjustable = "box" ) # Force equilateral axes
125+ plt .show ()
126+
127+ # Filter Voronoi vertices to keep only those inside the original polygon
128+ centerline = np .array (
129+ [v for v in voronoi .vertices if shapely_poly_original .contains (sh .Point (v ))]
130+ )
131+
132+ # Add ports as start and end points
133+ centerline = np .vstack ((port_list [0 ].center , centerline , port_list [1 ].center ))
134+
135+ # Consider points that are 3× the standard deviation away from the mean distance to be artifacts
136+ centerline = sort_points_nearest_neighbor (centerline , start_idx = 0 )
137+ centerline = filter_points_by_std_distance (centerline )
138+
139+ # The points are not guaranteed to be ordered, so we need to sort them
140+ # Initially sort the centerline by euclidean distance from (0, 0)
141+ # centerline = centerline[np.argsort(np.linalg.norm(centerline, axis=1))]
142+ centerline = sort_points_nearest_neighbor (centerline , start_idx = 0 )
143+
144+ # Because of Voronoi tessellation zig-zagging on less sampled polygons,
145+ # take only half the points by averaging consecutive pairs
146+ if len (centerline ) % 2 != 0 :
147+ centerline = centerline [:- 1 ] # Ensure even number of points
148+ centerline = (centerline [::2 ] + centerline [1 ::2 ]) / 2
149+
150+ # Filter again just in case
151+ centerline = np .array (
152+ [v for v in centerline if shapely_poly_original .contains (sh .Point (v ))]
153+ )
154+
155+ # Re-add ports as start and end points
156+ centerline = np .vstack ((port_list [0 ].center , centerline , port_list [1 ].center ))
157+ centerline = sort_points_nearest_neighbor (centerline )
158+
159+ # Resample the centerline to have a specific resolution for more robust curvature post-processing
160+ # Use a fraction of the original number of samples to avoid oversampling
161+ centerline = resample_polygon_points_w_interpolator (
162+ centerline , n_samples_coeff = 1 / 3
163+ )
164+ # This is inefficient but works for now
165+ centerline = sort_points_nearest_neighbor (centerline )
166+
167+ # Convert to microns
168+ centerline *= 1e-3
169+
170+ return centerline
171+
172+
58173def centerline_single_poly_2_ports (poly , under_sampling , port_list ) -> np .ndarray :
59174 """Returns the centerline for a single polygon that has 2 ports.
60175
@@ -252,6 +367,7 @@ def extract_paths(
252367 evanescent_coupling : bool = False ,
253368 consider_ports : list [str ] | None = None ,
254369 port_positions : list [tuple [float , float ]] | None = None ,
370+ ** kwargs ,
255371) -> dict :
256372 """Extracts the centerline of a component or instance from a GDS file.
257373
@@ -262,7 +378,7 @@ def extract_paths(
262378 component: gdsfactory component or instance to extract from.
263379 layer: layer to extract the centerline from.
264380 plot: If True, we plot the extracted paths.
265- filter_function: optional Function to filter the centerline.
381+ filter_function: optional function to filter the centerline.
266382 under_sampling: under sampling factor of the polygon points.
267383 evanescent_coupling: if True, it assumes that there is evanescent coupling
268384 between ports not physically connected.
@@ -275,6 +391,7 @@ def extract_paths(
275391 Note - this will not work in cases where the specified port positions are only coupled
276392 evanescently. A workaround for this limitation is to specify one additional port that is
277393 physically connected to the ports of interest, for each port of interest.
394+ **kwargs: Extra keyword arguments to pass to the centerline extraction function.
278395 """
279396 ev_paths = None
280397
@@ -330,13 +447,14 @@ def extract_paths(
330447 if poly [0 ].is_box (): # only 4 points, no undersampling
331448 centerline = centerline_single_poly_2_ports (poly , 1 , ports_list )
332449 else :
333- centerline = centerline_single_poly_2_ports (
334- poly , under_sampling , ports_list
450+ centerline = centerline_voronoi_2_ports (
451+ poly ,
452+ ports_list ,
453+ ** kwargs ,
335454 )
336455 if filter_function is not None :
337456 centerline = filter_function (centerline )
338- p = gf .Path (centerline )
339- paths [f"{ ports_list [0 ].name } ;{ ports_list [1 ].name } " ] = p
457+ paths [f"{ ports_list [0 ].name } ;{ ports_list [1 ].name } " ] = gf .Path (centerline )
340458
341459 else :
342460 # Single polygon and more than 2 ports - MMI
@@ -387,13 +505,16 @@ def extract_paths(
387505 ]
388506 if len (ports_poly ) == 2 :
389507 # Each polygon has two ports - simple case
390- centerline = centerline_single_poly_2_ports (
391- poly , under_sampling , ports_poly
508+ centerline = centerline_voronoi_2_ports (
509+ poly ,
510+ ports_poly ,
511+ ** kwargs ,
392512 )
393513 if filter_function is not None :
394514 centerline = filter_function (centerline )
395- p = gf .Path (centerline )
396- paths [f"{ ports_poly [0 ].name } ;{ ports_poly [1 ].name } " ] = p
515+ paths [f"{ ports_poly [0 ].name } ;{ ports_poly [1 ].name } " ] = gf .Path (
516+ centerline
517+ )
397518
398519 elif len (ports_poly ) == 0 :
399520 # No ports in the polygon - continue
@@ -423,11 +544,9 @@ def extract_paths(
423544 if port1 == port2 :
424545 # Same port - skip
425546 continue
426- if (
427- f"{ port1 } ;{ port2 } " in paths
428- or f"{ port2 } ;{ port1 } " in paths
429- or f"{ port1 } ;{ port2 } " in ev_paths
430- or f"{ port2 } ;{ port1 } " in ev_paths
547+ if any (
548+ s in paths | ev_paths
549+ for s in (f"{ port1 } ;{ port2 } " , f"{ port2 } ;{ port1 } " )
431550 ):
432551 # The path has already been computed
433552 continue
@@ -462,8 +581,8 @@ def extract_paths(
462581 # Find the point closest to the center of the polygon
463582 center = np .array (
464583 [
465- simplified_component .dcenter . x ,
466- simplified_component .dcenter . y ,
584+ simplified_component .dcenter [ 0 ] ,
585+ simplified_component .dcenter [ 1 ] ,
467586 ]
468587 )
469588
@@ -535,14 +654,15 @@ def extract_paths(
535654 plt .plot (
536655 centerline .points [:, 0 ],
537656 centerline .points [:, 1 ],
538- "--" ,
657+ "x --" ,
539658 label = ports ,
540659 )
541660 plt .legend ()
542661 plt .title ("Direct paths" )
543662 plt .xlabel ("X-coordinate" )
544663 plt .ylabel ("Y-coordinate" )
545664 plt .grid (True )
665+ plt .gca ().set_aspect ("equal" , adjustable = "box" ) # Force equilateral axes
546666
547667 if ev_paths is not None :
548668 plt .figure ()
@@ -580,6 +700,9 @@ def get_min_radius_and_length_path_dict(path_dict: dict) -> dict:
580700def get_min_radius_and_length (path : gf .Path ) -> tuple [float , float ]:
581701 """Get the minimum radius of curvature and the length of a path."""
582702 _ , K = path .curvature ()
703+ # Ignore the end points if possible as these may have artifacts
704+ if len (K ) > 3 :
705+ K = K [1 :- 1 ]
583706 radius = 1 / K
584707 min_radius = np .nanmin (np .abs (radius ))
585708 return min_radius , path .length ()
@@ -595,14 +718,15 @@ def plot_curvature(path: gf.Path, rmax: int | float = 200) -> None:
595718 s , K = path .curvature ()
596719 radius = 1 / K
597720 valid_indices = (radius > - rmax ) & (radius < rmax )
598- radius2 = radius [valid_indices ]
721+ curvature2 = K [valid_indices ]
599722 s2 = s [valid_indices ]
600723
601724 plt .figure (figsize = (10 , 5 ))
602- plt .plot (s2 , radius2 , ".-" )
725+ plt .plot (s2 , curvature2 , ".-" )
603726 plt .xlabel ("Position along curve (arc length)" )
604- plt .ylabel ("Radius of curvature " )
727+ plt .ylabel ("Curvature (units⁻¹) " )
605728 plt .show ()
729+ return plt
606730
607731
608732def plot_radius (path : gf .Path , rmax : float = 200 ) -> plt .Figure :
@@ -621,7 +745,7 @@ def plot_radius(path: gf.Path, rmax: float = 200) -> plt.Figure:
621745 fig , ax = plt .subplots (1 , 1 , figsize = (15 , 5 ))
622746 ax .plot (s2 , radius2 , ".-" )
623747 ax .set_xlabel ("Position along curve (arc length)" )
624- ax .set_ylabel ("Radius of curvature" )
748+ ax .set_ylabel ("Radius of curvature (units) " )
625749 ax .grid (True )
626750 return fig
627751
0 commit comments