From ea9b7c7f5c5162faf38d433c9a9bf278d8e26a05 Mon Sep 17 00:00:00 2001 From: Niyaz Beysengulov Date: Fri, 13 Feb 2026 17:48:35 -0600 Subject: [PATCH 1/9] init thoughts on greens function calculations --- zeroheliumkit/fem/freefemer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zeroheliumkit/fem/freefemer.py b/zeroheliumkit/fem/freefemer.py index 22ad750..7dc5225 100755 --- a/zeroheliumkit/fem/freefemer.py +++ b/zeroheliumkit/fem/freefemer.py @@ -517,6 +517,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. From 6697d392840f64a9045233737be83ae67db32802 Mon Sep 17 00:00:00 2001 From: Niyaz Beysengulov Date: Fri, 13 Feb 2026 17:49:29 -0600 Subject: [PATCH 2/9] init thoughts on greens function calculations add --- zeroheliumkit/fem/freefemer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zeroheliumkit/fem/freefemer.py b/zeroheliumkit/fem/freefemer.py index 7dc5225..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. """ From 540eb5d7221a42a524b3160ed31ad19868cf68fe Mon Sep 17 00:00:00 2001 From: Niyaz Beysengulov Date: Wed, 4 Mar 2026 17:23:13 -0600 Subject: [PATCH 3/9] changed Structure.colors to being a class attribute, GeomCollection is a child of SuperStructure, small bug fix and code cleanup --- zeroheliumkit/src/__init__.py | 4 +-- zeroheliumkit/src/core.py | 61 +++++----------------------------- zeroheliumkit/src/supercore.py | 49 +++++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 57 deletions(-) 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/core.py b/zeroheliumkit/src/core.py index 4be9740..ed36b29 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.") @@ -514,15 +513,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 +580,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/supercore.py b/zeroheliumkit/src/supercore.py index 9c79902..f2f094e 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(): From 94e79112f47467d51a7a4f9b5c634ea391c803d3 Mon Sep 17 00:00:00 2001 From: Niyaz Date: Mon, 23 Mar 2026 10:24:32 -0500 Subject: [PATCH 4/9] small bug fix --- zeroheliumkit/helpers/resonator_calc.py | 6 +++--- zeroheliumkit/src/core.py | 2 +- zeroheliumkit/src/importing.py | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) 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/core.py b/zeroheliumkit/src/core.py index ed36b29..9ef6364 100755 --- a/zeroheliumkit/src/core.py +++ b/zeroheliumkit/src/core.py @@ -449,7 +449,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) diff --git a/zeroheliumkit/src/importing.py b/zeroheliumkit/src/importing.py index 4f8f9e4..c556a55 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): @@ -56,7 +57,7 @@ def preapre_gds(self) -> None: 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}) From 0f2f53421e6c8a29a85fc759810e7a96f20630dc Mon Sep 17 00:00:00 2001 From: Niyaz Beysengulov Date: Mon, 23 Mar 2026 14:17:42 -0500 Subject: [PATCH 5/9] small bug fix in continuouslinebuilder --- zeroheliumkit/src/supercore.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zeroheliumkit/src/supercore.py b/zeroheliumkit/src/supercore.py index f2f094e..a82824c 100755 --- a/zeroheliumkit/src/supercore.py +++ b/zeroheliumkit/src/supercore.py @@ -704,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 From 4be9ec71afbe18550a9650c7f2a77cfa2e364378 Mon Sep 17 00:00:00 2001 From: Niyaz Beysengulov Date: Tue, 24 Mar 2026 10:39:38 -0500 Subject: [PATCH 6/9] added 'ignore' arg to cut, crop, slice methods --- zeroheliumkit/src/core.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/zeroheliumkit/src/core.py b/zeroheliumkit/src/core.py index 9ef6364..0b8bb5e 100755 --- a/zeroheliumkit/src/core.py +++ b/zeroheliumkit/src/core.py @@ -152,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. @@ -163,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. @@ -178,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. @@ -191,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 From d9bf23a66bb58cd04d323db28c98d0aa6f133608 Mon Sep 17 00:00:00 2001 From: Niyaz Beysengulov Date: Thu, 18 Jun 2026 16:29:34 -0500 Subject: [PATCH 7/9] updated gds export, Layer.cut can handle Layer objects too --- zeroheliumkit/src/anchors.py | 8 ++++++-- zeroheliumkit/src/core.py | 4 ++-- zeroheliumkit/src/importing.py | 8 ++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/zeroheliumkit/src/anchors.py b/zeroheliumkit/src/anchors.py index 49585db..6387c02 100755 --- a/zeroheliumkit/src/anchors.py +++ b/zeroheliumkit/src/anchors.py @@ -18,6 +18,8 @@ import copy import numpy as np import matplotlib.pyplot as plt +from __future__ import annotations +from typing import Self from tabulate import tabulate from shapely import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection from shapely import (affinity, unary_union, @@ -1051,8 +1053,8 @@ def add(self, geom: Polygon | MultiPolygon) -> 'Layer': @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. @@ -1064,6 +1066,8 @@ def cut(self, 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 diff --git a/zeroheliumkit/src/core.py b/zeroheliumkit/src/core.py index 0b8bb5e..953665d 100755 --- a/zeroheliumkit/src/core.py +++ b/zeroheliumkit/src/core.py @@ -371,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. @@ -381,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() diff --git a/zeroheliumkit/src/importing.py b/zeroheliumkit/src/importing.py index c556a55..d82305f 100755 --- a/zeroheliumkit/src/importing.py +++ b/zeroheliumkit/src/importing.py @@ -37,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. @@ -52,7 +52,7 @@ 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(): From 55dbc98bdda098a850cb9a61a63e892ec5be5e8d Mon Sep 17 00:00:00 2001 From: Niyaz Beysengulov Date: Thu, 18 Jun 2026 17:09:16 -0500 Subject: [PATCH 8/9] bug fix --- zeroheliumkit/src/anchors.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/zeroheliumkit/src/anchors.py b/zeroheliumkit/src/anchors.py index 6387c02..af6802c 100755 --- a/zeroheliumkit/src/anchors.py +++ b/zeroheliumkit/src/anchors.py @@ -14,11 +14,12 @@ 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 __future__ import annotations + from typing import Self from tabulate import tabulate from shapely import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection @@ -1038,16 +1039,18 @@ 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]) @@ -1059,7 +1062,7 @@ def cut(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. @@ -1075,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)): From 7aa84c2672ba1f7f9fa1b7f421dbd31b73af2648 Mon Sep 17 00:00:00 2001 From: Niyaz Beysengulov Date: Mon, 22 Jun 2026 17:30:08 -0500 Subject: [PATCH 9/9] small changes --- zeroheliumkit/src/geometries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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