Repository: gdsfactory/gdsfactory

Issue:
gdsfactory.grid() function rotation argument is not in degrees
I have to multiply the rotation angle by 90 to get units of degrees.

```python
import gdsfactory as gf
     
    components_list = []
    for width1 in [1, 6, 9]:
        for width2 in [1, 2, 4, 8]:
            D = gf.components.text("Hello", layer=(1, 0))
            components_list.append(D)
    
    c = gf.grid(
        tuple(components_list),
        spacing=(10, 5),
        shape=(3, 4),
        align_x="x",
        align_y="y",
        rotation=90*180,
        mirror = True,
    )

```

Where I would expect to write

```python
import gdsfactory as gf
     
    components_list = []
    for width1 in [1, 6, 9]:
        for width2 in [1, 2, 4, 8]:
            D = gf.components.text("Hello", layer=(1, 0))
            components_list.append(D)
    
    c = gf.grid(
        tuple(components_list),
        spacing=(10, 5),
        shape=(3, 4),
        align_x="x",
        align_y="y",
        rotation=180,
        mirror = True,
    )

```
to rotate it by 180 degrees.

I think it can be fixed by removing the "// 90" on line 79 of [gdsfactory/grid.py](https://github.com/gdsfactory/gdsfactory/blob/main/gdsfactory/grid.py).
I think the same issue appear on line 162 in the grid_with_text() function.

Hints from issue comments:
@joamatab: Hi Mathieu,

Thank you for reporting the issue and for suggesting the fix

See fix here https://github.com/gdsfactory/gdsfactory/pull/3612

Solution patch:
diff --git a/gdsfactory/component.py b/gdsfactory/component.py
index 1755d2578..721e5716c 100644
--- a/gdsfactory/component.py
+++ b/gdsfactory/component.py
@@ -30,6 +30,7 @@ from gdsfactory._deprecation import deprecate
 from gdsfactory.config import CONF, GDSDIR_TEMP
 from gdsfactory.functions import get_polygons, get_polygons_points
 from gdsfactory.serialization import clean_value_json, convert_tuples_to_lists
+from gdsfactory.utils import to_kdb_dpoints
 
 if TYPE_CHECKING:
     import networkx as nx  # type: ignore[import-untyped]
@@ -53,6 +54,8 @@ if TYPE_CHECKING:
 
 cell_without_validator = cell
 
+_PolygonPoints: TypeAlias = "npt.NDArray[np.floating[Any]] | kdb.DPolygon | kdb.Polygon | kdb.DSimplePolygon | kdb.Region | Coordinates"
+
 
 class LockedError(AttributeError):
     """Raised when an attempt is made to modify a locked cell.
@@ -92,6 +95,20 @@ def ensure_tuple_of_tuples(points: Any) -> tuple[tuple[float, float], ...]:
     return points  # type: ignore
 
 
+def points_to_polygon(
+    points: _PolygonPoints,
+) -> kdb.Polygon | kdb.DPolygon | kdb.DSimplePolygon | kdb.Region:
+    if isinstance(points, tuple | list | np.ndarray):
+        points = ensure_tuple_of_tuples(points)
+        polygon = kdb.DPolygon()
+        polygon.assign_hull(to_kdb_dpoints(points))
+    elif isinstance(
+        points, kdb.Polygon | kdb.DPolygon | kdb.DSimplePolygon | kdb.Region
+    ):
+        return points
+    return kdb.DPolygon(to_kdb_dpoints(points))
+
+
 def size(region: kdb.Region, offset: float, dbu: float = 1e3) -> kdb.Region:
     return region.dup().size(int(offset * dbu))
 
@@ -214,7 +231,7 @@ class ComponentReference(kf.Instance):
 
     @property
     def info(self) -> dict[str, Any]:
-        deprecate("info", "ref.cell.info", stacklevel=3)
+        deprecate("info", "cell.info", stacklevel=3)
         return self.cell.info.model_dump()
 
     def connect(  # type: ignore[override]
@@ -281,9 +298,9 @@ class ComponentReference(kf.Instance):
         self.set_property(PROPID.NAME, value)
 
     @property
-    def parent(self) -> kf.KCell | Component:
+    def parent(self) -> kf.KCell:
         """Returns the parent Component."""
-        deprecate("parent", "ref.cell")
+        deprecate("parent", "cell")
         return self.cell
 
     def __eq__(self, other: Any) -> bool:
@@ -446,41 +463,6 @@ class ComponentBase(BaseKCell, ABC):
     @abstractmethod
     def dup(self) -> Self: ...
 
-    def add_polygon(
-        self,
-        points: (
-            "npt.NDArray[np.floating[Any]] | kdb.DPolygon | kdb.Polygon | kdb.DSimplePolygon | kdb.Region | Coordinates"
-        ),
-        layer: "LayerSpec",
-    ) -> kdb.Shape:
-        """Adds a Polygon to the Component and returns a klayout Shape.
-
-        Args:
-            points: Coordinates of the vertices of the Polygon.
-            layer: layer spec to add polygon on.
-        """
-        from gdsfactory.pdk import get_layer
-
-        if self._locked:
-            raise LockedError(self)
-
-        _layer = get_layer(layer)
-
-        if isinstance(points, tuple | list | np.ndarray):
-            points = ensure_tuple_of_tuples(points)
-            polygon: kdb.Polygon | kdb.DPolygon | kdb.DSimplePolygon | kdb.Region = (
-                kdb.DPolygon()
-            )
-            polygon.assign_hull(points)  # type: ignore[arg-type,union-attr]
-        elif isinstance(
-            points, kdb.Polygon | kdb.DPolygon | kdb.DSimplePolygon | kdb.Region
-        ):
-            polygon = points
-        else:
-            polygon = kdb.DPolygon(points)  # type: ignore[arg-type]
-
-        return self.shapes(_layer).insert(polygon)  # type: ignore[no-any-return]
-
     def add_label(
         self,
         text: str = "hello",
@@ -614,8 +596,7 @@ class ComponentBase(BaseKCell, ABC):
         gdspath = gdspath or gdsdir / f"{name[: CONF.max_cellname_length]}.gds"
         gdspath = pathlib.Path(gdspath)
 
-        if not gdspath.parent.is_dir():
-            gdspath.parent.mkdir(parents=True, exist_ok=True)
+        gdspath.parent.mkdir(parents=True, exist_ok=True)
 
         if save_options is None:
             save_options = save_layout_options()
@@ -626,6 +607,7 @@ class ComponentBase(BaseKCell, ABC):
         if kwargs:
             for k in kwargs:
                 deprecate(k)
+
         self.write(filename=gdspath, save_options=save_options)
         return pathlib.Path(gdspath)
 
@@ -1143,12 +1125,10 @@ class Component(ComponentBase, kf.KCell):  # type: ignore
 
         if recursive:
             iterator = self._kdb_cell.begin_shapes_rec(layer)
-
-            while not (iterator.at_end()):
-                shape = iterator.shape()
-                iterator.next()
-                if shape.is_path():
-                    paths.append(shape.dpath.transformed(iterator.dtrans()))
+            iterator.shape_flags = kdb.Shapes.SPaths
+            paths.extend(
+                it.shape().dpath.transformed(it.dtrans()) for it in iterator.each()
+            )
         else:
             paths.extend(
                 shape.dpath
@@ -1173,12 +1153,10 @@ class Component(ComponentBase, kf.KCell):  # type: ignore
 
         if recursive:
             iterator = self._kdb_cell.begin_shapes_rec(layer)
-
-            while not (iterator.at_end()):
-                shape = iterator.shape()
-                iterator.next()
-                if shape.is_box():
-                    boxes.append(shape.dbox.transformed(iterator.dtrans()))
+            iterator.shape_flags = kdb.Shapes.SBoxes
+            boxes.extend(
+                it.shape().dbox.transformed(it.dtrans()) for it in iterator.each()
+            )
         else:
             boxes.extend(
                 shape.dbox
@@ -1197,19 +1175,21 @@ class Component(ComponentBase, kf.KCell):  # type: ignore
         """
         from gdsfactory import get_layer
 
+        texts: list[kf.kdb.DText] = []
         layer_enum = get_layer(layer)
 
         if recursive:
-            return [
-                shape.dtext.transformed(iterator.dtrans())
-                for iterator in self._kdb_cell.begin_shapes_rec(layer_enum)
-                if (shape := iterator.shape()).is_text()
-            ]
+            iterator = self._kdb_cell.begin_shapes_rec(layer_enum)
+            iterator.shape_flags = kdb.Shapes.STexts
+            texts.extend(
+                it.shape().dtext.transformed(it.dtrans()) for it in iterator.each()
+            )
         else:
-            return [
+            texts.extend(
                 shape.dtext
                 for shape in self._kdb_cell.shapes(layer_enum).each(kdb.Shapes.STexts)
-            ]
+            )
+        return texts
 
     def area(self, layer: "LayerSpec") -> float:
         """Returns the area of the Component in um2."""
@@ -1223,7 +1203,7 @@ class Component(ComponentBase, kf.KCell):  # type: ignore
     def get_polygons(
         self,
         merge: bool = False,
-        by: Literal["index"] | Literal["name"] | Literal["tuple"] = "index",
+        by: Literal["index", "name", "tuple"] = "index",
         layers: "LayerSpecs | None" = None,
     ) -> dict[tuple[int, int] | str | int, list[kf.kdb.Polygon]]:
         """Returns a dict of Polygons per layer.
@@ -1235,13 +1215,14 @@ class Component(ComponentBase, kf.KCell):  # type: ignore
         """
         if merge and self._locked:
             raise LockedError(self)
+
         return get_polygons(self, merge=merge, by=by, layers=layers)
 
     def get_polygons_points(
         self,
         merge: bool = False,
         scale: float | None = None,
-        by: Literal["index"] | Literal["name"] | Literal["tuple"] = "index",
+        by: Literal["index", "name", "tuple"] = "index",
         layers: "LayerSpecs | None" = None,
     ) -> dict[int | str | tuple[int, int], list[npt.NDArray[np.floating[Any]]]]:
         """Returns a dict with list of points per layer.
@@ -1397,6 +1378,8 @@ class Component(ComponentBase, kf.KCell):  # type: ignore
         self.remove_layers([layer])
         self._kdb_cell.shapes(layer_index).insert(region)
 
+        self.kcl.layout.end_changes()
+
     def offset(self, layer: "LayerSpec", distance: float) -> None:
         """Offsets a Component layer by a distance in um.
 
@@ -1413,15 +1396,47 @@ class Component(ComponentBase, kf.KCell):  # type: ignore
 
         layer_index = get_layer(layer)
         region = kdb.Region(self._kdb_cell.begin_shapes_rec(layer_index))
-        region.size(+distance_dbu)
+        region.size(distance_dbu)
         self.remove_layers([layer])
         self._kdb_cell.shapes(layer_index).insert(region)
 
+        self.kcl.layout.end_changes()
+
     def ref(self, *args: Any, **kwargs: Any) -> ComponentReference:
         """Returns a Component Instance."""
         deprecate("ref", "add_ref")
         return self.add_ref(*args, **kwargs)
 
+    @overload
+    def add_polygon(self, points: kdb.Region, layer: "LayerSpec") -> None: ...
+
+    @overload
+    def add_polygon(
+        self,
+        points: "npt.NDArray[np.floating[Any]] | kdb.DPolygon | kdb.Polygon | kdb.DSimplePolygon | Coordinates",
+        layer: "LayerSpec",
+    ) -> kdb.Shape: ...
+
+    def add_polygon(
+        self, points: _PolygonPoints, layer: "LayerSpec"
+    ) -> kdb.Shape | None:
+        """Adds a Polygon to the Component and returns a klayout Shape.
+
+        Args:
+            points: Coordinates of the vertices of the Polygon.
+            layer: layer spec to add polygon on.
+        """
+        from gdsfactory.pdk import get_layer
+
+        if self._locked:
+            raise LockedError(self)
+
+        _layer = get_layer(layer)
+
+        polygon = points_to_polygon(points)
+
+        return self._kdb_cell.shapes(_layer).insert(polygon)
+
 
 class ComponentAllAngle(ComponentBase, kf.VKCell):  # type: ignore
     def plot(self, **kwargs: Any) -> None:  # type: ignore
@@ -1458,6 +1473,30 @@ class ComponentAllAngle(ComponentBase, kf.VKCell):  # type: ignore
 
         return c
 
+    def add_polygon(self, points: _PolygonPoints, layer: "LayerSpec") -> None:
+        """Adds a Polygon to the Component and returns a klayout Shape.
+
+        Args:
+            points: Coordinates of the vertices of the Polygon.
+            layer: layer spec to add polygon on.
+        """
+        from gdsfactory.pdk import get_layer
+
+        if self._locked:
+            raise LockedError(self)
+
+        _layer = get_layer(layer)
+
+        polygon = points_to_polygon(points)
+
+        return self.shapes(_layer).insert(polygon)
+
+    def get_polygons(self, layer: "LayerSpec") -> list[kf.kdb.DPolygon]:
+        """Returns a list of polygons from the Component."""
+        from gdsfactory import get_layer
+
+        return [x for x in self.shapes(get_layer(layer)) if isinstance(x, kdb.DPolygon)]
+
 
 def container(
     component: "ComponentSpec",
@@ -1485,7 +1524,7 @@ def container(
 
 def component_with_function(
     component: "ComponentSpec",
-    function: Callable[..., None] | None = None,
+    function: Callable[[Component], None] | None = None,
     **kwargs: Any,
 ) -> gf.Component:
     """Returns new component with a component reference.
diff --git a/gdsfactory/functions.py b/gdsfactory/functions.py
index 13c6a54c9..2484ee7bd 100644
--- a/gdsfactory/functions.py
+++ b/gdsfactory/functions.py
@@ -189,7 +189,7 @@ def get_polygons_points(
     component_or_instance: "Component | kf.Instance",
     merge: bool = False,
     scale: float | None = None,
-    by: Literal["index"] | Literal["name"] | Literal["tuple"] = "index",
+    by: Literal["index", "name", "tuple"] = "index",
     layers: LayerSpecs | None = None,
 ) -> dict[int | str | tuple[int, int], list[npt.NDArray[np.floating[Any]]]]:
     """Returns a dict with list of points per layer.
diff --git a/gdsfactory/grid.py b/gdsfactory/grid.py
index e13e175d8..15a773b7b 100644
--- a/gdsfactory/grid.py
+++ b/gdsfactory/grid.py
@@ -76,7 +76,7 @@ def grid(
         ),
         align_x=align_x,
         align_y=align_y,
-        rotation=round(rotation // 90),  # type: ignore
+        rotation=rotation,  # type: ignore
         mirror=mirror,
     )
     for i, instances_list in enumerate(instances):
@@ -147,6 +147,11 @@ def grid_with_text(
         list(labels) if labels else [None] * len(component_list)
     )
 
+    if len(labels_not_none) != len(component_list):
+        raise ValueError(
+            f"Number of labels {len(labels_not_none)} must match number of components {len(component_list)}"
+        )
+
     c = gf.Component()
     instances = kf.grid(
         c,
@@ -159,7 +164,7 @@ def grid_with_text(
         ),
         align_x=align_x,
         align_y=align_y,
-        rotation=round(rotation // 90),  # type: ignore[arg-type]
+        rotation=rotation,  # type: ignore[arg-type]
         mirror=mirror,
     )
     for i, instances_list in enumerate(instances):
@@ -191,18 +196,18 @@ if __name__ == "__main__":
     # test_grid()
     # components = [gf.components.rectangle(size=(i, i)) for i in range(40, 66, 5)]
     # c = tuple(gf.components.rectangle(size=(i, i)) for i in range(40, 66, 10))
-    # c = tuple([gf.components.triangle(x=i) for i in range(1, 10)])
-    components = tuple(gf.components.rectangle(size=(i, i)) for i in range(1, 3))
+    components = tuple([gf.components.triangle(x=i) for i in range(1, 10)])
+    # components = tuple(gf.components.rectangle(size=(i, i)) for i in range(1, 3))
     # print(len(c))
 
-    c = grid_with_text(
+    c = grid(
         components,
         shape=(3, 3),
-        # rotation=90,
+        rotation=90,
         mirror=False,
         spacing=(200.0, 200.0),
         # spacing=1,
-        text_offsets=((0, 100), (0, -100)),
-        labels=["r1", "r2"],
+        # text_offsets=((0, 100), (0, -100)),
+        # labels=["r1", "r2"],
     )
     c.show()


Test patch:
diff --git a/tests/test_component.py b/tests/test_component.py
index 226525aa5..47a56d22b 100644
--- a/tests/test_component.py
+++ b/tests/test_component.py
@@ -1,15 +1,42 @@
+from typing import Any
+
 import kfactory as kf
+import klayout.db as kdb
+import numpy as np
 import pytest
 
 import gdsfactory as gf
-from gdsfactory.component import LockedError
+from gdsfactory.component import (
+    ComponentReference,
+    LockedError,
+    component_with_function,
+    container,
+    copy,
+    ensure_tuple_of_tuples,
+    points_to_polygon,
+    size,
+)
+from gdsfactory.config import GDSDIR_TEMP
 from gdsfactory.generic_tech import LAYER
+from gdsfactory.pdk import get_layer
 
 
 def test_component_copy() -> None:
     c1 = gf.components.mzi(delta_length=10)
     c2 = c1.copy()
-    assert c1.settings.delta_length == 10
+    assert c1.settings["delta_length"] == 10
+    assert c1.info == c2.info
+
+    assert len(c1.ports) == len(c2.ports)
+    assert len(list(c1.insts)) == len(list(c2.insts)), (
+        f"{len(list(c1.insts))} != {len(list(c2.insts))}"
+    )
+
+
+def test_component_all_angle_copy() -> None:
+    c1 = gf.components.straight_all_angle(length=10)
+    c2 = c1.dup()
+    assert c1.settings["length"] == 10
     assert c1.info == c2.info
 
     assert len(c1.ports) == len(c2.ports)
@@ -63,6 +90,13 @@ def test_trim() -> None:
     c1.trim(left=-5, right=5, top=5, bottom=-5)
     assert c1_area == c1.area(layer=layer), f"{c1_area} != {c1.area(layer=layer)}"
 
+    c2 = gf.c.rectangle(size=(9, 9), centered=True, layer=layer).copy()
+    c2.flatten()
+    c2_area = c2.area(layer=layer)
+
+    c2.trim(left=-5, right=5, top=5, bottom=-5, flatten=True)
+    assert c2_area == c2.area(layer=layer), f"{c2_area} != {c2.area(layer=layer)}"
+
 
 def test_from_kcell() -> None:
     kf.kcl.infos = kf.LayerInfos(WG=kf.kdb.LayerInfo(1, 0))
@@ -103,3 +137,723 @@ def test_locked_cell() -> None:
 
     with pytest.raises(LockedError):
         c.add_port(name="o1", center=(0, 0), width=0.5, orientation=0, layer="WG")
+
+    with pytest.raises(LockedError):
+        c.get_polygons_points(merge=True)
+
+    with pytest.raises(LockedError):
+        c.get_polygons(merge=True)
+
+    with pytest.raises(LockedError):
+        c.add_ref(gf.Component())
+
+    with pytest.raises(LockedError):
+        c.trim(left=-5, right=5, top=5, bottom=-5)
+
+    with pytest.raises(LockedError):
+        c.absorb(c.add_ref(gf.Component()))
+
+    with pytest.raises(LockedError):
+        c.absorb(c.add_ref(gf.Component()))
+
+    with pytest.raises(LockedError):
+        c.add(gf.Instance(gf.Component().kcl, kdb.Instance()))
+
+
+def test_deprecated_methods() -> None:
+    c = gf.Component()
+
+    with pytest.warns(DeprecationWarning):
+        _ = c.named_references
+
+    with pytest.warns(DeprecationWarning):
+        _ = c.references
+
+    with pytest.warns(DeprecationWarning):
+        c = gf.Component()
+        c2 = gf.Component()
+        c2.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(1, 0))
+        ref = c.add_array(c2, columns=2, rows=3, spacing=(20, 30))
+        assert len(list(c.insts)) == 1
+        assert ref.na == 2
+        assert ref.nb == 3
+        assert ref.a.x == 20 / c.kcl.layout.dbu
+        assert ref.b.y == 30 / c.kcl.layout.dbu
+
+    with pytest.warns(DeprecationWarning):
+        c = gf.Component()
+        c2 = gf.Component()
+        ref = c.add_ref(c2, spacing=(20, 30), columns=2, rows=3)
+        assert len(list(c.insts)) == 1
+        assert ref.na == 2
+        assert ref.nb == 3
+        assert ref.a.x == 20 / c.kcl.layout.dbu
+        assert ref.b.y == 30 / c.kcl.layout.dbu
+
+    with pytest.warns(DeprecationWarning):
+        c = gf.Component()
+        c2 = gf.Component()
+        ref = c.add_ref(c2, alias="test_alias")
+        assert len(list(c.insts)) == 1
+        assert ref.name == "test_alias"
+
+    with pytest.warns(DeprecationWarning):
+        c.ref(gf.components.straight())
+
+    with pytest.warns(DeprecationWarning):
+        c = gf.Component()
+        c2 = gf.Component()
+        ref = c.add_ref(c2)
+        _ = ref.info
+        assert ref.cell.info.model_dump() == ref.info
+
+    with pytest.warns(DeprecationWarning):
+        c = gf.Component()
+        c2 = gf.Component()
+        ref = c.add_ref(c2)
+        _ = ref.parent
+        assert ref.cell == ref.parent
+
+    with pytest.warns(DeprecationWarning):
+        c = gf.Component()
+        c.write_gds(gdspath="test_deprecated_write_gds.gds", random_var=1)
+
+
+def test_locked_cell_all_angle() -> None:
+    c = gf.ComponentAllAngle()
+    c._locked = True
+
+    with pytest.raises(LockedError):
+        c.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(2, 0))
+
+    with pytest.raises(LockedError):
+        c.add_port(name="o1", center=(0, 0), width=0.5, orientation=0, layer="WG")
+
+    with pytest.raises(LockedError):
+        c.add_label()
+
+    with pytest.raises(LockedError):
+        c.add_route_info("strip", length=10)
+
+    with pytest.raises(LockedError):
+        c.copy_child_info(gf.components.straight().copy())
+
+
+def test_ensure_tuple_of_tuples() -> None:
+    import numpy as np
+
+    points_np = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
+    result_np = ensure_tuple_of_tuples(points_np)
+    assert isinstance(result_np, tuple)
+    assert all(isinstance(p, tuple) for p in result_np)
+    assert result_np == ((1.0, 2.0), (3.0, 4.0), (5.0, 6.0))
+
+    points_list = [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]
+    result_list = ensure_tuple_of_tuples(points_list)
+    assert isinstance(result_list, tuple)
+    assert all(isinstance(p, tuple) for p in result_list)
+    assert result_list == ((1.0, 2.0), (3.0, 4.0), (5.0, 6.0))
+
+    points_np_list = [np.array([1.0, 2.0]), np.array([3.0, 4.0])]
+    result_np_list = ensure_tuple_of_tuples(points_np_list)
+    assert isinstance(result_np_list, tuple)
+    assert all(isinstance(p, tuple) for p in result_np_list)
+    assert result_np_list == ((1.0, 2.0), (3.0, 4.0))
+
+    points_tuple = ((1.0, 2.0), (3.0, 4.0))
+    result_tuple = ensure_tuple_of_tuples(points_tuple)
+    assert result_tuple == points_tuple
+
+
+def test_points_to_polygon() -> None:
+    import klayout.db as kdb
+    import numpy as np
+
+    points_np = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
+    result_np = points_to_polygon(points_np)
+    assert isinstance(result_np, kdb.DPolygon)
+
+    points_tuple = ((1.0, 2.0), (3.0, 4.0), (5.0, 6.0))
+    result_tuple = points_to_polygon(points_tuple)
+    assert isinstance(result_tuple, kdb.DPolygon)
+
+    kdb_polygon = kdb.DPolygon(kdb.DBox(0, 0, 10, 10))
+    result_kdb = points_to_polygon(kdb_polygon)
+    assert result_kdb == kdb_polygon
+
+    kdb_simple = kdb.DSimplePolygon(kdb.DBox(0, 0, 10, 10))
+    result_simple = points_to_polygon(kdb_simple)
+    assert isinstance(result_simple, kdb.DPolygon | kdb.DSimplePolygon)
+
+    region = kdb.Region()
+    region.insert(kdb.Box(0, 0, 1000, 1000))
+    result_region = points_to_polygon(region)
+    assert isinstance(result_region, kdb.Region | kdb.DPolygon)
+
+
+def test_size() -> None:
+    region = kdb.Region()
+    region.insert(kdb.Box(0, 0, 1000, 1000))
+
+    result = size(region, offset=0.1)
+    assert result.bbox().width() > region.bbox().width()
+
+    result = size(region, offset=-0.1)
+    assert result.bbox().width() < region.bbox().width()
+
+    result = size(region, offset=0.1, dbu=2000)
+    assert result.bbox().width() > region.bbox().width()
+
+
+def test_region_copy() -> None:
+    region = kdb.Region()
+    region.insert(kdb.Box(0, 0, 1000, 1000))
+    region_copy = copy(region)
+    assert region_copy.bbox().width() == region.bbox().width()
+
+
+def test_component_reference_properties() -> None:
+    c = gf.Component()
+    straight = gf.components.straight(length=10).copy()
+    ref = c << straight
+
+    assert ref.center == ref.dcenter
+    assert ref.x == ref.dx
+    assert ref.y == ref.dy
+    assert ref.xmin == ref.dxmin
+    assert ref.xmax == ref.dxmax
+    assert ref.ymin == ref.dymin
+    assert ref.ymax == ref.dymax
+    assert ref.xsize == ref.dxsize
+    assert ref.ysize == ref.dysize
+    assert ref.size_info == ref.dsize_info
+
+
+def test_component_reference_setters() -> None:
+    c = gf.Component()
+    straight = gf.components.straight(length=10).copy()
+    ref = c << straight
+
+    ref.x = 5.0  # type: ignore
+    assert ref.dx == 5.0
+    ref.y = 10.0  # type: ignore
+    assert ref.dy == 10.0
+
+
+def test_component_reference_transformations() -> None:
+    c = gf.Component()
+    straight = gf.components.straight(length=10).copy()
+    ref = c << straight
+
+    original_pos = ref.center
+    ref.move((1.0, 2.0))  # type: ignore
+    assert ref.center != original_pos
+
+    ref.rotate(90)  # type: ignore
+    ref.mirror()
+
+
+def test_component_reference_name() -> None:
+    c = gf.Component()
+    straight = gf.components.straight(length=10).copy()
+    ref = c << straight
+
+    ref.name = "test_instance"
+    assert ref.name == "test_instance"
+
+
+def test_component_reference_equality_and_hash() -> None:
+    c = gf.Component()
+    straight = gf.components.straight(length=10).copy()
+    ref = c << straight
+    inst2 = c << straight
+    ref2 = ComponentReference(inst2)
+
+    assert ref != ref2
+    assert ref == ref
+    assert ref != ""
+    hash(ref)
+
+
+def test_component_reference_flatten() -> None:
+    c = gf.Component()
+    straight = gf.components.straight(length=10).copy()
+    ref = c << straight
+    ref.flatten()
+
+
+def test_component_reference_connect() -> None:
+    c = gf.Component()
+    straight = gf.components.straight(length=10).copy()
+    c2 = gf.components.straight(length=10).copy()
+    inst2 = c << c2
+    port = straight.add_port(
+        name="o1", center=(0, 0), width=0.5, orientation=0, layer=(1, 0)
+    )
+    ref = c << straight
+
+    with pytest.warns(DeprecationWarning):
+        ref.connect("o1", other=inst2, destination=port, other_port_name="o1")
+    with pytest.warns(DeprecationWarning):
+        ref.connect("o1", port, overlap=1.0)
+    with pytest.warns(DeprecationWarning):
+        ref.connect("o1", port, preserve_orientation=True)
+
+
+def test_component_reference_deprecated_attributes() -> None:
+    c = gf.Component()
+    straight = gf.components.straight(length=10).copy()
+    ref = c << straight
+
+    with pytest.warns(DeprecationWarning):
+        info = ref.info
+        assert isinstance(info, dict)
+
+    with pytest.warns(DeprecationWarning):
+        parent = ref.parent
+        assert parent == ref.cell
+
+
+def test_component_references_getitem() -> None:
+    c = gf.Component()
+    straight = gf.components.straight(length=10).copy()
+    ref = c << straight
+    ref2 = c << straight
+    ref2.name = "test_ref"
+
+    assert c.insts[0] == ref
+    assert c.insts[1] == ref2
+
+    assert c.insts["test_ref"] == ref2
+
+
+def test_component_references_iter() -> None:
+    c = gf.Component()
+    straight = gf.components.straight(length=10).copy()
+    ref = c << straight
+    ref2 = c << straight
+
+    refs = list(c.insts)
+    assert len(refs) == 2
+    assert refs[0] == ref
+    assert refs[1] == ref2
+    assert all(isinstance(r, ComponentReference) for r in refs)
+
+
+def test_component_references_delitem() -> None:
+    c = gf.Component()
+    straight = gf.components.straight(length=10).copy()
+    c << straight
+
+    del c.insts[0]
+    assert len(c.insts) == 0
+
+
+def test_component_all_angle_add_port() -> None:
+    c = gf.ComponentAllAngle()
+
+    port1 = c.add_port(
+        name="p1",
+        center=(10, 20),
+        width=0.5,
+        orientation=90,
+        layer="WG",
+        port_type="optical",
+    )
+    assert port1.name == "p1"
+    assert port1.dwidth == 0.5
+    assert port1.orientation == 90
+    assert port1.port_type == "optical"
+    assert port1.dcenter == (10, 20)
+
+    port2 = c.add_port(
+        name="p2",
+        center=kdb.DPoint(30, 40),
+        width=1.0,
+        orientation=0,
+        layer="WG",
+        port_type="electrical",
+    )
+    assert port2.name == "p2"
+    assert port2.dwidth == 1.0
+    assert port2.orientation == 0
+    assert port2.port_type == "electrical"
+    assert port2.dcenter == (30, 40)
+
+    port3 = c.add_port(
+        name="p3",
+        center=(50, 60),
+        orientation=180,
+        cross_section="strip",
+        port_type="optical",
+    )
+    assert port3.name == "p3"
+    assert port3.orientation == 180
+    assert port3.port_type == "optical"
+    assert port3.dcenter == (50, 60)
+
+    with pytest.raises(ValueError, match="Must specify orientation"):
+        c.add_port(name="p4", center=(0, 0), width=0.5, layer="WG")
+
+    with pytest.raises(ValueError, match="Must specify center"):
+        c.add_port(name="p5", width=0.5, orientation=90, layer="WG")
+
+    with pytest.raises(ValueError, match="Must specify layer or cross_section"):
+        c.add_port(name="p6", center=(0, 0), width=0.5, orientation=90)
+
+    with pytest.raises(ValueError, match="Must specify width or cross_section"):
+        c.add_port(name="p7", center=(0, 0), orientation=90, layer="WG")
+
+    with pytest.warns(UserWarning, match="Port type invalid not in"):
+        c.add_port(
+            name="p8",
+            center=(0, 0),
+            width=0.5,
+            orientation=90,
+            layer="WG",
+            port_type="invalid",
+        )
+
+
+def test_component_all_angle_add_polygon() -> None:
+    c = gf.ComponentAllAngle()
+
+    points1 = [(0, 0), (10, 0), (10, 10), (0, 10)]
+    c.add_polygon(points1, layer="WG")
+    assert len(list(c.shapes(get_layer(LAYER.WG)).each())) == 1
+
+    points2 = np.array([(20, 0), (30, 0), (30, 10), (20, 10)])
+    c.add_polygon(points2, layer="WG")
+    assert len(list(c.shapes(get_layer(LAYER.WG)).each())) == 2
+
+    points3 = [
+        kf.kdb.DPoint(40, 0),
+        kf.kdb.DPoint(50, 0),
+        kf.kdb.DPoint(50, 10),
+        kf.kdb.DPoint(40, 10),
+    ]
+    dpoly = kf.kdb.DPolygon()
+    dpoly.assign_hull(points3)
+    c.add_polygon(dpoly, layer="WG")
+    assert len(list(c.shapes(get_layer(LAYER.WG)).each())) == 3
+    points4 = [
+        kf.kdb.Point(60, 0),
+        kf.kdb.Point(70, 0),
+        kf.kdb.Point(70, 10),
+        kf.kdb.Point(60, 10),
+    ]
+    poly = kf.kdb.Polygon()
+    poly.assign_hull(points4)
+    c.add_polygon(poly, layer="WG")
+    assert len(list(c.shapes(get_layer(LAYER.WG)).each())) == 4
+    points5 = [
+        kf.kdb.DPoint(80, 0),
+        kf.kdb.DPoint(90, 0),
+        kf.kdb.DPoint(90, 10),
+        kf.kdb.DPoint(80, 10),
+    ]
+    dsimpoly = kf.kdb.DSimplePolygon(points5)
+    c.add_polygon(dsimpoly, layer="WG")
+    assert len(list(c.shapes(get_layer(LAYER.WG)).each())) == 5
+
+    c._locked = True
+    with pytest.raises(LockedError):
+        c.add_polygon(points1, layer="WG")
+
+
+def test_component_all_angle_add_label() -> None:
+    c = gf.ComponentAllAngle()
+    c.add_label(text="test", position=(10, 20), layer="WG")
+    assert len(list(c.shapes(get_layer(LAYER.WG)).each())) == 1
+
+    c.add_label(text="test", position=kf.kdb.DPoint(30, 40), layer="WG")
+    assert len(list(c.shapes(get_layer(LAYER.WG)).each())) == 2
+
+
+def test_component_write_gds() -> None:
+    c = gf.Component("test_component_all_angle_write_gds")
+
+    with pytest.warns(UserWarning, match="gdspath and gdsdir have both been specified"):
+        c.write_gds(
+            gdspath=GDSDIR_TEMP / "test_component_write_gds.gds",
+            gdsdir=GDSDIR_TEMP,
+        )
+
+    path = c.write_gds(with_metadata=False)
+    assert path.exists()
+
+    save_options = kdb.SaveLayoutOptions()
+    save_options.write_context_info = False
+    path = c.write_gds(save_options=save_options)
+    assert path.exists()
+
+    path = c.write_gds(gdsdir=GDSDIR_TEMP)
+    assert path.exists()
+    assert path.parent == GDSDIR_TEMP
+
+    path = c.write_gds(gdspath=GDSDIR_TEMP / "custom_name.gds")
+    assert path.exists()
+    assert path.name == "custom_name.gds"
+
+
+def test_component_copy_child_info() -> None:
+    c1 = gf.Component()
+    c2 = gf.Component()
+    c2.info["test_info"] = "test_value"
+    c1.copy_child_info(c2)
+    assert c1.info["test_info"] == "test_value"
+
+
+def test_container() -> None:
+    c = gf.Component()
+    c.info["test_info"] = "test_value"
+
+    def test_function(component: gf.Component, **kwargs: Any) -> None:
+        component.info["new_info"] = kwargs.get("value", "new_value")
+
+    result = container(c, function=test_function, value="custom_value")
+
+    assert result.info["test_info"] == "test_value"
+    assert result.info["new_info"] == "custom_value"
+    assert len(result.insts) == 1
+
+
+def test_component_with_function() -> None:
+    c = gf.Component()
+    c.info["test_info"] = "test_value"
+
+    def test_function(component: gf.Component) -> None:
+        component.info["new_info"] = "new_value"
+
+    result = component_with_function(c, function=test_function)
+
+    assert result.info["test_info"] == "test_value"
+    assert result.info["new_info"] == "new_value"
+    assert len(result.insts) == 1
+
+
+def test_plot() -> None:
+    c = gf.Component()
+    c.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(1, 0))
+    c.plot()
+    c.pprint_ports()
+    c.to_graphviz()
+    c.to_dict(True)
+
+
+def test_offset() -> None:
+    c = gf.Component()
+    c.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(1, 0))
+    c.offset(layer=(1, 0), distance=10)
+
+    c.kcl.layout.end_changes()
+
+    assert c.dbbox(get_layer((1, 0))) == kdb.Box(-10, -10, 20, 20)
+
+
+def test_over_under() -> None:
+    c = gf.Component()
+
+    c.add_polygon([(0, 0), (0, 5), (5, 5), (5, 0)], layer=(1, 0))
+    c.add_polygon([(5.001, 0), (5.001, 5), (10, 5), (10, 0)], layer=(1, 0))
+
+    c.add_polygon([(0, 0), (0, 0.0005), (0, 0.0005), (0.0005, 0)], layer=(1, 0))
+
+    c.over_under(layer=(1, 0), distance=1)
+
+    assert len(c.shapes(get_layer((1, 0)))) == 1
+    assert c.dbbox(get_layer((1, 0))) == kdb.Box(0, 0, 10, 5)
+
+
+def test_component_all_angle_plot() -> None:
+    c = gf.ComponentAllAngle()
+    c.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(1, 0))
+    c.plot()
+
+
+def test_component_to_3d() -> None:
+    c = gf.Component()
+    c.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(1, 0))
+    c.to_3d()
+
+
+def test_remap_layers() -> None:
+    c = gf.Component()
+    c.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(1, 0))
+    c.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(2, 0))
+
+    c.remap_layers({(1, 0): (3, 0)})
+    assert c.area((1, 0)) == 0, f"{c.area((1, 0))}"
+    assert c.area((2, 0)) == 100, f"{c.area((2, 0))}"
+    assert c.area((3, 0)) == 100, f"{c.area((3, 0))}"
+
+
+def test_copy_layers() -> None:
+    c = gf.Component()
+    c.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(1, 0))
+
+    c2 = gf.Component()
+    c2.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(5, 0))
+    ref = c.add_ref(c2)
+    c.copy_layers({(1, 0): (3, 0), (5, 0): (6, 0)}, recursive=False)
+    assert c.area((1, 0)) == 100, f"{c.area((1, 0))}"
+    assert c.area((3, 0)) == 100, f"{c.area((3, 0))}"
+    assert ref.cell.area((5, 0)) == 100, f"{ref.cell.area((5, 0))}"
+    assert ref.cell.area((6, 0)) == 0, f"{ref.cell.area((6, 0))}"
+
+    c3 = gf.Component()
+    ref2 = c3.add_ref(c)
+    c3.copy_layers({(1, 0): (3, 0)}, recursive=True)
+    assert c3.area((1, 0)) == 100, f"{c3.area((1, 0))}"
+    assert c3.area((3, 0)) == 100, f"{c3.area((3, 0))}"
+    assert ref2.cell.area((1, 0)) == 100, f"{ref2.cell.area((1, 0))}"
+    assert ref2.cell.area((3, 0)) == 100, f"{ref2.cell.area((3, 0))}"
+
+
+def test_get_labels() -> None:
+    c = gf.Component()
+    c.add_label(text="test1", position=(10, 20), layer="WG")
+    c.add_label(text="test2", position=(30, 40), layer="WG")
+
+    labels = c.get_labels(layer="WG", recursive=False)
+    assert len(labels) == 2
+    assert labels[0].string == "test1"
+    assert labels[0].x == 10
+    assert labels[0].y == 20
+    assert labels[1].string == "test2"
+    assert labels[1].x == 30
+    assert labels[1].y == 40
+
+    c2 = gf.Component()
+    ref = c2 << c
+    ref.move((100, 100))
+
+    labels = c2.get_labels(layer="WG", recursive=True)
+    assert len(labels) == 2
+    assert labels[0].string == "test1"
+    assert labels[0].x == 110
+    assert labels[0].y == 120
+    assert labels[1].string == "test2"
+    assert labels[1].x == 130
+    assert labels[1].y == 140
+
+
+def test_get_boxes() -> None:
+    c = gf.Component()
+    box = kf.kdb.DBox(0, 0, 10, 10)
+    c.shapes(get_layer((1, 0))).insert(box)
+
+    boxes = c.get_boxes(layer=(1, 0), recursive=False)
+    assert len(boxes) == 1
+    assert boxes[0].left == 0
+    assert boxes[0].right == 10
+    assert boxes[0].top == 10
+    assert boxes[0].bottom == 0
+
+    c2 = gf.Component()
+    ref = c2 << c
+    ref.move((100, 100))
+
+    c2.show()
+
+    boxes = c2.get_boxes(layer=(1, 0), recursive=True)
+    assert len(boxes) == 1
+    assert boxes[0].left == 100
+    assert boxes[0].right == 110
+    assert boxes[0].top == 110
+    assert boxes[0].bottom == 100
+
+
+def test_get_paths() -> None:
+    c = gf.Component()
+    path = kf.kdb.DPath([kf.kdb.DPoint(0, 0), kf.kdb.DPoint(10, 10)], 1)
+    c.shapes(get_layer((1, 0))).insert(path)
+
+    paths = c.get_paths(layer=(1, 0), recursive=False)
+    assert len(paths) == 1
+    points = list(paths[0].each_point())
+    assert points[0].x == 0
+    assert points[0].y == 0
+    assert points[1].x == 10
+    assert points[1].y == 10
+
+    c2 = gf.Component()
+    ref = c2 << c
+    ref.move((100, 100))
+
+    paths = c2.get_paths(layer=(1, 0), recursive=True)
+    assert len(paths) == 1
+    points = list(paths[0].each_point())
+    assert points[0].x == 100
+    assert points[0].y == 100
+    assert points[1].x == 110
+    assert points[1].y == 110
+
+
+def test_component_all_angle_flatten() -> None:
+    c = gf.ComponentAllAngle()
+    c.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(1, 0))
+
+    c2 = gf.ComponentAllAngle()
+    c2.add_polygon([(0, 0), (0, 5), (5, 5), (5, 0)], layer=(2, 0))
+
+    c3 = gf.ComponentAllAngle()
+    c3 << c
+    c3 << c2
+
+    c3.flatten()
+
+    assert len(list(c.shapes(get_layer((1, 0))))) == 1
+    assert len(list(c2.shapes(get_layer((2, 0))))) == 1
+
+    assert len(list(c3.shapes(get_layer((1, 0))))) == 1
+    assert len(list(c3.shapes(get_layer((2, 0))))) == 1
+
+    poly1 = next(iter(c3.shapes(get_layer((1, 0)))))
+    poly2 = next(iter(c3.shapes(get_layer((2, 0)))))
+
+    assert poly1.bbox().left == 0
+    assert poly1.bbox().right == 10
+    assert poly2.bbox().left == 0
+    assert poly2.bbox().right == 5
+
+
+def test_component_all_angle_add_polygon_get_polygon() -> None:
+    c = gf.ComponentAllAngle()
+    c.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(1, 0))
+    c.add_polygon([(5, 5), (5, 15), (15, 15), (15, 5)], layer=(2, 0))
+
+    polygons = c.get_polygons(layer=(1, 0))
+    assert len(polygons) == 1
+    assert polygons[0].bbox() == kdb.DBox(0, 0, 10, 10)
+
+    polygons = c.get_polygons(layer=(2, 0))
+    assert len(polygons) == 1
+    assert polygons[0].bbox() == kdb.DBox(5, 5, 15, 15)
+
+
+def test_component_add_ref_raises() -> None:
+    c = gf.Component()
+    c2 = gf.Component()
+
+    with pytest.raises(ValueError):
+        c.add_ref(c2, rows=2, row_pitch=0)
+
+    with pytest.raises(ValueError):
+        c.add_ref(c2, columns=2, column_pitch=0)
+
+
+def test_component_absorb() -> None:
+    c = gf.Component()
+    c2 = gf.Component()
+    c2.add_polygon([(0, 0), (0, 10), (10, 10), (10, 0)], layer=(1, 0))
+
+    ref = c.add_ref(c2)
+
+    c.absorb(ref)
+    assert len(list(c.insts)) == 0
+    assert len(list(c.shapes(get_layer((1, 0))))) == 1
+
+
+if __name__ == "__main__":
+    test_locked_cell()