# This code is part of KQCircuits
# Copyright (C) 2022 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, atan
from kqcircuits.pya_resolver import pya
from kqcircuits.util.parameters import Param, pdt, add_parameters_from
from kqcircuits.elements.element import Element
from kqcircuits.elements.finger_capacitor_square import FingerCapacitorSquare
[docs]
@add_parameters_from(FingerCapacitorSquare, "fixed_length", "a2", "b2", finger_width=10, finger_gap=5)
class SmoothCapacitor(Element):
"""The PCell declaration for a smooth finger capacitor.
SmoothCapacitor is a finger capacitor, which has continuous geometry changes
through the capacitance range. This leads to continuous capacitance function,
which enables using capacitor inside numerical optimization methods.
Capacitance range is achieved by changing single parameter called `finger_control`.
"""
finger_control = Param(
pdt.TypeDouble,
"Continuously adjust finger number",
2.1,
docstring="Parameter for capacitor growth (related to number of fingers per side)",
)
ground_gap = Param(pdt.TypeDouble, "Gap between ground and finger", 10, unit="μm")
[docs]
def can_create_from_shape_impl(self):
return self.shape.is_path()
[docs]
def segment_points(self, start_pos, start_ang, turn, length):
def unit_vector(radians):
return pya.DVector(cos(radians), sin(radians))
if length == 0:
return []
if turn == 0:
return [start_pos + length * unit_vector(start_ang)]
r = length / turn # signed radius
center = start_pos + r * unit_vector(start_ang + pi / 2) # center of the turn circle
num_pnts = max(round(abs(turn) * self.n / (2 * pi)), 1) # number of new points
return [center - r * unit_vector(start_ang + pi / 2 + turn * (n + 1) / num_pnts) for n in range(num_pnts)]
[docs]
def t_poly(self, bend, length):
r1 = self.finger_width / 2
r2 = r1 + self.finger_gap
# Bottom 180-degree bend starting from origin
ang = 3 * pi / 2
pnts = self.segment_points(pya.DPoint(0, 0), ang, -pi, pi * r1)
ang -= pi
# Bend before straight segment
bend0 = min(bend, pi / 2)
pnts += self.segment_points(pnts[-1], ang, bend0, bend0 * r2)
ang += bend0
# Straight segment, 180-degree bend, and straight segment back
pnts += self.segment_points(pnts[-1], ang, 0.0, length)
pnts += self.segment_points(pnts[-1], ang, -pi, pi * r1)
ang -= pi
pnts += self.segment_points(pnts[-1], ang, 0.0, length)
# Possible turn upwards if bend > pi/2
turn = bend - pi / 2
if turn > 0.0:
pnts += self.segment_points(pnts[-1], ang, turn, turn * r2)
ang += turn
# The last bend back to origin
last_bend = ang + pi / 2
if last_bend > 1e-13:
r3 = pnts[-1].x / (cos(last_bend) - 1)
pnts += self.segment_points(pnts[-1], ang, -last_bend, last_bend * r3)
pnts += self.segment_points(pnts[-1], -pi / 2, 0.0, max(pnts[-1].y, 0.0))
return pya.DPolygon(pnts)
[docs]
def finger_polygon(self, order_number):
if self.finger_control <= order_number: # The finger does not exist for given order_number.
return None
scale = self.finger_width + self.finger_gap
x_max = max(self.finger_control, 1.0 / self.finger_control) * scale - self.finger_gap / 2
trans = pya.DTrans(0, order_number % 2 == 1) * pya.DTrans(x_max, (order_number - 0.5) * scale)
if self.finger_control <= 1.0:
return self.t_poly(0.0, scale).transformed(trans)
t_len = scale * pi / 2 # length of 90-degree turn segment
s_len = scale * (2 * self.finger_control - 3) # length of straight segment
f_len = s_len + 2 * t_len # total length of finger (including two 90-degree turns and straight)
x = (self.finger_control - order_number) * f_len
if x < t_len: # The first turn is not full 90 degrees.
return self.t_poly((x / t_len) * pi / 2, 0.0).transformed(trans)
if s_len < 0.0: # The first turn is limited by finger length. This only happens when order_number=0.
return self.t_poly(pi / 2 - 2 * atan(-s_len / scale), -s_len).transformed(trans)
if x < t_len + s_len: # The straight segment is not full length.
return self.t_poly(pi / 2, x - t_len).transformed(trans)
if x < 2 * f_len - t_len: # The straight segment is full, but the last turn does not exist yet.
return self.t_poly(pi / 2, s_len).transformed(trans)
if x < 2 * f_len: # The last turn is below 90 degrees.
return self.t_poly(pi * (1 + (x - 2 * f_len) / (2 * t_len)), s_len).transformed(trans)
return self.t_poly(pi, s_len).transformed(trans)
[docs]
def super_smoothen_region(self, reg, r):
rr = r / self.layout.dbu
reg_mod = reg.sized(rr, 5).sized(-rr, 5).rounded_corners(rr, 0, self.n).rounded_corners(0, rr, self.n)
reg += reg_mod
return reg.smoothed(1)
[docs]
def get_finger_regions(self):
# List of finger polygons
i = 0
polys = []
while True:
poly = self.finger_polygon(i)
if poly is None:
break
polys.append(poly)
i += 1
# Create finger pad regions
right_fingers = pya.Region([poly.to_itype(self.layout.dbu) for poly in polys])
left_fingers = right_fingers.transformed(pya.Trans(2))
return right_fingers, left_fingers
[docs]
def middle_gap_fill(self):
scale = self.finger_width + self.finger_gap
x_max = max(self.finger_control, 1.0 / self.finger_control) * scale - self.finger_gap / 2
x_mid = x_max - self.finger_width
y = scale / 2
x = (x_mid + x_max) / 2
l = 2 * x if self.finger_control < 1 else scale
rr = (self.finger_width / 2 + self.ground_gap) / self.layout.dbu
return (
pya.Region(
pya.DPolygon(
[pya.DPoint(-x, y), pya.DPoint(l - x, y), pya.DPoint(x, -y), pya.DPoint(x - l, -y)]
).to_itype(self.layout.dbu)
)
.sized(rr, 5)
.rounded_corners(rr, rr, self.n)
)
[docs]
def insert_wg_joint(self, reg, x0, xr, r):
rr = r / self.layout.dbu
reg += pya.Region(pya.DBox(xr, -r, 2 * x0 - xr, r).to_itype(self.layout.dbu)).rounded_corners(rr, rr, self.n)
reg -= pya.Region(pya.DBox(x0, -r, 2 * x0 - xr, r).to_itype(self.layout.dbu))
[docs]
def build(self):
# constants
scale = self.finger_width + self.finger_gap
x_max = max(self.finger_control, 1.0 / self.finger_control) * scale - self.finger_gap / 2
x_mid = x_max - self.finger_width
xport = x_max + self.ground_gap
# Ground etch region
right_fingers, left_fingers = self.get_finger_regions()
region_ground = right_fingers + left_fingers
region_ground.size(self.ground_gap / self.layout.dbu, 5)
region_ground += self.middle_gap_fill()
a2 = self.a if self.a2 < 0 else self.a2
b2 = self.b if self.b2 < 0 else self.b2
self.insert_wg_joint(region_ground, xport, x_mid - self.ground_gap, b2 + a2 / 2)
self.insert_wg_joint(region_ground, -xport, -x_mid + self.ground_gap, self.b + self.a / 2)
region_ground = self.super_smoothen_region(region_ground, self.finger_gap + self.ground_gap)
# Finalize finger pad regions
self.insert_wg_joint(right_fingers, xport, x_mid, a2 / 2)
self.insert_wg_joint(left_fingers, -xport, -x_mid, self.a / 2)
right_fingers = self.super_smoothen_region(right_fingers, self.finger_gap)
left_fingers = self.super_smoothen_region(left_fingers, self.finger_gap)
# Insert waveguide segments in both ends, if fixed_length is set
if self.fixed_length != 0:
xfixed = self.fixed_length / 2
if xfixed < xport:
raise ValueError(f"SmoothCapacitor parameters not compatible with fixed_length={self.fixed_length}")
region_ground += pya.Region(
pya.DBox(xport, -b2 - a2 / 2, xfixed, b2 + a2 / 2).to_itype(self.layout.dbu)
) + pya.Region(
pya.DBox(-xfixed, -self.b - self.a / 2, -xport, self.b + self.a / 2).to_itype(self.layout.dbu)
)
else:
xfixed = xport
# Always insert tolerance to secure trace connection
right_fingers += pya.Region(pya.DBox(xport - 0.001, -a2 / 2, xfixed + 1, a2 / 2).to_itype(self.layout.dbu))
left_fingers += pya.Region(
pya.DBox(-xfixed - 1, -self.a / 2, -xport + 0.001, self.a / 2).to_itype(self.layout.dbu)
)
xport = xfixed
# Create shapes into cell
region = region_ground - right_fingers - left_fingers
self.cell.shapes(self.get_layer("base_metal_gap_wo_grid")).insert(region)
# protection
region_protection = region_ground.sized(self.margin / self.layout.dbu, 5)
self.add_protection(region_protection)
# Add size into annotation layer
self.cell.shapes(self.get_layer("annotations")).insert(pya.DText(str(round(self.finger_control, 5)), 0, 0))
# Create ports
self.add_port("a", pya.DPoint(-xport, 0), pya.DVector(-1, 0))
self.add_port("b", pya.DPoint(xport, 0), pya.DVector(1, 0))
[docs]
@classmethod
def get_sim_ports(cls, simulation):
return Element.left_and_right_waveguides(simulation)