From fcea28868b1c1a80c214ac251c402ad40322ef01 Mon Sep 17 00:00:00 2001 From: Jukka Date: Mon, 16 Dec 2024 15:37:18 +0200 Subject: [PATCH] Add fixed radius rounding method `force_rounded_corners` Modify circular_capacitor to use the function Add tests for `force_rounded_corners` --- .../kqcircuits/elements/circular_capacitor.py | 4 +- .../python/kqcircuits/util/geometry_helper.py | 61 +++++++++++++++++++ .../test_force_rounded_corners.py | 51 ++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 tests/util/geometry_helper/test_force_rounded_corners.py diff --git a/klayout_package/python/kqcircuits/elements/circular_capacitor.py b/klayout_package/python/kqcircuits/elements/circular_capacitor.py index b8da330c0..bed6d42ba 100644 --- a/klayout_package/python/kqcircuits/elements/circular_capacitor.py +++ b/klayout_package/python/kqcircuits/elements/circular_capacitor.py @@ -20,7 +20,7 @@ import math from kqcircuits.pya_resolver import pya from kqcircuits.util.parameters import Param, pdt, add_parameters_from -from kqcircuits.util.geometry_helper import circle_polygon, arc_points +from kqcircuits.util.geometry_helper import circle_polygon, arc_points, force_rounded_corners from kqcircuits.elements.element import Element from kqcircuits.elements.finger_capacitor_square import FingerCapacitorSquare, eval_a2, eval_b2 @@ -91,7 +91,7 @@ def build(self): ] ).to_itype(self.layout.dbu) ) - capacitor_neg.round_corners(5 / self.layout.dbu, 5 / self.layout.dbu, self.n // 2) + capacitor_neg = force_rounded_corners(capacitor_neg, 5 / self.layout.dbu, 5 / self.layout.dbu, self.n // 2) self._add_waveguides(capacitor_neg, x_end, y_left, y_right) # define the capacitor in the ground diff --git a/klayout_package/python/kqcircuits/util/geometry_helper.py b/klayout_package/python/kqcircuits/util/geometry_helper.py index 19f91335c..d90e2e61d 100644 --- a/klayout_package/python/kqcircuits/util/geometry_helper.py +++ b/klayout_package/python/kqcircuits/util/geometry_helper.py @@ -561,3 +561,64 @@ def round_dpath_width(dpath: pya.DPath, dbu: float, precision: int = 2) -> pya.D ipath = dpath.to_itype(dbu) ipath.width -= ipath.width % precision return ipath.to_dtype(dbu) + + +def force_rounded_corners(region: pya.Region, r_inner: float, r_outer: float, n: int) -> pya.Region: + """Returns region with rounded corners by trying to force radius as given by r_inner and r_outer. + + This function is useful when corner rounding is wanted next to curved segment. The point of curved segment that is + closest to the corner limits the radius produced by the klayout round_corners method. This function solves this + problem by removing the points next to the corner that prevent the full rounding radius from taking effect in the + round_corners method. + + Please note that this function can't guarantee full radius in cases, where two corners are close to each other. + For example, if two 90 degree angles are closer than 2 * r distance apart, then the rounding radius is decreased. + + Args: + region: Region whose corners need to be rounded + r_inner: Inner corner radius (in database units) + r_outer: Outer corner radius (in database units) + n: The number of points per circle + + Returns: + Region with rounded corners + """ + + corner_max_cos = np.cos(3 * np.pi / n) # consider point as corner if cos is below this + + def process_points(pts: list[pya.Point]): + i0 = 0 + while i0 < len(pts): + if len(pts) < 3: + return [] + i1, i2, i3 = (i0 + 1) % len(pts), (i0 + 2) % len(pts), (i0 + 3) % len(pts) + p0, p1, p2, p3 = pts[i0 % len(pts)], pts[i1], pts[i2], pts[i3] + v0, v1, v2 = p1 - p0, p2 - p1, p3 - p2 + l0, l1, l2 = v0.length(), v1.length(), v2.length() + cos0, cos1 = v0.sprod(v1) / (l0 * l1), v1.sprod(v2) / (l1 * l2) + if cos0 > corner_max_cos or cos1 > corner_max_cos: # do nothing between two corners + r0, r1 = r_inner if v0.vprod(v1) > 0 else r_outer, r_inner if v1.vprod(v2) > 0 else r_outer + cut0, cut1 = r0 * np.sqrt((1 - cos0) / (1 + cos0)), r1 * np.sqrt((1 - cos1) / (1 + cos1)) # r*tan(a/2) + if cut0 + cut1 > l1: + div, x0, x1 = v0.vprod(v2), v2.vprod(p0 - p3), v0.vprod(p0 - p3) + if x1 * div < 0 < x0 * div: + p_cross = p0 + x0 / div * v0 + if p_cross not in (p0, p3): + pts[i1] = p_cross + pts = [p for i, p in enumerate(pts) if i != i2] + i0 -= 1 + int(i2 < i0) + continue + pts = [p for i, p in enumerate(pts) if i not in (i1, i2)] + i0 -= 1 + int(i1 < i0) + int(i2 < i0) + continue + i0 += 1 + return pts + + # Create new region and insert rounded shapes into it + result = pya.Region() + for polygon in region.each_merged(): + poly = pya.Polygon(process_points(list(polygon.each_point_hull()))) + for hole in range(polygon.holes()): + poly.insert_hole(process_points(list(polygon.each_point_hole(hole)))) + result.insert(poly.round_corners(r_inner, r_outer, n)) + return result diff --git a/tests/util/geometry_helper/test_force_rounded_corners.py b/tests/util/geometry_helper/test_force_rounded_corners.py new file mode 100644 index 000000000..1155b870d --- /dev/null +++ b/tests/util/geometry_helper/test_force_rounded_corners.py @@ -0,0 +1,51 @@ +# This code is part of KQCircuits +# Copyright (C) 2024 IQM Finland Oy +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. If not, see +# https://www.gnu.org/licenses/gpl-3.0.html. +# +# The software distribution should follow IQM trademark policy for open-source software +# (meetiqm.com/iqm-open-source-trademark-policy). IQM welcomes contributions to the code. +# Please see our contribution agreements for individuals (meetiqm.com/iqm-individual-contributor-license-agreement) +# and organizations (meetiqm.com/iqm-organization-contributor-license-agreement). + +from kqcircuits.pya_resolver import pya +from kqcircuits.util.geometry_helper import force_rounded_corners + + +def test_conserve_narrow_rectangle(): + w, h, r, n = 1000, 10000, 5000, 100 + region = pya.Region(pya.Box(-w, -h, w, h)) + assert abs(force_rounded_corners(region, r, 0, n).area() - region.area()) < 100 # inner rounding + assert abs(force_rounded_corners(region, 0, r, n).area() - 39141292) < 100 # outer rounding + + +def test_half_sphere(): + w, r, n = 20000, 5000, 100 + region = pya.Region(pya.Box(-w, -w, w, w)).rounded_corners(w, w, n) - pya.Region(pya.Box(0, -w, w, w)) + assert abs(force_rounded_corners(region, r, 0, n).area() - region.area()) < 100 # inner rounding + assert abs(force_rounded_corners(region, 0, r, n).area() - 611108787) < 100 # outer rounding + + +def test_half_sphere_hole(): + d, w, r, n = 30000, 20000, 5000, 100 + hole = pya.Region(pya.Box(-w, -w, w, w)).rounded_corners(w, w, n) - pya.Region(pya.Box(0, -w, w, w)) + region = pya.Region(pya.Box(-d, -d, d, d)) - hole + assert abs(force_rounded_corners(region, r, 0, n).area() - 2988891213) < 100 # inner rounding + assert abs(force_rounded_corners(region, 0, r, n).area() - 2950038234) < 100 # outer rounding + + +def test_boat_shape(): + w, d, r, n = 20000, 10000, 5000, 100 + region = pya.Region(pya.Box(-w - d, -w, w - d, w)).rounded_corners(w, w, n) & pya.Region( + pya.Box(-w + d, -w, w + d, w) + ).rounded_corners(w, w, n) + assert abs(force_rounded_corners(region, r, 0, n).area() - region.area()) < 100 # inner rounding + assert abs(force_rounded_corners(region, 0, r, n).area() - 486053824) < 100 # outer rounding