<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Fundamentals
Step1:  Patterns
A very important philosophy in dphox is to only implement things not already implemented in shapely unless there is a much more efficient batch implementation (e.g. vectorized transforms using numpy arrays). To this end, we will present many functionalities below that are very simple extensions of shapely transformations, owing to the seamless translation between Pattern and shapely geometries.
Text rendering
Using the dp.text function it is possible to render any text using LaTeX assuming you've installed the fonts in your computer or using default fonts (as is the case in a default Colab kernel). Behind the scenes, we leverage matplotlib's LaTeX path patches. Below, we will generate the symbol $\pi$ and manipulate it in the further examples.
We also add a port $p$ of width $1$ at some location $(3, 1)$ with angle $0$, and you should note how it transforms along with the overall geometry.
Step2:  translate
First let's experiment with translating the pattern. Note that I need to make a copy of the original pattern each time (this is a deepcopy) because I do not want to apply the transformations sequentially. The transformations also return object itself.
Step3:  rotate
Now, let's rotate and see what happens, again using the copy trick.
Step4:  We can choose any point of rotation so let's also do this about the center.
Step5:  scale
We can rescale our $\pi$ geometry in the x and/or y dimensions as follows.
Step6:  skew
We can skew our $\pi$ geometry in the x and/or y dimensions as follows.
Step7:  align
Sometimes, it might be easier to just align and/or stack designs next to each other, especially in cases where no port reference points / orientations are defined. In such a case, we may use the align, halign, valign functions. Inspiration for this feature comes from phidl.
Step8:  halign
Here we now align another smaller box to the edge of the circle using halign and valign.
Step9:  valign
Step10:  to
The to command allows ports in different devices to be aligned to each other. If a from_port is not specified, assume the port is at the origin (0, 0) with an angle of $180^\circ$ in the reference plane of the pattern.
Step11:  Curves and paths
Before we discuss the link, offset, symmetrize, loopify, and turn_connect operations, we will discuss the various fundamental building blocks or elements for curves and paths.
straight
A straight path or waveguide can be defined based on a width $w$ and length $\ell$ and simply consists of two points.
Step12:  turn
A smooth turn can be defined based on a width $w$ or taper function $w(t)$, radius $r$, Euler fraction $e$ (linearly ramps the curvature to reduce photonic bend loss).
Note that the Euler parameter increases the length of the bend but takes up the roughly same bounding box area.
Step13:  taper
A taper follows the polynomial width function $w(t)$.
We typically define this based on a polynomial function $w(t) = w_0 + w_1 t + w_2 t^2 + w_3 t^3 \cdots$ , so we define these in dphox explicitly. The nice thing about this form $w(t)$ is that the sum of the coefficients gives the overall width at the end ($t = 1$) and $w_0$ gives the initial width.
Why use a nonlinear taper?
- A quadratic taper is the minimal function that allows for $C_2$ smooth tapering transition at either one end.
- A cubic taper is the minimal function that allows for $C_2$ smooth tapering transitions on both ends.
Step14:  arc
An arc of specified angle $\alpha$, radiu $r$, similar to a circular bend except now the center is at the origin.
Step15:  bezier_sbend
An sbend following a classic cubic, 4-pole bezier structure defined based on a width $w$ or taper function $w(t)$, bend width displacement $\delta x$, bend height displacement $\delta y$. The poles are placed at $(0, 0), (\delta x / 2, 0), (\delta x / 2, \delta y), (\delta x, \delta y)$.
Step16:  turn_sbend
An sbend based on circular/Euler turns rather than bezier curves, and involve bending up and down by the same angle (assumed to be less than 90 degrees). The input parameters are an effective radius $r$ and a bend height $\delta y$ for the sbend. If the radius is smaller than twice the bend height, we use 90 degree turns and allow the straight segment to cover the full bend height.
Step17:  Operations
link
The link operation is your friend. It allows you to compose elements into a full path. Think of link like building a road. As an example of link consider the trombone and racetrack functions below (also defined with more options in dphox).
Step18:  segments
We can also visualize the individual elements of link by plotting all of the geometries in teh racetrack curve, which we refer here as segments.
Step19:  reverse
The reverse() operation simply reverses the curve to move in the opposite direction and flips the ports.
Step20:  interpolated
Interpolation of a curve is important in cases where there are multiple segments to a curve with varying resolution. Interpolation allows for tapering of geometries with equal segment lengths along the curve, and can be called using .interpolated. Below is an example for when the radius of a turn_sbend is smaller than twice the bend height; as you can see the taper is more evenly distributed in the interpolated case.
Step21:  symmetrized
The symmetrization of a curve or path mirrors any curve or path at its endpoint.
Step22:  We can apply the symmetrization many times to build funky ring structures.
<ASSISTANT_TASK:>
Python Code:
import dphox as dp
import numpy as np
import holoviews as hv
hv.extension('bokeh')
Explanation: Fundamentals: patterns and curves
A Pattern in dphox is analogous to shapely's MultiPolygon, and contains a set of polygons represented by a list of $2 \times N$ numpy arrays.
A Pattern can be treated pretty much like a shapely geometry in many respects. While we wrap boolean operations in Pattern using shapely's API, we do not use shapely's transform operations. This is because those are not vectorized efficiently over all of the geometries as we do in Pattern.
A Curve in dphox is analogous to shapely's MultiLineString, and consists of a list of $2 \times N$ numpy arrays like Pattern, except we do not assume the first and last points are connected.
A path is a Pattern, which is a Curve with thickness or width, which may vary along a curve.
dphox supports any curve or path that can be represented by piecewise parametric function(s): straight lines, circular and elliptical turns, Euler and Archimedian spiral turns, Manhattan routes, and much more. These are very useful for photonic and metal routing.
In dphox (and similar libraries as gdspy), a parametric function is generally defined in terms of a variable $t \in [0, 1]$. This can be used to define both the curve and the varying widths along the curve. The resolution or number of evaluations of the path, is generally defined for any curve that isn't "straight" and we typically use 100 as the default here, though that can vary.
Imports
End of explanation
pi = dp.text(r"$\pi$")
pi.port['p'] = dp.Port(3, 1) 
pi.hvplot().opts(title='pi')
Explanation: Patterns
A very important philosophy in dphox is to only implement things not already implemented in shapely unless there is a much more efficient batch implementation (e.g. vectorized transforms using numpy arrays). To this end, we will present many functionalities below that are very simple extensions of shapely transformations, owing to the seamless translation between Pattern and shapely geometries.
Text rendering
Using the dp.text function it is possible to render any text using LaTeX assuming you've installed the fonts in your computer or using default fonts (as is the case in a default Colab kernel). Behind the scenes, we leverage matplotlib's LaTeX path patches. Below, we will generate the symbol $\pi$ and manipulate it in the further examples.
We also add a port $p$ of width $1$ at some location $(3, 1)$ with angle $0$, and you should note how it transforms along with the overall geometry.
End of explanation
pi1 = pi.copy.translate()  # no translation
pi2 = pi.copy.translate(10)  # translation by 10
pi3 = pi.copy.translate(10, 10)  # translation by (10, 10)
b = dp.Pattern(pi1, pi2, pi3).bounds
(pi1.hvplot() * pi2.hvplot('blue') * pi3.hvplot('red')).opts(xlim=(b[0], b[2]), ylim=(b[1], b[3]), title='translation')
Explanation: translate
First let's experiment with translating the pattern. Note that I need to make a copy of the original pattern each time (this is a deepcopy) because I do not want to apply the transformations sequentially. The transformations also return object itself.
End of explanation
pi1 = pi.copy.rotate(45)  # rotate by 45 degrees about the origin
pi2 = pi.copy.rotate(90)  # rotate by 90 degrees about the center of the pattern
b = dp.Pattern(pi, pi1, pi2).bounds
(pi.hvplot() * pi1.hvplot('blue') * pi2.hvplot('red')).opts(xlim=(b[0], b[2]), ylim=(b[1], b[3]), title='rotation')
Explanation: rotate
Now, let's rotate and see what happens, again using the copy trick.
End of explanation
pi1 = pi.copy.rotate(45, pi.center)  # rotate by 45 degrees about the origin
pi2 = pi.copy.rotate(90, pi.center)  # rotate by 90 degrees about the center of the pattern
b = dp.Pattern(pi, pi1, pi2).bounds
(pi.hvplot() * pi1.hvplot('blue') * pi2.hvplot('red')).opts(xlim=(b[0], b[2]), ylim=(b[1], b[3]), title='rotation')
Explanation: We can choose any point of rotation so let's also do this about the center.
End of explanation
pi1 = pi.copy.scale(4, origin=pi.center)  # rotate by 45 degrees about the origin
pi2 = pi.copy.scale(2, 2, pi.center)  # rotate by 90 degrees about the center of the pattern
b = dp.Pattern(pi, pi1, pi2).bounds
(pi.hvplot() * pi1.hvplot('blue') * pi2.hvplot('red')).opts(xlim=(b[0], b[2]), ylim=(b[1], b[3]), title='scale')
Explanation: scale
We can rescale our $\pi$ geometry in the x and/or y dimensions as follows.
End of explanation
pi1 = pi.copy.skew(0.5, origin=pi.center)  # rotate by 45 degrees about the origin
pi2 = pi.copy.skew(0, -0.5, pi.center)  # rotate by 90 degrees about the center of the pattern
b = dp.Pattern(pi, pi1, pi2).bounds
(pi.hvplot().opts(title='no skew') + pi1.hvplot('blue').opts(title='xskew') + pi2.hvplot('red').opts(title='yskew'))
Explanation: skew
We can skew our $\pi$ geometry in the x and/or y dimensions as follows.
End of explanation
circle = dp.Circle(5)
circle.align(pi)
b = dp.Pattern(circle, pi).bounds
(pi.hvplot() * circle.hvplot('green')).opts(xlim=(b[0], b[2]), ylim=(b[1], b[3]), title='scale')
Explanation: align
Sometimes, it might be easier to just align and/or stack designs next to each other, especially in cases where no port reference points / orientations are defined. In such a case, we may use the align, halign, valign functions. Inspiration for this feature comes from phidl.
End of explanation
box = dp.Box((3, 3))  # centered at (0, 0) by default.
aligned_boxes = {
    'default': box.copy.halign(circle),
    'opposite=True': box.copy.halign(circle, opposite=True),
    'left=False': box.copy.halign(circle, left=False),
    'left=False,opposite=True': box.copy.halign(circle, left=False, opposite=True),
}
plots = []
for name, bx in aligned_boxes.items():
    b = dp.Pattern(circle, bx, pi).bounds
    plots.append(
        (pi.hvplot() * circle.hvplot('green') * bx.hvplot('blue', plot_ports=False)).opts(
            xlim=(b[0], b[2]), ylim=(b[1], b[3]), title=name
        )
    )
hv.Layout(plots).cols(2).opts(shared_axes=False)
Explanation: halign
Here we now align another smaller box to the edge of the circle using halign and valign.
End of explanation
box.halign(circle, opposite=True)  # to create a wider plot
aligned_boxes = {
    'default': box.copy.valign(circle),
    'opposite=True': box.copy.valign(circle, opposite=True),
    'bottom=False': box.copy.valign(circle, bottom=False),
    'bottom=False,opposite=True': box.copy.valign(circle, bottom=False, opposite=True),
}
plots = []
for name, bx in aligned_boxes.items():
    b = dp.Pattern(circle, bx, pi).bounds
    plots.append(
        (pi.hvplot() * circle.hvplot('green') * bx.hvplot('blue', plot_ports=False)).opts(
            xlim=(b[0], b[2]), ylim=(b[1], b[3]), title=name
        )
    )
hv.Layout(plots).cols(2).opts(shared_axes=False)
Explanation: valign
End of explanation
box = dp.Box((3, 3))
box.port = {'n': dp.Port(a=45)} # 45 degree reference port.
aligned_boxes = {
    'to n from origin': pi.copy.to(box.port['n']),
    'to n from p': pi.copy.to(box.port['n'], from_port='p')
}
plots = []
for name, bx in aligned_boxes.items():
    b = dp.Pattern(bx, box).bounds
    plots.append(
        (box.hvplot() * bx.hvplot('blue')).opts(
            xlim=(b[0], b[2]), ylim=(b[1], b[3]), title=name
        )
    )
hv.Layout(plots).cols(2).opts(shared_axes=False)
aligned_boxes = {
    'to p from origin': box.copy.to(pi.port['p']),
    'to p from n': box.copy.to(pi.port['p'], from_port='n')
}
plots = []
for name, bx in aligned_boxes.items():
    b = dp.Pattern(bx, pi).bounds
    plots.append(
        (bx.hvplot() * pi.hvplot('blue')).opts(
            xlim=(b[0], b[2]), ylim=(b[1], b[3]), title=name
        )
    )
hv.Layout(plots).cols(2).opts(shared_axes=False)
Explanation: to
The to command allows ports in different devices to be aligned to each other. If a from_port is not specified, assume the port is at the origin (0, 0) with an angle of $180^\circ$ in the reference plane of the pattern.
End of explanation
straight_curve = dp.straight(3) # A turn of radius 5.
straight_path = dp.straight(3).path(1) # A turn of radius 5 and width 1
straight_curve.hvplot().opts(title='straight curve', ylim=(-2, 2)) + straight_path.hvplot().opts(title='straight path', ylim=(-2, 2))
hv.DynamicMap(lambda width, length: dp.straight(length).path(width).hvplot().opts(
    xlim=(0, 5), ylim=(-2, 2)),
    kdims=['width', 'length']).redim.range(
    width=(0.1, 0.5), length=(1, 5)).opts(framewise=True)
Explanation: Curves and paths
Before we discuss the link, offset, symmetrize, loopify, and turn_connect operations, we will discuss the various fundamental building blocks or elements for curves and paths.
straight
A straight path or waveguide can be defined based on a width $w$ and length $\ell$ and simply consists of two points.
End of explanation
turn_curve = dp.turn(5, 90) # A turn of radius 5.
turn_path = dp.turn(5, 90).path(1) # A turn of radius 5 and width 1
turn_curve.hvplot().opts(title='turn curve') + turn_path.hvplot().opts(title='turn path')
dmap = hv.DynamicMap(lambda width, radius, angle, euler: dp.turn(radius, angle, euler).path(width).hvplot().opts(
    xlim=(-10, 10), ylim=(-10, 10)),
    kdims=['width', 'radius', 'angle', 'euler'])
dmap.redim.range(width=(0.3, 0.7), radius=(3., 5.), angle=(-180, 180), euler=(0, 0.5)).redim.step(radius=0.1, euler=0.05).redim.default(angle=90, width=0.5, radius=5)
Explanation: turn
A smooth turn can be defined based on a width $w$ or taper function $w(t)$, radius $r$, Euler fraction $e$ (linearly ramps the curvature to reduce photonic bend loss).
Note that the Euler parameter increases the length of the bend but takes up the roughly same bounding box area.
End of explanation
cubic = dp.taper(5).path(dp.cubic_taper_fn(1, 0.5))
quad = dp.taper(5).path(dp.quad_taper_fn(1, 0.5))
linear = dp.taper(5).path(dp.linear_taper_fn(1, 0.5))
linear_plot = linear.hvplot().opts(title='linear taper (1 to 0.5)', ylim=(-2, 2))
quad_plot = quad.hvplot().opts(title='quadratic taper (1 to 0.5)', ylim=(-2, 2))
cubic_plot = cubic.hvplot().opts(title='cubic taper (1 to 0.5)', ylim=(-2, 2))
linear_plot + quad_plot + cubic_plot
def taper_plot(length, init_w, final_w):
    cubic = dp.taper(length).path(dp.cubic_taper_fn(init_w, final_w))
    quad = dp.taper(length).path(dp.quad_taper_fn(init_w, final_w))
    linear = dp.taper(length).path(dp.linear_taper_fn(init_w, final_w))
    linear_plot = linear.hvplot().opts(title=f'linear taper ({init_w} to {final_w})', xlim=(0, 10), ylim=(-5, 5))
    quad_plot = quad.hvplot().opts(title=f'quadratic taper ({init_w} to {final_w})', xlim=(0, 10), ylim=(-5, 5))
    cubic_plot = cubic.hvplot().opts(title=f'cubic taper ({init_w} to {final_w})', xlim=(0, 10), ylim=(-5, 5))
    return linear_plot + quad_plot + cubic_plot
dmap = hv.DynamicMap(lambda length, init_w, final_w: taper_plot(length, init_w, final_w), kdims=['length', 'init_w', 'final_w'])
dmap.redim.range(length=(5., 10.), init_w=(3., 5.), final_w=(2., 6.)).redim.default(length=10)
Explanation: taper
A taper follows the polynomial width function $w(t)$.
We typically define this based on a polynomial function $w(t) = w_0 + w_1 t + w_2 t^2 + w_3 t^3 \cdots$ , so we define these in dphox explicitly. The nice thing about this form $w(t)$ is that the sum of the coefficients gives the overall width at the end ($t = 1$) and $w_0$ gives the initial width.
Why use a nonlinear taper?
- A quadratic taper is the minimal function that allows for $C_2$ smooth tapering transition at either one end.
- A cubic taper is the minimal function that allows for $C_2$ smooth tapering transitions on both ends.
End of explanation
curve = dp.arc(120, 5)
path = curve.path(1)
path_taper = curve.path(dp.cubic_taper_fn(0.5, 2))
arc_curve_plot = curve.hvplot().opts(xlim=(0, 6), ylim=(-5, 5), title='arc curve')
arc_path_plot = path.hvplot().opts(xlim=(0, 6), ylim=(-5, 5), title='arc path')
arc_path_taper_plot = path_taper.hvplot().opts(xlim=(0, 6), ylim=(-5, 5), title='arc path, cubic taper')
arc_curve_plot + arc_path_plot + arc_path_taper_plot
Explanation: arc
An arc of specified angle $\alpha$, radiu $r$, similar to a circular bend except now the center is at the origin.
End of explanation
curve = dp.bezier_sbend(bend_x=15, bend_y=10)
path = dp.bezier_sbend(15, 10).path(1)
path_taper = dp.bezier_sbend(15, 10).path(dp.cubic_taper_fn(0.5, 2))
curve.hvplot().opts(title='bezier curve') + path.hvplot().opts(title='bezier path') + path_taper.hvplot().opts(title='bezier path, cubic taper')
Explanation: bezier_sbend
An sbend following a classic cubic, 4-pole bezier structure defined based on a width $w$ or taper function $w(t)$, bend width displacement $\delta x$, bend height displacement $\delta y$. The poles are placed at $(0, 0), (\delta x / 2, 0), (\delta x / 2, \delta y), (\delta x, \delta y)$.
End of explanation
curve = dp.turn_sbend(height=5, radius=5)
path = dp.turn_sbend(5, 5).path(1)
path_taper = dp.turn_sbend(5, 5).interpolated.path(dp.cubic_taper_fn(0.5, 2))
curve.hvplot().opts(title='turn_sbend curve') + path.hvplot().opts(title='turn_sbend path') + path_taper.hvplot().opts(title='turn_sbend path, cubic taper')
Explanation: turn_sbend
An sbend based on circular/Euler turns rather than bezier curves, and involve bending up and down by the same angle (assumed to be less than 90 degrees). The input parameters are an effective radius $r$ and a bend height $\delta y$ for the sbend. If the radius is smaller than twice the bend height, we use 90 degree turns and allow the straight segment to cover the full bend height.
End of explanation
def racetrack(radius: float, length: float):
    return dp.link(dp.left_uturn(radius), length, dp.left_uturn(radius), length)
def trombone(radius: float, length: float):
    return dp.link(dp.left_turn(radius), length, dp.right_uturn(radius), length, dp.left_turn(radius))
racetrack_curve = racetrack(5, 10)
trombone_curve = trombone(5, 10)
racetrack_plot = racetrack_curve.path(1).hvplot(alpha=0.2) * racetrack_curve.hvplot(alternate_color='green', line_width=4)
trombone_plot = trombone_curve.path(2).hvplot(alpha=0.2) * trombone_curve.hvplot(alternate_color='green', line_width=4)
(racetrack_plot.opts(title='racetrack') + trombone_plot.opts(title='trombone')).opts(shared_axes=False)
Explanation: Operations
link
The link operation is your friend. It allows you to compose elements into a full path. Think of link like building a road. As an example of link consider the trombone and racetrack functions below (also defined with more options in dphox).
End of explanation
racetrack_segments = racetrack_curve.segments
xmin, ymin, xmax, ymax = racetrack_curve.bounds
hv.Overlay([segment.hvplot() for segment in racetrack_segments]).opts(xlim=(xmin - 2, xmax + 2), ylim=(ymin - 2, ymax + 2))
Explanation: segments
We can also visualize the individual elements of link by plotting all of the geometries in teh racetrack curve, which we refer here as segments.
End of explanation
taper = dp.taper(5).path(dp.cubic_taper_fn(1, 0.5))
reverse_taper = dp.taper(5).reverse().path(dp.cubic_taper_fn(1, 0.5))
(taper.hvplot().opts(title='forward') + reverse_taper.hvplot().opts(title='backward')).opts(shared_axes=False).cols(1)
Explanation: reverse
The reverse() operation simply reverses the curve to move in the opposite direction and flips the ports.
End of explanation
path_taper = dp.turn_sbend(20, 5).path(dp.cubic_taper_fn(0.5, 2))
path_taper_interp = dp.turn_sbend(20, 5).interpolated.path(dp.cubic_taper_fn(0.5, 2))
path_taper.hvplot().opts(title='noninterpolated', fontsize=10) + path_taper_interp.hvplot().opts(title='interpolated', fontsize=10)
Explanation: interpolated
Interpolation of a curve is important in cases where there are multiple segments to a curve with varying resolution. Interpolation allows for tapering of geometries with equal segment lengths along the curve, and can be called using .interpolated. Below is an example for when the radius of a turn_sbend is smaller than twice the bend height; as you can see the taper is more evenly distributed in the interpolated case.
End of explanation
trombone_taper = path_taper_interp.symmetrized()
trombone_taper.hvplot(alpha=0.5) * trombone_taper.curve.hvplot(alternate_color='red', line_width=6)
Explanation: symmetrized
The symmetrization of a curve or path mirrors any curve or path at its endpoint.
End of explanation
path1 = dp.link(dp.turn(5, -45).path(0.5), trombone_taper, dp.turn(5, -45).path(0.5)).symmetrized().symmetrized()
path2 = dp.link(dp.turn(5, -45).path(0.5), trombone_taper.symmetrized(), dp.turn(5, -45).path(0.5)).symmetrized().symmetrized()
(path1.hvplot() * path1.curve.hvplot(alternate_color='red') + path2.hvplot() * path2.curve.hvplot(alternate_color='red')).opts(shared_axes=False)
Explanation: We can apply the symmetrization many times to build funky ring structures.
End of explanation
<END_TASK>