class RectangleShape(Shape): """ Creates a rectangular shape. """ p1 = CoordParameter(default=(0, 0), doc='Bottom left corner coordinate.') p2 = CoordParameter(default=(2, 2), doc='Top right corner coodinate.') def create_points(self, points): points = [[self.p1[0], self.p1[1]], [self.p1[0], self.p2[1]], [self.p2[0], self.p2[1]], [self.p2[0], self.p1[1]]] return points
class RouteSquareShape(__RouteSimple__): gds_layer = LayerParameter(name='ArcLayer', number=91) radius = NumberParameter(default=5) width = NumberParameter(default=1) size = CoordParameter(default=(3, 3)) def create_midpoint1(self): return [-self.size[0], 0] def create_midpoint2(self): return [0, self.size[1]] def create_width1(self): return self.width def create_width2(self): return self.width def create_orientation1(self): return 90 def create_orientation2(self): return 0 def create_points(self, points): w = self.width / 2 s1, s2 = self.size pts = [[w, -w], [-s1, -w], [-s1, w], [-w, w], [-w, s2], [w, s2]] points = np.array([pts]) return points
class WedgeShape(Shape): """ wedge, or symmetric trapezium. specified by the center of baselines and the length of the baselines """ begin_coord = CoordParameter(default=(0, 0)) end_coord = CoordParameter(default=(10, 0)) begin_width = NumberParameter(default=3) end_width = NumberParameter(default=1) def create_points(self, points): dist = geom.distance(self.end_coord, self.begin_coord) cosangle = (self.end_coord[0] - self.begin_coord[0]) / dist sinangle = (self.end_coord[1] - self.begin_coord[1]) / dist points = [(self.begin_coord[0] + sinangle * self.begin_width / 2.0, self.begin_coord[1] - cosangle * self.begin_width / 2.0), (self.begin_coord[0] - sinangle * self.begin_width / 2.0, self.begin_coord[1] + cosangle * self.begin_width / 2.0), (self.end_coord[0] - sinangle * self.end_width / 2.0, self.end_coord[1] + cosangle * self.end_width / 2.0), (self.end_coord[0] + sinangle * self.end_width / 2.0, self.end_coord[1] - cosangle * self.end_width / 2.0)] return points
class CircleShape(Shape): """ Creates a circle shape. """ box_size = CoordParameter( default=(2.0, 2.0), doc='The width and height of the circle as a coordinate.') start_angle = FloatParameter(default=0.0, doc='Starting angle of the circle shape.') end_angle = FloatParameter( default=360.0, doc='Degree to which the circle must be completed.') angle_step = IntegerParameter(default=3, doc='The smoothness of the circle.') def create_points(self, points): sa = self.start_angle * constants.DEG2RAD ea = self.end_angle * constants.DEG2RAD h_radius = self.box_size[0] / 2.0 v_radius = self.box_size[1] / 2.0 n_s = float(self.end_angle - self.start_angle) / self.angle_step n_steps = int(math.ceil(abs(n_s))) * np.sign(n_s) if n_steps == 0: if sa == ea: pts = np.array([[ math.cos(sa) * h_radius + self.center[0], math.sin(sa) * v_radius + self.center[1] ]]) else: pts = np.array([[ math.cos(sa) * h_radius + self.center[0], math.sin(sa) * v_radius + self.center[1] ], [ math.cos(ea) * h_radius + self.center[0], math.sin(ea) * v_radius + self.center[1] ]]) return pts angle_step = float(ea - sa) / n_steps if self.clockwise: angle_step = -angle_step sign = -1 else: sign = +1 while sign * sa > sign * ea: ea += sign * 2 * math.pi angles = np.arange(sa, ea + 0.5 * angle_step, angle_step) pts = np.column_stack((np.cos(angles), np.sin(angles))) \ * np.array([(h_radius, v_radius)]) \ + np.array([(self.center[0], self.center[1])]) points = pts return points
class Label(__Label__): """ Label that contains a text description. Example ------- >>> lbl = spira.Label(text='P1', position=(0,0)) >>> [SPiRA: Label] (P1 at (0,0), texttype 0) """ position = CoordParameter(default=(0, 0)) orientation = NumberParameter(default=0) def __init__(self, position, **kwargs): super().__init__(position=position, **kwargs) def __repr__(self): if self is None: return 'Label is None!' string = "[SPiRA: Label] ({} at ({}), layer {})" return string.format(self.text, self.position, self.layer.name)
class SRef(__RefElement__): """ Cell reference (SRef) is the reference to a cell layout to create a hierarchical layout structure. It creates copies of the ports and terminals defined by the cell. These copied ports can be used to connect different cell reference instances. Examples -------- >>> cell = spira.Cell(name='Layout') >>> sref = spira.SRef(structure=cell) """ midpoint = CoordParameter(default=(0, 0)) def __init__(self, reference, midpoint=(0, 0), alias=None, transformation=None, **kwargs): super().__init__(reference=reference, midpoint=midpoint, alias=alias, transformation=transformation, **kwargs) def __repr__(self): name = self.reference.name ps = "[SPiRA: SRef] (\"{}\", alias {}, midpoint {}, transforms {})" return (ps.format(name, self.alias, self.midpoint, self.transformation)) def __str__(self): return self.__repr__() def __hash__(self): return hash(self.__repr__()) def __deepcopy__(self, memo): # return SRef( return self.__class__( alias=self.alias, reference=deepcopy(self.reference), midpoint=deepcopy(self.midpoint), # midpoint=self.midpoint, transformation=deepcopy(self.transformation)) def __eq__(self, other): if not isinstance(other, SRef): return False return ((self.reference == other.reference) and (self.midpoint == other.position) and (self.transformation == other.transformation)) def id_string(self): return self.__repr__() def dependencies(self): from spira.yevon.gdsii.cell_list import CellList d = CellList() d.add(self.reference) d.add(self.reference.dependencies()) return d def net_source(self): return 'source: {}'.format(self.reference.name) def net_target(self): return 'target: {}'.format(self.reference.name) def is_valid_path(self): return True def expand_transform(self): from spira.yevon.gdsii.sref import SRef from spira.yevon.gdsii.polygon import Polygon from spira.core.transforms.identity import IdentityTransform C = self.reference.__class__( name='{}_{}'.format(self.reference.name, self.transformation.id_string()), elements=deepcopy(self.reference.elements), ports=deepcopy(self.reference.ports)) T = self.transformation + spira.Translation(self.midpoint) for i, e in enumerate(C.elements): if isinstance(e, SRef): e.midpoint = self.transformation.apply_to_coord( e.midpoint).move(self.midpoint) e.transform(self.transformation) elif isinstance(e, Polygon): e.transform(T) C.ports.transform(T) self.reference = C self.transformation = None self.midpoint = (0, 0) return self def expand_flat_copy(self): """ """ D = self.reference.expand_flat_copy() return SRef(reference=D) # return self.__class__(reference=D) def flat_copy(self, level=-1): if level == 0: return spira.ElementList(self.__copy__()) elems = self.reference.elements.flat_copy(level - 1) T = self.transformation + Translation(self.midpoint) elems = elems.transform(T) return elems def flatten(self, level=-1, name_tree=[]): if level == 0: return self.reference name_tree.append(self.alias) nt = deepcopy(name_tree) D = self.reference.flatten(level, name_tree=nt) name_tree.pop() return D.elements def flat_container(self, cc, name_tree=[]): if self.alias is None: self.alias = '' name_tree.append(self.alias) nt = deepcopy(name_tree) self.reference.flat_container(cc, name_tree=nt) name_tree.pop() def move(self, midpoint=(0, 0), destination=None, axis=None): """ Move the reference internal port to the destination. Example: -------- >>> S.move() """ if destination is None: destination = midpoint midpoint = [0, 0] if isinstance(midpoint, Coord): o = midpoint elif np.array(midpoint).size == 2: o = midpoint elif midpoint in self.ports.get_names(): o = self.ports[midpoint.name].midpoint elif issubclass(type(midpoint), __Port__): o = midpoint.midpoint else: raise ValueError("[PHIDL] [DeviceReference.move()] ``midpoint`` " + "not array-like, a port, or port name") if issubclass(type(destination), __Port__): d = destination.midpoint elif isinstance(destination, Coord): d = destination elif np.array(destination).size == 2: d = destination elif destination in self.ports.get_names(): d = self.ports[destination.name].midpoint else: raise ValueError( "[PHIDL] [DeviceReference.move()] ``destination`` " + "not array-like, a port, or port name") position = np.array([d[0], d[1]]) - np.array([o[0], o[1]]) self.midpoint = Coord(self.midpoint[0] + position[0], self.midpoint[1] + position[1]) # dxdy = Coord(self.midpoint[0] + position[0], self.midpoint[1] + position[1]) # self.translate(dxdy) return self def connect(self, port, destination, ignore_process=False): """ Connect the reference internal port with an external port. Example: -------- >>> S.connect() """ if issubclass(type(port), __Port__): p = port elif port in self.ports.get_names(): if issubclass(type(port), __Port__): p = self.ports[port.name] elif isinstance(port, str): p = self.ports[port] else: raise ValueError( "[SPiRA] connect() did not receive a Port or " + "valid port name - received ({}), ports available " + "are ({})".format(port, self.ports.get_names())) if not isinstance(destination, spira.Port): raise ValueError('Destination has to be a port.') if (ignore_process is False) and (p.process != destination.process): raise ValueError('Cannot connect ports from different processes.') T = vector_match_transform(v1=p, v2=destination) self.midpoint = T.apply_to_coord(self.midpoint) self.transform(T - spira.Translation(self.midpoint)) # self.transformation = T - spira.Translation(self.midpoint) # self.transformation = T # print(T - spira.Translation(self.midpoint)) # print(T) return self def distance_alignment(self, port, destination, distance): """ Align the reference using an internal port with an external port. Example: -------- >>> S.distance_alignment() """ destination = deepcopy(destination) self.connect(port, destination) L = line_from_point_angle(point=destination.midpoint, angle=destination.orientation) dx, dy = L.get_coord_from_distance(destination, distance) # self.move(midpoint=self.midpoint, destination=(dx,dy)) T = spira.Translation(translation=(dx, dy)) self.midpoint = T.apply_to_coord(self.midpoint) # self.transform(T - spira.Translation(self.midpoint)) # self.transform(T) return self def center_alignment(self, p1, p2): """ Place the reference `midpoint` at the virtual intersection line of two ports. Example ------- >>> s.center_alignment(p1=self.p3, p2=self.via1_i5.ports['M5_P2']) """ l1 = line_from_point_angle(point=p1.midpoint, angle=p1.orientation) l2 = line_from_point_angle(point=p2.midpoint, angle=p2.orientation) coord = l1.intersection(l2) self.move(midpoint=self.midpoint, destination=coord) return self # FIXME~~~~ Look at lieze_dcsfq.py def port_alignment(self, ports, p1, p2): """ Align `ports[0]` of reference with `p1` and `ports[1]` of reference with external port `p2`. Example ------- >>> """ self.connect(ports[0], deepcopy(p1)) # print(self.transformation) if isinstance(ports[1], str): pin1 = self.ports[ports[1]] elif issubclass(type(ports[1]), __Port__): # pin1 = ports[1].transform(self.transformation) pin1 = ports[1].transform(spira.Translation(self.midpoint)) # print(pin1) # print(p2) # print(ports[0]) if ports[0].orientation in (0, 180): T = vector_match_axis(v1=pin1, v2=p2, axis='x') elif ports[0].orientation in (90, 270): T = vector_match_axis(v1=pin1, v2=p2, axis='y') T = T + self.transformation # self.midpoint = T.apply_to_coord(Coord(0,0)) # self.midpoint = T.apply_to_coord(self.midpoint) # self.transform(T + spira.Translation(self.midpoint)) self.transform(T) return self def stretch_by_factor(self, factor=(1, 1), center=(0, 0)): """ Strecth the entire instance by a factor and around a specified center. Example ------- >>> S.stretch_by_factor(factor=(2,1)) """ S = self.expand_flat_copy() T = spira.Stretch(stretch_factor=factor, stretch_center=center) for i, e in enumerate(S.reference.elements): S.reference.elements[i].transform(T) return self def stretch_p2c(self, port_name, destination_coord): """ Stretch port to port. Stretch the polygon by moving the polygon edge port to the destination port location. Note ---- The opposite port position is used as the stretching center. This overcomes the issue of distorting the entiry structure. Example ------- >>> S.stretch_p2p(port_name='S1:Sr1:E3_R1', destination_coord=(10,0)) """ from spira.yevon.gdsii.polygon import Polygon from spira.yevon.geometry.bbox_info import bbox_info_opposite_boundary_port D = self.expand_flat_copy() port = D.ports[port_name] destination = D.ports[destination_name] for i, e in enumerate(D.reference.elements.polygons): if e.id_string() == port.local_pid: opposite_port = bbox_info_opposite_boundary_port(e, port) T = stretching.stretch_element_by_port(self, opposite_port, port, destination) T.apply(D.reference.elements[i]) def stretch_p2p(self, port_name, destination_name): """ Stretch port to port. Stretch the polygon by moving the polygon edge port to the destination port location. Note ---- The opposite port position is used as the stretching center. This overcomes the issue of distorting the entiry structure. Example ------- >>> S.stretch_p2p(port_name='S1:Sr1:E3_R1', destination_name='S2:Sr2:E1_R1') """ from spira.yevon.gdsii.polygon import Polygon from spira.yevon.geometry.ports import PortList from spira.yevon.geometry.bbox_info import bbox_info_opposite_boundary_port D = self.expand_flat_copy() # print(D.ports) # print('\n------------------\n') # print('\n*************************************') ports = PortList() for e in D.reference.elements.polygons: # print(e.edge_ports) ports += e.edge_ports port = ports[port_name] destination = ports[destination_name] # print(port) # print(destination) for i, e in enumerate(D.reference.elements.polygons): if e.id_string() == port.local_pid: opposite_port = bbox_info_opposite_boundary_port(e, port) T = stretching.stretch_element_by_port(self, opposite_port, port, destination) T.apply(D.reference.elements[i]) def nets(self, lcar): """ """ from spira.yevon.geometry.nets.net_list import NetList nets = NetList() nets += self.reference.netlist # FIXME: Is this transformation required? # T = self.transformation + Translation(self.midpoint) # nets.transform(T) return nets
class Vector(Transformable, ParameterInitializer): """ Vector consisting of a point and an orientation. """ midpoint = CoordParameter(default=(0.0, 0.0), doc='Position of the point.') @property def x(self): return self.midpoint.x @property def y(self): return self.midpoint.y def get_angle_rad(self): if hasattr(self, '__angle__'): return constants.DEG2RAD * self.__angle__ else: return 0.0 def set_angle_rad(self, value): self.__angle__ = (constants.RAD2DEG * value) % 360.0 angle_rad = FunctionParameter( get_angle_rad, set_angle_rad, doc= "The outward facing orientation of the port in radians (stored in degrees by default, converted to radians if needed)" ) def get_angle_deg(self): if hasattr(self, '__angle__'): return self.__angle__ else: return 0.0 def set_angle_deg(self, value): self.__angle__ = value % 360.0 orientation = FunctionParameter( get_angle_deg, set_angle_deg, doc="The outward facing orientation of the port.") def cos(self): return cos(constants.DEG2RAD * self.__angle__) def sin(self): return sin(constants.DEG2RAD * self.__angle__) def tan(self): return tan(constants.DEG2RAD * self.__angle__) def flip(self): return Vector(midpoint=self.midpoint, orientation=(self.__angle__ + 180.0) % 360.0) def __getitem__(self, key): if key == 0: return self.midpoint[0] if key == 1: return self.midpoint[1] else: raise IndexError( "Vector supports only subscription[0] and [1], not " + str(key)) def __eq__(self, other): if not isinstance(other, Vector): return False return self.midpoint == other.midpoint and (self.angle_deg == other.angle_deg) def __ne__(self, other): return self.midpoint != other.midpoint or (self.angle_deg != other.angle_deg) def __repr__(self): return "<Vector (%f, %f), a=%f>" % (self.x, self.y, self.angle_deg) def transform(self, transformation): self.midpoint = transformation.apply_to_coord(self.midpoint) self.angle_deg = transformation.apply_to_angle(self.angle_deg) return self
class YtronShape(spira.Shape): """ Class for generating a yTron shape. """ rho = NumberParameter(default=0.2, doc='Angle of concave bend between the arms.') arm_lengths = CoordParameter( default=(5, 3), doc='Length or the left and right arms, respectively.') source_length = NumberParameter(default=5, doc='Length of the source arm.') arm_widths = CoordParameter( default=(2, 2), doc='Width of the left and right arms, respectively.') theta = NumberParameter(default=3, doc='Angle of the left and right arms.') theta_resolution = NumberParameter(default=10, doc='Smoothness of the concave bend.') xc = Parameter(fdef_name='create_xc') yc = Parameter(fdef_name='create_yc') arm_x_left = Parameter(fdef_name='create_arm_x_left') arm_y_left = Parameter(fdef_name='create_arm_y_left') arm_x_right = Parameter(fdef_name='create_arm_x_right') arm_y_right = Parameter(fdef_name='create_arm_y_right') rad_theta = Parameter(fdef_name='create_rad_theta') ml = Parameter(fdef_name='create_midpoint_left') mr = Parameter(fdef_name='create_midpoint_right') ms = Parameter(fdef_name='create_midpoint_source') def create_rad_theta(self): return self.theta * np.pi / 180 def create_xc(self): return self.rho * np.cos(self.rad_theta) def create_yc(self): return self.rho * np.sin(self.rad_theta) def create_arm_x_left(self): return self.arm_lengths[0] * np.sin(self.rad_theta) def create_arm_y_left(self): return self.arm_lengths[0] * np.cos(self.rad_theta) def create_arm_x_right(self): return self.arm_lengths[1] * np.sin(self.rad_theta) def create_arm_y_right(self): return self.arm_lengths[1] * np.cos(self.rad_theta) def create_midpoint_left(self): xc = -(self.xc + self.arm_x_left + self.arm_widths[0] / 2) yc = self.yc + self.arm_y_left return [xc, yc] def create_midpoint_right(self): xc = self.xc + self.arm_x_right + self.arm_widths[1] / 2 yc = self.yc + self.arm_y_right return [xc, yc] def create_midpoint_source(self): xc = (self.arm_widths[1] - self.arm_widths[0]) / 2 yc = -self.source_length + self.yc return [xc, yc] def create_points(self, points): theta = self.theta * np.pi / 180 theta_resolution = self.theta_resolution * np.pi / 180 theta_norm = int((np.pi - 2 * theta) / theta_resolution) + 2 thetalist = np.linspace(-(np.pi - theta), -theta, theta_norm) semicircle_x = self.rho * np.cos(thetalist) semicircle_y = self.rho * np.sin(thetalist) + self.rho xpts = semicircle_x.tolist() + [ self.xc + self.arm_x_right, self.xc + self.arm_x_right + self.arm_widths[1], self.xc + self.arm_widths[1], self.xc + self.arm_widths[1], 0, -(self.xc + self.arm_widths[0]), -(self.xc + self.arm_widths[0]), -(self.xc + self.arm_x_left + self.arm_widths[0]), -(self.xc + self.arm_x_left) ] ypts = semicircle_y.tolist() + [ self.yc + self.arm_y_right, self.yc + self.arm_y_right, self.yc, self.yc - self.source_length, self.yc - self.source_length, self.yc - self.source_length, self.yc, self.yc + self.arm_y_left, self.yc + self.arm_y_left ] points = np.array(list(zip(xpts, ypts))) return points
class __Shape__(Transformable, ParameterInitializer): center = CoordParameter() clockwise = BoolParameter(default=False) points = PointArrayParameter(fdef_name='create_points') def __init__(self, **kwargs): super().__init__(**kwargs) def create_points(self, points): return points @property def x_coords(self): """ Returns the x coordinates """ return self.points[:, 0] @property def y_coords(self): """ Returns the y coordinates """ return self.points[:, 1] @property def is_closed(self): return True @property def center_of_mass(self): """ Get the center of mass of the shape. Note: This is not the same as the bounding box center.""" c = np.mean(self.points, 0) return [c[0], c[1]] @property def orientation(self): """ Returns the orientation of the shape. Counterclockwise returns +1 and clockwise returns -1. """ pts = self.points T = np.roll(np.roll(pts, 1, 1), 1, 0) return -np.sign(sum(np.diff(pts * T, 1, 1))) @property def area(self): """ Returns the area of the shape. """ pts = self.points T = np.roll(np.roll(pts, 1, 1), 1, 0) return sum(abs(np.diff(pts * T, 1, 1))) * 0.5 @property def hash_string(self): import hashlib pts = np.array([self.points]) hash_key = np.sort([hashlib.sha1(p).hexdigest() for p in pts]) return str(hash_key[0]) @property def count(self): """ Number of points in the shape """ return self.__len__() @property def bbox_info(self): return bbox_info.bbox_info_from_numpy_array(self.points) def reverse_points(self): """ If orientation is clockwise, convert to counter-clockwise. """ sc = constants.CLIPPER_SCALE pts = st(self.points, sc) if pyclipper.Orientation(pts) is False: reverse_poly = pyclipper.ReversePath(pts) solution = pyclipper.SimplifyPolygon(reverse_poly) else: solution = pyclipper.SimplifyPolygon(pts) self.points = sf(solution, sc)[0] return self def segments(self): """ Returns a list of point pairs with the segments of the shape. """ p = self.points if len(p) < 2: return [] if self.is_closed: segments = list(zip(p, np.roll(p, shift=-1, axis=0))) else: segments = list(zip(p[:-1], p[1:])) return segments def snap_to_grid(self, grids_per_unit=None): """ Snaps all shape points to grid. """ from spira.settings import get_grids_per_unit if grids_per_unit is None: grids_per_unit = get_grids_per_unit() self.points = (np.floor(self.points * grids_per_unit + 0.5)) / grids_per_unit return self def move(self, pos): p = np.array([pos[0], pos[1]]) self.points += p return self def transform(self, transformation): self.points = transformation.apply_to_array(self.points) return self def make_clockwise(self): """ Make sure all points are clockwise ordered. """ x, y = self.x_coords, self.y_coords cx, cy = np.mean(x), np.mean(y) a = np.arctan2(y - cy, x - cx) order = a.ravel().argsort() self.points = np.column_stack((x[order], y[order])) return self def remove_identicals(self): """ Removes consecutive identical points """ from spira import settings pts = self.points if len(pts) > 1: identicals = np.prod(abs(pts - np.roll(self.points, -1, 0)) < 0.5 / settings.get_grids_per_unit(), 1) if not self.is_closed: identicals[-1] = False self.points = np.delete(pts, identicals.nonzero()[0], 0) return self def remove_straight_angles(self): """ removes points with turn zero or 180 degrees """ Shape.remove_identicals(self) pts = self.points if len(pts) > 1: straight = (abs(abs((self.turns_rad() + (0.5 * np.pi)) % np.pi) - 0.5 * np.pi) < 0.00001) if not self.is_closed: straight[0] = False straight[-1] = False self.points = np.delete(pts, straight.nonzero()[0], 0) return self def angles_rad(self): """ returns the angles (radians) of the connection between each point and the next """ pts = self.points R = np.roll(pts, -1, 0) radians = np.arctan2(R[:, 1] - pts[:, 1], R[:, 0] - pts[:, 0]) return radians def turns_rad(self): """ returns the angles (radians) of the turn in each point """ a = self.angles_rad() return (a - np.roll(a, 1, 0) + np.pi) % (2 * np.pi) - np.pi def intersections(self, other_shape): """ the intersections with this shape and the other shape """ from spira.yevon.utils import geometry as gm s = Shape(self.points) s.remove_straight_angles() segments1 = s.segments() if len(segments1) < 1: return [] s = Shape(other_shape) s.remove_straight_angles() segments2 = s.segments() if len(segments2) < 1: return [] intersections = [] for s1 in segments1: intersected_points = [] for s2 in segments2: if gm.lines_cross(s1[0], s1[1], s2[0], s2[1], inclusive=True): c = [gm.intersection(s1[0], s1[1], s2[0], s2[1])] intersected_points += c if len(intersected_points) > 0: pl = gm.sort_points_on_line([*s1, *intersected_points]) intersections += [pl[1], pl[2]] intersections = gm.points_unique(intersections) return Shape(intersections)
class Stretch(ReversibleTransform): """ Stretch an object using. Example ------- >>> s = Stretch()(shape) """ stretch_center = CoordParameter(default=(0, 0)) def set_stretch_factor(self, value): if isinstance(value, Coord): self.__stretch_factor__ = value else: self.__stretch_factor__ = Coord(value[0], value[1]) if self.__stretch_factor__[0] == 0.0 or self.__stretch_factor__[ 1] == 0.0: raise ValueError( "Error: Stretch factor cannot be zero in Stretch transform") stretch_factor = SetFunctionParameter('__stretch_factor__', set_stretch_factor) def __repr__(self): return "[SPiRA: Stretch] (factor {}, center {})".format( self.stretch_factor, self.stretch_center) def __str__(self): return self.__repr__() def apply_to_coord(self, coord): x1 = self.__stretch_factor__[0] * coord[0] x2 = (1 - self.__stretch_factor__[0]) * self.stretch_center[0] y1 = self.__stretch_factor__[1] * coord[1] y2 = (1 - self.__stretch_factor__[1]) * self.stretch_center[1] return Coord(x1 + x2, y1 + y2) def reverse_on_coord(self, coord): x1 = 1.0 / self.__stretch_factor__[0] * coord[0] x2 = (1 - 1.0 / self.__stretch_factor__[0]) * self.stretch_center[0] y1 = 1.0 / self.__stretch_factor__[1] * coord[1] y2 = (1 - 1.0 / self.__stretch_factor__[1]) * self.stretch_center[1] return Coord(x1 + x2, y1 + y2) def apply_to_array(self, coords): coords *= np.array([self.stretch_factor.x, self.stretch_factor.y]) x = (1 - self.__stretch_factor__.x) * self.stretch_center.x y = (1 - self.__stretch_factor__.y) * self.stretch_center.y coords += np.array([x, y]) return coords def reverse_on_array(self, coords): coords *= np.array( [1.0 / self.stretch_factor.x, 1.0 / self.stretch_factor.y]) x = (1 - 1.0 / self.__stretch_factor__.x) * self.stretch_center.x y = (1 - 1.0 / self.__stretch_factor__.y) * self.stretch_center.y coords += np.array([x, y]) return coords def apply_to_angle(self, angle): # FIXME: This is required for transforming polygon ports. # This is currently just a temporary fix. return angle def is_identity(self): """ Returns True if the transformation does nothing """ return ((self.stretch_factor.x == 1.0) and (self.stretch_factor.y == 1.0)) def id_string(self): return self.__repr__()
class YtronShape(Shape): """ Shape for generating a yTron device. """ rho = NumberParameter(default=0.2) arm_lengths = CoordParameter(default=(5, 3)) source_length = NumberParameter(default=5) arm_widths = CoordParameter(default=(2, 2)) theta = NumberParameter(default=2.5) theta_resolution = NumberParameter(default=10.0) xc = Parameter(fdef_name='create_xc') yc = Parameter(fdef_name='create_yc') arm_x_left = Parameter(fdef_name='create_arm_x_left') arm_y_left = Parameter(fdef_name='create_arm_y_left') arm_x_right = Parameter(fdef_name='create_arm_x_right') arm_y_right = Parameter(fdef_name='create_arm_y_right') rad_theta = Parameter(fdef_name='create_rad_theta') def create_rad_theta(self): return self.theta * np.pi / 180 def create_xc(self): return self.rho * np.cos(self.rad_theta) def create_yc(self): return self.rho * np.sin(self.rad_theta) def create_arm_x_left(self): return self.arm_lengths[0] * np.sin(self.rad_theta) def create_arm_y_left(self): return self.arm_lengths[0] * np.cos(self.rad_theta) def create_arm_x_right(self): return self.arm_lengths[1] * np.sin(self.rad_theta) def create_arm_y_right(self): return self.arm_lengths[1] * np.cos(self.rad_theta) def create_points(self, points): theta = self.theta * np.pi / 180 theta_resolution = self.theta_resolution * np.pi / 180 theta_norm = int((np.pi - 2 * theta) / theta_resolution) + 2 thetalist = np.linspace(-(np.pi - theta), -theta, theta_norm) semicircle_x = self.rho * np.cos(thetalist) semicircle_y = self.rho * np.sin(thetalist) + self.rho xpts = semicircle_x.tolist() + [ self.xc + self.arm_x_right, self.xc + self.arm_x_right + self.arm_widths[1], self.xc + self.arm_widths[1], self.xc + self.arm_widths[1], 0, -(self.xc + self.arm_widths[0]), -(self.xc + self.arm_widths[0]), -(self.xc + self.arm_x_left + self.arm_widths[0]), -(self.xc + self.arm_x_left) ] ypts = semicircle_y.tolist() + [ self.yc + self.arm_y_right, self.yc + self.arm_y_right, self.yc, self.yc - self.source_length, self.yc - self.source_length, self.yc - self.source_length, self.yc, self.yc + self.arm_y_left, self.yc + self.arm_y_left ] points = np.array(list(zip(xpts, ypts))) return points
class GenericTransform(ReversibleTransform): """ """ def __init__(self, translation=(0, 0), rotation=0, absolute_rotation=False, **kwargs): super().__init__(translation=translation, rotation=rotation, absolute_rotation=absolute_rotation, **kwargs) def set_rotation(self, value): self.__rotation__ = value % 360.0 if value % 90.0 == 0.0: if self.__rotation__ == 0.0: self.__ca__ = 1.0 self.__sa__ = 0.0 elif self.__rotation__ == 90.0: self.__ca__ = 0.0 self.__sa__ = 1.0 elif self.__rotation__ == 180.0: self.__ca__ = -1.0 self.__sa__ = 0.0 elif self.__rotation__ == 270.0: self.__ca__ = 0.0 self.__sa__ = -1.0 else: self.__ca__ = np.cos(value * constants.DEG2RAD) self.__sa__ = np.sin(value * constants.DEG2RAD) rotation = SetFunctionParameter(local_name='__rotation__', fset=set_rotation, default=0.0) magnification = NumberParameter(default=1) reflection = BoolParameter(default=False) translation = CoordParameter(local_name='__translation__') absolute_rotation = BoolParameter(local_name='__absolute_rotation__', default=False) def __str__(self): """ Gives a string representing the transform. """ return "<T {}, R {}, RF {}, M {}>".format(str(self.translation), str(self.rotation), str(self.reflection), str(self.magnification)) def __translate__(self, coord): C = Coord(coord[0] + self.translation.x, coord[1] + self.translation.y) return C def __rotate__(self, coord): return Coord(coord[0] * self.__ca__ - coord[1] * self.__sa__, coord[0] * self.__sa__ + coord[1] * self.__ca__) def __magnify__(self, coord): return Coord(coord[0] * self.magnification, coord[1] * self.magnification) def __inv_translate__(self, coord): return Coord(coord[0] - self.translation.x, coord[1] - self.translation.y) def __inv_rotate__(self, coord): return Coord(coord[0] * self.__ca__ + coord[1] * self.__sa__, -coord[0] * self.__sa__ + coord[1] * self.__ca__) def __inv_magnify__(self, coord): return Coord(coord[0] / self.magnification, coord[1] / self.magnification) def __reflect__(self, coords, p1=(0, 0), p2=(1, 0)): if self.reflection is True: points = np.array(coords.to_numpy_array()) p1 = np.array(p1) p2 = np.array(p2) if np.asarray(points).ndim == 1: t = np.dot((p2 - p1), (points - p1)) / norm(p2 - p1)**2 pts = 2 * (p1 + (p2 - p1) * t) - points if np.asarray(points).ndim == 2: raise ValueError('This is a array, not an coordinate.') else: pts = coords pts = Coord(pts[0], pts[1]) return pts def __reflect_array__(self, coords, p1=(0, 0), p2=(1, 0)): if self.reflection is True: points = np.array(coords) p1 = np.array(p1) p2 = np.array(p2) if np.asarray(points).ndim == 1: raise ValueError('This is a coordinate, not an array.') if np.asarray(points).ndim == 2: t = np.dot((p2 - p1), (p2 - p1)) / norm(p2 - p1)**2 pts = np.array([2 * (p1 + (p2 - p1) * t) - p for p in points]) else: pts = coords return pts def __translate_array__(self, coords): # FIXME: Why should I convert to float! coords = [[float(c[0]), float(c[1])] for c in coords] coords += np.array([self.translation.x, self.translation.y]) return coords def __rotate_array__(self, coords): x_a = np.array([self.__ca__, -self.__sa__]) y_a = np.array([self.__sa__, self.__ca__]) coords = np.transpose( np.vstack((np.sum(coords * x_a, 1), np.sum(coords * y_a, 1)))) return coords def __magnify_array__(self, coords): coords *= np.array([self.magnification, self.magnification]) return coords def apply_to_coord(self, coord): coord = self.__reflect__(coord) coord = self.__rotate__(coord) coord = self.__magnify__(coord) coord = self.__translate__(coord) return coord def apply_to_array(self, coords): coords = self.__reflect_array__(coords) coords = self.__rotate_array__(coords) coords = self.__magnify_array__(coords) coords = self.__translate_array__(coords) return coords def apply_to_angle(self, angle): a = angle if self.reflection: a = -a a += self.rotation return a % 360.0 def __add__(self, other): if other is None: return deepcopy(self) # if issubclass(type(other), GenericTransform): if isinstance(other, GenericTransform): T = GenericTransform() if other.reflection is True: s_1 = -1 else: s_1 = 1 M1 = 1.0 if not self.absolute_rotation: T.rotation = s_1 * self.rotation + other.rotation ca = other.__ca__ sa = other.__sa__ # print('A') else: # print('B') T.rotation = s_1 * self.rotation ca = 1.0 sa = 0.0 # Counterclockwise rotation # cx = self.translation.x + ca * other.translation.x * M1 - s_1 * sa * other.translation.y * M1 # cy = self.translation.y + sa * other.translation.x * M1 + s_1 * ca * other.translation.y * M1 cx = other.translation.x + ca * self.translation.x * M1 - s_1 * sa * self.translation.y * M1 cy = other.translation.y + sa * self.translation.x * M1 + s_1 * ca * self.translation.y * M1 # cx = other.translation.x + ca * self.translation.x * M1 + s_1 * sa * self.translation.y * M1 # cy = -other.translation.y + sa * self.translation.x * M1 + s_1 * ca * self.translation.y * M1 T.translation = Coord(cx, cy) T.absolute_rotation = self.absolute_rotation or other.absolute_rotation T.reflection = (not self.reflection == other.reflection) else: T = Transform.__add__(self, other) return T def __iadd__(self, other): # return self.__add__(other) if other is None: return self # if issubclass(type(other), GenericTransform): if isinstance(other, GenericTransform): T = GenericTransform() if other.reflection is True: s_1 = -1 else: s_1 = 1 M1 = 1.0 if not self.absolute_rotation: self.rotation = s_1 * self.rotation + other.rotation ca = other.__ca__ sa = other.__sa__ else: self.rotation = s_1 * self.rotation ca = 1 sa = 0 # Counterclockwise rotation # cx = self.translation.x + ca * other.translation.x * M1 - s_1 * sa * other.translation.y * M1 # cy = self.translation.y + sa * other.translation.x * M1 + s_1 * ca * other.translation.y * M1 cx = other.translation.x + ca * self.translation.x * M1 - s_1 * sa * self.translation.y * M1 cy = other.translation.y + sa * self.translation.x * M1 + s_1 * ca * self.translation.y * M1 # cx = other.translation.x + ca * self.translation.x * M1 + s_1 * sa * self.translation.y * M1 # cy = -other.translation.y + sa * self.translation.x * M1 + s_1 * ca * self.translation.y * M1 self.translation = Coord(cx, cy) self.absolute_rotation = self.absolute_rotation or other.absolute_rotation self.reflection = (not self.reflection == other.reflection) else: raise ValueError('Cannot add transforms') return self def __sub__(self, other): """ returns the concatenation of this transform and the reverse of other """ if other is None: return copy.deepcopy(self) if not isinstance(other, ReversibleTransform): raise TypeError("Cannot subtract an irreversible transform") return self.__add__(-other) def __isub__(self, other): """ concatenates the reverse of other to this transform """ if other is None: return self if not isinstance(other, ReversibleTransform): raise TypeError("Cannot subtract an irreversible transform") return self.__iadd__(self, -other) def __eq__(self, other): if other is None: return self.is_identity() if not isinstance(other, GenericTransform): return False return ((self.rotation == other.rotation) and (self.translation == other.translation) and (self.reflection == other.reflection) and (self.magnification == other.magnification)) def __ne__(self, other): """ checks if the transforms do different things """ if other is None: return not self.is_identity() if not isinstance(other, GenericTransform): return False return ((self.rotation != other.rotation) or (self.translation != other.translation) or (self.reflection != other.reflection) or (self.magnification != other.magnification)) def __neg__(self): from spira.core.transforms.translation import Translation from spira.core.transforms.rotation import Rotation T = Translation(translation=-self.translation) + Rotation( rotation=-self.rotation, rotation_center=(0, 0)) # T = Translation(translation=-self.translation) # T += Rotation(rotation=-self.rotation, rotation_center=(0,0)) return T def id_string(self): """ Gives a hash of the transform (for naming purposes) """ return self.__str__() def is_identity(self): """ Returns True if the transformation does nothing """ return ((self.rotation == 0.0) and (self.translation.x == 0.0) and (self.translation.y == 0.0) and (not self.reflection) and (self.magnification == 1.0))
class __Shape__(Transformable, ParameterInitializer): center = CoordParameter() clockwise = BoolParameter(default=False) points = PointArrayParameter(fdef_name='create_points') def __init__(self, **kwargs): super().__init__(**kwargs) def create_points(self, points): return points @property def x_coords(self): """ Returns the x coordinates """ return self.points[:, 0] @property def y_coords(self): """ Returns the y coordinates """ return self.points[:, 1] @property def is_closed(self): return True @property def center_of_mass(self): """ Get the center of mass of the shape. Note: This is not the same as the bounding box center.""" c = np.mean(self.points, 0) return [c[0], c[1]] @property def orientation(self): """ Returns the orientation of the shape. Counterclockwise returns +1 and clockwise returns -1. """ pts = self.points T = np.roll(np.roll(pts, 1, 1), 1, 0) return -np.sign(sum(np.diff(pts * T, 1, 1))) @property def area(self): """ Returns the area of the shape. """ pts = self.points T = np.roll(np.roll(pts, 1, 1), 1, 0) return sum(abs(np.diff(pts * T, 1, 1))) * 0.5 @property def hash_string(self): import hashlib pts = np.array([self.points]) hash_key = np.sort([hashlib.sha1(p).hexdigest() for p in pts]) return str(hash_key) @property def count(self): """ Number of points in the shape """ return self.__len__() @property def bbox_info(self): return bbox_info.bbox_info_from_numpy_array(self.points) # @property def segments(self): """ Returns a list of point pairs with the segments of the shape. """ p = self.points if len(p) < 2: return [] if self.is_closed: segments = list(zip(p, np.roll(p, shift=-1, axis=0))) else: segments = list(zip(p[:-1], p[1:])) # segments = [Coord(s[0], s[1]).snap_to_grid() for s in segments] # ss = [] # for segment in segments: # seg = [Coord(s[0], s[1]).snap_to_grid() for s in segment] # ss.append(seg) # segments = ss # s1 = Coord(s1[0], s1[1]).snap_to_grid() return segments def snap_to_grid(self, grids_per_unit=None): """ Snaps all shape points to grid. """ from spira.settings import get_grids_per_unit if grids_per_unit is None: grids_per_unit = get_grids_per_unit() # print(self.points) self.points = (np.floor(self.points * grids_per_unit + 0.5)) / grids_per_unit # print(self.points) # print('\n\n===========================') return self def move(self, pos): p = np.array([pos[0], pos[1]]) self.points += p return self def transform(self, transformation): self.points = transformation.apply_to_array(self.points) return self def make_clockwise(self): """ Make sure all points are clockwise ordered. """ x, y = self.x_coords, self.y_coords cx, cy = np.mean(x), np.mean(y) a = np.arctan2(y - cy, x - cx) order = a.ravel().argsort() self.points = np.column_stack((x[order], y[order])) return self def remove_identicals(self): """ Removes consecutive identical points """ # FIXME: in some cases a series of many points close together # is removed, even if they form together a valid shape. from spira import settings pts = self.points if len(pts) > 1: identicals = np.prod( abs(pts - np.roll(self.points, -1, 0)) < 0.5 / settings.get_grids_per_unit(), 1) if not self.is_closed: identicals[-1] = False self.points = np.delete(pts, identicals.nonzero()[0], 0) return self def remove_straight_angles(self): """ removes points with turn zero or 180 degrees """ Shape.remove_identicals(self) pts = self.points if len(pts) > 1: straight = (abs( abs((self.turns_rad() + (0.5 * np.pi)) % np.pi) - 0.5 * np.pi) < 0.00001 ) # (self.turns_rad()%np.pi == 0.0) if not self.is_closed: straight[0] = False straight[-1] = False self.points = np.delete(pts, straight.nonzero()[0], 0) return self def angles_rad(self): """ returns the angles (radians) of the connection between each point and the next """ pts = self.points R = np.roll(pts, -1, 0) radians = np.arctan2(R[:, 1] - pts[:, 1], R[:, 0] - pts[:, 0]) return radians def turns_rad(self): """ returns the angles (radians) of the turn in each point """ a = self.angles_rad() return (a - np.roll(a, 1, 0) + np.pi) % (2 * np.pi) - np.pi def intersections(self, other_shape): """ the intersections with this shape and the other shape """ from spira.yevon.utils.geometry import intersection, lines_cross, lines_coincide, sort_points_on_line, points_unique s = Shape(self.points) s.remove_straight_angles() segments1 = s.segments() if len(segments1) < 1: return [] s = Shape(other_shape) s.remove_straight_angles() segments2 = s.segments() if len(segments2) < 1: return [] intersections = [] for s1 in segments1: intersected_points = [] for s2 in segments2: if lines_cross(s1[0], s1[1], s2[0], s2[1], inclusive=True): # print(s1, s2) c = [intersection(s1[0], s1[1], s2[0], s2[1])] # print(c) # print('wjefbwefb') intersected_points += c # FIXME Must this be a 0 or a 1!!! if len(intersected_points) > 0: # if len(intersected_points) > 1: pl = sort_points_on_line([*s1, *intersected_points]) intersections += [pl[1], pl[2]] intersections = points_unique(intersections) return Shape(intersections) def insert(self, i, item): """ Inserts a list of points. """ if isinstance(item, Shape): self.points = np.insert(self.points, i, item.points, axis=0) elif isinstance(item, (list, np.ndarray)): if isinstance(item[0], Coord): item[0] = item[0].to_numpy_array() if len(item) > 1: if isinstance(item[1], Coord): item[1] = item[1].to_numpy_array() self.points = np.insert(self.points, i, item, axis=0) elif isinstance(item, (Coord, tuple)): self.points = np.insert(self.points, i, [(item[0], item[1])], axis=0) else: raise TypeError("Wrong type " + str(type(item)) + " to extend Shape with") return self def id_string(self): return self.__str__()
class ParabolicShape(Shape): """ parabolic wedge (taper) """ begin_coord = CoordParameter(default=(0, 0)) end_coord = CoordParameter(default=(0, 0)) begin_width = NumberParameter(default=3) end_width = NumberParameter(default=1) def create_points(self, pts): if (self.begin_width > self.end_width): ew = self.begin_width ec = self.begin_coord bw = self.end_width bc = self.end_coord else: bw = self.begin_width bc = self.begin_coord ew = self.end_width ec = self.end_coord length = geom.distance(ec, bc) angle = geom.angle_rad(ec, bc) sinangle = np.sin(angle) cosangle = np.cos(angle) dx = 0.01 if abs(ew - bw) < dx: pts.extend([ (bc[0] + sinangle * bw / 2.0, bc[1] - cosangle * bw / 2.0), (bc[0] - sinangle * bw / 2.0, bc[1] + cosangle * bw / 2.0), (ec[0] - sinangle * bw / 2.0, ec[1] + cosangle * ew / 2.0), (ec[0] + sinangle * bw / 2.0, ec[1] - cosangle * ew / 2.0), (bc[0] + sinangle * bw / 2.0, bc[1] - cosangle * bw / 2.0) ]) return pts if length < 0.0025: return pts a = 4.0 * length / (ew**2 - bw**2) y0 = a * bw**2 / 4.0 y = y0 width = bw east_shape = [(bc[0] + sinangle * bw / 2.0, bc[1] - cosangle * bw / 2.0)] west_shape = [(bc[0] - sinangle * bw / 2.0, bc[1] + cosangle * bw / 2.0)] READY = False while (not READY): width = width + 4 * dx + 4 * math.sqrt(dx * (width + dx)) y = a * width**2 / 4.0 if (y - y0 > length): READY = True coord = ec width = ew else: coord = (bc[0] + (y - y0) * cosangle, bc[1] + (y - y0) * sinangle) east_shape.append((coord[0] + sinangle * width / 2.0, coord[1] - cosangle * width / 2.0)) west_shape.append((coord[0] - sinangle * width / 2.0, coord[1] + cosangle * width / 2.0)) east_shape.reverse() pts += west_shape pts += east_shape return pts