Source code for tidy3d.components.grid.corner_finder

"""Find corners of structures on a 2D plane."""

from __future__ import annotations

from typing import Any, Literal, Optional

import numpy as np
import pydantic.v1 as pd

from tidy3d.components.base import Tidy3dBaseModel, cached_property
from tidy3d.components.geometry.base import Box, ClipOperation
from tidy3d.components.geometry.utils import merging_geometries_on_plane
from tidy3d.components.medium import PEC, LossyMetalMedium
from tidy3d.components.structure import Structure
from tidy3d.components.types import ArrayFloat1D, ArrayFloat2D, Axis, Shapely
from tidy3d.constants import inf

CORNER_ANGLE_THRESOLD = 0.1 * np.pi


[docs] class CornerFinderSpec(Tidy3dBaseModel): """Specification for corner detection on a 2D plane.""" medium: Literal["metal", "dielectric", "all"] = pd.Field( "metal", title="Material Type For Corner Identification", description="Find corners of structures made of :class:`.Medium`, " "which can take value ``metal`` for PEC and lossy metal, ``dielectric`` " "for non-metallic materials, and ``all`` for all materials.", ) angle_threshold: float = pd.Field( CORNER_ANGLE_THRESOLD, title="Angle Threshold In Corner Identification", description="A vertex is qualified as a corner if the angle spanned by its two edges " "is larger than the supplementary angle of " "this threshold value.", ge=0, lt=np.pi, ) distance_threshold: Optional[pd.PositiveFloat] = pd.Field( None, title="Distance Threshold In Corner Identification", description="If not ``None`` and the distance of the vertex to its neighboring vertices " "is below the threshold value based on Douglas-Peucker algorithm, the vertex is disqualified as a corner.", ) concave_resolution: Optional[pd.PositiveInt] = pd.Field( None, title="Concave Region Resolution.", description="Specifies number of steps to use for determining `dl_min` based on concave featues." "If set to ``None``, then the corresponding `dl_min` reduction is not applied.", ) convex_resolution: Optional[pd.PositiveInt] = pd.Field( None, title="Convex Region Resolution.", description="Specifies number of steps to use for determining `dl_min` based on convex featues." "If set to ``None``, then the corresponding `dl_min` reduction is not applied.", ) mixed_resolution: Optional[pd.PositiveInt] = pd.Field( None, title="Mixed Region Resolution.", description="Specifies number of steps to use for determining `dl_min` based on mixed featues." "If set to ``None``, then the corresponding `dl_min` reduction is not applied.", ) @cached_property def _no_min_dl_override(self): return all( ( self.concave_resolution is None, self.convex_resolution is None, self.mixed_resolution is None, ) ) @classmethod def _merged_pec_on_plane( cls, normal_axis: Axis, coord: float, structure_list: list[Structure], center: tuple[float, float] = [0, 0, 0], size: tuple[float, float, float] = [inf, inf, inf], interior_disjoint_geometries: bool = False, ) -> list[tuple[Any, Shapely]]: """On a 2D plane specified by axis = `normal_axis` and coordinate `coord`, merge geometries made of PEC. Parameters ---------- normal_axis : Axis Axis normal to the 2D plane. coord : float Position of plane along the normal axis. structure_list : List[Structure] List of structures present in simulation. center : Tuple[float, float] = [0, 0, 0] Center of the 2D plane (coordinate along ``axis`` is ignored) size : Tuple[float, float, float] = [inf, inf, inf] Size of the 2D plane (size along ``axis`` is ignored) interior_disjoint_geometries: bool = False If ``True``, geometries on the plane must not be overlapping. Returns ------- List[Tuple[Any, Shapely]] List of shapes and their property value on the plane after merging. """ # Construct plane slice_center = list(center) slice_size = list(size) slice_center[normal_axis] = coord slice_size[normal_axis] = 0 plane = Box(center=slice_center, size=slice_size) # prepare geometry and medium list geometry_list = [structure.geometry for structure in structure_list] # For metal, we don't distinguish between LossyMetal and PEC, # so they'll be merged to PEC. Other materials are considered as dielectric. medium_list = (structure.medium for structure in structure_list) medium_list = [ PEC if (mat.is_pec or isinstance(mat, LossyMetalMedium)) else mat for mat in medium_list ] # merge geometries merged_geos = merging_geometries_on_plane( geometry_list, plane, medium_list, interior_disjoint_geometries ) return merged_geos def _corners_and_convexity( self, normal_axis: Axis, coord: float, structure_list: list[Structure], ravel: bool, interior_disjoint_geometries: bool = False, ) -> tuple[ArrayFloat2D, ArrayFloat1D]: """On a 2D plane specified by axis = `normal_axis` and coordinate `coord`, find out corners of merged geometries made of PEC. Parameters ---------- normal_axis : Axis Axis normal to the 2D plane. coord : float Position of plane along the normal axis. structure_list : List[Structure] List of structures present in simulation. ravel : bool Whether to put the resulting corners in a single list or per polygon. interior_disjoint_geometries: bool = False If ``True``, geometries made of different materials on the plane must not be overlapping. Returns ------- Tuple[ArrayFloat2D, ArrayFloat1D] Corner coordinates and their convexity. """ # merge geometries merged_geos = self._merged_pec_on_plane( normal_axis=normal_axis, coord=coord, structure_list=structure_list, interior_disjoint_geometries=interior_disjoint_geometries, ) # corner finder corner_list = [] convexity_list = [] for mat, shapes in merged_geos: if self.medium != "all" and mat.is_pec != (self.medium == "metal"): continue polygon_list = ClipOperation.to_polygon_list(shapes) for poly in polygon_list: poly = poly.normalize().buffer(0) if self.distance_threshold is not None: poly = poly.simplify(self.distance_threshold, preserve_topology=True) corners_xy, corners_convexity = self._filter_collinear_vertices( list(poly.exterior.coords) ) corner_list.append(corners_xy) convexity_list.append(corners_convexity) # in case the polygon has holes for poly_inner in poly.interiors: corners_xy, corners_convexity = self._filter_collinear_vertices( list(poly_inner.coords) ) corner_list.append(corners_xy) convexity_list.append(corners_convexity) return self._ravel_corners_and_convexity(ravel, corner_list, convexity_list) def _ravel_corners_and_convexity( self, ravel: bool, corner_list, convexity_list ) -> tuple[ArrayFloat2D, ArrayFloat1D]: """Whether to put the resulting corners in a single list or per polygon.""" if ravel and len(corner_list) > 0: return np.concatenate(corner_list), np.concatenate(convexity_list) return corner_list, convexity_list
[docs] def corners( self, normal_axis: Axis, coord: float, structure_list: list[Structure], interior_disjoint_geometries: bool = False, ) -> ArrayFloat2D: """On a 2D plane specified by axis = `normal_axis` and coordinate `coord`, find out corners of merged geometries made of `medium`. Parameters ---------- normal_axis : Axis Axis normal to the 2D plane. coord : float Position of plane along the normal axis. structure_list : List[Structure] List of structures present in simulation. interior_disjoint_geometries: bool = False If ``True``, geometries made of different materials on the plane must not be overlapping. Returns ------- ArrayFloat2D Corner coordinates. """ corner_list, _ = self._corners_and_convexity( normal_axis=normal_axis, coord=coord, structure_list=structure_list, ravel=True, interior_disjoint_geometries=interior_disjoint_geometries, ) return corner_list
def _filter_collinear_vertices( self, vertices: ArrayFloat2D ) -> tuple[ArrayFloat2D, ArrayFloat1D]: """Filter collinear vertices of a polygon, and return corners locations and their convexity. Parameters ---------- vertices : ArrayFloat2D Polygon vertices from shapely.Polygon. The last vertex is identical to the 1st vertex to make a valid polygon. Returns ------- ArrayFloat2D Corner coordinates. ArrayFloat1D Convexity of corners: True for outer corners, False for inner corners. """ def normalize(v): return v / np.linalg.norm(v, axis=-1)[:, np.newaxis] # drop the last vertex, which is identical to the 1st one. vs_orig = np.array(vertices[:-1]) # compute unit vector to next and previous vertex vs_next = np.roll(vs_orig, axis=0, shift=-1) vs_previous = np.roll(vs_orig, axis=0, shift=+1) unit_next = normalize(vs_next - vs_orig) unit_previous = normalize(vs_previous - vs_orig) # angle inner_product = np.sum(unit_next * unit_previous, axis=-1) inner_product = np.where(inner_product > 1, 1, inner_product) inner_product = np.where(inner_product < -1, -1, inner_product) angle = np.arccos(inner_product) num_vs = len(vs_orig) cross_product = np.cross( np.hstack([unit_next, np.zeros((num_vs, 1))]), np.hstack([unit_previous, np.zeros((num_vs, 1))]), axis=-1, ) convexity = cross_product[:, 2] < 0 ind_filter = angle <= np.pi - self.angle_threshold return vs_orig[ind_filter], convexity[ind_filter]