# This code is part of KQCircuits
# Copyright (C) 2021 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 math import pi, tan, degrees, atan2, sqrt
from kqcircuits.elements.airbridges.airbridge import Airbridge
from kqcircuits.elements.airbridges.airbridge_multi_face import AirbridgeMultiFace
from kqcircuits.elements.element import Element
from kqcircuits.elements.flip_chip_connectors.flip_chip_connector_rf import FlipChipConnectorRf
from kqcircuits.elements.waveguide_coplanar import WaveguideCoplanar
from kqcircuits.elements.waveguide_coplanar_curved import WaveguideCoplanarCurved
from kqcircuits.pya_resolver import pya
from kqcircuits.util.geometry_helper import vector_length_and_direction, is_clockwise, get_angle
from kqcircuits.util.parameters import Param, pdt, add_parameters_from
[docs]
@add_parameters_from(AirbridgeMultiFace)
@add_parameters_from(FlipChipConnectorRf, "connector_a", "connector_b", "round_connector")
@add_parameters_from(WaveguideCoplanar, "term1", "term2")
class SpiralResonatorPolygon(Element):
"""The PCell declaration for a polygon shaped spiral resonator.
The resonator waveguide starts at the first point of `self.input_path` and goes through each each of its points. The
last point of `self.input_path` will connect to the first point of `self.poly_path`, unless the length of
`self.input_path` is already longer than `self.length`.
The polygon shape is defined by `self.poly_path`, where each point is a vertex of the polygon. The points should
either be in clockwise or counter-clockwise order. The waveguide will first continue along the polygon edges, and
then spiral inside the polygon such that each segment will be parallel to one of the edges defined by
`self.polygon_path`.
Note, that if you want the segment from `self.input_path` to `self.poly_path` to be "a part of the spiral"
(following the polygon shape), you will need to choose the points such that the edge
`self.input_path[-1] - self.poly_path[0]` is parallel to the edge `self.poly_path[0] - self.poly_path[-1]`.
The spacing between waveguides in the spiral can either be chosen manually or automatically. The automatic spacing
attempts to find the largest possible spacing.
Airbridges can optionally be placed with a given spacing along the resonator waveguide by setting non-zero value to
`bridge_spacing`. Alternatively, one can independently set number of airbridges on each straight segment of spiral
polygon by using parameter `n_bridges_pattern`.
Non-negative value to `connector_dist` inserts face-to-face connector to spiral resonator so that the beginning and
the end of resonator will be on different faces.
"""
length = Param(pdt.TypeDouble, "Resonator length", 5000, unit="μm")
input_path = Param(pdt.TypeShape, "Input waveguide path", pya.DPath([pya.DPoint(-200, 0), pya.DPoint(0, 0)], 10))
poly_path = Param(
pdt.TypeShape, "Polygon path", pya.DPath([pya.DPoint(0, 800), pya.DPoint(1000, 0), pya.DPoint(0, -800)], 10)
)
auto_spacing = Param(pdt.TypeBoolean, "Use automatic spacing", True)
include_connector_length = Param(pdt.TypeBoolean, "Include connector length", False)
manual_spacing = Param(pdt.TypeList, "Manual spacing pattern", [300], unit="[μm]")
bridge_spacing = Param(pdt.TypeDouble, "Airbridge spacing", 0, unit="μm")
n_bridges_pattern = Param(pdt.TypeList, "Pattern for number of airbridges on edges", [0])
connector_dist = Param(
pdt.TypeDouble,
"Face to face connector distance from beginning",
-1,
unit="µm",
docstring="Negative value means single face resonator without connector.",
)
[docs]
def build(self):
if self.connector_dist >= 0 and not self.include_connector_length:
conn_cell = self.add_element(FlipChipConnectorRf)
conn_ref = self.get_refpoints(conn_cell)
port0 = self.face_ids[0] + "_port"
port1 = self.face_ids[1] + "_port"
conn_len, _ = vector_length_and_direction(conn_ref[port1] - conn_ref[port0])
self.length += conn_len
if isinstance(self.input_path, list):
self.input_path = pya.DPath(self.input_path, 1)
if isinstance(self.poly_path, list):
self.poly_path = pya.DPath(self.poly_path, 1)
if self.auto_spacing:
self._produce_resonator_automatic_spacing()
else:
self._produce_resonator_manual_spacing()
def _produce_resonator_automatic_spacing(self):
"""Produces polygon spiral resonator with automatically determined waveguide spacing.
This creates resonators with different spacing, until it finds the largest spacing that can be used to create
a valid resonator. Only the final resonator with optimal spacing is inserted to `self.cell` in the end.
"""
def polygon_min_diameter(path):
p = list(path.each_point())
n = len(p)
diams = []
for i in range(n):
_, v = vector_length_and_direction(p[(i + 1) % n] - p[i])
diams.append(max(abs(v.vprod(p[j % n] - p[i])) for j in range(i + 2, i + n)))
return min(diams)
# find optimal spacing using bisection method
min_spacing, max_spacing = 0, polygon_min_diameter(self.poly_path) / 2
optimal_points = self._produce_path_points([min_spacing])
if optimal_points is None:
self.raise_error_on_cell(
"Cannot create a resonator with the given parameters. Try decreasing the turn radius.",
(self.input_path.bbox() + self.poly_path.bbox()).center(),
)
spacing_tolerance = 0.001
while max_spacing - min_spacing > spacing_tolerance:
spacing = (min_spacing + max_spacing) / 2
points = self._produce_path_points([spacing])
if points is not None:
optimal_points = points
min_spacing = spacing
else:
max_spacing = spacing
self._produce_resonator(optimal_points)
def _produce_resonator_manual_spacing(self):
"""Produces polygon spiral resonator with spacing defined by `self.manual_spacing`.
If the resonator cannot be created with the chosen spacing, it will instead raise a ValueError.
"""
sp = [float(s) for s in self.manual_spacing] if isinstance(self.manual_spacing, list) else [self.manual_spacing]
points = self._produce_path_points(sp)
if points is None:
self.raise_error_on_cell(
"Cannot create a resonator with the given parameters. Try decreasing the spacings "
"or the turn radius.",
(self.input_path.bbox() + self.poly_path.bbox()).center(),
)
self._produce_resonator(points)
def _produce_path_points(self, spacing):
"""Creates resonator path points with the given spacing.
Function _produce_resonator takes these points as an argument.
If spacing is unsuitable for creating points, the return value is None.
Args:
spacing: spacing between waveguide centers inside the polygon
Returns:
List of DPoints or None
"""
def get_updated_length(pts, prev_length):
"""Updates the resonator length by adding the last point.
Args:
pts: list of DPoints
prev_length: resonator length before adding the last point
Returns:
resonator length including all points (or None if waveguide bends can't fit)
"""
if len(pts) <= 1:
return 0.0
last_segment_len = (pts[-1] - pts[-2]).length()
if len(pts) == 2:
return last_segment_len
# compute new length for the resonator
_, _, alpha1, alpha2, _ = WaveguideCoplanar.get_corner_data(pts[-3], pts[-2], pts[-1], self.r)
abs_curve = pi - abs(pi - abs(alpha2 - alpha1))
corner_cut_dist = self.r * tan(abs_curve / 2)
updated_length = prev_length - 2 * corner_cut_dist + self.r * abs_curve + last_segment_len
# if the new segment is not long enough for the curve in the beginning, resonator cannot be created
if last_segment_len < corner_cut_dist - 1e-5:
return None
# if the previous segment is not long enough for the curves at each end, resonator cannot be created
if len(pts) > 3:
corner_cut_dist += self._corner_cut_distance(pts[-4], pts[-3], pts[-2])[0]
if (pts[-2] - pts[-3]).length() < corner_cut_dist - 1e-5:
return None
return updated_length
# segments based on input_path points
input_points = list(self.input_path.each_point())
length = 0.0
points = []
for ip in input_points:
# Update length after adding a point
points.append(ip)
length = get_updated_length(points, length)
if length is None:
return None
# Test if the resonator is long enough
if length >= self.length:
return points
# segments based on poly_path points
poly_points = list(self.poly_path.each_point())
n_poly_points = len(poly_points)
if n_poly_points > 2:
poly_edges = [pya.DEdge(poly_points[i], poly_points[(i + 1) % n_poly_points]) for i in range(n_poly_points)]
clockwise = is_clockwise(poly_points)
# get the normal vectors (toward inside of polygon) of each edge
normals = []
for edge in poly_edges:
_, direction = vector_length_and_direction(edge.p2 - edge.p1)
normals.append(
pya.DVector(direction.y, -direction.x) if clockwise else pya.DVector(-direction.y, direction.x)
)
# define amount of spacing for the first round
shifts = [0.0] * len(poly_edges)
if len(points) > 0:
_, input_dir = vector_length_and_direction(poly_points[0] - points[-1])
_, poly_dir = vector_length_and_direction(poly_points[0] - poly_points[-1])
shifts[-1] = max(0.0, spacing[-1] * input_dir.sprod(poly_dir))
i = 0
current_edge = poly_edges[-1]
while True:
# get the edge shifted with spacing from the corresponding poly edge
next_edge_without_shift = poly_edges[i % n_poly_points]
shift = shifts[i % n_poly_points] * normals[i % n_poly_points]
shifts[i % n_poly_points] += spacing[i % len(spacing)]
next_edge = pya.DEdge(next_edge_without_shift.p1 + shift, next_edge_without_shift.p2 + shift)
# Find intersection point of the lines defined by current_edge and next_edge.
# We use `extended` since we want intersections between the "infinite" lines instead of finite edges.
# Use "intersection_point = current_edge.cut_point(next_edge)" after Klayout 0.26 support is dropped.
intersection_point = current_edge.extended(1e7).crossing_point(next_edge.extended(1e7))
# if the shift was so large that the new segment would end up in the opposite direction,
# resonator cannot be created
if i > 0 >= (intersection_point - points[-1]).sprod_sign(current_edge.d()):
return None
# Append point and update length
points.append(intersection_point)
length = get_updated_length(points, length)
if length is None:
return None
# Test if the resonator is long enough
if length >= self.length:
if i < n_poly_points: # Outest segments don't need overlapping consideration
return points
# Check outer curve shortcut length to avoid inner segments overlapping with the outer curve.
i_out = len(points) - n_poly_points - 1
if i_out <= 0: # Outer curve doesn't exist because input_path is empty
return points
corner_diff = points[-1] - points[i_out] # vector from outer corner to inner corner
_, inner_dir = vector_length_and_direction(points[-1] - points[-2])
s_cut = corner_diff.sprod(inner_dir)
if s_cut >= 0: # For concave corner, segment cannot overlap with outer curve
return points
# For convex corner, allow straight segment until the outer curve begins.
_, outer_dir = vector_length_and_direction(points[i_out] - points[i_out - 1])
r_cut, _ = self._corner_cut_distance(points[i_out - 1], points[i_out], points[i_out + 1])
if length - max(0.0, s_cut + r_cut * outer_dir.sprod(inner_dir)) >= self.length:
return points
# prepare for the next iteration
current_edge = next_edge
i += 1
return None
def _produce_resonator(self, points):
"""Produces a polygon spiral resonator with the given path points
Args:
points: List of DPoints created by function _produce_path_points
"""
tmp_cell = self.add_element(WaveguideCoplanar, path=points)
length = tmp_cell.length()
# handle correctly the last waveguide segment
last_segment_curved = self._fix_waveguide_end(points, length)
term2 = 0 if last_segment_curved else self.term2
# produce bridges
self._produce_airbridges(points)
# insert waveguide with or without connector
if self.connector_dist >= 0:
self._produce_wg_with_connector(points, term2)
else:
self.insert_cell(WaveguideCoplanar, path=points, term2=term2)
self.add_port("a", points[0], points[0] - points[1])
self.add_port("b", points[-1], points[-1] - points[-2])
def _fix_waveguide_end(self, points, current_length):
"""Modifies the last points and places a WaveguideCoplanarCurved element at the end if needed.
This is required since WaveguideCoplanar cannot end in the middle of a curved segment.
Args:
points: list of points used to create the resonator waveguide, may be modified by this method
current_length: length of the resonator if points are not modified
Returns:
True if the last segment is curved and False if it's straight
"""
extra_len = current_length - self.length
last_seg_len, last_seg_dir = vector_length_and_direction(points[-1] - points[-2])
if len(points) > 2:
v1, v2, alpha1, alpha2, corner_pos = WaveguideCoplanar.get_corner_data(
points[-3], points[-2], points[-1], self.r
)
# distance between points[-2] and start of the curve
corner_cut_dist = self.r * tan((pi - abs(pi - abs(alpha2 - alpha1))) / 2)
# check if last waveguide segment is too short to be straight
if last_seg_len - extra_len < corner_cut_dist:
# remove last point and move the new last point to the start position of the old curve
points.pop()
_, new_last_dir = vector_length_and_direction(points[-1] - points[-2])
points[-1] -= corner_cut_dist * new_last_dir
# calculate how long the new curve piece needs to be
if len(points) > 2:
tmp_cell = self.add_element(WaveguideCoplanar, path=points)
curve_length = self.length - tmp_cell.length()
if curve_length <= 0.0:
points[-1] += curve_length * new_last_dir
return False
else:
curve_length = self.length - (points[-1] - points[-2]).length()
curve_alpha = curve_length / self.r
# add new curve piece at the waveguide end
face = int(self.connector_dist >= 0)
curve_cell = self.add_element(
WaveguideCoplanarCurved, alpha=curve_alpha, face_ids=[self.face_ids[face]]
)
curve_trans = pya.DCplxTrans(
1, degrees(alpha1) - v1.vprod_sign(v2) * 90, v1.vprod_sign(v2) < 0, corner_pos
)
self.insert_cell(curve_cell, curve_trans)
WaveguideCoplanarCurved.produce_curve_termination(self, curve_alpha, self.term2, curve_trans, face)
return True
# set last point to correct position based on length
points[-1] = points[-1] - extra_len * last_seg_dir
return False
def _produce_airbridges(self, points):
"""Produces airbridges defined either by self.bridge_spacing or self.n_bridges_pattern.
All airbridges have at least half bridge width distance to end of the straight.
Args:
points: list of points used to create the resonator waveguide
"""
# Create airbridges by self.bridge_spacing
bridge_width = Airbridge.get_schema()["bridge_width"].default
if self.bridge_spacing > 0.0:
dist_to_next_bridge = self.bridge_spacing
for i in range(0, len(points) - 1):
segment_len, segment_dir = vector_length_and_direction(points[i + 1] - points[i])
cut_dist, curve_len = (
self._corner_cut_distance(points[i], points[i + 1], points[i + 2])
if i + 2 < len(points)
else (0.0, 0.0)
)
end_of_straight = segment_len - bridge_width - cut_dist
angle = degrees(atan2(segment_dir.y, segment_dir.x))
while dist_to_next_bridge < end_of_straight:
pos = points[i] + dist_to_next_bridge * segment_dir
self.insert_cell(Airbridge, pya.DCplxTrans(1, angle, False, pos))
dist_to_next_bridge += self.bridge_spacing
dist_to_next_bridge = max(
dist_to_next_bridge - segment_len + 2 * cut_dist - curve_len, cut_dist + bridge_width
)
# Create airbridges by self.n_bridges_pattern
nb = [int(n) for n in self.n_bridges_pattern] if isinstance(self.n_bridges_pattern, list) else []
n_beg = self.input_path.num_points()
if any(nb) and n_beg < len(points) - 2:
cut_dist0, _ = self._corner_cut_distance(points[n_beg - 1], points[n_beg], points[n_beg + 1])
for i in range(n_beg, len(points) - 2):
segment_len, segment_dir = vector_length_and_direction(points[i + 1] - points[i])
cut_dist1, _ = self._corner_cut_distance(points[i], points[i + 1], points[i + 2])
n_bridges = nb[(i - n_beg) % len(nb)]
shift = 0.5 - 0.5 * (((i - n_beg) // len(nb)) % 2)
ab_dist = (segment_len - cut_dist0 - cut_dist1 - 2 * bridge_width) / (n_bridges - 0.5)
angle = degrees(atan2(segment_dir.y, segment_dir.x))
for b in range(n_bridges):
pos = points[i] + (cut_dist0 + bridge_width + (b + shift) * ab_dist) * segment_dir
self.insert_cell(Airbridge, pya.DCplxTrans(1, angle, False, pos))
cut_dist0 = cut_dist1
def _produce_wg_with_connector(self, points, term2):
"""Produces waveguide with face-to-face connector.
Args:
points: list of points used to create the resonator waveguide
term2: end termination
"""
# add connector cell and get connector length
conn_cell = self.add_element(FlipChipConnectorRf)
conn_ref = self.get_refpoints(conn_cell)
port0 = self.face_ids[0] + "_port"
port1 = self.face_ids[1] + "_port"
conn_len, conn_dir = vector_length_and_direction(conn_ref[port1] - conn_ref[port0])
def insert_wg_with_connector(segment, distance):
s_len, s_dir = vector_length_and_direction(points[segment + 1] - points[segment])
b_pos = points[segment] + distance * s_dir
ang = get_angle(s_dir) - get_angle(conn_dir)
trans = pya.DCplxTrans(1.0, ang, False, b_pos) * pya.DTrans(-conn_ref[port0])
t_pos = self.insert_cell(conn_cell, trans=trans)[1][port1]
if segment == 0 and distance < 1e-3:
WaveguideCoplanar.produce_end_termination(self, t_pos, b_pos, self.term1)
else:
self.insert_cell(WaveguideCoplanar, path=points[: segment + 1] + [b_pos], term2=0)
if segment + 2 == len(points) and s_len - conn_len - distance < 1e-3:
WaveguideCoplanar.produce_end_termination(self, b_pos, t_pos, term2, face_index=1)
else:
self.insert_cell(
WaveguideCoplanar,
path=[t_pos] + points[segment + 1 :],
term1=0,
term2=term2,
face_ids=self.face_ids[1::-1],
)
last = {} # parameters for last possible connector position
prev_len = 0.0
prev_cut_dist = 0.0
for i, p in enumerate(points):
if i + 1 >= len(points):
if last:
insert_wg_with_connector(**last)
return
self.raise_error_on_cell(
"Face-to-face connector cannot fit.", (self.input_path.bbox() + self.poly_path.bbox()).center()
)
corner_cut_dist, corner_length = (
(0.0, 0.0) if i + 2 == len(points) else self._corner_cut_distance(p, points[i + 1], points[i + 2])
)
segment_len, _ = vector_length_and_direction(points[i + 1] - p)
straight_len = segment_len - prev_cut_dist - corner_cut_dist
if conn_len <= straight_len:
last = {"segment": i, "distance": segment_len - corner_cut_dist - conn_len}
dist = self.connector_dist - conn_len / 2 - prev_len + prev_cut_dist
if dist <= last["distance"]:
insert_wg_with_connector(i, max(prev_cut_dist, dist))
return
prev_len += straight_len + corner_length
prev_cut_dist = corner_cut_dist
def _corner_cut_distance(self, point1, point2, point3):
"""Returns the distance from waveguide path corner to the start of the curve and the curve length.
Args:
point1: point before corner
point2: corner point
point3: point after corner
Returns:
corner cut distance, curve length
"""
_, _, alpha1, alpha2, _ = WaveguideCoplanar.get_corner_data(point1, point2, point3, self.r)
abs_curve = pi - abs(pi - abs(alpha2 - alpha1))
return self.r * tan(abs_curve / 2), self.r * abs_curve
[docs]
def rectangular_parameters(
above_space=500,
below_space=400,
right_space=1000,
x_spacing=100,
y_spacing=100,
bridges_left=False,
bridges_bottom=False,
bridges_right=False,
bridges_top=False,
r=Element.get_schema()["r"].default,
**kwargs,
):
"""A utility function to easily produce rectangular spiral resonator (old SpiralResonatorRectangle).
Args:
above_space: Space above the input (µm)
below_space: Space below the input (µm)
right_space: Space right of the input (µm)
x_spacing: Spacing between vertical segments (µm)
y_spacing: Spacing between horizontal segments (µm)
bridges_left: Crossing airbridges left
bridges_bottom: Crossing airbridges bottom
bridges_right: Crossing airbridges right
bridges_top: Crossing airbridges top
r: Turn radius (µm)
Returns:
dictionary of parameters for SpiralResonatorPolygon
"""
defaults = {"manual_spacing": [y_spacing, x_spacing], "r": r}
if above_space == 0:
params = {
"input_path": pya.DPath([], 10),
"poly_path": pya.DPath(
[
pya.DPoint(0, above_space),
pya.DPoint(right_space, above_space),
pya.DPoint(right_space, -below_space),
pya.DPoint(0, -below_space),
],
10,
),
"n_bridges_pattern": [bridges_top, bridges_right, bridges_bottom, bridges_left],
}
elif below_space == 0:
params = {
"input_path": pya.DPath([], 10),
"poly_path": pya.DPath(
[
pya.DPoint(0, -below_space),
pya.DPoint(right_space, -below_space),
pya.DPoint(right_space, above_space),
pya.DPoint(0, above_space),
],
10,
),
"n_bridges_pattern": [bridges_bottom, bridges_right, bridges_top, bridges_left],
}
elif above_space > below_space:
x1 = sqrt(above_space / (4 * r - above_space)) * r if above_space < 2 * r else r
x2 = (sqrt((4 * r - above_space) * above_space) if above_space < 2 * r else 2 * r) - x1
params = {
"input_path": pya.DPath([pya.DPoint(0, 0), pya.DPoint(x1, 0)], 10),
"poly_path": pya.DPath(
[
pya.DPoint(x2, above_space),
pya.DPoint(right_space, above_space),
pya.DPoint(right_space, -below_space),
pya.DPoint(x2, -below_space),
],
10,
),
"n_bridges_pattern": [bridges_top, bridges_right, bridges_bottom, bridges_left],
}
else:
x1 = sqrt(below_space / (4 * r - below_space)) * r if below_space < 2 * r else r
x2 = (sqrt((4 * r - below_space) * below_space) if below_space < 2 * r else 2 * r) - x1
params = {
"input_path": pya.DPath([pya.DPoint(0, 0), pya.DPoint(x1, 0)], 10),
"poly_path": pya.DPath(
[
pya.DPoint(x2, -below_space),
pya.DPoint(right_space, -below_space),
pya.DPoint(right_space, above_space),
pya.DPoint(x2, above_space),
],
10,
),
"n_bridges_pattern": [bridges_bottom, bridges_right, bridges_top, bridges_left],
}
return {**defaults, **params, **kwargs}