diff --git a/zeroheliumkit/fem/freefemer.py b/zeroheliumkit/fem/freefemer.py index 22ad750..9bac32d 100755 --- a/zeroheliumkit/fem/freefemer.py +++ b/zeroheliumkit/fem/freefemer.py @@ -338,6 +338,7 @@ def add_helium_curvature_edp(self, extract_cfg: ExtractConfig) -> str: def write_edpScript(self): + # TODO: split the logic for caseA (coupling constants only) and caseB (Greens function extraction) into separate functions for better readability and maintainability """ Creates the main FreeFEM script based on the configuration and physical surfaces. """ @@ -517,6 +518,10 @@ def script_problem_definition(self, electrode_name: str) -> str: return code + def script_include_charge(self, coordinate: list | tuple): + pass + + def script_save_data(self, config: dict) -> str: """ Generates a code block for extracting 2D slice data based on the provided configuration. diff --git a/zeroheliumkit/helpers/resonator_calc.py b/zeroheliumkit/helpers/resonator_calc.py index d90d5f6..587484f 100755 --- a/zeroheliumkit/helpers/resonator_calc.py +++ b/zeroheliumkit/helpers/resonator_calc.py @@ -1,6 +1,6 @@ import numpy as np -from math import pi, sinh +from math import pi, sinh, tanh from scipy.special import ellipk from tabulate import tabulate @@ -193,7 +193,7 @@ def resonator_frequency(self, "width, um": round(self.width/um, 2), "gap, um": round(self.gap/um, 2), "eps ": round(self.eps_substrate), - "eps_eff": round(self.eps_eff, 2), + "eps_eff": round(self.eps_eff2, 2), "impedance, Ohm": round(self.Z, 2), "L, nH/m": round(self.L * 1e9, 3), "C, pF/m":round(self.C* 1e12, 3) @@ -218,7 +218,7 @@ def resonator_length(self, "width, um": round(self.width/um, 2), "gap, um": round(self.gap/um, 2), "eps ": round(self.eps_substrate), - "eps_eff": round(self.eps_eff, 2), + "eps_eff": round(self.eps_eff2, 2), "impedance, Ohm": round(self.Z, 2), "L, nH/m": round(self.L * 1e9, 3), "C, pF/m":round(self.C* 1e12, 3) diff --git a/zeroheliumkit/src/__init__.py b/zeroheliumkit/src/__init__.py index b29618c..8c63db8 100755 --- a/zeroheliumkit/src/__init__.py +++ b/zeroheliumkit/src/__init__.py @@ -1,6 +1,6 @@ from .anchors import Anchor, MultiAnchor, Skeletone, Layer -from .core import Entity, Structure, GeomCollection -from .supercore import SuperStructure, ContinuousLineBuilder, RoutingConfig, ObjsAlongConfig +from .core import Entity, Structure +from .supercore import SuperStructure, ContinuousLineBuilder, RoutingConfig, ObjsAlongConfig, GeomCollection from .geometries import (StraightLine, ArbitraryLine, Taper, Fillet, MicroChannels, SpiralInductor, IDC, diff --git a/zeroheliumkit/src/anchors.py b/zeroheliumkit/src/anchors.py index 49585db..af6802c 100755 --- a/zeroheliumkit/src/anchors.py +++ b/zeroheliumkit/src/anchors.py @@ -14,10 +14,13 @@ Provides methods for creating and manipulating these paths. `Layer`: Represents a layer containing polygons with attributes such as name, color, and grid snapping. """ +from __future__ import annotations import copy import numpy as np import matplotlib.pyplot as plt + +from typing import Self from tabulate import tabulate from shapely import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection from shapely import (affinity, unary_union, @@ -1036,34 +1039,38 @@ def mirror( @snap_on_grid(attr="polygons") - def add(self, geom: Polygon | MultiPolygon) -> 'Layer': + def add(self, geom: Polygon | MultiPolygon | Layer) -> Self: """ Adds a polygon or multipolygon to the layer. Args: - geom (Polygon | MultiPolygon): The polygon or multipolygon to add. + geom (Polygon | MultiPolygon | Layer): The polygon or multipolygon to add. Returns: Updated instance (self) of the class with the added polygon. """ + if isinstance(geom, Layer): + geom = geom.polygons return unary_union([self.polygons, geom]) @snap_on_grid(attr="polygons") def cut(self, - geom: Polygon | MultiPolygon, - loc: tuple[float, float]=None) -> 'Layer': + geom: Polygon | MultiPolygon | Layer, + loc: tuple[float, float]=None) -> Self: """ Cuts the layer with a polygon or multipolygon. Args: - geom (Polygon | MultiPolygon): The polygon to be cut. + geom (Polygon | MultiPolygon | Layer): The polygon to be cut. loc (tuple[float, float], optional): The location where the polygon will be cut. Defaults to None. Returns: Updated instance (self) of the class with the cut geometry. """ + if isinstance(geom, Layer): + geom = geom.polygons cut_geom = affinity.translate(geom, xoff=loc[0], yoff=loc[1]) if loc else geom updated = self.polygons.difference(cut_geom) return updated @@ -1071,19 +1078,21 @@ def cut(self, @snap_on_grid(attr="polygons") def crop(self, - geom: Polygon | MultiPolygon, - loc: tuple[float, float] = None) -> 'Layer': + geom: Polygon | MultiPolygon | Layer, + loc: tuple[float, float] = None) -> Self: """ Crops the layer with a polygon or multipolygon. Args: - geom (Polygon | MultiPolygon): The polygon to be used for cropping. + geom (Polygon | MultiPolygon | Layer): The polygon to be used for cropping. loc (tuple[float, float], optional): The location where the polygon will be applied. Defaults to None. Returns: Updated instance (self) of the class with the cropped geometry. """ + if isinstance(geom, Layer): + geom = geom.polygons crop_geom = affinity.translate(geom, xoff = loc[0], yoff = loc[1]) if loc else geom updated = self.polygons.intersection(crop_geom) if isinstance(updated, (Point, MultiPoint, LineString, MultiLineString)): diff --git a/zeroheliumkit/src/core.py b/zeroheliumkit/src/core.py index 4be9740..953665d 100755 --- a/zeroheliumkit/src/core.py +++ b/zeroheliumkit/src/core.py @@ -13,8 +13,7 @@ import matplotlib.pyplot as plt from warnings import warn -from shapely import (Point, MultiPoint, LineString, MultiLineString, - Polygon, MultiPolygon, GeometryCollection) +from shapely import Point, LineString, Polygon, MultiPolygon from .plotting import interactive_widget_handler, listify_colors, ColorHandler from .importing import Exporter_DXF, Exporter_GDS, Exporter_Pickle @@ -53,7 +52,7 @@ def __init__(self): self.layers = [] self.skeletone = Skeletone() self.anchors = MultiAnchor() - self.colors = ColorHandler({}) + # self.colors = ColorHandler({}) self.errors = None @@ -94,7 +93,7 @@ def add(self, layer: Layer): Updated instance (self) of the class with the new layer added. """ self.layers.append(layer.name) - self.colors.add_color(layer.name, layer.color[0], layer.color[1]) + # self.colors.add_color(layer.name, layer.color[0], layer.color[1]) setattr(self, layer.name, layer) return self @@ -112,7 +111,7 @@ def remove(self, lname: str): if lname in self.layers: self.layers.remove(lname) delattr(self, lname) - self.colors.remove_color(lname) + # self.colors.remove_color(lname) else: print(f"Layer '{lname}' not found in layers.") @@ -134,7 +133,7 @@ def rename(self, old: str, new: str) -> None: self.__dict__[new] = self.__dict__.pop(old) self.layers[self.layers.index(old)] = new self.__dict__[new].name = new - self.colors.rename_color(old, new) + # self.colors.rename_color(old, new) else: print(f"Layer '{old}' not found in layers.") @@ -153,7 +152,7 @@ def has_layer(self, name: str) -> bool: return name in self.layers - def cut(self, geom: Polygon | MultiPolygon, loc: tuple[float, float]=None): + def cut(self, geom: Polygon | MultiPolygon, loc: tuple[float, float]=None, ignore: list[str]=[]): """ Cuts the specified polygon from polygons in all layers. @@ -164,11 +163,12 @@ def cut(self, geom: Polygon | MultiPolygon, loc: tuple[float, float]=None): Updated instance (self) of the class with the specified polygon cut from all layers. """ for lname in self.layers: - getattr(self, lname).cut(geom, loc) + if lname not in ignore: + getattr(self, lname).cut(geom, loc) return self - def crop(self, geom: Polygon | MultiPolygon, loc: tuple[float, float]=None): + def crop(self, geom: Polygon | MultiPolygon, loc: tuple[float, float]=None, ignore: list[str]=[]): """ Crops polygons in all layers. @@ -179,11 +179,12 @@ def crop(self, geom: Polygon | MultiPolygon, loc: tuple[float, float]=None): Updated instance (self) of the class with polygons in all layers cropped by the specified polygon. """ for lname in self.layers: - getattr(self, lname).crop(geom, loc) + if lname not in ignore: + getattr(self, lname).crop(geom, loc) return self - def slice(self, slice_line: LineString | list[LineString]): + def slice(self, slice_line: LineString | list[LineString], ignore: list[str]=[]): """ Slices polygons in a layer using a given line. @@ -192,7 +193,8 @@ def slice(self, slice_line: LineString | list[LineString]): slice_line (LineString): The line used for slicing. """ for lname in self.layers: - getattr(self, lname).slice(slice_line) + if lname not in ignore: + getattr(self, lname).slice(slice_line) return self @@ -369,7 +371,7 @@ def export_pickle(self, filename: str) -> None: exp.save() - def export_gds(self, filename: str, layer_cfg: dict) -> None: + def export_gds(self, filename: str, layer_cfg: dict, cellname: str="toplevel") -> None: """ Exports all layers as a GDS file. @@ -379,7 +381,7 @@ def export_gds(self, filename: str, layer_cfg: dict) -> None: See `gdspy docs `_ for 'datatype' details. """ zhkdict = self.export_dict(remove_holes=True) - exp = Exporter_GDS(filename, zhkdict, layer_cfg) + exp = Exporter_GDS(filename, zhkdict, layer_cfg, cellname) exp.save() @@ -450,7 +452,7 @@ def quickplot( #plot layers plot_config = {k:v for k,v in plot_config.items() if k not in off} for lname, lcolor in plot_config.items(): - if self.has_layer(lname): + if self.has_layer(lname) and (not getattr(self, lname).is_empty): getattr(self, lname).color = lcolor getattr(self, lname).plot(ax=ax, show_idx=show_idx, labels=labels, **kwargs) @@ -514,15 +516,15 @@ def append(self, Defaults to None. """ s = structure.copy() - if move_s: - s.move(*move_s) if rotate_s: s.rotate(rotate_s, origin=(0,0)) + if move_s: + s.move(*move_s) attr_list_device = self.layers attr_list_structure = s.layers self.layers = list(set(attr_list_device + attr_list_structure)) - self.colors.colors = self.colors.colors | s.colors.colors + # self.colors.colors = self.colors.colors | s.colors.colors # snapping direction if direction_snap: @@ -581,47 +583,3 @@ def return_mirrored(self, aroundaxis: str, **kwargs) -> 'Structure': """ cc = self.copy() return cc.mirror(aroundaxis, **kwargs) - - -class GeomCollection(Structure): - """ - Represents a collection of geometries. - Class attributes are created by layers dictionary. - - Args: - layers (dict): Dictionary containing the layers and corresponding polygons/skeletone/anchors/colors. - """ - def __init__(self, layers: dict=None): - super().__init__() - if layers: - for items in layers.items(): - match items: - case ("skeletone", LineString()) | ("skeletone", MultiLineString()): - self.skeletone.lines = items[1] - case ("skeletone", Skeletone()): - self.skeletone = items[1] - case ("skeletone", GeometryCollection()): - warn(message="imported skeletone contains GeometryCollection object. It will be ignored.") - case ("anchors", MultiAnchor()): - self.anchors = items[1] - case ("anchors", MultiPoint()): - for i, pt in enumerate(items[1].geoms): - self.anchors.add(Anchor(pt, 0, "anchor" + str(i))) - case ("colors", ColorHandler()): - self.colors = items[1] - case (str(), Polygon()) | (str(), MultiPolygon()): - layer = Layer(name=items[0], polygons=items[1]) - self.layers.append(items[0]) - setattr(self, items[0], layer) - case _: - self.layers.append(items[0]) - setattr(self, *items) - - if not hasattr(self, "anchors"): - self.anchors = MultiAnchor() - - if not hasattr(self, "skeletone"): - self.skeletone = Skeletone() - - if self.colors.is_empty: - self.colors.update_colors(self.layers) diff --git a/zeroheliumkit/src/geometries.py b/zeroheliumkit/src/geometries.py index d5619f2..95d05c0 100755 --- a/zeroheliumkit/src/geometries.py +++ b/zeroheliumkit/src/geometries.py @@ -502,7 +502,7 @@ def __init__(self, # create polygons if layers: for k, width in layers.items(): - polygon = self.skeletone.buffer(offset=width/2, cap_style='square', **kwargs) + polygon = self.skeletone.buffer(offset=width/2, cap_style=cap_style, **kwargs) self.add(Layer(name=k, polygons=polygon)) # create anchors diff --git a/zeroheliumkit/src/importing.py b/zeroheliumkit/src/importing.py index 4f8f9e4..d82305f 100755 --- a/zeroheliumkit/src/importing.py +++ b/zeroheliumkit/src/importing.py @@ -10,6 +10,7 @@ from svgpathtools import parse_path, Line, CubicBezier, QuadraticBezier from .errors import * +from .utils import to_geometry_list def sample_bezier(bezier, num_points=20): @@ -36,13 +37,13 @@ class Exporter_GDS(): __slots__ = "name", "zhk_layers", "gdsii", "layer_cfg" - def __init__(self, name: str, zhk_layers: dict, layer_cfg: dict) -> None: + def __init__(self, name: str, zhk_layers: dict, layer_cfg: dict, cellname: str="toplevel") -> None: self.name = name self.zhk_layers = zhk_layers self.layer_cfg = layer_cfg - self.preapre_gds() + self.preapre_gds(cellname) - def preapre_gds(self) -> None: + def preapre_gds(self, cellname: str="toplevel") -> None: """ Prepare the GDSII library by creating a top-level cell and adding polygons. @@ -51,12 +52,12 @@ def preapre_gds(self) -> None: - gdstk polygons use `layer` and `datatype` (same concepts). """ self.gdsii = gdstk.Library() - cell = gdstk.Cell("toplevel") + cell = gdstk.Cell(cellname) self.gdsii.add(cell) for lname, l_property in self.layer_cfg.items(): polygons = self.zhk_layers[lname].polygons - for poly in polygons.geoms: + for poly in to_geometry_list(polygons): points = list(poly.exterior.coords) # Optional: shapely exterior repeats the first point at the end. @@ -179,7 +180,7 @@ def preapre_dxf(self) -> None: for i, lname in enumerate(self.layer_cfg): self.dxf.layers.add(lname, color = i + 1) polygons = self.zhk_layers[lname].polygons - for poly in polygons.geoms: + for poly in to_geometry_list(polygons): points = list(poly.exterior.coords) msp.add_lwpolyline(points, dxfattribs={"layer": lname, "color": BYLAYER}) diff --git a/zeroheliumkit/src/supercore.py b/zeroheliumkit/src/supercore.py index 9c79902..a82824c 100755 --- a/zeroheliumkit/src/supercore.py +++ b/zeroheliumkit/src/supercore.py @@ -11,13 +11,15 @@ import numpy as np +from warnings import warn from dataclasses import dataclass from shapely import (line_locate_point, line_interpolate_point, intersection_all, distance) -from shapely import LineString, Point +from shapely import LineString, Point, Polygon, MultiLineString, MultiPolygon, GeometryCollection, MultiPoint from .anchors import Anchor, MultiAnchor, Skeletone, Layer from .core import Structure, Entity from .geometries import ArcLine +from .plotting import ColorHandler from .utils import (fmodnew, flatten_lines, to_geometry_list, round_corner, buffer_line_with_variable_width) from .functions import get_normals_along_line from .routing import create_route @@ -231,7 +233,7 @@ def route(self, # remove or not to remove anchors used for routing if rm_anchor==True: - self.anchors.remove(anchors) + self.anchors.remove(*anchors) elif isinstance(rm_anchor, (str, tuple)): self.anchors.remove(rm_anchor) @@ -385,6 +387,49 @@ def round_corner( return self +class GeomCollection(SuperStructure): + """ + Represents a collection of geometries. + Class attributes are created by layers dictionary. + + Args: + layers (dict): Dictionary containing the layers and corresponding polygons/skeletone/anchors/colors. + """ + def __init__(self, layers: dict=None): + super().__init__(route_config={"radius": 50, "num_segments": 13}) + if layers: + for items in layers.items(): + match items: + case ("skeletone", LineString()) | ("skeletone", MultiLineString()): + self.skeletone.lines = items[1] + case ("skeletone", Skeletone()): + self.skeletone = items[1] + case ("skeletone", GeometryCollection()): + warn(message="imported skeletone contains GeometryCollection object. It will be ignored.") + case ("anchors", MultiAnchor()): + self.anchors = items[1] + case ("anchors", MultiPoint()): + for i, pt in enumerate(items[1].geoms): + self.anchors.add(Anchor(pt, 0, "anchor" + str(i))) + case ("colors", ColorHandler()): + pass + # self.colors = items[1] + case (str(), Polygon()) | (str(), MultiPolygon()): + layer = Layer(name=items[0], polygons=items[1]) + self.layers.append(items[0]) + setattr(self, items[0], layer) + case _: + self.layers.append(items[0]) + setattr(self, *items) + + if not hasattr(self, "anchors"): + self.anchors = MultiAnchor() + + if not hasattr(self, "skeletone"): + self.skeletone = Skeletone() + + # if self.colors.is_empty: + # self.colors.update_colors(self.layers) class ContinuousLineBuilder(): @@ -659,6 +704,9 @@ def add_along_skeletone(self, **kwargs) -> 'ContinuousLineBuilder': else: locs = np.linspace(start_point, end_point, num=num, endpoint=True)[1:-1] + if locs.size == 0: + warn(message="No locations to add objects along the skeleton line. Check the spacing and endpoints settings.") + return self pts = line_interpolate_point(self.skeletone.lines, locs, normalized=True).tolist() normal_angles = get_normals_along_line(self.skeletone.lines, locs) # figure out why extra_rotation is added