# 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, cos, sin, tan, atan, atan2, degrees, sqrt
from scipy.optimize import brentq
from kqcircuits.util.parameters import add_parameters_from
from kqcircuits.elements.waveguide_coplanar_straight import WaveguideCoplanarStraight
from kqcircuits.pya_resolver import pya
from kqcircuits.util.parameters import Param, pdt
from kqcircuits.elements.airbridges.airbridge import Airbridge
from kqcircuits.elements.element import Element
from kqcircuits.elements.waveguide_coplanar import WaveguideCoplanar
from kqcircuits.util.geometry_helper import vector_length_and_direction, get_angle
[docs]
@add_parameters_from(WaveguideCoplanarStraight, "ground_grid_in_trace")
class Meander(Element):
"""The PCell declaration for a meandering waveguide.
Defined by two points, total length and number of meanders.
The ``start_point`` and ``end_point`` can be moved in GUI using the "Move"-tool.
Alternatively, if a list of ``[x, y]`` coordinates is given for ``start_point`` and ``end_point``,
the GUI markers will not be shown. The latter is useful for
code-generated cells that cannot be edited in the GUI.
By default, the number of meanders is automatically chosen to minimize the area taken by bounding box of
the meander. Uses the same bending radius as the underlying waveguide. Equidistant airbridges can be placed in the
meander using ``n_bridges`` parameter.
.. MARKERS_FOR_PNG 0,0
"""
start_point = Param(pdt.TypeShape, "Start", pya.DPoint(-600, 0))
end_point = Param(pdt.TypeShape, "End", pya.DPoint(600, 0))
length = Param(pdt.TypeDouble, "Length", 3000, unit="μm")
meanders = Param(pdt.TypeInt, "Number of meanders (non-positive means automatic)", -1)
n_bridges = Param(pdt.TypeInt, "Number of bridges", 0)
meander_direction = Param(pdt.TypeInt, "Direction of first bend, +1 or -1", 1)
[docs]
def can_create_from_shape_impl(self):
return self.shape.is_path()
[docs]
def parameters_from_shape_impl(self):
points = [pya.DPoint(point * self.layout.dbu) for point in self.shape.each_point()]
self.start_point = points[0]
self.end_point = points[-1]
[docs]
def build(self):
if isinstance(self.start_point, list):
start = pya.DPoint(self.start_point[0], self.start_point[1])
else:
start = self.start_point
if isinstance(self.end_point, list):
end = pya.DPoint(self.end_point[0], self.end_point[1])
else:
end = self.end_point
angle = 180 / pi * atan2(end.y - start.y, end.x - start.x)
transf = pya.DCplxTrans(1, angle, self.meander_direction < 0, pya.DVector(start))
l_direct = start.distance(end)
if l_direct < 4 * self.r:
self.raise_error_on_cell(
"Cannot create a Meander because start and end points are too close to each other.", (start + end) / 2
)
if self.meanders < 1:
self.meanders = int(l_direct / (2 * self.r) - 1) # automatically choose maximum possible number of meanders
target_increment = self.length - l_direct # target length increment compared to straight segment
if target_increment < -1e-3:
self.raise_error_on_cell(
"Cannot create a Meander with the given parameters. Try increasing the length.", (start + end) / 2
)
def bend_corner_displacement(w):
"""Returns x-displacement of corner point as function of bend width w.
Waveguide starts as straight segment from origin (0,0) and ends with bend pointing towards x at (self.r,w).
"""
if w >= self.r:
return 0.0
return (self.r - w) / (1 - w / (2 * self.r))
def bend_length_increment(w):
"""Returns amount of waveguide length increment due to a single bend as function of bend width w.
Waveguide starts as straight segment from origin (0,0) and ends with bend pointing towards x at (self.r,w).
"""
if w >= self.r:
return self.r * (pi / 2 - 2) + w
h = w / self.r
x = (1 - h) / (1 - h / 2)
return self.r * (2 * atan(1 - x) + (x + h * (h - 1)) / sqrt(x**2 + h**2) - 1)
def meander_length_increment(w):
"""Returns amount of waveguide length increment due to all meander bends as function of meander width w."""
l0 = bend_length_increment(w / 4) # starting and ending bend increment
l1 = bend_length_increment(w / 2) # middle bend increment
return 4 * l0 + 2 * (self.meanders - 1) * l1
# Create meander points
points = [pya.DPoint(0, 0)]
if target_increment > 1e-3:
min_90deg_increment = meander_length_increment(4 * self.r) # minimal length increment for 90-degree bends
if target_increment >= min_90deg_increment: # if all meander bends are 90 degrees
width = 4 * self.r + (target_increment - min_90deg_increment) / self.meanders
else: # computation of meander width is not trivial, so we need to use root finding algorithm
width = brentq(lambda w: meander_length_increment(w) - target_increment, 0.0, 4 * self.r)
l_rest = l_direct / 2 - self.r * self.meanders # distance to first corner
x0 = bend_corner_displacement(width / 4) # x-displacement of corner points in both ends
x1 = bend_corner_displacement(width / 2) # x-displacement of corner points in the middle of meander
points.append(pya.DPoint(l_rest - x0, 0))
points.append(pya.DPoint(l_rest + x0, width / 2))
for i in range(1, self.meanders):
x = l_rest + 2 * self.r * i
points.append(pya.DPoint(x - x1, (-1) ** (i + 1) * width / 2))
points.append(pya.DPoint(x + x1, (-1) ** i * width / 2))
points.append(pya.DPoint(l_direct - l_rest - x0, (-1) ** (self.meanders + 1) * width / 2))
points.append(pya.DPoint(l_direct - l_rest + x0, 0))
points.append(pya.DPoint(l_direct, 0))
# Create airbridges
if self.n_bridges > 0:
self._produce_bridges(points, transf)
# Insert waveguide and ports
waveguide = self.add_element(WaveguideCoplanar, path=points, ground_grid_in_trace=self.ground_grid_in_trace)
wg_inst, _ = self.insert_cell(waveguide, transf)
self.copy_port("a", wg_inst)
self.copy_port("b", wg_inst)
def _produce_bridges(self, points, trans):
"""Produces equally spaced airbridges on top of the meander waveguide.
Args:
points: list of points for the waveguide path
trans: transformation applied to the entire meander
"""
def insert_bridge(position, angle):
self.insert_cell(Airbridge, trans * pya.DCplxTrans(1, angle, False, position))
bridge_separation = self.length / (self.n_bridges + 1)
dist_to_next = bridge_separation
n_inserted = 0
# Insert airbridges on bends and between bends
for i in range(1, len(points) - 1):
v1, _, a1, a2, c_pos = WaveguideCoplanar.get_corner_data(points[i - 1], points[i], points[i + 1], self.r)
alpha = (a2 - a1 + pi) % (2 * pi) - pi # turn angle (between -pi and pi) in radians
sign_r = self.r if alpha > 0 else -self.r # positive or negative radius depending on turn signature
length, direction = vector_length_and_direction(v1)
cut_dist = self.r * tan(abs(alpha) / 2) # distance between corner point and beginning of straights
dist_to_next -= length - cut_dist
while dist_to_next <= 0.0: # insert airbridges on straight segment before corner
insert_bridge(points[i] + (dist_to_next - cut_dist) * direction, degrees(a1))
dist_to_next += bridge_separation
n_inserted += 1
if n_inserted >= self.n_bridges:
return
dist_to_next -= alpha * sign_r
while dist_to_next <= 0.0: # insert airbridges on corner segment
a = a2 + dist_to_next / sign_r
insert_bridge(c_pos + sign_r * pya.DVector(sin(a), -cos(a)), degrees(a))
dist_to_next += bridge_separation
n_inserted += 1
if n_inserted >= self.n_bridges:
return
dist_to_next += cut_dist
# Insert airbridges on the last segment
length, direction = vector_length_and_direction(points[-1] - points[-2])
dist_to_next -= length
while dist_to_next <= 0.0: # insert airbridges on last straight segment
insert_bridge(points[-1] + dist_to_next * direction, get_angle(direction))
dist_to_next += bridge_separation
n_inserted += 1
if n_inserted >= self.n_bridges:
return