def add_arc(self, final_angle, radius, final_width=None, n_points=128, shortest=True, **kwargs): delta = final_angle - self.angle if not np.isclose(normalize_phase(delta), 0): if shortest: delta = normalize_phase(delta) self.add_bend(delta, radius, final_width, n_points, **kwargs) return self
def add_bend(self, angle, radius, final_width=None, n_points=128, **kwargs): # If number of points is None, default to 128 n_points = n_points if n_points else 128 self._current_port.set_port_properties(**kwargs) sample_points = max(int(abs(angle) / (np.pi / 2) * n_points), 2) final_width = final_width if final_width is not None else self.width angle = normalize_phase( angle, zero_to_two_pi=True) - (0 if angle > 0 else 2 * np.pi) self.add_parameterized_path( path=lambda t: [ radius * np.sin(abs(angle) * t), np.sign(angle) * -radius * (np.cos(angle * t) - 1) ], path_function_supports_numpy=True, path_derivative=lambda t: [ radius * np.cos(abs(angle) * t) * abs(angle), np.sign(angle) * radius * (np.sin(angle * t) * angle) ], width=lambda t: np.outer(1 - t, np.array(self.width)) + np.outer( t, np.array(final_width)), width_function_supports_numpy=True, sample_points=sample_points, sample_distance=0) return self
def get_shapely_object(self): # Let's do the actual rendering polygons = list() special_handling_chars = '\n' font = _fonts.FONTS[self.font] # Check the text for char in self.text: if char in special_handling_chars: continue assert char in font, 'Character "%s" is not supported by font "%s"' % ( char, self.font) max_x = 0 cursor_x, cursor_y = 0, 0 for i, char in enumerate(self.text): if char == '\n': cursor_x, cursor_y = 0, cursor_y - self.line_spacing continue char_font = font[char] cursor_x += char_font['width'] / 2 * self.height for line in char_font['lines']: points = np.array(line).T * self.height + (cursor_x, cursor_y) polygons.append(shapely.geometry.Polygon(points)) # Add kerning if i < len(self.text) - 1 and self.text[ i + 1] not in special_handling_chars: kerning = char_font['kerning'][self.text[i + 1]] cursor_x += (char_font['width'] / 2 + kerning) * self.height max_x = max(max_x, cursor_x + char_font['width'] / 2 * self.height) merged_polygon = shapely.ops.cascaded_union(polygons) # Handle the alignment, translation and rotation if not self.true_bbox_alignment: bbox = np.array([[0, max_x], [cursor_y, self.height]]).T else: bbox = np.array(merged_polygon.bounds).reshape(2, 2) offset = self._alignment.calculate_offset(bbox) self._bbox = bbox + offset if not np.isclose(normalize_phase(self.angle), 0): aligned_text = shapely.affinity.translate(merged_polygon, *offset) rotated_text = shapely.affinity.rotate(aligned_text, self.angle, origin=[0, 0], use_radians=True) final_text = shapely.affinity.translate(rotated_text, *self.origin) else: final_text = shapely.affinity.translate(merged_polygon, *(offset + self.origin)) return final_text
def __init__(self, origin, height, text='', alignment='left-bottom', angle=0., font='stencil', line_spacing=1.5, true_bbox_alignment=False): self.origin = origin self.height = height self.text = str(text) self._alignment = Alignment(alignment) self.angle = normalize_phase(angle) self.font = font self.line_spacing = height * line_spacing self.true_bbox_alignment = true_bbox_alignment self._bbox = None self._shapely_object = None
def add_bend(self, angle, radius, final_width=None, n_points=128, **kargs): # If number of points is None, default to 128 n_points = n_points if n_points else 128 self._current_port.set_port_properties(**kargs) final_width = final_width or self.width angle = normalize_phase(angle, zero_to_two_pi=True) - (0 if angle > 0 else 2 * np.pi) if not np.isclose(radius, 0) and not np.isclose(angle, 0) and radius > 0: if angle > 0: circle_center = (-np.sin(self.angle) * radius, np.cos(self.angle) * radius) + self.current_port.origin start_angle = -np.pi / 2 + self.angle else: circle_center = (np.sin(self.angle) * radius, -np.cos(self.angle) * radius) + self.current_port.origin start_angle = np.pi / 2 + self.angle end_angle = start_angle + angle # Calculate the points needed for this angle points = max(int(abs(end_angle - start_angle) / (np.pi / 2) * n_points), 2) phi = np.linspace(start_angle, end_angle, points) upper_radius_points = np.linspace(radius - self.width / 2, radius - final_width / 2, points) upper_line_points = np.array([upper_radius_points * np.cos(phi), upper_radius_points * np.sin(phi)]).T + circle_center lower_radius_points = np.linspace(radius + self.width / 2, radius + final_width / 2, points) lower_line_points = np.array([lower_radius_points * np.cos(phi), lower_radius_points * np.sin(phi)]).T + circle_center polygon = shapely.geometry.Polygon(np.concatenate([upper_line_points, lower_line_points[::-1, :]])) self._segments.append((self._current_port.copy(), polygon, abs(angle) * radius)) endpoint = shapely.geometry.Point(radius * np.cos(end_angle) + circle_center[0], radius * np.sin(end_angle) + circle_center[1]) self._current_port.origin = endpoint.coords[0] self._current_port.width = final_width self._current_port.angle += angle # assert self._segments[-1][1].is_valid, \ # 'Invalid polygon generated: %s' % shapely.validation.explain_validity(self._segments[-1][1]) return self
def _calculate(self, do_in_wgs=True, do_out_wgs=True): angular_spacing = self._angular_spacing assert angular_spacing * self._out_ports < np.pi, 'Not enough space for output ports' assert angular_spacing * self._in_ports < np.pi, 'Not enough space for input ports' if do_out_wgs: # Do the output side out_origin_port = Port(self._origin, self._angle, 1.).longitudinal_offset( -self._displacement / 2) out_fanout_wgs = [ Waveguide.make_at_port( out_origin_port.rotated(angle).longitudinal_offset( self._radius)) for angle in (np.arange(self._out_ports) - (self._out_ports - 1) / 2.) * angular_spacing ] for wg in out_fanout_wgs: wg.add_parameterized_path( path=lambda t: [t * self._taper_length, np.zeros_like(t)], path_derivative=lambda t: [np.ones_like(t) * self._taper_length, np.zeros_like(t)], path_function_supports_numpy=True, width=self._taper_function, **self._taper_options) if self._minimal_final_spacing is None: for wg in out_fanout_wgs: wg.add_bend(normalize_phase(-wg.angle + self._angle), self._wg_bend_radius) else: offsets = ( np.arange(self._out_ports) - (self._out_ports - 1) / 2.) * self._minimal_final_spacing final_port_heights = [ out_origin_port.parallel_offset(offset) for offset in offsets ] for wg, final_port_height, offset in zip( out_fanout_wgs, final_port_heights, offsets): if np.isclose(offset, 0): continue try: wg.add_route_single_circle_to_port( final_port_height.inverted_direction, on_line_only=True) except AssertionError: # No curve possible, use normal bend wg.add_bend(normalize_phase(-wg.angle + self._angle), self._wg_bend_radius) final_ports = [wg.current_port for wg in out_fanout_wgs] for wg in out_fanout_wgs: wg.add_straight_segment_until_level_of_port(final_ports) self._out_wgs = out_fanout_wgs if do_in_wgs: # Do the input side in_origin_port = Port(self._origin, self._angle + np.pi, 1.).longitudinal_offset(-self._displacement / 2) in_fanout_wgs = [ Waveguide.make_at_port( in_origin_port.rotated(angle).longitudinal_offset( self._radius)) for angle in (np.arange(self._in_ports) - (self._in_ports - 1) / 2.) * angular_spacing ] for wg in in_fanout_wgs: wg.add_parameterized_path( path=lambda t: [t * self._taper_length, np.zeros_like(t)], path_derivative=lambda t: [np.ones_like(t) * self._taper_length, np.zeros_like(t)], path_function_supports_numpy=True, width=self._taper_function, **self._taper_options) if self._minimal_final_spacing is None: for wg in in_fanout_wgs: wg.add_bend( normalize_phase(-wg.angle + self._angle - np.pi), self._wg_bend_radius) else: offsets = ( np.arange(self._in_ports) - (self._in_ports - 1) / 2.) * self._minimal_final_spacing final_port_heights = [ in_origin_port.parallel_offset(offset) for offset in offsets ] for wg, final_port_height, offset in zip( in_fanout_wgs, final_port_heights, offsets): if np.isclose(offset, 0): continue # wg.add_route_single_circle_to_port(final_port_height.inverted_direction, on_line_only=True) try: wg.add_route_single_circle_to_port( final_port_height.inverted_direction, on_line_only=True) except AssertionError: # No curve possible, use normal bend wg.add_bend( normalize_phase(-wg.angle + self._angle - np.pi), self._wg_bend_radius) final_ports = [wg.current_port for wg in in_fanout_wgs] for wg in in_fanout_wgs: wg.add_straight_segment_until_level_of_port(final_ports) self._in_wgs = in_fanout_wgs
def add_route_single_circle_to(self, final_coordinates, final_angle, final_width=None, max_bend_strength=None, on_line_only=False): """ Connect two points by straight lines and one circle. Works for geometries like round edges and others. The final straight line can also be omitted so that the waveguide only end on the line described by the support vector and angle. By default, this method tries to route to the target with the greatest possible circle. But the valid bending range may be limited via the `max_bend_strength` parameter. This method does not work for geometries which cannot be connected only by straight lines and one circle, such as parallel lines etc. Still, this method can prove extremely useful for routing to i.e. grating couplers etc. :param final_coordinates: Final destination point. :param final_angle: Final angle of the waveguide. :param final_width: Final width of the waveguide. :param max_bend_strength: The maximum allowed bending radius. :param on_line_only: Omit the last straight line and only route to described line. """ # We are given an out angle, but the internal math is for inward pointing lines final_angle = normalize_phase(final_angle) + np.pi final_width = final_width if final_width is not None else self.width # We need to to some linear algebra. We first find the intersection of the two waveguides r1 = self._current_port.origin r2 = np.array(final_coordinates) intersection_point, distance = find_line_intersection( r1, self.angle, r2, final_angle) assert distance[ 0] >= 0, 'Origin waveguide is too close to target. No curve possible' assert on_line_only or distance[ 1] >= 0, 'Target position is too close to target. No curve possible' # Calculate the angle bisector u1 = np.array([np.cos(self.angle), np.sin(self.angle)]) u2 = np.array([np.cos(final_angle), np.sin(final_angle)]) u_half = u1 + u2 half_angle = np.arctan2(u_half[1], u_half[0]) diff_angle = normalize_phase(self.angle - final_angle - np.pi) _, r1 = find_line_intersection(r1, self.angle + np.pi / 2, intersection_point, half_angle) if not on_line_only: max_poss_radius = min( np.abs([ r1[0], distance[0] / np.tan(diff_angle / 2), distance[1] / np.tan(diff_angle / 2) ])) else: max_poss_radius = min( np.abs([r1[0], distance[0] / np.tan(diff_angle / 2)])) radius = min([max_bend_strength, max_poss_radius ]) if max_bend_strength is not None else max_poss_radius d = abs(radius * np.tan(diff_angle / 2)) if on_line_only: segments = [distance[0] - d, radius * diff_angle] else: segments = [distance[0] - d, radius * diff_angle, distance[1] - d] segment_ratio = np.cumsum(segments / sum(segments)) segment_widths = [(final_width - self.current_port.width) * ratio + self.current_port.width for ratio in segment_ratio] tmp_wg = Waveguide.make_at_port(self._current_port) tmp_wg.add_straight_segment(length=distance[0] - d, final_width=segment_widths[0]) tmp_wg.add_bend(-diff_angle, radius, final_width=segment_widths[1]) if not on_line_only: tmp_wg.add_straight_segment(distance[1] - d, final_width=segment_widths[2]) self._segments.append( (self._current_port.copy(), tmp_wg.get_shapely_object(), tmp_wg.get_shapely_outline(), tmp_wg.length, tmp_wg.center_coordinates)) self._current_port = tmp_wg.current_port return self
def angle(self): """ The angle of the port. """ return normalize_phase(self._angle)