Exemple #1
0
def import_oas(filename, cellname = None, flatten = False):
    if filename.lower().endswith('.gds'):
        # you are looking for import_gds
        retval = pg.import_gds(filename, cellname = cellname, flatten = flatten)
        return retval
    try:
        import klayout.db as pya
    except ImportError as err:
        err.args = ('[PHIDL] klayout package needed to import OASIS. pip install klayout\n' + err.args[0], ) + err.args[1:]
        raise
    if not filename.lower().endswith('.oas'): filename += '.oas'
    fileroot = os.path.splitext(filename)[0]
    tempfilename = fileroot + '-tmp.gds'

    layout = pya.Layout()
    layout.read(filename)
    # We want to end up with one Device. If the imported layout has multiple top cells,
    # a new toplevel is created, and they go into the second level
    if len(layout.top_cells()) > 1:
        topcell = layout.create_cell('toplevel')
        rot_DTrans = pya.DTrans.R0
        origin = pya.DPoint(0, 0)
        for childcell in layout.top_cells():
            if childcell == topcell: continue
            topcell.insert(pya.DCellInstArray(childcell.cell_index(), pya.DTrans(rot_DTrans, origin)))
    else:
        topcell = layout.top_cell()
    topcell.write(tempfilename)

    retval = pg.import_gds(tempfilename, cellname = cellname, flatten = flatten)
    os.remove(tempfilename)
    return retval
Exemple #2
0
        def draw_gds_cell(self, cell):
            logger.warning("Using default draw_gds_cell method in %s.", self.name)
            layout = cell.layout()
            gdscell = self.get_gds_cell(layout)

            origin = kdb.DPoint(0, 0)
            cell.insert_cell(gdscell, origin, 0)
            return cell
def test_rectangle_write(top_cell):
    TOP, layout = top_cell()
    layer = "1/0"
    center = kdb.DPoint(0, 0)
    width = 20
    height = 10
    ex = kdb.DVector(1, 1)
    ey = kdb.DVector(0, 1)
    r = rectangle(center, width, height, ex, ey)
    assert repr(r) == "(-10,-15;-10,-5;10,15;10,5)"

    insert_shape(TOP, layer, r)
    TOP.write("tests/tmp/test_rectangle.gds")
Exemple #4
0
    def origin_ex_ey(self, multiple_of_90=False):  # pylint: disable=unused-argument
        EX = kdb.DVector(1, 0)
        cp = self.get_cell_params()
        origin = kdb.DPoint(0, 0)
        # if 'angle_ex' not in cp.__dict__:
        #     cp.angle_ex = 0
        if multiple_of_90:
            if cp.angle_ex % 90 != 0:
                raise RuntimeError("Specify an angle multiple of 90 degrees")

        from math import pi

        ex = rotate(EX, cp.angle_ex * pi / 180)
        ey = rotate90(ex)
        return origin, ex, ey
def test_pad_pcell(top_cell):
    pad = DCPad(name="testname")
    pad.params.layer_metal = kdb.LayerInfo(1, 0)
    pad.params.layer_opening = kdb.LayerInfo(2, 0)

    # This will get automatically converted to LayerInfo
    # No Error
    pad.params.layer_metal = "1/0"

    # TODO set defaults here
    TOP, layout = top_cell()
    cell, ports = pad.new_cell(layout)
    assert "el0" in ports
    origin, angle = kdb.DPoint(0, 0), 0
    TOP.insert_cell(cell, origin, angle)
    TOP.write("tests/tmp/pad.gds")
Exemple #6
0
    def bezier_optimal(P0, P3, *args, **kwargs):
        """ If inside KLayout, return computed list of KLayout points.
        """
        P0 = _Point(P0.x, P0.y)
        P3 = _Point(P3.x, P3.y)
        scale = (P3 - P0).norm()  # rough length.
        # if scale > 1000:  # if in nanometers, convert to microns
        #     scale /= 1000
        # This function returns a np.array of Points.
        # We need to convert to array of Point coordinates
        new_bezier_line = _bezier_optimal_pure(P0, P3, *args, **kwargs)
        bezier_point_coordinates = lambda t: np.array(
            [new_bezier_line(t).x, new_bezier_line(t).y])

        t_sampled, bezier_point_coordinates_sampled = sample_function(
            bezier_point_coordinates, [0, 1],
            tol=0.005 / scale)  # tol about 5 nm

        # The following adds two points right after the first and before the last point
        # to guarantee that the first edge of the path goes out in the direction
        # of the 'port'.

        insert_at = np.argmax(0.001 / scale < t_sampled)
        t_sampled = np.insert(t_sampled, insert_at, 0.001 / scale)
        bezier_point_coordinates_sampled = np.insert(
            bezier_point_coordinates_sampled,
            insert_at,
            bezier_point_coordinates(0.001 / scale),
            axis=1,
        )  # add a point right after the first one
        insert_at = np.argmax(1 - 0.001 / scale < t_sampled)
        # t_sampled = np.insert(t_sampled, insert_at, 1 - 0.001 / scale)
        bezier_point_coordinates_sampled = np.insert(
            bezier_point_coordinates_sampled,
            insert_at,
            bezier_point_coordinates(1 - 0.001 / scale),
            axis=1,
        )  # add a point right before the last one
        # bezier_point_coordinates_sampled = \
        #     np.append(bezier_point_coordinates_sampled, np.atleast_2d(bezier_point_coordinates(1 + .001 / scale)).T,
        #               axis=1)  # finish the waveguide a little bit after

        return [
            pya.DPoint(x, y)
            for (x, y) in zip(*(bezier_point_coordinates_sampled))
        ]
Exemple #7
0
def main():
    layout = pya.Layout()
    TOP = layout.create_cell("TOP")

    layer = pya.LayerInfo(1, 0)  # First layer

    origin = pya.DPoint(0, 0)
    ex = pya.DVector(1, 0)
    ey = pya.DVector(0, 1)

    angles = np.linspace(-170, 170, 13)

    for i, angle_0 in enumerate(angles):
        for j, angle_3 in enumerate(angles):
            curve = bezier_curve(origin + ey * i * 150 + ex * j * 150, angle_0,
                                 angle_3, ex, ey)
            layout_waveguide(TOP, layer, curve, width=0.5)

    layout.write("bezier_waveguides.gds")
Exemple #8
0
    def get_points(self):
        from math import atan2, pi

        P1, C, P2 = self.P1, self.C, self.P2

        r = (P2 - C).norm()

        theta_start = atan2((P1 - C).y, (P1 - C).x)
        theta_end = atan2((P2 - C).y, (P2 - C).x)
        if self.ccw:
            theta_end = (theta_end - theta_start) % (2 * pi) + theta_start
        else:
            theta_start = (theta_start - theta_end) % (2 * pi) + theta_end
            theta_start, theta_end = theta_end, theta_start

        arc_function = lambda t: np.array([r * np.cos(t), r * np.sin(t)])

        # in the function below, theta_start must be smaller than theta_end
        t, coords = sample_function(arc_function, [theta_start, theta_end],
                                    tol=0.002 / r)

        # This yields a better polygon
        # The idea is to place a point right after the first one, to
        # make sure the arc starts in the right direction
        insert_at = np.argmax(theta_start + 0.001 <= t)
        t = np.insert(t, insert_at, theta_start + 0.001)
        coords = np.insert(coords,
                           insert_at,
                           arc_function(theta_start + 0.001),
                           axis=1)
        insert_at = np.argmax(theta_end - 0.001 <= t)
        coords = np.insert(coords,
                           insert_at,
                           arc_function(theta_end - 0.001),
                           axis=1)  # finish the waveguide a little bit after

        # create original waveguide poligon prior to clipping and rotation
        dpoints_list = [C + kdb.DPoint(x, y) for x, y in zip(*coords)]
        if not self.ccw:
            dpoints_list = list(reversed(dpoints_list))
        return dpoints_list
def test_gdscellcache(top_cell):

    gds_dir = gdslibpath
    princeton_logo = GDSCell("princeton_logo", "princeton_logo_simple.gds",
                             gds_dir)(name="xyz")
    TOP, layout = top_cell()
    ex = kdb.DPoint(1, 0)

    for i in range(10):
        # The new_cell method will create a new cell every time it is called.
        plogo, _ = princeton_logo.new_cell(layout)
        size = (plogo.dbbox().p2 - plogo.dbbox().p1).norm()
        angle = 10 * i
        origin = ex * i * size
        TOP.insert_cell(plogo, origin, angle)

    # The top cell will contain several instances of different cells
    # 'plogo'. All 'plogos' will contain the same instance of the inner
    # gdscell loaded from a file.
    TOP.write("tests/tmp/princeton_logo_testcache.gds")

    # ony one cell "xyz" exists
    cell_count = 0
    for cell in layout.each_cell():
        if cell.name.startswith("xyz"):
            cell_count += 1
    assert cell_count == 10

    # 10 instances of cell "xyz" exists
    inst_count = 0
    for inst in TOP.each_inst():
        if inst.cell.name.startswith("xyz"):
            inst_count += 1
    assert inst_count == 10

    cell_count = 0
    for cell in layout.each_cell():
        if cell.name.startswith("princeton_logo"):
            cell_count += 1
    assert cell_count == 1
Exemple #10
0
def test_gdscell(top_cell):

    gds_dir = gdslibpath
    princeton_logo = GDSCell("princeton_logo", "princeton_logo_simple.gds",
                             gds_dir)(name="xyz")
    TOP, layout = top_cell()
    ex = kdb.DPoint(1, 0)
    plogo, _ = princeton_logo.new_cell(layout)
    size = (plogo.dbbox().p2 - plogo.dbbox().p1).norm()
    for i in range(10):
        angle = 10 * i
        origin = ex * i * size
        TOP.insert_cell(plogo, origin, angle)

    # The top cell will contain several instances of the same cell
    # Deleting cell named 'priceton_logo' will delete all instances:
    # plogo.delete()
    TOP.write("tests/tmp/princeton_logo_test.gds")

    cell_count = 0
    for cell in layout.each_cell():
        if cell.name.startswith("xyz"):
            cell_count += 1
    assert cell_count == 1
Exemple #11
0
        def wrapper_draw(self, cell):
            layout = cell.layout()
            try:
                layer_map_dict[layout]
            except KeyError:
                layer_map_dict[layout] = pya.LayerMap()

            # Adding the dbu of the layout in the hash (bit us in the butt last time)
            short_hash_pcell = produce_hash(self,
                                            extra=(layout.dbu, extra_hash))

            # cache paths
            cache_fname = f"cache_{self.__class__.__qualname__}_{short_hash_pcell}"
            cache_fname_gds = cache_fname + ".gds"
            cache_fname_pkl = cache_fname + ".klayout.pkl"

            os.makedirs(cache_dir, mode=0o775, exist_ok=True)

            cache_fpath_gds = os.path.join(cache_dir, cache_fname_gds)
            cache_fpath_pkl = os.path.join(cache_dir, cache_fname_pkl)

            if os.path.isfile(cache_fpath_gds) and os.path.isfile(
                    cache_fpath_pkl):
                with open(cache_fpath_pkl, "rb") as file:
                    ports, read_short_hash_pcell, cellname = pickle.load(file)  # pylint: disable=unused-variable

                logger.debug(
                    f"Reading from cache: {cache_fname}: {cellname}, {ports}")
                print("r", end="", flush=True)
                if not layout.has_cell(cache_fname):
                    read_layout(layout,
                                cache_fpath_gds,
                                disambiguation_name=cellname)
                retrieved_cell = layout.cell(cache_fname)
                cell.insert(
                    pya.DCellInstArray(
                        retrieved_cell.cell_index(),
                        pya.DTrans(pya.DTrans.R0, pya.DPoint(0, 0)),
                    ))
                # cell.move_tree(retrieved_cell)
            else:
                if layout.has_cell(cache_fname):
                    logger.warning(
                        f"WARNING: {cache_fname_gds} does not exist but {cache_fname} is in layout."
                    )

                # populating .gds and .pkl
                empty_layout = pya.Layout()
                empty_layout.dbu = layout.dbu
                empty_cell = empty_layout.create_cell(cell.name)
                filled_cell, ports = draw(self, empty_cell)

                logger.debug(
                    f"Writing to cache: {cache_fname}: {filled_cell.name}, {ports}"
                )
                print("w", end="", flush=True)

                cellname, filled_cell.name = filled_cell.name, cache_fname
                # There can be duplicate cell names in subcells here.
                # We are saving a list of them inside a property named CACHE_PROP_ID
                # So we need to allow the properties to be saved inside the gds file (incompatible with the GDS2 standard)
                save_options = pya.SaveLayoutOptions()
                save_options.gds2_write_file_properties = True
                empty_layout.write(cache_fpath_gds, save_options)
                with open(cache_fpath_pkl, "wb") as file:
                    pickle.dump((ports, short_hash_pcell, cellname), file)

                # Make sure we delete the empty_layout to not grow
                # helps debug
                layer_map_dict.pop(empty_layout, None)
                del empty_layout
                assert not layout.has_cell(cache_fname)

                read_layout(layout,
                            cache_fpath_gds,
                            disambiguation_name=cellname)
                retrieved_cell = layout.cell(cache_fname)
                cell.insert(
                    pya.DCellInstArray(
                        retrieved_cell.cell_index(),
                        pya.DTrans(pya.DTrans.R0, pya.DPoint(0, 0)),
                    ))

            return cell, ports
Exemple #12
0
def test_float_operations():
    assert kdb.DPoint(1, 2) / 1.0 == kdb.DPoint(1, 2)
    assert 0.5 * kdb.DPoint(1, 2) == kdb.DPoint(0.5, 1)
def main():
    def trace_rounded_path(cell, layer, rounded_path, width):
        points = []
        for item in rounded_path:
            points.extend(item.get_points())

        dpath = kdb.DPath(points, width, 0, 0)

        cell.shapes(layer).insert(dpath)

    def trace_reference_path(cell, layer, points, width):
        dpath = kdb.DPath(points, width, 0, 0)
        cell.shapes(layer).insert(dpath)

    layout = kdb.Layout()
    TOP = layout.create_cell("TOP")
    layer = kdb.LayerInfo(10, 0)
    layerRec = kdb.LayerInfo(1001, 0)

    ex, ey = kdb.DPoint(1, 0), kdb.DPoint(0, 1)

    points = [0 * ex, 10 * ex, 10 * (ex + ey), 30 * ex]
    origin = 0 * ey
    points = [origin + point for point in points]
    x = compute_rounded_path(points, 3)
    trace_rounded_path(TOP, layer, x, 0.5)
    trace_reference_path(TOP, layerRec, points, 0.5)

    points = [0 * ex, 10 * ex, 5 * (ex - ey), 17 * ex, 30 * ex]
    origin = 30 * ey
    points = [origin + point for point in points]
    x = compute_rounded_path(points, 3)
    trace_rounded_path(TOP, layer, x, 0.5)
    trace_reference_path(TOP, layerRec, points, 0.5)

    radius = 3
    for ex2 in (ex, -ex):
        points = [2 * ex2]
        for d in np.arange(1, 10, 2.5):
            origin = points[-1]
            displacements = [
                4 * radius * ex2,
                4 * radius * ex2 + d * ey - 1 * d * ex2,
                d * ey,
                (d + 2 * radius) * ey,
            ]
            points += [origin + displacement for displacement in displacements]
        origin = 15 * ex + 40 * ey
        points = [origin + point for point in points]
        x = compute_rounded_path(points, radius)
        trace_rounded_path(TOP, layer, x, 0.5)
        trace_reference_path(TOP, layerRec, points, 0.5)

    # Layout tapered waveguide
    points = [
        0 * ex,
        100 * ex,
        100 * ex + 20 * ey,
        10 * ex + 5 * ey,
        10 * ex + 25 * ey,
        100 * ex + 30 * ey,
    ]

    # Untapered
    origin = 40 * ex
    points_ = [origin + point for point in points]
    layout_waveguide_from_points(TOP, layer, points_, 0.5, 5)

    # Tapered
    origin = 40 * ex + 40 * ey
    points_ = [origin + point for point in points]
    layout_waveguide_from_points(
        TOP, layer, points_, 0.5, 5, taper_width=3, taper_length=10
    )

    print("Wrote waveguide_rounding.gds")
    TOP.write("waveguide_rounding.gds")
Exemple #14
0
        def wrapper_draw(self, cell):
            global layer_map_dict
            layout = cell.layout()
            try:
                layer_map_dict[layout]
            except KeyError:
                layer_map_dict[layout] = pya.LayerMap()

            # Adding the dbu of the layout in the hash (bit us in the butt last time)
            short_hash_pcell = produce_hash(self, extra=(layout.dbu, extra_hash))

            # cache paths
            cache_fname = f"cache_{self.__class__.__qualname__}_{short_hash_pcell}"
            cache_fname_gds = cache_fname + ".gds"
            cache_fname_pkl = cache_fname + ".klayout.pkl"

            os.makedirs(cache_dir, mode=0o775, exist_ok=True)

            cache_fpath_gds = os.path.join(cache_dir, cache_fname_gds)
            cache_fpath_pkl = os.path.join(cache_dir, cache_fname_pkl)

            if os.path.isfile(cache_fpath_gds) and os.path.isfile(cache_fpath_pkl):
                with open(cache_fpath_pkl, "rb") as file:
                    ports, read_short_hash_pcell, cellname = pickle.load(file)
                if debug:
                    print(f"Reading from cache: {cache_fname}: {cellname}, {ports}")
                else:
                    print("r", end="", flush=True)
                if not layout.has_cell(cache_fname):
                    read_layout(layout, cache_fpath_gds)
                retrieved_cell = layout.cell(cache_fname)
                cell.insert(
                    pya.DCellInstArray(
                        retrieved_cell.cell_index(),
                        pya.DTrans(pya.DTrans.R0, pya.DPoint(0, 0)),
                    )
                )
                # cell.move_tree(retrieved_cell)
            else:
                if layout.has_cell(cache_fname):
                    print(
                        f"WARNING: {cache_fname_gds} does not exist but {cache_fname} is in layout."
                    )

                # populating .gds and .pkl
                empty_layout = pya.Layout()
                empty_layout.dbu = layout.dbu
                empty_cell = empty_layout.create_cell(cell.name)
                filled_cell, ports = draw(self, empty_cell)

                if debug:
                    print(
                        f"Writing to cache: {cache_fname}: {filled_cell.name}, {ports}"
                    )
                else:
                    print("w", end="", flush=True)

                cellname, filled_cell.name = filled_cell.name, cache_fname
                filled_cell.write(cache_fpath_gds)
                with open(cache_fpath_pkl, "wb") as file:
                    pickle.dump((ports, short_hash_pcell, cellname), file)

                # Make sure we delete the empty_layout to not grow
                # helps debug
                layer_map_dict.pop(empty_layout, None)
                del empty_layout
                assert not layout.has_cell(cache_fname)

                read_layout(layout, cache_fpath_gds)
                retrieved_cell = layout.cell(cache_fname)
                cell.insert(
                    pya.DCellInstArray(
                        retrieved_cell.cell_index(),
                        pya.DTrans(pya.DTrans.R0, pya.DPoint(0, 0)),
                    )
                )

            return cell, ports
Exemple #15
0
def layout_waveguide_angle2(cell, layer, points_list, width, angle_from,
                            angle_to):
    """Lays out a waveguide (or trace) with a certain width along
    given points and with fixed orientation at all points.

    This is very useful for laying out Bezier curves with or without adiabatic tapers.

    Args:
        cell: cell to place into
        layer: layer to place into. It is done with cell.shapes(layer).insert(pya.Polygon)
        points_list: list of pya.DPoint (at least 2 points)
        width (microns): constant or list. If list, then it has to have the same length as points
        angle_from (degrees): normal angle of the first waveguide point
        angle_to (degrees): normal angle of the last waveguide point

    """
    if len(points_list) < 2:
        raise NotImplemented("ERROR: points_list too short")
        return

    def norm(self):
        return sqrt(self.x**2 + self.y**2)

    try:
        if len(width) == len(points_list):
            width_iterator = iter(width)
        elif len(width) == 2:
            # assume width[0] is initial width and
            # width[1] is final width
            # interpolate with points_list
            L = curve_length(points_list)
            distance = 0
            widths_list = [width[0]]
            widths_func = lambda t: (1 - t) * width[0] + t * width[1]
            old_point = points_list[0]
            for point in points_list[1:]:
                distance += norm(point - old_point)
                old_point = point
                widths_list.append(widths_func(distance / L))
            width_iterator = iter(widths_list)
        else:
            width_iterator = repeat(width[0])
    except TypeError:
        width_iterator = repeat(width)
    finally:
        points_iterator = iter(points_list)

    points_low = list()
    points_high = list()

    point_width_list = list(zip(points_iterator, width_iterator))
    N = len(point_width_list)

    angle_list = list(np.linspace(angle_from, angle_to, N))

    for i in range(0, N):
        point, width = point_width_list[i]
        angle = angle_list[i]
        theta = angle * pi / 180

        point_high = point + 0.5 * width * pya.DPoint(cos(theta + pi / 2),
                                                      sin(theta + pi / 2))
        points_high.append(point_high)
        point_low = point + 0.5 * width * pya.DPoint(cos(theta - pi / 2),
                                                     sin(theta - pi / 2))
        points_low.append(point_low)

    polygon_points = points_high + list(reversed(points_low))

    poly = pya.DSimplePolygon(polygon_points)
    cell.shapes(layer).insert(poly)
    return poly
Exemple #16
0
def waveguide_dpolygon(points_list, width, dbu, smooth=True):
    """Returns a polygon outlining a waveguide.

    This was updated over many iterations of failure. It can be used for both
    smooth optical waveguides or DC metal traces with corners. It is better
    than klayout's Path because it can have varying width.

    Args:
        points_list: list of pya.DPoint (at least 2 points)
        width (microns): constant or list. If list, then it has to have the same length as points
        dbu: dbu: typically 0.001, only used for accuracy calculations.
        smooth: tries to smooth final polygons to avoid very sharp edges (greater than 130 deg)
    Returns:
        polygon DPoints

    """
    if len(points_list) < 2:
        raise NotImplementedError("ERROR: points_list too short")
        return

    def norm(self):
        return sqrt(self.x**2 + self.y**2)

    # Prepares a joint point and width iterators
    try:
        if len(width) == len(points_list):
            width_iterator = iter(width)
        elif len(width) == 2:
            # assume width[0] is initial width and
            # width[1] is final width
            # interpolate with points_list
            L = curve_length(points_list)
            distance = 0
            widths_list = [width[0]]
            widths_func = lambda t: (1 - t) * width[0] + t * width[1]
            old_point = points_list[0]
            for point in points_list[1:]:
                distance += norm(point - old_point)
                old_point = point
                widths_list.append(widths_func(distance / L))
            width_iterator = iter(widths_list)
        else:
            width_iterator = repeat(width[0])
    except TypeError:
        width_iterator = repeat(width)
    finally:
        points_iterator = iter(points_list)

    points_low = list()
    points_high = list()

    def cos_angle(point1, point2):
        cos_angle = point1 * point2 / norm(point1) / norm(point2)

        # ensure it's between -1 and 1 (nontrivial numerically)
        if abs(cos_angle) > 1:
            return cos_angle / abs(cos_angle)
        else:
            return cos_angle

    def sin_angle(point1, point2):
        return cross_prod(point1, point2) / norm(point1) / norm(point2)

    point_width_list = list(zip(points_iterator, width_iterator))
    N = len(point_width_list)

    first_point, first_width = point_width_list[0]
    next_point, next_width = point_width_list[1]

    delta = next_point - first_point
    theta = np.arctan2(delta.y, delta.x)
    first_high_point = first_point + 0.5 * first_width * pya.DPoint(
        cos(theta + pi / 2), sin(theta + pi / 2))
    first_low_point = first_point + 0.5 * first_width * pya.DPoint(
        cos(theta - pi / 2), sin(theta - pi / 2))
    points_high.append(first_high_point)
    points_low.append(first_low_point)

    for i in range(1, N - 1):
        prev_point, prev_width = point_width_list[i - 1]
        point, width = point_width_list[i]
        next_point, next_width = point_width_list[i + 1]
        delta_prev = point - prev_point
        delta_next = next_point - point

        # based on these points, there are two algorithms available:
        # 1. arc algorithm. it detects you are trying to draw an arc
        # so it will compute the center and radius of that arc and
        # layout accordingly.
        # 2. linear trace algorithm. it is not an arc, and you want
        # straight lines with sharp corners.

        # to detect an arc, the points need to go in the same direction
        # and the width has to be bigger than the smallest distance between
        # two points.

        is_small = (min(delta_next.norm(), delta_prev.norm()) < width)
        is_arc = cos_angle(delta_next, delta_prev) > cos(30 * pi / 180)
        is_arc = is_arc and is_small
        center_arc, radius = find_arc(prev_point, point, next_point)
        if is_arc and radius < np.inf:  # algorithm 1
            ray = point - center_arc
            ray /= ray.norm()
            # if orientation is positive, the arc is going counterclockwise
            orientation = (cross_prod(ray, delta_prev) > 0) * 2 - 1
            points_low.append(point + orientation * width * ray / 2)
            points_high.append(point - orientation * width * ray / 2)
        else:  # algorithm 2
            theta_prev = np.arctan2(delta_prev.y, delta_prev.x)
            theta_next = np.arctan2(delta_next.y, delta_next.x)

            next_point_high = next_point + 0.5 * next_width * pya.DPoint(
                cos(theta_next + pi / 2), sin(theta_next + pi / 2))
            next_point_low = next_point + 0.5 * next_width * pya.DPoint(
                cos(theta_next - pi / 2), sin(theta_next - pi / 2))

            forward_point_high = point + 0.5 * width * pya.DPoint(
                cos(theta_next + pi / 2), sin(theta_next + pi / 2))
            forward_point_low = point + 0.5 * width * pya.DPoint(
                cos(theta_next - pi / 2), sin(theta_next - pi / 2))

            prev_point_high = prev_point + 0.5 * prev_width * pya.DPoint(
                cos(theta_prev + pi / 2), sin(theta_prev + pi / 2))
            prev_point_low = prev_point + 0.5 * prev_width * pya.DPoint(
                cos(theta_prev - pi / 2), sin(theta_prev - pi / 2))

            backward_point_high = point + 0.5 * width * pya.DPoint(
                cos(theta_prev + pi / 2), sin(theta_prev + pi / 2))
            backward_point_low = point + 0.5 * width * pya.DPoint(
                cos(theta_prev - pi / 2), sin(theta_prev - pi / 2))

            fix_angle = lambda theta: ((theta + pi) % (2 * pi)) - pi

            # High point decision
            next_high_edge = pya.DEdge(forward_point_high, next_point_high)
            prev_high_edge = pya.DEdge(backward_point_high, prev_point_high)

            if next_high_edge.crossed_by(prev_high_edge):
                intersect_point = next_high_edge.crossing_point(prev_high_edge)
                points_high.append(intersect_point)
            else:
                cos_dd = cos_angle(delta_next, delta_prev)
                if width * (1 - cos_dd) > dbu and fix_angle(theta_next -
                                                            theta_prev) < 0:
                    points_high.append(backward_point_high)
                    points_high.append(forward_point_high)
                else:
                    points_high.append(
                        (backward_point_high + forward_point_high) * 0.5)

            # Low point decision
            next_low_edge = pya.DEdge(forward_point_low, next_point_low)
            prev_low_edge = pya.DEdge(backward_point_low, prev_point_low)

            if next_low_edge.crossed_by(prev_low_edge):
                intersect_point = next_low_edge.crossing_point(prev_low_edge)
                points_low.append(intersect_point)
            else:
                cos_dd = cos_angle(delta_next, delta_prev)
                if width * (1 - cos_dd) > dbu and fix_angle(theta_next -
                                                            theta_prev) > 0:
                    points_low.append(backward_point_low)
                    points_low.append(forward_point_low)
                else:
                    points_low.append(
                        (backward_point_low + forward_point_low) * 0.5)

    last_point, last_width = point_width_list[-1]
    point, width = point_width_list[-2]
    delta = last_point - point
    theta = np.arctan2(delta.y, delta.x)
    final_high_point = last_point + 0.5 * last_width * pya.DPoint(
        cos(theta + pi / 2), sin(theta + pi / 2))
    final_low_point = last_point + 0.5 * last_width * pya.DPoint(
        cos(theta - pi / 2), sin(theta - pi / 2))
    if (final_high_point - points_high[-1]) * delta > 0:
        points_high.append(final_high_point)
    if (final_low_point - points_low[-1]) * delta > 0:
        points_low.append(final_low_point)

    # Append point only if the area of the triangle built with
    # neighboring edges is above a certain threshold.
    # In addition, if smooth is true:
    # Append point only if change in direction is less than 130 degrees.

    def smooth_append(point_list, point):
        if len(point_list) < 1:
            point_list.append(point)
            return point_list
        elif len(point_list) < 2:
            curr_edge = point - point_list[-1]
            if norm(curr_edge) > 0:
                point_list.append(point)
                return point_list

        curr_edge = point - point_list[-1]
        if norm(curr_edge) > 0:
            prev_edge = point_list[-1] - point_list[-2]

            # Only add new point if the area of the triangle built with
            # current edge and previous edge is greater than dbu^2/2
            if abs(cross_prod(prev_edge, curr_edge)) > dbu**2 / 2:
                if smooth:
                    # avoid corners when smoothing
                    if cos_angle(curr_edge, prev_edge) > cos(130 / 180 * pi):
                        point_list.append(point)
                    else:
                        # edge case when there is prev_edge is small and
                        # needs to be deleted to get rid of the corner
                        if norm(curr_edge) > norm(prev_edge):
                            point_list[-1] = point
                else:
                    point_list.append(point)
            # avoid unnecessary points
            else:
                point_list[-1] = point
        return point_list

    if debug and False:
        print("Points to be smoothed:")
        for point, width in point_width_list:
            print(point, width)

    smooth_points_high = list(reduce(smooth_append, points_high, list()))
    smooth_points_low = list(reduce(smooth_append, points_low, list()))
    # smooth_points_low = points_low
    # polygon_dpoints = points_high + list(reversed(points_low))
    # polygon_dpoints = list(reduce(smooth_append, polygon_dpoints, list()))
    polygon_dpoints = smooth_points_high + list(reversed(smooth_points_low))
    return pya.DSimplePolygon(polygon_dpoints)