From b36c45dec97e39cab6ce91f29c8938875c73ca93 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Wed, 13 Nov 2019 10:40:38 +0100 Subject: [PATCH 01/11] Added new spiral part where length can be set --- gdshelpers/parts/spiral.py | 268 +++++++++++++++++++++++++------------ 1 file changed, 184 insertions(+), 84 deletions(-) diff --git a/gdshelpers/parts/spiral.py b/gdshelpers/parts/spiral.py index d50d2f7..058c3a5 100644 --- a/gdshelpers/parts/spiral.py +++ b/gdshelpers/parts/spiral.py @@ -1,122 +1,222 @@ import numpy as np - -from gdshelpers.geometry import geometric_union -from gdshelpers.parts import Port from gdshelpers.parts.waveguide import Waveguide +from gdshelpers.parts.port import Port + + +def _arc_length_indefinite_integral(theta, a, b): + """ + The indefinite integral + + .. math:: + \f{x} = \int r^2 + \frac{\,dr}{\,d\theta}\,d\theta + + which is needed to calculate the arc length of a spiral. + """ + return np.sqrt( np.square((a + b * theta)) + np.square(b) ) * (a+b*theta) / (2*b) + \ + 0.5 * b * np.log(np.sqrt( np.square(a + b*theta) + np.square(b) ) + a + b*theta ) + +def _arc_length_integral(theta, a, b): + return _arc_length_indefinite_integral(theta, a, b) - _arc_length_indefinite_integral(0, a, b) + +def spiral_length_angle(theta, a, b, out_angle): + return (_arc_length_integral(theta, a, b) + # Inward spiral + np.pi * a + # Two semi-circles in the center + _arc_length_integral(theta + out_angle, a, b)) # Outward spiral + +def spiral_length_inline(theta, a, b): + return (spiral_length_angle(theta, a, b, 0.5*np.pi) + + 0.5 * np.pi * (0.5 * a) + # right bend at the end (min_bend_radius = (0.5 * a)) + ((a + b * theta) - (0.5 * a))) # from endpoint to startpoint height + +def spiral_length_inline_rel(theta, a, b): + return (spiral_length_inline(theta, a, b) - + (a + b * (theta + 0.5 * np.pi) + (0.5 * a))) # subtract the direct path from input to output + +def spiral_theta(length, wg_width, gap, min_bend_radius, length_function, *args): + """ + Numerically calculate the theta that is needed for a given spiral variant (length_function) in order + to have the desired length. + + :param length_function: A function which takes theta, a, b plus arbitrary *args. + """ + from scipy.optimize import fsolve + a = 2*min_bend_radius + b = 2*(wg_width + gap) / (2.*np.pi) + return fsolve(lambda x: length_function(x, a, b, *args) - length, 20*np.pi) + +def spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0): + theta = min_theta + t * (max_theta - min_theta) + r = a + b * theta + return r * np.array([np.sin(theta + theta_offset), -np.cos(theta + theta_offset)]) + np.array([0, a])[:, None] + +def d_spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0): + theta = min_theta + t * (max_theta - min_theta) + r = a + b * theta + return r * np.array([np.cos(theta + theta_offset), np.sin(theta + theta_offset)]) -class Spiral: - def __init__(self, origin, angle, width, num, gap, inner_gap): +class Spiral(object): + """ + An archimedean spiral, where the length can be numerically calculated. + + Options for the output position are: + + * `inline` + Output on the same height and direction as input. + + * `inline_rel` + Output on the same height and direction as input, but the considered length is only the difference of the spiral compared to a straight path. + This is useful when building MZIs, where the other path is parallel to the spiral. + + * `opposite` + Output at opposite direction as input + + * `single_inside` + The spiral will only do a single turn and stop in the inside + + * `single_outside` + The spiral will only do a single turn, but start in the inside and stop at the outside + + * , where phi is the angle where the output should be located + """ + + def __init__(self, origin, angle, width, gap, min_bend_radius, theta, output_type='opposite', sample_distance=0.50, sample_points=100): """ - Creates a Spiral around the given origin - - :param origin: position of the center of the spiral - :param angle: angle of the outer two waveguides - :param width: width of the waveguide - :param num: number of turns - :param gap: gap between two waveguides - :param inner_gap: inner radius of the spiral + Create an archimedean spiral following the spiral equation :math:`r = a + b \theta`. + + :param origin: + :param angle: + :param width: + :param gap: Gap between the waveguide. Since this is an archimedean spiral, the gap is constant across the whole spiral. + :param min_bend_radius: The minimum bend radius. This will set the bend of the two semi-circles in the center. + It follows that `a = 2 * min_bend_radius`, where `a` as defined in the spiral equation above. + :param theta: The total angle to turn """ self._origin_port = Port(origin, angle, width) self.gap = gap - self.inner_gap = inner_gap - self.num = num - self.wg_in = None - self.wg_out = None + self.min_bend_radius = min_bend_radius + self.total_theta = theta + self._wg = None + self.sample_points = sample_points + self.sample_distance = sample_distance + self.output_type = output_type + + if self.output_type == "inline" or self.output_type == "inline_rel": + self.out_theta = self.total_theta + 0.5*np.pi + elif self.output_type == "opposite": + self.out_theta = self.total_theta + elif self.output_type == "single_inside" or self.output_type == "single_outside": + self.out_theta = self.total_theta + else: + self.out_theta = self.total_theta + self.output_type @classmethod - def make_at_port(cls, port, num, gap, inner_gap): - """ - Creates a Spiral around the given port + def make_at_port(cls, port, **kwargs): + default_port_param = dict(port.get_parameters()) + default_port_param.update(kwargs) + del default_port_param['origin'] + del default_port_param['angle'] + del default_port_param['width'] - :param port: port at which the spiral starts - :param num: number of turns - :param gap: gap between two waveguides - :param inner_gap: inner radius of the spiral - """ - return cls(port.parallel_offset(-num * (port.total_width + gap) - inner_gap).origin, - port.angle, port.width, num, gap, inner_gap) + return cls(port.origin, port.angle, port.width, **default_port_param) - ### - # Let's allow the user to change the values - # hidden in _origin_port. Hence the internal use - # of a Port is transparent. - @property - def origin(self): - return self._origin_port.origin - - @origin.setter - def origin(self, origin): - self._origin_port.origin = origin - - @property - def angle(self): - return self._origin_port.angle - - @angle.setter - def angle(self, angle): - self._origin_port.angle = angle + @classmethod + def make_at_port_with_length(cls, port, gap, min_bend_radius, target_length, output_type='opposite', **kwargs): + if output_type == "inline": + length_fn = [spiral_length_inline] + elif output_type == "inline_rel": + length_fn = [spiral_length_inline_rel] + elif output_type == "opposite": + length_fn = [spiral_length_angle, 0] + elif output_type == "single_inside" or output_type == "single_outside": + length_fn = [_arc_length_integral] + else: + length_fn = [spiral_length_angle, output_type] + + theta = float(spiral_theta(target_length, port.width, gap, min_bend_radius, *length_fn)) + return cls.make_at_port(port, gap=gap, min_bend_radius=min_bend_radius, theta=theta, output_type=output_type, **kwargs) @property def width(self): return self._origin_port.width - @width.setter - def width(self, width): - self._origin_port.width = width + @property + def wg(self): + if not self._wg: + self._generate() + return self._wg @property - def in_port(self): - return self._origin_port.inverted_direction.parallel_offset( - -self.num * (self._origin_port.total_width + self.gap) - self.inner_gap) + def length(self): + return self.wg.length @property def out_port(self): - return self._origin_port.parallel_offset( - -self.num * (self._origin_port.total_width + self.gap) - self.inner_gap) + return self.wg.current_port - @property - def length(self): - if not self.wg_in or not self.wg_out: + """@property + def port_offset(self): + if not self.wg: self._generate() - return self.wg_in.length + self.wg_out.length + + return self._origin_port.origin - self.wg.current_port.origin + #return np.sqrt(diff[0]**2 + diff[1]**2) + + @property + def relative_length(self): + # Return the length of the delay line with the distance between input and output subtracted (hence the real path difference when putting it in an MZI) + return self.length - self.port_offset + """ def _generate(self): - def path(a): - return (self.num * (self._origin_port.total_width + self.gap) * np.abs(1 - a) + self.inner_gap) * np.array( - (np.sin(np.pi * a * self.num), np.cos(np.pi * a * self.num))) + self._wg = Waveguide.make_at_port(self._origin_port) + a = 2*self.min_bend_radius + b = 2*(self.width + self.gap) / (2.*np.pi) + outer_r = (a + b*self.total_theta) + + if self.output_type != "single_outside": + self._wg.add_parameterized_path(lambda x: -spiral_out_path(1-x, a=a, b=b, max_theta=self.total_theta, theta_offset=-self.total_theta) - np.array([0, -a + outer_r])[:, None], sample_distance=self.sample_distance, sample_points=self.sample_points, + path_derivative=lambda x: d_spiral_out_path(1-x, a=a, b=b, max_theta=self.total_theta, theta_offset=-self.total_theta), + path_function_supports_numpy=True) - self.wg_in = Waveguide.make_at_port(self._origin_port) - self.wg_in.add_parameterized_path(path) + if self.output_type != "single_inside" and self.output_type != "single_outside": + self._wg.add_bend(-np.pi, self.min_bend_radius) + self._wg.add_bend(np.pi, self.min_bend_radius) - self.wg_out = Waveguide.make_at_port(self._origin_port.inverted_direction) - self.wg_out.add_parameterized_path(path) + if self.output_type != "single_inside": + self._wg.add_parameterized_path(lambda x: spiral_out_path(x, a=a, b=b, max_theta=self.out_theta), sample_distance=self.sample_distance, sample_points=self.sample_points, + path_derivative=lambda x: d_spiral_out_path(x, a=a, b=b, max_theta=self.out_theta), + path_function_supports_numpy=True) - self.wg_in.add_route_single_circle_to_port(self._origin_port.rotated(-np.pi * (self.num % 2))) - self.wg_in.add_route_single_circle_to_port(self.wg_out.port) + if self.output_type == "inline" or self.output_type == "inline_rel": + self._wg.add_straight_segment((outer_r - self.min_bend_radius)) + self._wg.add_bend(-0.5*np.pi, self.min_bend_radius) def get_shapely_object(self): - if not self.wg_in or not self.wg_out: - self._generate() - return geometric_union([self.wg_in, self.wg_out]) + return self.wg.get_shapely_object() -def _example(): +if __name__ == '__main__': from gdshelpers.geometry.chip import Cell + from gdshelpers.parts.text import Text + cell = Cell('Spiral') - wg = Waveguide((0, 0), 1, 1) - wg.add_straight_segment(30) - spiral = Spiral.make_at_port(wg.current_port, 2, 5, 50) - print(spiral.out_port.origin) - wg2 = Waveguide.make_at_port(spiral.out_port) - wg2.add_straight_segment(100) - - print(spiral.length) + def demo_spiral(origin, output_type, target_length, gap, port_y_offset=0): + wg = Waveguide(origin + np.array([0, port_y_offset]), 0, 1) + wg.add_straight_segment(30) + spiral = Spiral.make_at_port_with_length(wg.current_port, gap=gap, min_bend_radius=35., target_length=target_length, output_type=output_type, sample_distance=1) + text = Text(np.array([150, -130]) + origin, 20, "output: {}\n\nlength: {} um\nreal_length: {:.4f}um".format(output_type, target_length, spiral.length)) + spiral.wg.add_straight_segment(30) + cell.add_to_layer(1, wg, spiral) + cell.add_to_layer(2, text) - cell = Cell('Spiral') - cell.add_to_layer(1, wg, spiral, wg2) - cell.show() + # Create normal demo spirals + for i,output_type in enumerate(['opposite', 'inline', 'inline_rel', -0.5*np.pi, 0.25*np.pi, np.pi]): + demo_spiral(((i//4)*700, (i%4)*250), output_type, 2000, gap=5.) + # Create spirals with single turn + demo_spiral((1*700, 2*250), 'single_inside', 2000, gap=1.5) + demo_spiral((1*700, 3*250), 'single_outside', 2000, gap=1.5, port_y_offset=-150) -if __name__ == '__main__': - _example() + cell.show() + #cell.save("spiral_test") From c47687e1c071ac1478e717c4adfd331d2cf992b9 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Wed, 13 Nov 2019 11:09:17 +0100 Subject: [PATCH 02/11] Allow setting the winding direction of the spiral. --- gdshelpers/parts/spiral.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/gdshelpers/parts/spiral.py b/gdshelpers/parts/spiral.py index 058c3a5..2a6d35a 100644 --- a/gdshelpers/parts/spiral.py +++ b/gdshelpers/parts/spiral.py @@ -44,15 +44,15 @@ def spiral_theta(length, wg_width, gap, min_bend_radius, length_function, *args) b = 2*(wg_width + gap) / (2.*np.pi) return fsolve(lambda x: length_function(x, a, b, *args) - length, 20*np.pi) -def spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0): +def spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0, direction=-1): theta = min_theta + t * (max_theta - min_theta) r = a + b * theta - return r * np.array([np.sin(theta + theta_offset), -np.cos(theta + theta_offset)]) + np.array([0, a])[:, None] + return r * np.array([np.sin(theta + theta_offset), -direction*np.cos(theta + theta_offset)]) + np.array([0, direction*a])[:, None] -def d_spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0): +def d_spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0, direction=-1): theta = min_theta + t * (max_theta - min_theta) r = a + b * theta - return r * np.array([np.cos(theta + theta_offset), np.sin(theta + theta_offset)]) + return r * np.array([np.cos(theta + theta_offset), direction*np.sin(theta + theta_offset)]) class Spiral(object): @@ -80,7 +80,7 @@ class Spiral(object): * , where phi is the angle where the output should be located """ - def __init__(self, origin, angle, width, gap, min_bend_radius, theta, output_type='opposite', sample_distance=0.50, sample_points=100): + def __init__(self, origin, angle, width, gap, min_bend_radius, theta, output_type='opposite', winding_direction='right', sample_distance=0.50, sample_points=100): """ Create an archimedean spiral following the spiral equation :math:`r = a + b \theta`. @@ -100,6 +100,7 @@ def __init__(self, origin, angle, width, gap, min_bend_radius, theta, output_typ self.sample_points = sample_points self.sample_distance = sample_distance self.output_type = output_type + self.winding_direction = -1 if winding_direction == "left" else 1 if self.output_type == "inline" or self.output_type == "inline_rel": self.out_theta = self.total_theta + 0.5*np.pi @@ -175,22 +176,22 @@ def _generate(self): outer_r = (a + b*self.total_theta) if self.output_type != "single_outside": - self._wg.add_parameterized_path(lambda x: -spiral_out_path(1-x, a=a, b=b, max_theta=self.total_theta, theta_offset=-self.total_theta) - np.array([0, -a + outer_r])[:, None], sample_distance=self.sample_distance, sample_points=self.sample_points, - path_derivative=lambda x: d_spiral_out_path(1-x, a=a, b=b, max_theta=self.total_theta, theta_offset=-self.total_theta), + self._wg.add_parameterized_path(lambda x: -spiral_out_path(1-x, a=a, b=b, max_theta=self.total_theta, theta_offset=-self.total_theta, direction=self.winding_direction) - self.winding_direction*np.array([0, -a + outer_r])[:, None], sample_distance=self.sample_distance, sample_points=self.sample_points, + path_derivative=lambda x: d_spiral_out_path(1-x, a=a, b=b, max_theta=self.total_theta, theta_offset=-self.total_theta, direction=self.winding_direction), path_function_supports_numpy=True) if self.output_type != "single_inside" and self.output_type != "single_outside": - self._wg.add_bend(-np.pi, self.min_bend_radius) - self._wg.add_bend(np.pi, self.min_bend_radius) + self._wg.add_bend(-self.winding_direction*np.pi, self.min_bend_radius) + self._wg.add_bend(self.winding_direction*np.pi, self.min_bend_radius) if self.output_type != "single_inside": - self._wg.add_parameterized_path(lambda x: spiral_out_path(x, a=a, b=b, max_theta=self.out_theta), sample_distance=self.sample_distance, sample_points=self.sample_points, - path_derivative=lambda x: d_spiral_out_path(x, a=a, b=b, max_theta=self.out_theta), + self._wg.add_parameterized_path(lambda x: spiral_out_path(x, a=a, b=b, max_theta=self.out_theta, direction=self.winding_direction), sample_distance=self.sample_distance, sample_points=self.sample_points, + path_derivative=lambda x: d_spiral_out_path(x, a=a, b=b, max_theta=self.out_theta, direction=self.winding_direction), path_function_supports_numpy=True) if self.output_type == "inline" or self.output_type == "inline_rel": self._wg.add_straight_segment((outer_r - self.min_bend_radius)) - self._wg.add_bend(-0.5*np.pi, self.min_bend_radius) + self._wg.add_bend(-0.5*self.winding_direction*np.pi, self.min_bend_radius) def get_shapely_object(self): return self.wg.get_shapely_object() From ffcbc2da7d6ca098531bed3cba7b9ee3fd00047d Mon Sep 17 00:00:00 2001 From: fbeutel Date: Wed, 13 Nov 2019 13:38:51 +0100 Subject: [PATCH 03/11] minor fix --- gdshelpers/parts/spiral.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdshelpers/parts/spiral.py b/gdshelpers/parts/spiral.py index 2a6d35a..b926d5c 100644 --- a/gdshelpers/parts/spiral.py +++ b/gdshelpers/parts/spiral.py @@ -55,7 +55,7 @@ def d_spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0, direction return r * np.array([np.cos(theta + theta_offset), direction*np.sin(theta + theta_offset)]) -class Spiral(object): +class Spiral: """ An archimedean spiral, where the length can be numerically calculated. From c7bd62660bfd61a0c03f39ffa38ead2bfc9db67e Mon Sep 17 00:00:00 2001 From: fbeutel Date: Thu, 14 Nov 2019 19:37:18 +0100 Subject: [PATCH 04/11] Rename private methods with leading underscore --- gdshelpers/parts/spiral.py | 48 ++++++++++++++------------------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/gdshelpers/parts/spiral.py b/gdshelpers/parts/spiral.py index b926d5c..226e124 100644 --- a/gdshelpers/parts/spiral.py +++ b/gdshelpers/parts/spiral.py @@ -18,21 +18,21 @@ def _arc_length_indefinite_integral(theta, a, b): def _arc_length_integral(theta, a, b): return _arc_length_indefinite_integral(theta, a, b) - _arc_length_indefinite_integral(0, a, b) -def spiral_length_angle(theta, a, b, out_angle): +def _spiral_length_angle(theta, a, b, out_angle): return (_arc_length_integral(theta, a, b) + # Inward spiral np.pi * a + # Two semi-circles in the center _arc_length_integral(theta + out_angle, a, b)) # Outward spiral -def spiral_length_inline(theta, a, b): - return (spiral_length_angle(theta, a, b, 0.5*np.pi) + +def _spiral_length_inline(theta, a, b): + return (_spiral_length_angle(theta, a, b, 0.5*np.pi) + 0.5 * np.pi * (0.5 * a) + # right bend at the end (min_bend_radius = (0.5 * a)) ((a + b * theta) - (0.5 * a))) # from endpoint to startpoint height -def spiral_length_inline_rel(theta, a, b): - return (spiral_length_inline(theta, a, b) - +def _spiral_length_inline_rel(theta, a, b): + return (_spiral_length_inline(theta, a, b) - (a + b * (theta + 0.5 * np.pi) + (0.5 * a))) # subtract the direct path from input to output -def spiral_theta(length, wg_width, gap, min_bend_radius, length_function, *args): +def _spiral_theta(length, wg_width, gap, min_bend_radius, length_function, *args): """ Numerically calculate the theta that is needed for a given spiral variant (length_function) in order to have the desired length. @@ -44,12 +44,12 @@ def spiral_theta(length, wg_width, gap, min_bend_radius, length_function, *args) b = 2*(wg_width + gap) / (2.*np.pi) return fsolve(lambda x: length_function(x, a, b, *args) - length, 20*np.pi) -def spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0, direction=-1): +def _spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0, direction=-1): theta = min_theta + t * (max_theta - min_theta) r = a + b * theta return r * np.array([np.sin(theta + theta_offset), -direction*np.cos(theta + theta_offset)]) + np.array([0, direction*a])[:, None] -def d_spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0, direction=-1): +def _d_spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0, direction=-1): theta = min_theta + t * (max_theta - min_theta) r = a + b * theta return r * np.array([np.cos(theta + theta_offset), direction*np.sin(theta + theta_offset)]) @@ -124,17 +124,17 @@ def make_at_port(cls, port, **kwargs): @classmethod def make_at_port_with_length(cls, port, gap, min_bend_radius, target_length, output_type='opposite', **kwargs): if output_type == "inline": - length_fn = [spiral_length_inline] + length_fn = [_spiral_length_inline] elif output_type == "inline_rel": - length_fn = [spiral_length_inline_rel] + length_fn = [_spiral_length_inline_rel] elif output_type == "opposite": - length_fn = [spiral_length_angle, 0] + length_fn = [_spiral_length_angle, 0] elif output_type == "single_inside" or output_type == "single_outside": length_fn = [_arc_length_integral] else: - length_fn = [spiral_length_angle, output_type] + length_fn = [_spiral_length_angle, output_type] - theta = float(spiral_theta(target_length, port.width, gap, min_bend_radius, *length_fn)) + theta = float(_spiral_theta(target_length, port.width, gap, min_bend_radius, *length_fn)) return cls.make_at_port(port, gap=gap, min_bend_radius=min_bend_radius, theta=theta, output_type=output_type, **kwargs) @property @@ -155,20 +155,6 @@ def length(self): def out_port(self): return self.wg.current_port - """@property - def port_offset(self): - if not self.wg: - self._generate() - - return self._origin_port.origin - self.wg.current_port.origin - #return np.sqrt(diff[0]**2 + diff[1]**2) - - @property - def relative_length(self): - # Return the length of the delay line with the distance between input and output subtracted (hence the real path difference when putting it in an MZI) - return self.length - self.port_offset - """ - def _generate(self): self._wg = Waveguide.make_at_port(self._origin_port) a = 2*self.min_bend_radius @@ -176,8 +162,8 @@ def _generate(self): outer_r = (a + b*self.total_theta) if self.output_type != "single_outside": - self._wg.add_parameterized_path(lambda x: -spiral_out_path(1-x, a=a, b=b, max_theta=self.total_theta, theta_offset=-self.total_theta, direction=self.winding_direction) - self.winding_direction*np.array([0, -a + outer_r])[:, None], sample_distance=self.sample_distance, sample_points=self.sample_points, - path_derivative=lambda x: d_spiral_out_path(1-x, a=a, b=b, max_theta=self.total_theta, theta_offset=-self.total_theta, direction=self.winding_direction), + self._wg.add_parameterized_path(lambda x: -_spiral_out_path(1-x, a=a, b=b, max_theta=self.total_theta, theta_offset=-self.total_theta, direction=self.winding_direction) - self.winding_direction*np.array([0, -a + outer_r])[:, None], sample_distance=self.sample_distance, sample_points=self.sample_points, + path_derivative=lambda x: _d_spiral_out_path(1-x, a=a, b=b, max_theta=self.total_theta, theta_offset=-self.total_theta, direction=self.winding_direction), path_function_supports_numpy=True) if self.output_type != "single_inside" and self.output_type != "single_outside": @@ -185,8 +171,8 @@ def _generate(self): self._wg.add_bend(self.winding_direction*np.pi, self.min_bend_radius) if self.output_type != "single_inside": - self._wg.add_parameterized_path(lambda x: spiral_out_path(x, a=a, b=b, max_theta=self.out_theta, direction=self.winding_direction), sample_distance=self.sample_distance, sample_points=self.sample_points, - path_derivative=lambda x: d_spiral_out_path(x, a=a, b=b, max_theta=self.out_theta, direction=self.winding_direction), + self._wg.add_parameterized_path(lambda x: _spiral_out_path(x, a=a, b=b, max_theta=self.out_theta, direction=self.winding_direction), sample_distance=self.sample_distance, sample_points=self.sample_points, + path_derivative=lambda x: _d_spiral_out_path(x, a=a, b=b, max_theta=self.out_theta, direction=self.winding_direction), path_function_supports_numpy=True) if self.output_type == "inline" or self.output_type == "inline_rel": From 51092eef42743cc52bd2e5722914032251872cd9 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Thu, 14 Nov 2019 19:42:27 +0100 Subject: [PATCH 05/11] Rename new spiral to Spiral2 and re-add old spiral --- gdshelpers/parts/spiral.py | 121 ++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/gdshelpers/parts/spiral.py b/gdshelpers/parts/spiral.py index 226e124..8962013 100644 --- a/gdshelpers/parts/spiral.py +++ b/gdshelpers/parts/spiral.py @@ -1,8 +1,123 @@ import numpy as np + +from gdshelpers.geometry import geometric_union from gdshelpers.parts.waveguide import Waveguide from gdshelpers.parts.port import Port +class Spiral: + def __init__(self, origin, angle, width, num, gap, inner_gap): + """ + Creates a Spiral around the given origin + + :param origin: position of the center of the spiral + :param angle: angle of the outer two waveguides + :param width: width of the waveguide + :param num: number of turns + :param gap: gap between two waveguides + :param inner_gap: inner radius of the spiral + """ + self._origin_port = Port(origin, angle, width) + self.gap = gap + self.inner_gap = inner_gap + self.num = num + self.wg_in = None + self.wg_out = None + + @classmethod + def make_at_port(cls, port, num, gap, inner_gap): + """ + Creates a Spiral around the given port + + :param port: port at which the spiral starts + :param num: number of turns + :param gap: gap between two waveguides + :param inner_gap: inner radius of the spiral + """ + return cls(port.parallel_offset(-num * (port.total_width + gap) - inner_gap).origin, + port.angle, port.width, num, gap, inner_gap) + + ### + # Let's allow the user to change the values + # hidden in _origin_port. Hence the internal use + # of a Port is transparent. + @property + def origin(self): + return self._origin_port.origin + + @origin.setter + def origin(self, origin): + self._origin_port.origin = origin + + @property + def angle(self): + return self._origin_port.angle + + @angle.setter + def angle(self, angle): + self._origin_port.angle = angle + + @property + def width(self): + return self._origin_port.width + + @width.setter + def width(self, width): + self._origin_port.width = width + + @property + def in_port(self): + return self._origin_port.inverted_direction.parallel_offset( + -self.num * (self._origin_port.total_width + self.gap) - self.inner_gap) + + @property + def out_port(self): + return self._origin_port.parallel_offset( + -self.num * (self._origin_port.total_width + self.gap) - self.inner_gap) + + @property + def length(self): + if not self.wg_in or not self.wg_out: + self._generate() + return self.wg_in.length + self.wg_out.length + + def _generate(self): + def path(a): + return (self.num * (self._origin_port.total_width + self.gap) * np.abs(1 - a) + self.inner_gap) * np.array( + (np.sin(np.pi * a * self.num), np.cos(np.pi * a * self.num))) + + self.wg_in = Waveguide.make_at_port(self._origin_port) + self.wg_in.add_parameterized_path(path) + + self.wg_out = Waveguide.make_at_port(self._origin_port.inverted_direction) + self.wg_out.add_parameterized_path(path) + + self.wg_in.add_route_single_circle_to_port(self._origin_port.rotated(-np.pi * (self.num % 2))) + self.wg_in.add_route_single_circle_to_port(self.wg_out.port) + + def get_shapely_object(self): + if not self.wg_in or not self.wg_out: + self._generate() + return geometric_union([self.wg_in, self.wg_out]) + + +def _example_old(): + from gdshelpers.geometry.chip import Cell + + wg = Waveguide((0, 0), 1, 1) + wg.add_straight_segment(30) + spiral = Spiral.make_at_port(wg.current_port, 2, 5, 50) + print(spiral.out_port.origin) + wg2 = Waveguide.make_at_port(spiral.out_port) + wg2.add_straight_segment(100) + + print(spiral.length) + + cell = Cell('Spiral') + cell.add_to_layer(1, wg, spiral, wg2) + cell.show() + + def _arc_length_indefinite_integral(theta, a, b): """ The indefinite integral @@ -55,7 +170,7 @@ def _d_spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0, directio return r * np.array([np.cos(theta + theta_offset), direction*np.sin(theta + theta_offset)]) -class Spiral: +class Spiral2: """ An archimedean spiral, where the length can be numerically calculated. @@ -191,7 +306,7 @@ def get_shapely_object(self): def demo_spiral(origin, output_type, target_length, gap, port_y_offset=0): wg = Waveguide(origin + np.array([0, port_y_offset]), 0, 1) wg.add_straight_segment(30) - spiral = Spiral.make_at_port_with_length(wg.current_port, gap=gap, min_bend_radius=35., target_length=target_length, output_type=output_type, sample_distance=1) + spiral = Spiral2.make_at_port_with_length(wg.current_port, gap=gap, min_bend_radius=35., target_length=target_length, output_type=output_type, sample_distance=1) text = Text(np.array([150, -130]) + origin, 20, "output: {}\n\nlength: {} um\nreal_length: {:.4f}um".format(output_type, target_length, spiral.length)) spiral.wg.add_straight_segment(30) cell.add_to_layer(1, wg, spiral) @@ -206,4 +321,4 @@ def demo_spiral(origin, output_type, target_length, gap, port_y_offset=0): demo_spiral((1*700, 3*250), 'single_outside', 2000, gap=1.5, port_y_offset=-150) cell.show() - #cell.save("spiral_test") + cell.save("spiral_test") From b21b0a18db686a0c7811677bb653a3ce5d6d3872 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Thu, 14 Nov 2019 20:02:10 +0100 Subject: [PATCH 06/11] Spiral2 now also works with waveguides of multiple widths (coplanar waveguides) --- gdshelpers/parts/spiral.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gdshelpers/parts/spiral.py b/gdshelpers/parts/spiral.py index 8962013..c845815 100644 --- a/gdshelpers/parts/spiral.py +++ b/gdshelpers/parts/spiral.py @@ -156,7 +156,7 @@ def _spiral_theta(length, wg_width, gap, min_bend_radius, length_function, *args """ from scipy.optimize import fsolve a = 2*min_bend_radius - b = 2*(wg_width + gap) / (2.*np.pi) + b = 2*(np.sum(wg_width) + gap) / (2.*np.pi) return fsolve(lambda x: length_function(x, a, b, *args) - length, 20*np.pi) def _spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0, direction=-1): @@ -273,7 +273,7 @@ def out_port(self): def _generate(self): self._wg = Waveguide.make_at_port(self._origin_port) a = 2*self.min_bend_radius - b = 2*(self.width + self.gap) / (2.*np.pi) + b = 2*(np.sum(self.width) + self.gap) / (2.*np.pi) outer_r = (a + b*self.total_theta) if self.output_type != "single_outside": @@ -303,8 +303,8 @@ def get_shapely_object(self): from gdshelpers.parts.text import Text cell = Cell('Spiral') - def demo_spiral(origin, output_type, target_length, gap, port_y_offset=0): - wg = Waveguide(origin + np.array([0, port_y_offset]), 0, 1) + def demo_spiral(origin, output_type, target_length, gap, port_y_offset=0, width=1): + wg = Waveguide(origin + np.array([0, port_y_offset]), 0, width) wg.add_straight_segment(30) spiral = Spiral2.make_at_port_with_length(wg.current_port, gap=gap, min_bend_radius=35., target_length=target_length, output_type=output_type, sample_distance=1) text = Text(np.array([150, -130]) + origin, 20, "output: {}\n\nlength: {} um\nreal_length: {:.4f}um".format(output_type, target_length, spiral.length)) @@ -314,7 +314,7 @@ def demo_spiral(origin, output_type, target_length, gap, port_y_offset=0): # Create normal demo spirals for i,output_type in enumerate(['opposite', 'inline', 'inline_rel', -0.5*np.pi, 0.25*np.pi, np.pi]): - demo_spiral(((i//4)*700, (i%4)*250), output_type, 2000, gap=5.) + demo_spiral(((i//4)*700, (i%4)*250), output_type, 2000, gap=6., width=[1, 3, 1, 3, 1]) # Create spirals with single turn demo_spiral((1*700, 2*250), 'single_inside', 2000, gap=1.5) From bdbc195f6aef4d53d0a35606b4c1b7aefd7d4f4c Mon Sep 17 00:00:00 2001 From: fbeutel Date: Mon, 18 Nov 2019 17:24:20 +0100 Subject: [PATCH 07/11] inline and inline_rel spiral are now more symmetric for coplanar waveguides --- gdshelpers/parts/spiral.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/gdshelpers/parts/spiral.py b/gdshelpers/parts/spiral.py index c845815..7c4f5e5 100644 --- a/gdshelpers/parts/spiral.py +++ b/gdshelpers/parts/spiral.py @@ -139,9 +139,10 @@ def _spiral_length_angle(theta, a, b, out_angle): _arc_length_integral(theta + out_angle, a, b)) # Outward spiral def _spiral_length_inline(theta, a, b): - return (_spiral_length_angle(theta, a, b, 0.5*np.pi) + - 0.5 * np.pi * (0.5 * a) + # right bend at the end (min_bend_radius = (0.5 * a)) - ((a + b * theta) - (0.5 * a))) # from endpoint to startpoint height + return (_spiral_length_angle(theta, a, b, 0) + + (a + b*(theta+0.5*np.pi)) - (0.5 * a) + + np.pi * (0.5 * a) + # two bends + (2*(a + b * theta) - 2*(0.5 * a))) # from endpoint to startpoint height def _spiral_length_inline_rel(theta, a, b): return (_spiral_length_inline(theta, a, b) - @@ -218,7 +219,7 @@ def __init__(self, origin, angle, width, gap, min_bend_radius, theta, output_typ self.winding_direction = -1 if winding_direction == "left" else 1 if self.output_type == "inline" or self.output_type == "inline_rel": - self.out_theta = self.total_theta + 0.5*np.pi + self.out_theta = self.total_theta elif self.output_type == "opposite": self.out_theta = self.total_theta elif self.output_type == "single_inside" or self.output_type == "single_outside": @@ -291,7 +292,9 @@ def _generate(self): path_function_supports_numpy=True) if self.output_type == "inline" or self.output_type == "inline_rel": - self._wg.add_straight_segment((outer_r - self.min_bend_radius)) + self._wg.add_straight_segment(a + b*(self.out_theta+0.5*np.pi) - self.min_bend_radius) + self._wg.add_bend(0.5*self.winding_direction*np.pi, self.min_bend_radius) + self._wg.add_straight_segment((2*outer_r - 2 * self.min_bend_radius)) self._wg.add_bend(-0.5*self.winding_direction*np.pi, self.min_bend_radius) def get_shapely_object(self): From 3891d8c7cc439f61ca93cff536e544563201459f Mon Sep 17 00:00:00 2001 From: fbeutel Date: Wed, 11 Mar 2020 14:34:05 +0100 Subject: [PATCH 08/11] - Allow for an offset from the center for a spiral - Fix spiral direction --- gdshelpers/parts/spiral.py | 128 ++++++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 31 deletions(-) diff --git a/gdshelpers/parts/spiral.py b/gdshelpers/parts/spiral.py index 7c4f5e5..823cd64 100644 --- a/gdshelpers/parts/spiral.py +++ b/gdshelpers/parts/spiral.py @@ -130,45 +130,67 @@ def _arc_length_indefinite_integral(theta, a, b): return np.sqrt( np.square((a + b * theta)) + np.square(b) ) * (a+b*theta) / (2*b) + \ 0.5 * b * np.log(np.sqrt( np.square(a + b*theta) + np.square(b) ) + a + b*theta ) -def _arc_length_integral(theta, a, b): +def _arc_length_integral(theta, a, b, offset): return _arc_length_indefinite_integral(theta, a, b) - _arc_length_indefinite_integral(0, a, b) -def _spiral_length_angle(theta, a, b, out_angle): - return (_arc_length_integral(theta, a, b) + # Inward spiral - np.pi * a + # Two semi-circles in the center - _arc_length_integral(theta + out_angle, a, b)) # Outward spiral +def _spiral_length_angle(theta, a, b, offset, out_angle): + _, _, circle_length = _circle_segment_params(a, b) + return (_arc_length_integral(theta, a - offset, b, offset) + # Inward spiral + 2*circle_length + #2 *np.pi * a + # Two semi-circles in the center + _arc_length_integral(theta + out_angle, a + offset, b, offset)) # Outward spiral -def _spiral_length_inline(theta, a, b): - return (_spiral_length_angle(theta, a, b, 0) + +def _spiral_length_inline(theta, a, b, offset): + return (_spiral_length_angle(theta, a, b, 0, offset) + (a + b*(theta+0.5*np.pi)) - (0.5 * a) + np.pi * (0.5 * a) + # two bends (2*(a + b * theta) - 2*(0.5 * a))) # from endpoint to startpoint height -def _spiral_length_inline_rel(theta, a, b): - return (_spiral_length_inline(theta, a, b) - +def _spiral_length_inline_rel(theta, a, b, offset): + return (_spiral_length_inline(theta, a, b, offset) - (a + b * (theta + 0.5 * np.pi) + (0.5 * a))) # subtract the direct path from input to output -def _spiral_theta(length, wg_width, gap, min_bend_radius, length_function, *args): +def _spiral_theta(length, wg_width, gap, min_bend_radius, offset, length_function, *args): """ Numerically calculate the theta that is needed for a given spiral variant (length_function) in order to have the desired length. - :param length_function: A function which takes theta, a, b plus arbitrary *args. + :param length_function: A function which takes theta, a, b, offset plus arbitrary *args. """ from scipy.optimize import fsolve a = 2*min_bend_radius b = 2*(np.sum(wg_width) + gap) / (2.*np.pi) - return fsolve(lambda x: length_function(x, a, b, *args) - length, 20*np.pi) + return fsolve(lambda x: length_function(x, a, b, offset, *args) - length, 20*np.pi) -def _spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0, direction=-1): - theta = min_theta + t * (max_theta - min_theta) +def _circle_segment_params(a, b): + """ + Calculate the inner semi-circle segments. + Since the facet of the spiral in the center has a slightly different angle than the tangent of a circle with the min_bend_radius + we don't use a full semi circle, but only start the part of the semi circle at the point where the tangent matches the spiral tangent. + This semi-circle segment needs to be extended in height by a straight piece (missing_height) to connect the spiral with it's center point. + + Returns circ_angle, missing_height, circ_length + """ + rot_in_1 = np.arctan2(b, a) + circ_angle = 0.5 * np.pi - rot_in_1 + circ_seg_height = a * np.sin(circ_angle) # = 2 * min_bend_radius * sin(angle / 2), since full_angle = 2*circ_angle + missing_height = a - circ_seg_height + return circ_angle, missing_height, circ_angle * a + missing_height + +def _spiral_out_path(t, a, b, max_theta, theta_offset=0, direction=-1): + theta = t * max_theta r = a + b * theta - return r * np.array([np.sin(theta + theta_offset), -direction*np.cos(theta + theta_offset)]) + np.array([0, direction*a])[:, None] + #return np.array([r*np.sin(theta + theta_offset), -r*direction*np.cos(theta + theta_offset) + direction*a])# + np.array([0, direction*a])[:, None] + return np.array([r*np.sin(theta + theta_offset), -r*direction*np.cos(theta + theta_offset)])# + np.array([0, direction*a])[:, None] -def _d_spiral_out_path(t, a, b, max_theta, min_theta=0, theta_offset=0, direction=-1): - theta = min_theta + t * (max_theta - min_theta) +def _d_spiral_out_path(t, a, b, max_theta, theta_offset=0, direction=-1): + """ + The derivative of the spiral. + Note that, unlike a circle, the y-component is not 0 for theta = n*2pi + """ + theta = t * max_theta r = a + b * theta - return r * np.array([np.cos(theta + theta_offset), direction*np.sin(theta + theta_offset)]) + return r * max_theta * np.array([np.cos(theta + theta_offset), direction*np.sin(theta + theta_offset)]) +\ + b * max_theta * np.array([np.sin(theta + theta_offset), -direction*np.cos(theta + theta_offset)]) class Spiral2: @@ -196,7 +218,7 @@ class Spiral2: * , where phi is the angle where the output should be located """ - def __init__(self, origin, angle, width, gap, min_bend_radius, theta, output_type='opposite', winding_direction='right', sample_distance=0.50, sample_points=100): + def __init__(self, origin, angle, width, gap, min_bend_radius, theta, output_type='opposite', offset=0, winding_direction='right', sample_distance=0.50, sample_points=100): """ Create an archimedean spiral following the spiral equation :math:`r = a + b \theta`. @@ -217,6 +239,7 @@ def __init__(self, origin, angle, width, gap, min_bend_radius, theta, output_typ self.sample_distance = sample_distance self.output_type = output_type self.winding_direction = -1 if winding_direction == "left" else 1 + self.offset = offset if self.output_type == "inline" or self.output_type == "inline_rel": self.out_theta = self.total_theta @@ -238,7 +261,7 @@ def make_at_port(cls, port, **kwargs): return cls(port.origin, port.angle, port.width, **default_port_param) @classmethod - def make_at_port_with_length(cls, port, gap, min_bend_radius, target_length, output_type='opposite', **kwargs): + def make_at_port_with_length(cls, port, gap, min_bend_radius, target_length, output_type='opposite', offset=0, **kwargs): if output_type == "inline": length_fn = [_spiral_length_inline] elif output_type == "inline_rel": @@ -250,8 +273,8 @@ def make_at_port_with_length(cls, port, gap, min_bend_radius, target_length, out else: length_fn = [_spiral_length_angle, output_type] - theta = float(_spiral_theta(target_length, port.width, gap, min_bend_radius, *length_fn)) - return cls.make_at_port(port, gap=gap, min_bend_radius=min_bend_radius, theta=theta, output_type=output_type, **kwargs) + theta = float(_spiral_theta(target_length, port.width, gap, min_bend_radius, offset, *length_fn)) + return cls.make_at_port(port, gap=gap, min_bend_radius=min_bend_radius, theta=theta, output_type=output_type, offset=offset, **kwargs) @property def width(self): @@ -274,24 +297,53 @@ def out_port(self): def _generate(self): self._wg = Waveguide.make_at_port(self._origin_port) a = 2*self.min_bend_radius + a_in, a_out = a - self.winding_direction * self.offset, a + self.winding_direction * self.offset b = 2*(np.sum(self.width) + self.gap) / (2.*np.pi) - outer_r = (a + b*self.total_theta) - + + # Rotate the spiral such that the input facet of the spiral has an [1,0] normal vector + in_args = dict(a=a_in, b=b, max_theta=self.total_theta, theta_offset=0, direction=self.winding_direction) + d_in_args = dict(a=a, b=b, max_theta=self.total_theta, theta_offset=0, direction=self.winding_direction) + d_in_0 = _d_spiral_out_path(1, **d_in_args) + rot_in = np.arctan2(d_in_0[1], d_in_0[0]) + in_args['theta_offset'] -= rot_in + d_in_args['theta_offset'] -= rot_in + in_offset = -_spiral_out_path(1, **in_args) + + # ...same for the output spiral + out_args = dict(a=a_out, b=b, max_theta=self.out_theta, theta_offset=0, direction=self.winding_direction) + dout_args = dict(a=a, b=b, max_theta=self.out_theta, theta_offset=0, direction=self.winding_direction) + d_out_0 = _d_spiral_out_path(0, **dout_args) + rot_out = np.arctan2(d_out_0[1], d_out_0[0]) + out_args['theta_offset'] -= rot_out + dout_args['theta_offset'] -= rot_out + out_offset = _spiral_out_path(0, **out_args) + + # Generate the spiral if self.output_type != "single_outside": - self._wg.add_parameterized_path(lambda x: -_spiral_out_path(1-x, a=a, b=b, max_theta=self.total_theta, theta_offset=-self.total_theta, direction=self.winding_direction) - self.winding_direction*np.array([0, -a + outer_r])[:, None], sample_distance=self.sample_distance, sample_points=self.sample_points, - path_derivative=lambda x: _d_spiral_out_path(1-x, a=a, b=b, max_theta=self.total_theta, theta_offset=-self.total_theta, direction=self.winding_direction), + self._wg.add_parameterized_path(lambda x: -_spiral_out_path(1-x, **in_args) - in_offset[:, None], sample_distance=self.sample_distance, sample_points=self.sample_points, + path_derivative=lambda x: _d_spiral_out_path(1-x, **d_in_args), path_function_supports_numpy=True) if self.output_type != "single_inside" and self.output_type != "single_outside": - self._wg.add_bend(-self.winding_direction*np.pi, self.min_bend_radius) - self._wg.add_bend(self.winding_direction*np.pi, self.min_bend_radius) + r = self.min_bend_radius - self.winding_direction*self.offset + circ_angle, missing_height, _ = _circle_segment_params(a, b) + self._wg.add_bend(-self.winding_direction*circ_angle, r) + self._wg.add_straight_segment(missing_height) + self._wg.add_bend(-self.winding_direction*circ_angle, r) + + r = self.min_bend_radius + self.winding_direction*self.offset + circ_angle, missing_height, _ = _circle_segment_params(a, b) + self._wg.add_bend(self.winding_direction*circ_angle, r) + self._wg.add_straight_segment(missing_height) + self._wg.add_bend(self.winding_direction*circ_angle, r) if self.output_type != "single_inside": - self._wg.add_parameterized_path(lambda x: _spiral_out_path(x, a=a, b=b, max_theta=self.out_theta, direction=self.winding_direction), sample_distance=self.sample_distance, sample_points=self.sample_points, - path_derivative=lambda x: _d_spiral_out_path(x, a=a, b=b, max_theta=self.out_theta, direction=self.winding_direction), + self._wg.add_parameterized_path(lambda x: _spiral_out_path(x, **out_args) - out_offset[:, None], sample_distance=self.sample_distance, sample_points=self.sample_points, + path_derivative=lambda x: _d_spiral_out_path(x, **dout_args), path_function_supports_numpy=True) if self.output_type == "inline" or self.output_type == "inline_rel": + outer_r = (a + b*self.total_theta) self._wg.add_straight_segment(a + b*(self.out_theta+0.5*np.pi) - self.min_bend_radius) self._wg.add_bend(0.5*self.winding_direction*np.pi, self.min_bend_radius) self._wg.add_straight_segment((2*outer_r - 2 * self.min_bend_radius)) @@ -316,12 +368,26 @@ def demo_spiral(origin, output_type, target_length, gap, port_y_offset=0, width= cell.add_to_layer(2, text) # Create normal demo spirals + for i,output_type in enumerate(['opposite', 'inline', 'inline_rel', -0.5*np.pi, 0.25*np.pi, np.pi]): - demo_spiral(((i//4)*700, (i%4)*250), output_type, 2000, gap=6., width=[1, 3, 1, 3, 1]) + demo_spiral(((i//4)*700, (i%4)*250), output_type, 5000, gap=3., width=1) # Create spirals with single turn demo_spiral((1*700, 2*250), 'single_inside', 2000, gap=1.5) demo_spiral((1*700, 3*250), 'single_outside', 2000, gap=1.5, port_y_offset=-150) + + + """wg = Waveguide(np.array([0, 0]), 0, 1.3) + wg.add_straight_segment(30) + spiral = Spiral2.make_at_port_with_length(wg.current_port, gap=80., min_bend_radius=35., target_length=20000, output_type='opposite', sample_distance=10) + #spiral.wg.add_straight_segment(30) + cell.add_to_layer(1, wg, spiral)""" + + demo_spiral((2000, 0), 'inline', 11000, gap=10., width=1) + demo_spiral((2000, 600), 'inline', 11000, gap=18., width=1) + demo_spiral((2000, 1200), 'inline', 11000, gap=20., width=1) + demo_spiral((2000, 3*800), 'inline', 11000, gap=21., width=[1,5,1,5,1]) + demo_spiral((2000, 4*800), 'inline', 11000, gap=54., width=1) cell.show() cell.save("spiral_test") From 585f8dad3c7cc51995ab75d4f00c0abcc1eb710b Mon Sep 17 00:00:00 2001 From: fbeutel Date: Tue, 27 Oct 2020 10:13:47 +0100 Subject: [PATCH 09/11] Fix minor bug with calculation of the output height --- gdshelpers/parts/spiral.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gdshelpers/parts/spiral.py b/gdshelpers/parts/spiral.py index 823cd64..8266850 100644 --- a/gdshelpers/parts/spiral.py +++ b/gdshelpers/parts/spiral.py @@ -300,7 +300,9 @@ def _generate(self): a_in, a_out = a - self.winding_direction * self.offset, a + self.winding_direction * self.offset b = 2*(np.sum(self.width) + self.gap) / (2.*np.pi) - # Rotate the spiral such that the input facet of the spiral has an [1,0] normal vector + # Rotate the spiral such that the input facet of the spiral has an [1,0] normal vector. + # (The tangent of a archimedean spiral is slightly different to that of a circle, so at the top it has a normal vector slightly + # different that [1,0].) in_args = dict(a=a_in, b=b, max_theta=self.total_theta, theta_offset=0, direction=self.winding_direction) d_in_args = dict(a=a, b=b, max_theta=self.total_theta, theta_offset=0, direction=self.winding_direction) d_in_0 = _d_spiral_out_path(1, **d_in_args) @@ -343,10 +345,10 @@ def _generate(self): path_function_supports_numpy=True) if self.output_type == "inline" or self.output_type == "inline_rel": - outer_r = (a + b*self.total_theta) + y_diff = self._origin_port.origin[1]-self._wg.port.origin[1] self._wg.add_straight_segment(a + b*(self.out_theta+0.5*np.pi) - self.min_bend_radius) self._wg.add_bend(0.5*self.winding_direction*np.pi, self.min_bend_radius) - self._wg.add_straight_segment((2*outer_r - 2 * self.min_bend_radius)) + self._wg.add_straight_segment((y_diff - 2 * self.min_bend_radius)) self._wg.add_bend(-0.5*self.winding_direction*np.pi, self.min_bend_radius) def get_shapely_object(self): From cc261cef4c2d9ee78a26843e4c89b8464f24c95b Mon Sep 17 00:00:00 2001 From: fbeutel Date: Tue, 27 Oct 2020 10:19:58 +0100 Subject: [PATCH 10/11] Rename spiral to ArchSpiral, improve documentation and add test case --- gdshelpers/parts/spiral.py | 33 +++++++++++++--------------- gdshelpers/tests/test_arch_spiral.py | 18 +++++++++++++++ 2 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 gdshelpers/tests/test_arch_spiral.py diff --git a/gdshelpers/parts/spiral.py b/gdshelpers/parts/spiral.py index 8266850..9d7973e 100644 --- a/gdshelpers/parts/spiral.py +++ b/gdshelpers/parts/spiral.py @@ -177,6 +177,9 @@ def _circle_segment_params(a, b): return circ_angle, missing_height, circ_angle * a + missing_height def _spiral_out_path(t, a, b, max_theta, theta_offset=0, direction=-1): + """ + :param theta_offset: This basically rotates the spiral by the given angle, but doesn't change the length of the spiral. + """ theta = t * max_theta r = a + b * theta #return np.array([r*np.sin(theta + theta_offset), -r*direction*np.cos(theta + theta_offset) + direction*a])# + np.array([0, direction*a])[:, None] @@ -193,29 +196,29 @@ def _d_spiral_out_path(t, a, b, max_theta, theta_offset=0, direction=-1): b * max_theta * np.array([np.sin(theta + theta_offset), -direction*np.cos(theta + theta_offset)]) -class Spiral2: +class ArchSpiral: """ An archimedean spiral, where the length can be numerically calculated. Options for the output position are: - * `inline` + * `"inline"` Output on the same height and direction as input. - * `inline_rel` + * `"inline_rel"` Output on the same height and direction as input, but the considered length is only the difference of the spiral compared to a straight path. This is useful when building MZIs, where the other path is parallel to the spiral. - * `opposite` + * `"opposite"` Output at opposite direction as input - * `single_inside` + * `"single_inside"` The spiral will only do a single turn and stop in the inside - * `single_outside` + * `"single_outside"` The spiral will only do a single turn, but start in the inside and stop at the outside - * , where phi is the angle where the output should be located + * ``, where phi is the angle where the output should be located """ def __init__(self, origin, angle, width, gap, min_bend_radius, theta, output_type='opposite', offset=0, winding_direction='right', sample_distance=0.50, sample_points=100): @@ -363,33 +366,27 @@ def get_shapely_object(self): def demo_spiral(origin, output_type, target_length, gap, port_y_offset=0, width=1): wg = Waveguide(origin + np.array([0, port_y_offset]), 0, width) wg.add_straight_segment(30) - spiral = Spiral2.make_at_port_with_length(wg.current_port, gap=gap, min_bend_radius=35., target_length=target_length, output_type=output_type, sample_distance=1) + spiral = ArchSpiral.make_at_port_with_length(wg.current_port, gap=gap, min_bend_radius=35., target_length=target_length, output_type=output_type, sample_distance=1) text = Text(np.array([150, -130]) + origin, 20, "output: {}\n\nlength: {} um\nreal_length: {:.4f}um".format(output_type, target_length, spiral.length)) spiral.wg.add_straight_segment(30) cell.add_to_layer(1, wg, spiral) cell.add_to_layer(2, text) # Create normal demo spirals - for i,output_type in enumerate(['opposite', 'inline', 'inline_rel', -0.5*np.pi, 0.25*np.pi, np.pi]): demo_spiral(((i//4)*700, (i%4)*250), output_type, 5000, gap=3., width=1) # Create spirals with single turn demo_spiral((1*700, 2*250), 'single_inside', 2000, gap=1.5) demo_spiral((1*700, 3*250), 'single_outside', 2000, gap=1.5, port_y_offset=-150) - - - """wg = Waveguide(np.array([0, 0]), 0, 1.3) - wg.add_straight_segment(30) - spiral = Spiral2.make_at_port_with_length(wg.current_port, gap=80., min_bend_radius=35., target_length=20000, output_type='opposite', sample_distance=10) - #spiral.wg.add_straight_segment(30) - cell.add_to_layer(1, wg, spiral)""" - demo_spiral((2000, 0), 'inline', 11000, gap=10., width=1) + """demo_spiral((2000, 0), 'inline', 11000, gap=10., width=1) demo_spiral((2000, 600), 'inline', 11000, gap=18., width=1) demo_spiral((2000, 1200), 'inline', 11000, gap=20., width=1) demo_spiral((2000, 3*800), 'inline', 11000, gap=21., width=[1,5,1,5,1]) - demo_spiral((2000, 4*800), 'inline', 11000, gap=54., width=1) + demo_spiral((2000, 4*800), 'inline', 11000, gap=54., width=1)""" + + cell.show() cell.save("spiral_test") diff --git a/gdshelpers/tests/test_arch_spiral.py b/gdshelpers/tests/test_arch_spiral.py new file mode 100644 index 0000000..893a1f5 --- /dev/null +++ b/gdshelpers/tests/test_arch_spiral.py @@ -0,0 +1,18 @@ +import unittest +import numpy as np + +from gdshelpers.parts.spiral import ArchSpiral +from gdshelpers.parts.waveguide import Waveguide + + +class ArchSpiralTestCase(unittest.TestCase): + def test_inline(self): + start_origin = [0, 0] + + wg = Waveguide(np.array(start_origin), 0, 1.3) + wg.add_straight_segment(30) + spiral = ArchSpiral.make_at_port_with_length(wg.current_port, gap=80., min_bend_radius=35., target_length=20000, output_type='inline', sample_distance=50) + spiral.wg.add_straight_segment(30) + + self.assertAlmostEqual(spiral.out_port.origin[1], start_origin[1]) + From 9da6fec4576f0aecc1a36699adabf0af909d7908 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Thu, 29 Jul 2021 12:03:34 +0200 Subject: [PATCH 11/11] Fix flake8 formatting --- gdshelpers/parts/spiral.py | 92 +++++++++++++++++----------- gdshelpers/tests/test_arch_spiral.py | 6 +- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/gdshelpers/parts/spiral.py b/gdshelpers/parts/spiral.py index 9d7973e..62c0a9b 100644 --- a/gdshelpers/parts/spiral.py +++ b/gdshelpers/parts/spiral.py @@ -120,34 +120,39 @@ def _example_old(): def _arc_length_indefinite_integral(theta, a, b): """ - The indefinite integral + The indefinite integral .. math:: \f{x} = \int r^2 + \frac{\,dr}{\,d\theta}\,d\theta - + which is needed to calculate the arc length of a spiral. """ - return np.sqrt( np.square((a + b * theta)) + np.square(b) ) * (a+b*theta) / (2*b) + \ - 0.5 * b * np.log(np.sqrt( np.square(a + b*theta) + np.square(b) ) + a + b*theta ) + return np.sqrt(np.square((a + b * theta)) + np.square(b)) * (a+b*theta) / (2*b) + \ + 0.5 * b * np.log(np.sqrt(np.square(a + b*theta) + np.square(b)) + a + b * theta) + def _arc_length_integral(theta, a, b, offset): return _arc_length_indefinite_integral(theta, a, b) - _arc_length_indefinite_integral(0, a, b) + def _spiral_length_angle(theta, a, b, offset, out_angle): _, _, circle_length = _circle_segment_params(a, b) - return (_arc_length_integral(theta, a - offset, b, offset) + # Inward spiral - 2*circle_length + #2 *np.pi * a + # Two semi-circles in the center - _arc_length_integral(theta + out_angle, a + offset, b, offset)) # Outward spiral + return (_arc_length_integral(theta, a - offset, b, offset) + # Inward spiral + 2 * circle_length + # 2 *np.pi * a + # Two semi-circles in the center + _arc_length_integral(theta + out_angle, a + offset, b, offset)) # Outward spiral + def _spiral_length_inline(theta, a, b, offset): return (_spiral_length_angle(theta, a, b, 0, offset) + - (a + b*(theta+0.5*np.pi)) - (0.5 * a) + - np.pi * (0.5 * a) + # two bends - (2*(a + b * theta) - 2*(0.5 * a))) # from endpoint to startpoint height + (a + b*(theta+0.5*np.pi)) - (0.5 * a) + + np.pi * (0.5 * a) + # two bends + (2*(a + b * theta) - 2*(0.5 * a))) # from endpoint to startpoint height + def _spiral_length_inline_rel(theta, a, b, offset): return (_spiral_length_inline(theta, a, b, offset) - - (a + b * (theta + 0.5 * np.pi) + (0.5 * a))) # subtract the direct path from input to output + (a + b * (theta + 0.5 * np.pi) + (0.5 * a))) # subtract the direct path from input to output + def _spiral_theta(length, wg_width, gap, min_bend_radius, offset, length_function, *args): """ @@ -161,29 +166,32 @@ def _spiral_theta(length, wg_width, gap, min_bend_radius, offset, length_functio b = 2*(np.sum(wg_width) + gap) / (2.*np.pi) return fsolve(lambda x: length_function(x, a, b, offset, *args) - length, 20*np.pi) + def _circle_segment_params(a, b): """ Calculate the inner semi-circle segments. - Since the facet of the spiral in the center has a slightly different angle than the tangent of a circle with the min_bend_radius - we don't use a full semi circle, but only start the part of the semi circle at the point where the tangent matches the spiral tangent. - This semi-circle segment needs to be extended in height by a straight piece (missing_height) to connect the spiral with it's center point. + Since the facet of the spiral in the center has a slightly different angle than the tangent of a circle with the + min_bend_radius we don't use a full semi circle, but only start the part of the semi circle at the point where the + tangent matches the spiral tangent. This semi-circle segment needs to be extended in height by a straight piece + (missing_height) to connect the spiral with it's center point. Returns circ_angle, missing_height, circ_length """ rot_in_1 = np.arctan2(b, a) circ_angle = 0.5 * np.pi - rot_in_1 - circ_seg_height = a * np.sin(circ_angle) # = 2 * min_bend_radius * sin(angle / 2), since full_angle = 2*circ_angle + circ_seg_height = a * np.sin(circ_angle) # = 2 * min_bend_radius * sin(angle / 2), since full_angle = 2*circ_angle missing_height = a - circ_seg_height return circ_angle, missing_height, circ_angle * a + missing_height + def _spiral_out_path(t, a, b, max_theta, theta_offset=0, direction=-1): """ - :param theta_offset: This basically rotates the spiral by the given angle, but doesn't change the length of the spiral. + :param theta_offset: This rotates the spiral by the given angle, but doesn't change the length of the spiral. """ theta = t * max_theta r = a + b * theta - #return np.array([r*np.sin(theta + theta_offset), -r*direction*np.cos(theta + theta_offset) + direction*a])# + np.array([0, direction*a])[:, None] - return np.array([r*np.sin(theta + theta_offset), -r*direction*np.cos(theta + theta_offset)])# + np.array([0, direction*a])[:, None] + return np.array([r*np.sin(theta + theta_offset), -r*direction*np.cos(theta + theta_offset)]) + def _d_spiral_out_path(t, a, b, max_theta, theta_offset=0, direction=-1): """ @@ -193,7 +201,7 @@ def _d_spiral_out_path(t, a, b, max_theta, theta_offset=0, direction=-1): theta = t * max_theta r = a + b * theta return r * max_theta * np.array([np.cos(theta + theta_offset), direction*np.sin(theta + theta_offset)]) +\ - b * max_theta * np.array([np.sin(theta + theta_offset), -direction*np.cos(theta + theta_offset)]) + b * max_theta * np.array([np.sin(theta + theta_offset), -direction*np.cos(theta + theta_offset)]) class ArchSpiral: @@ -206,7 +214,8 @@ class ArchSpiral: Output on the same height and direction as input. * `"inline_rel"` - Output on the same height and direction as input, but the considered length is only the difference of the spiral compared to a straight path. + Output on the same height and direction as input, but the considered length is only the difference of the + spiral compared to a straight path. This is useful when building MZIs, where the other path is parallel to the spiral. * `"opposite"` @@ -221,16 +230,19 @@ class ArchSpiral: * ``, where phi is the angle where the output should be located """ - def __init__(self, origin, angle, width, gap, min_bend_radius, theta, output_type='opposite', offset=0, winding_direction='right', sample_distance=0.50, sample_points=100): + def __init__(self, origin, angle, width, gap, min_bend_radius, theta, output_type='opposite', offset=0, + winding_direction='right', sample_distance=0.50, sample_points=100): """ Create an archimedean spiral following the spiral equation :math:`r = a + b \theta`. - + :param origin: :param angle: :param width: - :param gap: Gap between the waveguide. Since this is an archimedean spiral, the gap is constant across the whole spiral. + :param gap: Gap between the waveguide. Since this is an archimedean spiral, the gap is constant across the + whole spiral. :param min_bend_radius: The minimum bend radius. This will set the bend of the two semi-circles in the center. - It follows that `a = 2 * min_bend_radius`, where `a` as defined in the spiral equation above. + It follows that `a = 2 * min_bend_radius`, where `a` as defined in the spiral equation + above. :param theta: The total angle to turn """ self._origin_port = Port(origin, angle, width) @@ -264,7 +276,8 @@ def make_at_port(cls, port, **kwargs): return cls(port.origin, port.angle, port.width, **default_port_param) @classmethod - def make_at_port_with_length(cls, port, gap, min_bend_radius, target_length, output_type='opposite', offset=0, **kwargs): + def make_at_port_with_length(cls, port, gap, min_bend_radius, target_length, output_type='opposite', offset=0, + **kwargs): if output_type == "inline": length_fn = [_spiral_length_inline] elif output_type == "inline_rel": @@ -277,7 +290,8 @@ def make_at_port_with_length(cls, port, gap, min_bend_radius, target_length, out length_fn = [_spiral_length_angle, output_type] theta = float(_spiral_theta(target_length, port.width, gap, min_bend_radius, offset, *length_fn)) - return cls.make_at_port(port, gap=gap, min_bend_radius=min_bend_radius, theta=theta, output_type=output_type, offset=offset, **kwargs) + return cls.make_at_port(port, gap=gap, min_bend_radius=min_bend_radius, theta=theta, output_type=output_type, + offset=offset, **kwargs) @property def width(self): @@ -302,10 +316,10 @@ def _generate(self): a = 2*self.min_bend_radius a_in, a_out = a - self.winding_direction * self.offset, a + self.winding_direction * self.offset b = 2*(np.sum(self.width) + self.gap) / (2.*np.pi) - + # Rotate the spiral such that the input facet of the spiral has an [1,0] normal vector. - # (The tangent of a archimedean spiral is slightly different to that of a circle, so at the top it has a normal vector slightly - # different that [1,0].) + # (The tangent of a archimedean spiral is slightly different to that of a circle, so at the top it has a normal + # vector slightly different that [1,0].) in_args = dict(a=a_in, b=b, max_theta=self.total_theta, theta_offset=0, direction=self.winding_direction) d_in_args = dict(a=a, b=b, max_theta=self.total_theta, theta_offset=0, direction=self.winding_direction) d_in_0 = _d_spiral_out_path(1, **d_in_args) @@ -325,7 +339,8 @@ def _generate(self): # Generate the spiral if self.output_type != "single_outside": - self._wg.add_parameterized_path(lambda x: -_spiral_out_path(1-x, **in_args) - in_offset[:, None], sample_distance=self.sample_distance, sample_points=self.sample_points, + self._wg.add_parameterized_path(lambda x: -_spiral_out_path(1-x, **in_args) - in_offset[:, None], + sample_distance=self.sample_distance, sample_points=self.sample_points, path_derivative=lambda x: _d_spiral_out_path(1-x, **d_in_args), path_function_supports_numpy=True) @@ -343,7 +358,8 @@ def _generate(self): self._wg.add_bend(self.winding_direction*circ_angle, r) if self.output_type != "single_inside": - self._wg.add_parameterized_path(lambda x: _spiral_out_path(x, **out_args) - out_offset[:, None], sample_distance=self.sample_distance, sample_points=self.sample_points, + self._wg.add_parameterized_path(lambda x: _spiral_out_path(x, **out_args) - out_offset[:, None], + sample_distance=self.sample_distance, sample_points=self.sample_points, path_derivative=lambda x: _d_spiral_out_path(x, **dout_args), path_function_supports_numpy=True) @@ -366,15 +382,19 @@ def get_shapely_object(self): def demo_spiral(origin, output_type, target_length, gap, port_y_offset=0, width=1): wg = Waveguide(origin + np.array([0, port_y_offset]), 0, width) wg.add_straight_segment(30) - spiral = ArchSpiral.make_at_port_with_length(wg.current_port, gap=gap, min_bend_radius=35., target_length=target_length, output_type=output_type, sample_distance=1) - text = Text(np.array([150, -130]) + origin, 20, "output: {}\n\nlength: {} um\nreal_length: {:.4f}um".format(output_type, target_length, spiral.length)) + spiral = ArchSpiral.make_at_port_with_length( + wg.current_port, gap=gap, min_bend_radius=35., target_length=target_length, output_type=output_type, + sample_distance=1) + text = Text(np.array([150, -130]) + origin, 20, + "output: {}\n\nlength: {} um\nreal_length: {:.4f}um".format(output_type, target_length, + spiral.length)) spiral.wg.add_straight_segment(30) cell.add_to_layer(1, wg, spiral) cell.add_to_layer(2, text) # Create normal demo spirals - for i,output_type in enumerate(['opposite', 'inline', 'inline_rel', -0.5*np.pi, 0.25*np.pi, np.pi]): - demo_spiral(((i//4)*700, (i%4)*250), output_type, 5000, gap=3., width=1) + for i, output_type in enumerate(['opposite', 'inline', 'inline_rel', -0.5*np.pi, 0.25*np.pi, np.pi]): + demo_spiral(((i // 4)*700, (i % 4)*250), output_type, 5000, gap=3., width=1) # Create spirals with single turn demo_spiral((1*700, 2*250), 'single_inside', 2000, gap=1.5) @@ -386,7 +406,5 @@ def demo_spiral(origin, output_type, target_length, gap, port_y_offset=0, width= demo_spiral((2000, 3*800), 'inline', 11000, gap=21., width=[1,5,1,5,1]) demo_spiral((2000, 4*800), 'inline', 11000, gap=54., width=1)""" - - cell.show() cell.save("spiral_test") diff --git a/gdshelpers/tests/test_arch_spiral.py b/gdshelpers/tests/test_arch_spiral.py index 893a1f5..b0b378f 100644 --- a/gdshelpers/tests/test_arch_spiral.py +++ b/gdshelpers/tests/test_arch_spiral.py @@ -11,8 +11,8 @@ def test_inline(self): wg = Waveguide(np.array(start_origin), 0, 1.3) wg.add_straight_segment(30) - spiral = ArchSpiral.make_at_port_with_length(wg.current_port, gap=80., min_bend_radius=35., target_length=20000, output_type='inline', sample_distance=50) + spiral = ArchSpiral.make_at_port_with_length(wg.current_port, gap=80., min_bend_radius=35., target_length=20000, + output_type='inline', sample_distance=50) spiral.wg.add_straight_segment(30) - - self.assertAlmostEqual(spiral.out_port.origin[1], start_origin[1]) + self.assertAlmostEqual(spiral.out_port.origin[1], start_origin[1])