Source code for zeroheliumkit.src.geometries

""" This submodule contains most frequently used geometries """

from math import pi, tanh, cos, tan
import numpy as np

from shapely import Point, LineString, Polygon
from shapely import affinity, unary_union, box

from .core import Entity, Structure
from .anchors import Anchor, Skeletone, Layer
from .utils import azimuth, buffer_along_path, round_polygon
from .functions import extract_coords_from_point
from .routing import get_fillet_params, make_fillet_line, normalize_anchors
from .settings import GRID_SIZE


# -------------------------------------
# Collection of simple geometries
# -------------------------------------

[docs] def Rectangle(width: float, height: float, location: tuple | Point=None, direction: float=None, round_radius: float=None, **kwargs) -> Polygon: """ Returns a rectangle Polygon Args: width (float): width of the rectangle height (float): height of the rectangle Example: >>> Rectangle(4, 2) """ poly = Polygon([(-width/2, -height/2), (-width/2, height/2), (width/2, height/2), (width/2, -height/2)]) if direction: poly = affinity.rotate(poly, direction, origin=(0,0)) if location: if isinstance(location, Point): location = (location.x, location.y) poly = affinity.translate(poly, *location) if round_radius: poly = round_polygon(poly, round_radius, **kwargs) return poly
[docs] def Square(size: float, location: tuple | Point=None, direction: float=None, round_radius: float=None, **kwargs) -> Polygon: """ Returns a square Polygon Args: size (float): size of the square Example: >>> Square(3) """ return Rectangle(size, size, location, direction, round_radius, **kwargs)
[docs] def RegularPolygon(edge_size: float=None, radius: float=None, location: tuple | Point=None, num_edges: int=6) -> Polygon: """ Returns a regular Polygon with specified number of edges. Size of the polygon defined by radius or edge size. Args: edge (float): size of edge radius (float): radius of the Regular polygon num_edges (int): number of edges Example: >>> RegularPolygon(edge=4, num_edges=6, location=(0,0)) """ coords = [] angle = 2 * np.pi / num_edges if edge_size: radius = edge_size/np.sin(angle/2) * 0.5 for i in range(num_edges): x = radius * np.cos(i * angle) y = radius * np.sin(i * angle) coords.append((x, y)) polygon = Polygon(coords) if location: if isinstance(location, Point): location = (location.x, location.y) return affinity.translate(polygon, *location) return polygon
[docs] def Circle(radius: float, location: tuple | Point=None, num_edges: int=100) -> Polygon: """ Returns a circle shape Polygon Args: radius (float): radius of the circle Example:-- >>> Circle(radius=5) """ return RegularPolygon(radius=radius, location=location, num_edges=num_edges)
[docs] def CircleSegment(radius: float=1, start_angle: float=0, end_angle: float=90, location: tuple | Point=None, num_edges: int=25) -> Polygon: """ Returns a polygon representing a circular segment. Args: radius (float): The radius of the circle segment. start_angle (float): The starting angle of the circular segment in degrees. end_angle (float): The ending angle of the circular segment in degrees. location (tuple or Point, optional): The location of the circular segment. If provided, the circular segment will be translated to this location. Defaults to None. num_edges (int, optional): The number of edges used to approximate the circular segment. Defaults to 25. Example: >>> segment = CircleSegment(radius=5, start_angle=45, end_angle=135) >>> print(segment) POLYGON ((3.535533905932737 3.535533905932737, 4.045084971874737 4.045084971874737, ...)) """ coords = [(0,0)] iteration_angle = (end_angle - start_angle) / (num_edges - 1) for i in range(num_edges): x = radius * np.cos(np.deg2rad(start_angle + i * iteration_angle)) y = radius * np.sin(np.deg2rad(start_angle + i * iteration_angle)) coords.append((x, y)) polygon = Polygon(coords) if location: if isinstance(location, Point): location = (location.x, location.y) return affinity.translate(polygon, *location) return polygon
[docs] def Ring(inner_radius: float, outer_radius: float, location: tuple | Point=None, num_edges: int=100) -> Polygon: """ Returns a ring shape Polygon Args: inner_radius (float): inner radius of the ring outer_radius (float): outer radius of the ring location (tuple or Point, optional): The location of the ring. If provided, the ring will be translated to this location. Defaults to None. num_edges (int, optional): The number of edges used to approximate the ring. Defaults to 100. Example: >>> Ring(3, 5) """ inner = Circle(inner_radius, num_edges=num_edges) outer = Circle(outer_radius, num_edges=num_edges) ring = outer.difference(inner) if location: if isinstance(location, Point): location = (location.x, location.y) return affinity.translate(ring, *location) return ring
[docs] def RingSector(inner_radius: float, outer_radius: float, start_angle: float, end_angle: float, location: tuple | Point=None, num_edges: int=100): """ Returns the intersection between a ring and a circular sector. Args: inner_radius (float): The inner radius of the ring. outer_radius (float): The outer radius of the ring. start_angle (float): The starting angle of the sector in degrees. end_angle (float): The ending angle of the sector in degrees. location (tuple | Point, optional): The location of the center of the ring. Defaults to None. num_edges (int, optional): The number of edges used to approximate the ring and sector. Defaults to 100. Example: >>> inner_radius = 2.0 >>> outer_radius = 4.0 >>> start_angle = 0.0 >>> end_angle = math.pi / 2 >>> location = (0, 0) >>> num_edges = 100 >>> result = RingSector(inner_radius, outer_radius, start_angle, end_angle, location, num_edges) >>> print(result) [(4.0, 0.0), (3.9999999999999996, 0.040000000000000036), (3.9999999999999996, 0.08000000000000007), ...] """ coords = [] iteration_angle = (end_angle - start_angle) / (num_edges - 1) for i in range(num_edges): x = inner_radius * np.cos(np.deg2rad(start_angle + i * iteration_angle)) y = inner_radius * np.sin(np.deg2rad(start_angle + i * iteration_angle)) coords.append((x, y)) for i in range(num_edges): x = outer_radius * np.cos(np.deg2rad(end_angle - i * iteration_angle)) y = outer_radius * np.sin(np.deg2rad(end_angle - i * iteration_angle)) coords.append((x, y)) polygon = Polygon(coords) if location: if isinstance(location, Point): location = (location.x, location.y) return affinity.translate(polygon, *location) return polygon
[docs] def ArcLine(centerx: float, centery: float, radius: float, start_angle: float, end_angle: float, numsegments: int=10) -> LineString: """ Returns LineString representing an arc. Args: centerx (float): center.x of arcline centery (float): center.y of arcline radius (float): radius of the arcline start_angle (float): starting angle end_angle (float): end angle numsegments (int, optional): number of the segments. Defaults to 10. Example: >>> ArcLine(centerx=0, centery=0, radius=5, start_angle=0, end_angle=180) """ theta = np.radians(np.linspace(start_angle, end_angle, num=numsegments, endpoint=True)) x = centerx + radius * np.cos(theta) y = centery + radius * np.sin(theta) return LineString(np.column_stack([x, y]))
[docs] def Meander(length: float=100, radius: float=50, direction: float=None, num_segments: int=100, input_radius: float=None, output_radius: float=None, mirror: str=None) -> LineString: """ Returns a 1D full Meander line. Args: length (float): Length of the straight section. radius (float): Radius of the round section. direction (float): Rotates the meander by the given value in the end. num_segments (int, optional): Number of segments in the round section. Defaults to 100. Example: >>> meander = Meander(10, 2, 45, 50) >>> print(meander) """ meander = Skeletone() if input_radius: assert input_radius < length/2, "input_radius should be less than length/2" meander.add(ArcLine(0, input_radius, input_radius, -90, 0, int(num_segments/2))) meander.add(LineString([(0,0), (0,length/2 - input_radius)])) else: meander.add(LineString([(0,0), (0,length/2)])) meander.add(ArcLine(radius, 0, radius, 180, 0, num_segments)) meander.add(LineString([(0,0), (0,-length)])) meander.add(ArcLine(radius, 0, radius, 180, 360, num_segments)) if output_radius: assert output_radius < length/2, "output_radius should be less than length/2" meander.add(LineString([(0,0), (0,length/2 - output_radius)])) meander.add(ArcLine(output_radius, 0, output_radius, 180, 90, int(num_segments/2))) else: meander.add(LineString([(0,0), (0,length/2)])) if mirror: meander.mirror(aroundaxis=mirror, keep_original=False) if direction: meander.rotate(direction, origin=(0,0)) return meander.lines
[docs] def MeanderHalf(length: float=100, radius: float=50, direction: float=None, num_segments: int=100, input_radius: float=None, output_radius: float=None, mirror: str=None) -> LineString: """ Returns a 1D half Meander line. Args: length (float): length of the straight section radius (float): radius of the round section direction (float): rotates the meander by given value in the end num_segments (int, optional): number of segments in round section. Defaults to 100 Example: >>> MeanderHalf(10, 5, 45, 50) """ hmeander = Skeletone() if input_radius: assert input_radius < length/2, "input_radius should be less than length/2" hmeander.add(ArcLine(0, input_radius, input_radius, -90, 0, int(num_segments/2))) hmeander.add(LineString([(0,0), (0,length/2 - input_radius)])) else: hmeander.add(LineString([(0,0), (0,length/2)])) hmeander.add(ArcLine(radius, 0, radius, 180, 0, num_segments)) if output_radius: assert output_radius < length/2, "output_radius should be less than length/2" hmeander.add(LineString([(0,0), (0,-length/2 + output_radius)])) hmeander.add(ArcLine(output_radius, 0, output_radius, 180, 270, int(num_segments/2))) else: hmeander.add(LineString([(0,0), (0,-length/2)])) if mirror: hmeander.mirror(aroundaxis=mirror, keep_original=False) if direction: hmeander.rotate(direction, origin=(0,0)) return hmeander.lines
[docs] def PinchGate(arm_w: float, arm_l: float, length: float, width: float) -> Polygon: """ Returns a Polygon representing a pinch gate. Args: arm_w (float): arm width arm_l (float): arm length length (float): length of the pinch gate width (float): width of the pinch gate Example: >>> PinchGate(2, 5, 10, 3) """ pts = [(-arm_w/2, arm_w/2), (arm_l - length/32, arm_w/2), (arm_l, length/8), (arm_l, 7*length/16), (arm_l + width/3, length/2), (arm_l + width, length/2), (arm_l + width, -length/2), (arm_l + width/3, -length/2), (arm_l, -7*length/16), (arm_l, -length/8), (arm_l - length/32, -arm_w/2), (-arm_w/2, -arm_w/2) ] return Polygon(pts)
[docs] def LineExtrudedRectangle(point: tuple | Point | Anchor, width: float, length: float, direction: float=0) -> Polygon: """ Returns a rectangle that is extruded from a point in specific direction. Args: point (tuple | Point | Anchor): The starting point of the extrusion. Can be a tuple, Point object, or Anchor object. width (float): The width of the rectangle. length (float): The length of the rectangle. direction (float): The direction of the extrusion in degrees. Default is 0. Example: >>> LineExtrudedRectangle(Anchor((10,5), 60, "a"), 2, 20) """ rect = Rectangle(length, width, (length/2, 0)) if isinstance(point, (tuple, Point)): rect = affinity.rotate(rect, direction, origin=(0,0)) if isinstance(point, Point): point = (point.x, point.y) rect = affinity.translate(rect, *point) elif isinstance(point, Anchor): rect = affinity.rotate(rect, point.direction, origin=(0,0)) rect = affinity.translate(rect, *point.coords) else: raise ValueError("point should be a tuple, Point, or Anchor object") return rect
[docs] def CornerCutterPolygon(radius: float=10, num_segments: int=7): """ Generates a polygon which is used to cut for corner rounding. Args: radius (float): The radius of the rounded corners. Default is 10. num_segments (int): The number of segments used to approximate the rounded corners. Must be greater than 2. Default is 7. """ if num_segments < 2: raise ValueError("Number of segments must be greater than 2") coords = [(0,0), (0,radius)] start_angle = 180 iteration_angle = 90 / (num_segments - 1) for i in range(num_segments - 2): x = radius * (1 + np.cos(np.deg2rad(start_angle + (i + 1) * iteration_angle))) y = radius * (1 + np.sin(np.deg2rad(start_angle + (i + 1) * iteration_angle))) coords.append((x, y)) coords.append((radius, 0)) return Polygon(coords)
[docs] def CornerRounder(corner: tuple | Point | Anchor, radius: float=10, angle: float=0, num_segments: int=7, margin: float=0.1): """ Creates a Polygon to Round a corner (use for cuts). Works only with 90 degree corners. Args: corner (tuple | Point | Anchor): The corner to be rounded. radius (float): The radius of the rounded corner. Default is 10. angle (float): The angle of the corner. Default is 0. num_segments (int): The number of segments used to approximate the rounded corner. Must be greater than 2. Default is 7. margin (float): The margin to be added to the corner. Default is 0.1. """ if isinstance(corner, tuple): corner = Point(corner) if isinstance(corner, Anchor): corner = corner.point width = margin * radius corner_polygon = CornerCutterPolygon(radius, num_segments) cutter = unary_union([corner_polygon, box(-width, -width, radius+width, 0), box(-width, -width, 0, radius+width)]) cutter = affinity.rotate(cutter, angle, origin=(0,0)) cutter = affinity.translate(cutter, xoff=corner.x, yoff=corner.y) return cutter
# ------------------------------------- # Multi-layer Geometry Classes # -------------------------------------
[docs] class StraightLine(Structure): """ Represents a straight line 'polygons' in different layers. Args: anchors (tuple, optional): Two anchors that define the start and end of the line. lendir (tuple, optional): A tuple containing the length and direction (in degrees) of the line. layers (dict, optional): A dictionary containing the names of the layers and their corresponding widths. alabel (tuple, optional): A tuple containing labels for the start and end anchors. cap_style (str, optional): The style of line ending. Valid options are 'square', 'round', or 'flat'. Raises: NameError: If the provided cap_style is not one of 'square', 'round', or 'flat'. AttributeError: If neither 'anchors' nor 'lendir' is provided, or if both are provided. Examples: >>> # Create a straight line with anchors >>> line = StraightLine(anchors=((0, 0), (10, 10)), cap_style='round') >>> # Create a straight line with length and direction >>> line = StraightLine(lendir=(5, 45), cap_style='square') >>> # Create a straight line with layers and anchor labels >>> line = StraightLine(anchors=((0, 0), (10, 10)), layers={'layer1': 1, 'layer2': 2}, alabel=('start', 'end')) """ def __init__(self, anchors: tuple=None, lendir: tuple=None, layers: dict=None, alabel: tuple=None, cap_style: str='square', **kwargs): super().__init__() if cap_style not in ["square", "round", "flat"]: raise NameError("please choose line_ending from square, round or flat") if anchors: p1 = extract_coords_from_point(anchors[0]) p2 = extract_coords_from_point(anchors[1]) elif lendir: length, direction = lendir p1 = (0, 0) p2 = (length * np.cos(direction * np.pi/180), length * np.sin(direction * np.pi/180)) else: raise AttributeError("provide only one argument: 'anchors' or 'lendir'") # create skeletone self.skeletone.lines = LineString([p1, p2]) # create polygons if layers: for k, width in layers.items(): polygon = self.skeletone.buffer(offset=width/2, cap_style='square', **kwargs) self.add(Layer(name=k, polygons=polygon)) # create anchors if alabel: angle = azimuth(p1, p2) self.anchors.add([Anchor(point=p1, direction=angle, label=alabel[0]), Anchor(point=p2, direction=angle, label=alabel[1])])
[docs] class ArbitraryLine(Structure): """ Represents an arbitrary line path (list of points) and polygons created along this path and width values on the points. Args: points (list): list of points along which a polygon will be constructed layers (dict): layers info, where the keys are the layer names and the values are the corresponding widths alabel (tuple): labels of the start and end-points. Example: >>> points = [(0, 0), (1, 1), (2, 0)] >>> layers = {'layer1': 0.1, 'layer2': 0.2} >>> alabel = ('start', 'end') >>> line = ArbitraryLine(points, layers, alabel) """ def __init__(self, points: list, layers: dict=None, alabel: tuple=None): super().__init__() # create skeletone self.skeletone.lines = LineString(points) # create polygons if layers: for k, width in layers.items(): polygon = buffer_along_path(points, width) self.add(Layer(name=k, polygons=polygon)) # create anchors if alabel: input_angle = azimuth(points[0], points[1]) output_angle = azimuth(points[-2], points[-1]) self.anchors.add([Anchor(points[0], input_angle, alabel[0]), Anchor(points[-1], output_angle, alabel[1])])
[docs] class Taper(ArbitraryLine): """ Represents a multilayer Taper geometry. Args: length (float): length of the tapered section layers (dict, optional): layer info. Dictionary values must be a (input width, output width) tuple. Defaults to None. alabel (tuple, optional): labels of the start and end-points. Defaults to None. Examples: >>> length = 10.0 >>> layers = {'layer1': (0.1, 0.2), 'layer2': (0.2, 0.5)} >>> alabel = ('start', 'end') >>> taper = Taper(length, layers, alabel) """ def __init__(self, length: float, layers: dict=None, alabel: tuple=None, extension_sizes: tuple=(0,0)): # preparing dictionary for supplying it into ArbitraryLine class for k, v in layers.items(): w1 = v[0] # input side width w2 = v[1] # output side width if w1==w2: # 1. if width are the same -> adding small difference # in order to avoid 'division by zero' error # 2. later during "setattr" operation this difference will be eliminated # by "set_precision" (by default) w2 = w2 + GRID_SIZE/10 layers[k] = np.asarray([w1, w1, w2, w2]) pts = [(-length/2 - extension_sizes[0], 0), (-length/2, 0), (length/2, 0), (length/2 + extension_sizes[1], 0) ] if extension_sizes == (0,0): pts = pts[1:-1] layers = {k: v[1:-1] for k, v in layers.items()} elif extension_sizes[0] == 0: pts = pts[1:] layers = {k: v[1:] for k, v in layers.items()} elif extension_sizes[1] == 0: pts = pts[:-1] layers = {k: v[:-1] for k, v in layers.items()} else: pass super().__init__(pts, layers, alabel)
[docs] class Fillet(Structure): """ Represents a multilayer Fillet geometry. Args: anchor (Anchor | tuple[Anchor, Anchor]): The anchor point(s) of the fillet. If a tuple is provided, it represents the start and end anchors of the fillet. radius (float): The radius of the fillet. num_segments (int): The number of line segments used to approximate the fillet curve. layers (dict, optional): A dictionary mapping layer names to widths for creating polygons. alabel (bool, optional): Adds anchors if True. Defaults to True. Example: >>> # Create a fillet from anchor A to anchor B with a radius of 10 and 8 line segments >>> fillet = Fillet((anchor_A, anchor_B), radius=10, layers={'layer1': 0.5, 'layer2': 0.3}) >>> # Create a normalized fillet >>> fillet = Fillet(Anchor(anchor_A, radius=10, layers={'layer1': 0.5, 'layer2': 0.3}) """ def __init__(self, anchor: Anchor | tuple[Anchor, Anchor], radius: float, num_segments: int=20, layers: dict=None, alabel: bool=True): super().__init__() # determine to make a normalized fillet (start from origin) or # make a fillet from the first anchor to the second anchor if isinstance(anchor, tuple): anchor_norm = normalize_anchors(anchor[0], anchor[1]) else: anchor_norm = anchor # get fillet params params = get_fillet_params(anchor_norm, radius) # create skeletone self.skeletone.lines = make_fillet_line(*params, radius, num_segments) # create polygons if layers: for k, width in layers.items(): polygon = self.skeletone.buffer(offset=width/2, cap_style='square') self.add(Layer(name=k, polygons=polygon)) # snap to first anchor if relevant and add anchors to structure if isinstance(anchor, tuple): self.rotate(anchor[0].direction).move(*anchor[0].coords) if alabel: self.anchors.add([anchor[0], anchor[1]]) else: if alabel: self.anchors.add([Anchor((0,0), 0, "origin"), anchor])
[docs] class MicroChannels(Structure): """ Creates microchannels for eHe or can be used to create IDC. Args: length (float): The length of the microchannels. spacing (float): The spacing between each microchannel. num (int): The number of microchannels. angle (float): The angle of the microchannels in degrees. layers (dict): A dictionary containing the names and widths of the layers. alabel (tuple, optional): A tuple containing the labels for the anchors. Example: >>> # Create a MicroChannels object with length 10, spacing 1, 3 microchannels, >>> # angle 45 degrees, layers {'layer1': 0.5, 'layer2': 0.3}, and anchors ('A', 'B'). >>> mc = MicroChannels(length=10, spacing=1, num=3, angle=45, >>> layers={'layer1': 0.5, 'layer2': 0.3}, alabel=('A', 'B')) """ def __init__(self, length: float, spacing: float, num: int, angle: float, layers: dict, alabel: tuple=None): super().__init__() # create skeletone slope = tan(angle * pi/180) l = length - spacing * slope * (num - 1) pts = lambda i: [(0, 0), (0, l + slope * spacing * i), (spacing, l + slope * spacing * (i + 1)), (spacing, 0)] for i in range(num - 1): self.skeletone.add(LineString(pts(i))) self.skeletone.add(LineString([(0, 0), (0, length), (spacing/2, length)])) # create polygon for k, width in layers.items(): polygon = self.skeletone.buffer(offset=width/2, cap_style='flat', join_style='round', quad_segs=3) self.add(Layer(name=k, polygons=polygon)) # create anchors if alabel: first, last = self.skeletone.boundary self.anchors.add([Anchor(point=first, direction=90, label=alabel[0]), Anchor(point=last, direction=0, label=alabel[1])])
[docs] class SpiralInductor(Entity): """ Represents a spiral inductor. Args: size (float): The size of the inductor. width (float): The width of each turn in the spiral. gap (float): The gap between each turn in the spiral. num_turns (int): The number of turns in the spiral. smallest_section_length (float): The length of the smallest section in the spiral. layers (dict): A dictionary mapping layer names to their respective widths. alabel (dict): A dictionary containing labels for the first and last anchor points. Example: >>> # Create a spiral inductor >>> inductor = SpiralInductor(size=10, width=1, gap=0.5, num_turns=3, ... smallest_section_length=0.2, ... layers={'layer1': 0.1, 'layer2': 0.2}, ... alabel=('start', 'end')) """ def __init__(self, size: float, width: float, gap: float, num_turns: int=5, smallest_section_length: float=0.1, layers: dict={"layer1": 0.1}, alabel: tuple=None): super().__init__() # create skeletone eps = 0.1 # this removes some artifacts at the corner of the central_pad unary_union process radius = width/2 self._gap = gap self._width = width coord_init = [(0, 0), (0, -size/2 - width/2), (size/2 + width + gap, -size/2 - width/2)] self.skeletone.lines = LineString(coord_init) central_pad = box(-size/2, -size/2, size/2, size/2).buffer(self._width+eps, join_style=1) central_pad.simplify(0.2, preserve_topology=True) # create polygons self.__construct_spiral(size, radius, num_turns, smallest_section_length) for k, w in layers.items(): polygon = self.skeletone.buffer(offset=w/2, cap_style='round', join_style='mitre', quad_segs=2) layer = Layer(name=k, polygons=polygon) layer.add(central_pad) self.add(layer) # create anchors if alabel: first, last = self.skeletone.lines.boundary.geoms self.anchors.add([Anchor(point=first, direction=0, label=alabel[0]), Anchor(point=last, direction=0, label=alabel[1])]) def __num_segments(self, R: float, smallest_segment: float): # limits the maximum number of segments in arc return int(10*tanh(pi * R/2/smallest_segment/20)) def __construct_spiral(self, size: float, radius: float, num_turns:int, ls: float): # create the spiral line self.skeletone.add(ArcLine(0, radius, radius, 270, 360, self.__num_segments(radius, ls))) self.skeletone.add(LineString([(0, 0), (0, size/2)])) for _ in range(num_turns): radius = radius + self._gap + self._width self.skeletone.add(LineString([(0, 0), (0, size/2)])) self.skeletone.add(ArcLine(-radius, 0, radius, 0, 90, self.__num_segments(radius, ls))) self.skeletone.add(LineString([(0, 0), (-size, 0)])) self.skeletone.add(ArcLine(0, -radius, radius, 90, 180, self.__num_segments(radius, ls))) self.skeletone.add(LineString([(0, 0), (0, -size)])) self.skeletone.add(ArcLine(radius, 0, radius, 180, 270, self.__num_segments(radius, ls))) self.skeletone.add(LineString([(0, 0), (size + self._width + self._gap, 0)])) self.skeletone.add(ArcLine(0, radius, radius, 270, 360, self.__num_segments(radius, ls))) self.skeletone.add(LineString([(0, 0), (0, size/2)])) self.skeletone.add(LineString([(0, 0), (2*self._gap + 2*self._width, 0)]))
[docs] class IDC(Entity): """ Represents a special IDC (symmetrical). Args: length (float): The length of the IDC. spacing (float): The spacing between each IDC. num (int): The number of IDCs to create. layers (dict): A dictionary mapping layer names to their widths. alabel (tuple): A tuple containing two labels for the anchors. Example: >>> length = 10.0 >>> spacing = 5.0 >>> num = 3 >>> layers = {'layer1': 0.5, 'layer2': 0.3} >>> alabel = ('start', 'end') >>> idc = IDC(length, spacing, num, layers, alabel) """ def __init__(self, length: float, spacing: float, num: int, layers: dict, alabel: tuple): super().__init__() pts = [(0, 0), (spacing/2, 0), (spacing/2,length), (spacing/2, -length), (spacing/2, 0), (spacing, 0)] self.skeletone.lines = LineString(pts) for _ in range(num): self.skeletone.add(LineString(pts)) for k, width in layers.items(): polygon = self.skeletone.buffer(offset=width/2, cap_style='square', join_style='round', quad_segs=2) self.add(Layer(name=k, polygons=polygon)) # create anchors if alabel: first, last = self.skeletone.lines.boundary.geoms self.anchors.add([Anchor(point=first, direction=0, label=alabel[0]), Anchor(point=last, direction=0, label=alabel[1])])