diff --git a/gplugins/common/utils/geometry.py b/gplugins/common/utils/geometry.py new file mode 100644 index 00000000..26e0312d --- /dev/null +++ b/gplugins/common/utils/geometry.py @@ -0,0 +1,31 @@ +from typing import List +import gdsfactory as gf +import kfactory as kf +from shapely.geometry import Polygon, MultiPolygon + +def region_to_shapely_polygons(region: kf.kdb.Region) -> MultiPolygon: + """Convert a kfactory Region to a list of Shapely polygons.""" + polygons = [] + for polygon_kdb in region.each(): + exterior_coords = [ + (gf.kcl.to_um(point.x), gf.kcl.to_um(point.y)) + for point in polygon_kdb.each_point_hull() + ] + # Extract hole coordinates + holes = [] + num_holes = polygon_kdb.holes() + for hole_idx in range(num_holes): + hole_coords = [] + for point in polygon_kdb.each_point_hole(hole_idx): + hole_coords.append((gf.kcl.to_um(point.x), gf.kcl.to_um(point.y))) + holes.append(hole_coords) + + + # Create Shapely polygon + if holes: + polygon = Polygon(exterior_coords, holes) + else: + polygon = Polygon(exterior_coords) + polygons.append(polygon) + + return MultiPolygon(polygons) diff --git a/gplugins/meshwell/get_meshwell_3D.py b/gplugins/meshwell/get_meshwell_3D.py index aa3b71df..a69b92e6 100644 --- a/gplugins/meshwell/get_meshwell_3D.py +++ b/gplugins/meshwell/get_meshwell_3D.py @@ -1,41 +1,13 @@ import gdsfactory as gf from meshwell.polyprism import PolyPrism -from typing import List, Dict +from typing import List, Dict, Literal from shapely.geometry import Polygon, MultiPolygon import math import kfactory as kf from gdsfactory.add_padding import add_padding_container, add_padding from functools import partial from gdsfactory.generic_tech.layer_map import LAYER -from typing import Literal - - -def region_to_shapely_polygons(region: kf.kdb.Region) -> List[Polygon]: - """Convert a kfactory Region to a list of Shapely polygons.""" - polygons = [] - for polygon_kdb in region.each(): - # Extract exterior coordinates - exterior_coords = [] - for point in polygon_kdb.each_point_hull(): - exterior_coords.append((gf.kcl.to_um(point.x), gf.kcl.to_um(point.y))) - - # Extract hole coordinates - holes = [] - for hole_idx in range(polygon_kdb.holes()): - hole_coords = [] - hole = polygon_kdb.hole(hole_idx) - for point in hole.each_point(): - hole_coords.append((gf.kcl.to_um(point.x), gf.kcl.to_um(point.y))) - holes.append(hole_coords) - - # Create Shapely polygon - if holes: - polygon = Polygon(exterior_coords, holes) - else: - polygon = Polygon(exterior_coords) - polygons.append(polygon) - - return MultiPolygon(polygons) +from gplugins.common.utils.geometry import region_to_shapely_polygons def build_buffer_dict_from_layer_level( @@ -132,25 +104,27 @@ def get_meshwell_prisms( return prisms - if __name__ == "__main__": - from gdsfactory.components import ge_detector_straight_si_contacts + from gdsfactory.components import ge_detector_straight_si_contacts, add_frame from gdsfactory.generic_tech.layer_stack import get_layer_stack from gdsfactory.generic_tech.layer_map import LAYER from meshwell.cad import cad from meshwell.mesh import mesh - prisms = get_meshwell_prisms( - component=ge_detector_straight_si_contacts(), - layer_stack=get_layer_stack(sidewall_angle_wg=0), - name_by="layer", - ) - - cad(entities_list=prisms, output_file="meshwell_prisms_3D.xao") - mesh( - input_file="meshwell_prisms_3D.xao", - output_file="meshwell_prisms_3D.msh", - default_characteristic_length=1000, - dim=3, - verbosity=10, - ) + + for component in [ge_detector_straight_si_contacts, add_frame]: + c = component() + prisms = get_meshwell_prisms( + component=c, + layer_stack=get_layer_stack(sidewall_angle_wg=0), + name_by="layer", + ) + + cad(entities_list=prisms, output_file=f"meshwell_prisms_3D_{c.name}.xao") + mesh( + input_file=f"meshwell_prisms_3D_{c.name}.xao", + output_file=f"meshwell_prisms_3D_{c.name}.msh", + default_characteristic_length=1000, + dim=3, + verbosity=10, + ) diff --git a/gplugins/meshwell/get_meshwell_cross_section.py b/gplugins/meshwell/get_meshwell_cross_section.py index 24efbdac..af54b900 100644 --- a/gplugins/meshwell/get_meshwell_cross_section.py +++ b/gplugins/meshwell/get_meshwell_cross_section.py @@ -11,125 +11,7 @@ from gdsfactory.generic_tech.layer_map import LAYER from typing import Literal import numpy as np - - -def region_to_shapely_polygons(region: kf.kdb.Region) -> List[Polygon]: - """Convert a kfactory Region to a list of Shapely polygons.""" - polygons = [] - for polygon_kdb in region.each(): - # Extract exterior coordinates - exterior_coords = [] - for point in polygon_kdb.each_point_hull(): - exterior_coords.append((gf.kcl.to_um(point.x), gf.kcl.to_um(point.y))) - - # Extract hole coordinates - holes = [] - for hole_idx in range(polygon_kdb.holes()): - hole_coords = [] - hole = polygon_kdb.hole(hole_idx) - for point in hole.each_point(): - hole_coords.append((gf.kcl.to_um(point.x), gf.kcl.to_um(point.y))) - holes.append(hole_coords) - - # Create Shapely polygon - if holes: - polygon = Polygon(exterior_coords, holes) - else: - polygon = Polygon(exterior_coords) - polygons.append(polygon) - - return MultiPolygon(polygons) - - -def build_buffer_dict_from_layer_level(layer_level: gf.technology.LayerLevel) -> Dict[float, float]: - """Build buffer dictionary from LayerLevel properties.""" - zmin = layer_level.zmin - zmax = zmin + layer_level.thickness - - # Priority 1: z_to_bias if available - if layer_level.z_to_bias is not None: - z_values, bias_values = layer_level.z_to_bias - return dict(zip(z_values, bias_values)) - - # Priority 2: Handle sidewall angle - if layer_level.sidewall_angle != 0.0: - angle_rad = math.radians(layer_level.sidewall_angle) - height = layer_level.thickness - width_to_z = layer_level.width_to_z - - # Calculate buffer change due to sidewall angle - # Positive angle means outward sloping, negative means inward - buffer_change = height * math.tan(angle_rad) - - if width_to_z == 0.0: # Reference at bottom - bottom_buffer = 0.0 - top_buffer = buffer_change - elif width_to_z == 1.0: # Reference at top - bottom_buffer = -buffer_change - top_buffer = 0.0 - else: # Reference somewhere in middle - ref_height = height * width_to_z - bottom_buffer = -ref_height * math.tan(angle_rad) - top_buffer = (height - ref_height) * math.tan(angle_rad) - - return {zmin: bottom_buffer, zmax: top_buffer} - - # Default: Simple extrusion - return {zmin: 0.0, zmax: 0.0} - - -def get_meshwell_prisms( - component: gf.Component, - layer_stack: gf.technology.LayerStack, - wafer_layer: gf.typings.Layer | None = LAYER.WAFER, - wafer_padding: float | None = 0.0, - name_by: Literal["layer", "material"] = "layer" -) -> List[PolyPrism]: - """Convert LayerStack + Component to meshwell PolyPrism objects.""" - prisms = [] - - if wafer_padding is not None and wafer_layer is not None: - component = add_padding_container(component=component, function=partial(add_padding, layers=(wafer_layer,), default=wafer_padding)) - - # Iterate through each layer in the stack - for layer_name, layer_level in layer_stack.layers.items(): - - # Get shapes for this layer from the component - region = layer_level.layer.get_shapes(component) - - # Skip if no shapes found - if region.is_empty(): - continue - - # Convert kfactory Region to Shapely polygons - shapely_polygons = region_to_shapely_polygons(region) - - # Skip if no valid polygons - if not shapely_polygons: - continue - - # Build buffer dictionary from layer level properties - buffers = build_buffer_dict_from_layer_level(layer_level) - - # Create PolyPrism object - if name_by == "layer": - physical_name = layer_name - elif name_by == "material": - physical_name = layer_level.material - else: - raise ValueError("name_by must be 'layer' or 'material'") - prism = PolyPrism( - polygons=shapely_polygons, - buffers=buffers, - physical_name=physical_name, - mesh_order=layer_level.mesh_order, - mesh_bool=True, - additive=False - ) - - prisms.append(prism) - - return prisms +from gplugins.common.utils.geometry import region_to_shapely_polygons def get_u_bounds_polygons( diff --git a/gplugins/meshwell/tests/test_meshwell.py b/gplugins/meshwell/tests/test_meshwell.py new file mode 100644 index 00000000..5134fe3a --- /dev/null +++ b/gplugins/meshwell/tests/test_meshwell.py @@ -0,0 +1,69 @@ +import pytest +import gdsfactory as gf + + +from gdsfactory.components import bend_circular, add_frame +from gdsfactory.generic_tech.layer_stack import get_layer_stack +from meshwell.cad import cad +from meshwell.mesh import mesh +from pathlib import Path +from tempfile import TemporaryDirectory +from gplugins.meshwell import ( + get_meshwell_prisms, get_meshwell_cross_section +) +from shapely.geometry import LineString + +@pytest.mark.parametrize("component", [(bend_circular), (add_frame)]) +def test_prisms(component) -> None: + prisms = get_meshwell_prisms( + component=component(), + layer_stack=get_layer_stack(sidewall_angle_wg=0), + name_by="layer", + ) + + with TemporaryDirectory() as tmp_dir: + xao_file = Path(tmp_dir) / "meshwell_prisms_3D.xao" + msh_file = Path(tmp_dir) / "meshwell_prisms_3D.msh" + cad(entities_list=prisms, output_file=xao_file) + mesh( + input_file=xao_file, + output_file=msh_file, + default_characteristic_length=1000, + dim=3, + verbosity=10, + ) + + +def test_prisms_empty_component() -> None: + """Test that get_meshwell_prisms handles empty components gracefully.""" + c = gf.Component() + prisms = get_meshwell_prisms( + component=c, + layer_stack=get_layer_stack(sidewall_angle_wg=0), + name_by="layer", + wafer_padding=None, + ) + assert len(prisms) == 0 + + +@pytest.mark.parametrize("component", [(bend_circular), (add_frame)]) +def test_cross_section(component) -> None: + cross_section_line = LineString([(4, -15), (4, 15)]) + surfaces = get_meshwell_cross_section( + component=component(), + line=cross_section_line, + layer_stack=get_layer_stack(sidewall_angle_wg=0), + name_by="layer", + ) + + with TemporaryDirectory() as tmp_dir: + xao_file = Path(tmp_dir) / "meshwell_prisms_3D.xao" + msh_file = Path(tmp_dir) / "meshwell_prisms_3D.msh" + cad(entities_list=surfaces, output_file=xao_file) + mesh( + input_file=xao_file, + output_file=msh_file, + default_characteristic_length=1000, + dim=2, + verbosity=10, + ) diff --git a/gplugins/palace/get_capacitance.py b/gplugins/palace/get_capacitance.py index 317066c5..467af676 100644 --- a/gplugins/palace/get_capacitance.py +++ b/gplugins/palace/get_capacitance.py @@ -246,6 +246,19 @@ def run_capacitive_simulation_palace( .. _Palace: https://github.com/awslabs/palace """ + if not isinstance(n_processes, int): + raise TypeError(f"n_processes must be an integer, got {type(n_processes)}") + if n_processes < 1: + raise ValueError(f"n_processes must be >= 1, got {n_processes}") + + if solver_config: + order = solver_config.get("Order") + if order is not None: + if not isinstance(order, int): + raise TypeError(f"Solver Order must be an integer, got {type(order)}") + if order < 1: + raise ValueError(f"Solver Order must be >= 1, got {order}") + if layer_stack is None: layer_stack = LayerStack( layers={ diff --git a/gplugins/palace/tests/test_palace.py b/gplugins/palace/tests/test_palace.py index ff10e72e..702378a9 100644 --- a/gplugins/palace/tests/test_palace.py +++ b/gplugins/palace/tests/test_palace.py @@ -63,17 +63,17 @@ def get_reasonable_mesh_parameters_capacitance(c: Component): # port_names=[port.name for port in c.ports], default_characteristic_length=50, resolution_specs={ - "bw": [ConstantInField(resolution=10, apply_to="surfaces")], + "bw": [ConstantInField(resolution=100, apply_to="surfaces")], "substrate": [ - ConstantInField(resolution=20, apply_to="curves"), - ConstantInField(resolution=15, apply_to="surfaces"), - ConstantInField(resolution=30, apply_to="volumes"), + ConstantInField(resolution=200, apply_to="curves"), + ConstantInField(resolution=150, apply_to="surfaces"), + ConstantInField(resolution=300, apply_to="volumes"), ], - "vacuum": [ConstantInField(resolution=20, apply_to="surfaces")], + "vacuum": [ConstantInField(resolution=200, apply_to="surfaces")], **{ f"bw@{port.name}___substrate": [ ThresholdField( - sizemin=2, distmin=4, distmax=30, sizemax=10, apply_to="curves" + sizemin=20, distmin=4, distmax=30, sizemax=100, apply_to="curves" ) ] # Older style: @@ -90,40 +90,69 @@ def get_reasonable_mesh_parameters_capacitance(c: Component): ) -def test_palace_capacitance_simulation_runs(geometry) -> None: - c = geometry - run_capacitive_simulation_palace( - c, +def test_palace_capacitance_simulation_runs(geometry, tmp_path) -> None: + results = run_capacitive_simulation_palace( + geometry, layer_stack=layer_stack, material_spec=material_spec, - mesh_parameters=get_reasonable_mesh_parameters_capacitance(c), + mesh_parameters=get_reasonable_mesh_parameters_capacitance(geometry), + simulation_folder=tmp_path, ) + assert results.capacitance_matrix + assert results.mesh_location + assert results.field_file_location @pytest.mark.parametrize("n_processes", [(1), (2), (4)]) def test_palace_capacitance_simulation_n_processes(geometry, n_processes) -> None: - c = geometry run_capacitive_simulation_palace( - c, + geometry, layer_stack=layer_stack, material_spec=material_spec, n_processes=n_processes, - mesh_parameters=get_reasonable_mesh_parameters_capacitance(c), + mesh_parameters=get_reasonable_mesh_parameters_capacitance(geometry), ) +@pytest.mark.parametrize("invalid_n_processes", [0, -1, -5, 1.5, "two", None]) +def test_palace_capacitance_simulation_invalid_n_processes( + geometry, invalid_n_processes +) -> None: + with pytest.raises((ValueError, TypeError)): + run_capacitive_simulation_palace( + geometry, + layer_stack=layer_stack, + material_spec=material_spec, + mesh_parameters=get_reasonable_mesh_parameters_capacitance(geometry), + n_processes=invalid_n_processes, + ) + + @pytest.mark.parametrize("element_order", [(1), (2), (3)]) def test_palace_capacitance_simulation_element_order(geometry, element_order) -> None: - c = geometry run_capacitive_simulation_palace( - c, + geometry, layer_stack=layer_stack, material_spec=material_spec, solver_config={"Order": element_order}, - mesh_parameters=get_reasonable_mesh_parameters_capacitance(c), + mesh_parameters=get_reasonable_mesh_parameters_capacitance(geometry), ) +@pytest.mark.parametrize("invalid_element_order", [0, -1, 1.5, "two"]) +def test_palace_capacitance_simulation_invalid_element_order( + geometry, invalid_element_order +) -> None: + with pytest.raises((ValueError, TypeError)): + run_capacitive_simulation_palace( + geometry, + layer_stack=layer_stack, + material_spec=material_spec, + solver_config={"Order": invalid_element_order}, + mesh_parameters=get_reasonable_mesh_parameters_capacitance(geometry), + ) + + @pytest.mark.skip(reason="TODO") def test_palace_capacitance_simulation_mesh_size_field(geometry) -> None: pass diff --git a/uv.lock b/uv.lock index 739a3839..27d4a2c2 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.14'", @@ -1341,7 +1341,7 @@ wheels = [ [[package]] name = "gplugins" -version = "1.4.2" +version = "2.0.0" source = { editable = "." } dependencies = [ { name = "gdsfactory" },