diff --git a/gplugins/klayout/netlist_spice_reader.py b/gplugins/klayout/netlist_spice_reader.py index 6d5c27de..4f982e19 100644 --- a/gplugins/klayout/netlist_spice_reader.py +++ b/gplugins/klayout/netlist_spice_reader.py @@ -58,13 +58,12 @@ def parse_element(self, s: str, element: str) -> kdb.ParseElementData: x_value, y_value = (int(e) / 1000 for e in location_matches.group(1, 2)) # Use default KLayout parser for rest of the SPICE - s, *_ = s.split("$") + s, *_ = s.split(" $") parsed = super().parse_element(s, element) parsed.parameters |= {"x": x_value, "y": y_value} # ensure uniqueness - parsed.model_name = parsed.model_name self.n_nodes += 1 return parsed @@ -72,6 +71,12 @@ def parse_element(self, s: str, element: str) -> kdb.ParseElementData: def hash_str_to_int(s: str) -> int: return int(hashlib.shake_128(s.encode()).hexdigest(4), 16) + def write_str_property_as_int(self, value: str) -> int: + """Store string property in hash map and return integer hash value.""" + hashed_value = CalibreSpiceReader.hash_str_to_int(value) + self.integer_to_string_map[hashed_value] = value + return hashed_value + @override def element( self, @@ -95,15 +100,8 @@ def element( if not clx: clx = kdb.DeviceClass() clx.name = model - for key, value in parameters.items(): + for key in parameters: clx.add_parameter(kdb.DeviceParameterDefinition(key)) - # map string variables to integers - if ( - isinstance(value, str) - and value not in self.integer_to_string_map.values() - ): - hashed_value = CalibreSpiceReader.hash_str_to_int(value) - self.integer_to_string_map[hashed_value] = value for i in range(len(nets)): clx.add_terminal(kdb.DeviceTerminalDefinition(str(i))) @@ -114,12 +112,17 @@ def element( device.connect_terminal(i, net) for key, value in parameters.items(): + # map string variables to integers + possibly_hashed_value = ( + self.write_str_property_as_int(value) + if isinstance(value, str) + else value + ) + device.set_parameter( key, ( - CalibreSpiceReader.hash_str_to_int(value) - if isinstance(value, str) - else value + possibly_hashed_value or 0 # default to 0 for None in order to still have the parameter field ), ) diff --git a/gplugins/klayout/plot_nets.py b/gplugins/klayout/plot_nets.py index 12d9e742..ef622bfd 100644 --- a/gplugins/klayout/plot_nets.py +++ b/gplugins/klayout/plot_nets.py @@ -33,7 +33,12 @@ def plot_nets( spice_reader: The KLayout Spice reader to use for parsing SPICE netlists. """ - G_connectivity = networkx_from_spice(**locals()) + G_connectivity = networkx_from_spice( + filepath=filepath, + include_labels=include_labels, + top_cell=top_cell, + spice_reader=spice_reader, + ) if nodes_to_reduce: diff --git a/gplugins/path_length_analysis/path_length_analysis.py b/gplugins/path_length_analysis/path_length_analysis.py index 0566372e..9dfcc447 100644 --- a/gplugins/path_length_analysis/path_length_analysis.py +++ b/gplugins/path_length_analysis/path_length_analysis.py @@ -24,6 +24,7 @@ from bokeh.palettes import Category10, Spectral4 from bokeh.plotting import figure, from_networkx from gdsfactory.component import Component, ComponentReference +from gdsfactory.get_netlist import get_netlist, get_netlist_recursive DEFAULT_CS_COLORS = { "xs_rc": "red", @@ -95,6 +96,7 @@ def report_pathlengths( result_dir: str | Path, visualize: bool = False, component_connectivity=None, + netlist=None, ) -> None: """Reports pathlengths for a given PIC. @@ -103,11 +105,15 @@ def report_pathlengths( result_dir: the directory to write the pathlength table to. visualize: whether to visualize the pathlength graph. component_connectivity: a dictionary of component connectivity information. + netlist: a netlist dictionary. If None, will be generated from the pic. """ print(f"Reporting pathlengths for {pic.name}...") pathlength_graph = get_edge_based_route_attr_graph( - pic, recursive=True, component_connectivity=component_connectivity + pic, + recursive=True, + component_connectivity=component_connectivity, + netlist=netlist, ) route_records = get_paths(pathlength_graph) @@ -332,7 +338,10 @@ def _get_edge_based_route_attr_graph( def get_edge_based_route_attr_graph( - pic: Component, recursive=False, component_connectivity=None + pic: Component, + recursive=False, + component_connectivity=None, + netlist: dict[str, Any] | None = None, ) -> nx.Graph: """ Gets a connectivity graph for the circuit, with all path attributes on edges and ports as nodes. @@ -342,17 +351,19 @@ def get_edge_based_route_attr_graph( recursive: True to expand all hierarchy. False to only report top-level connectivity. component_connectivity: a function to report connectivity for base components.\ None to treat as black boxes with no internal connectivity. + netlist: a netlist dictionary. If None, will be generated from the pic. Returns: A NetworkX Graph """ - from gdsfactory.get_netlist import get_netlist, get_netlist_recursive - - if recursive: - netlists = get_netlist_recursive(pic, component_suffix="") - netlist = netlists[pic.name] + if netlist is None: + if recursive: + netlists = get_netlist_recursive(pic, component_suffix="") + netlist = netlists[pic.name] + else: + netlist = get_netlist(pic) + netlists = None else: - netlist = get_netlist(pic) netlists = None return _get_edge_based_route_attr_graph( diff --git a/gplugins/path_length_analysis/path_length_analysis_from_gds.py b/gplugins/path_length_analysis/path_length_analysis_from_gds.py new file mode 100644 index 00000000..a89af573 --- /dev/null +++ b/gplugins/path_length_analysis/path_length_analysis_from_gds.py @@ -0,0 +1,164 @@ +from collections.abc import Callable +from functools import partial + +import gdsfactory as gf +import matplotlib.pyplot as plt +import numpy as np +from scipy.signal import savgol_filter + +filter_savgol_filter = partial(savgol_filter, window_length=11, polyorder=3, axis=0) + + +def extract_path( + component: gf.Component, + layer: gf.typings.LayerSpec = (1, 0), + plot: bool = False, + filter_function: Callable = None, + under_sampling: int = 1, +) -> gf.Path: + """Extracts the centerline of a component from a GDS file. + + Args: + component: GDS component. + layer: GDS layer to extract the centerline from. + plot: Plot the centerline. + filter_function: optional Function to filter the centerline. + under_sampling: under sampling factor. + """ + points = component.get_polygons(by_spec=layer)[0] + + # Assume the points are ordered and the first half is the outer curve, the second half is the inner curve + # This assumption might need to be adjusted based on your specific geometry + mid_index = len(points) // 2 + outer_points = points[:mid_index] + inner_points = points[mid_index:] + inner_points = inner_points[::-1] + + inner_points = inner_points[::under_sampling] + outer_points = outer_points[::under_sampling] + + centerline = np.mean([outer_points, inner_points], axis=0) + + if filter_function is not None: + centerline = filter_function(centerline) + + p = gf.Path(centerline) + if plot: + plt.figure() + plt.plot(outer_points[:, 0], outer_points[:, 1], "o", label="Outer Points") + plt.plot(inner_points[:, 0], inner_points[:, 1], "o", label="Inner Points") + plt.plot(centerline[:, 0], centerline[:, 1], "k--", label="Centerline") + plt.legend() + plt.title("Curve with Spline Interpolation for Inner and Outer Edges") + plt.xlabel("X-coordinate") + plt.ylabel("Y-coordinate") + plt.grid(True) + plt.show() + return p + + +def get_min_radius_and_length(path: gf.Path) -> tuple[float, float]: + """Get the minimum radius of curvature and the length of a path.""" + _, K = path.curvature() + radius = 1 / K + min_radius = np.min(np.abs(radius)) + return min_radius, path.length() + + +def plot_curvature(path: gf.Path, rmax: int | float = 200) -> None: + """Plot the curvature of a path. + + Args: + path: Path object. + rmax: Maximum radius of curvature to plot. + """ + s, K = path.curvature() + radius = 1 / K + valid_indices = (radius > -rmax) & (radius < rmax) + radius2 = radius[valid_indices] + s2 = s[valid_indices] + + plt.figure(figsize=(10, 5)) + plt.plot(s2, radius2, ".-") + plt.xlabel("Position along curve (arc length)") + plt.ylabel("Radius of curvature") + plt.show() + + +def plot_radius(path: gf.Path, rmax: float = 200) -> plt.Figure: + """Plot the radius of curvature of a path. + + Args: + path: Path object. + rmax: Maximum radius of curvature to plot. + """ + s, K = path.curvature() + radius = 1 / K + valid_indices = (radius > -rmax) & (radius < rmax) + radius2 = radius[valid_indices] + s2 = s[valid_indices] + + fig, ax = plt.subplots(1, 1, figsize=(15, 5)) + ax.plot(s2, radius2, ".-") + ax.set_xlabel("Position along curve (arc length)") + ax.set_ylabel("Radius of curvature") + ax.grid(True) + return fig + + +def _demo_routes(): + ys_right = [0, 10, 20, 40, 50, 80] + pitch = 127.0 + N = len(ys_right) + ys_left = [(i - N / 2) * pitch for i in range(N)] + layer = (1, 0) + + right_ports = [ + gf.Port( + f"R_{i}", center=(0, ys_right[i]), width=0.5, orientation=180, layer=layer + ) + for i in range(N) + ] + left_ports = [ + gf.Port( + f"L_{i}", center=(-200, ys_left[i]), width=0.5, orientation=0, layer=layer + ) + for i in range(N) + ] + + # you can also mess up the port order and it will sort them by default + left_ports.reverse() + + c = gf.Component(name="connect_bundle_v2") + routes = gf.routing.get_bundle( + left_ports, + right_ports, + sort_ports=True, + start_straight_length=100, + enforce_port_ordering=False, + ) + for route in routes: + c.add(route.references) + return c + + +if __name__ == "__main__": + # c0 = gf.components.bend_euler(npoints=20) + # c0 = gf.components.bend_euler(cross_section="xs_sc", with_arc_floorplan=True) + # c0 = gf.components.bend_circular() + # c0 = gf.components.bend_s(npoints=7) + # c0 = gf.components.coupler() + c0 = _demo_routes() + + gdspath = c0.write_gds() + n = c0.get_netlist() + c0.show() + + c = gf.import_gds(gdspath) + # p = extract_path(c, plot=False, window_length=None, polyorder=None) + p = extract_path(c, plot=True, under_sampling=5) + min_radius, length = get_min_radius_and_length(p) + print(f"Minimum radius of curvature: {min_radius:.2f}") + print(f"Length: {length:.2f}") + print(c0.info) + plot_radius(p) diff --git a/notebooks/11_get_netlist_spice.ipynb b/notebooks/11_get_netlist_spice.ipynb index 39eef549..3f9f82a2 100644 --- a/notebooks/11_get_netlist_spice.ipynb +++ b/notebooks/11_get_netlist_spice.ipynb @@ -23,7 +23,9 @@ "\n", "from gplugins.klayout.get_netlist import get_l2n, get_netlist\n", "from gplugins.klayout.plot_nets import plot_nets\n", - "from gplugins.klayout.netlist_graph import GdsfactorySpiceReader" + "from gplugins.klayout.netlist_spice_reader import (\n", + " GdsfactorySpiceReader,\n", + ")\n" ] }, { @@ -45,6 +47,7 @@ "source": [ "c = pads_correct()\n", "gdspath = c.write_gds()\n", + "c.show()\n", "c.plot()" ] }, diff --git a/notebooks/path_length_analysis.ipynb b/notebooks/path_length_analysis.ipynb index 77f86ebe..641de323 100644 --- a/notebooks/path_length_analysis.ipynb +++ b/notebooks/path_length_analysis.ipynb @@ -103,6 +103,108 @@ "cell_type": "markdown", "id": "6", "metadata": {}, + "source": [ + "## Extract pathlength and curvature from component\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "import gplugins.path_length_analysis.path_length_analysis_from_gds as pl\n", + "import gdsfactory as gf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "c0 = gf.components.bend_circular()\n", + "c0.plot()\n", + "gdspath = c0.write_gds()\n", + "n = c0.get_netlist()\n", + "\n", + "p = pl.extract_path(c0, plot=True)\n", + "min_radius, length = pl.get_min_radius_and_length(p)\n", + "print(f\"Minimum radius of curvature: {min_radius:.2f}\")\n", + "print(f\"Length: {length:.2f}\")\n", + "print(c0.info)\n", + "pl.plot_radius(p)" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "## Extract pathlength and curvature from GDS\n", + "\n", + "To extract from GDS you need to save the netlist." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "c0 = gf.components.bend_circular()\n", + "c0.plot()\n", + "gdspath = c0.write_gds()\n", + "n = c0.get_netlist()\n", + "c = gf.import_gds(gdspath) # import the GDS file\n", + "\n", + "p = pl.extract_path(c, plot=True)\n", + "min_radius, length = pl.get_min_radius_and_length(p)\n", + "print(f\"Minimum radius of curvature: {min_radius:.2f}\")\n", + "print(f\"Length: {length:.2f}\")\n", + "print(c0.info)\n", + "pl.plot_radius(p)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "p = pl.extract_path(c, plot=False, under_sampling=2)\n", + "min_radius, length = pl.get_min_radius_and_length(p)\n", + "print(f\"Minimum radius of curvature: {min_radius:.2f}\")\n", + "print(f\"Length: {length:.2f}\")\n", + "print(c0.info)\n", + "pl.plot_radius(p)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "p = pl.extract_path(c, plot=False, under_sampling=5)\n", + "min_radius, length = pl.get_min_radius_and_length(p)\n", + "print(f\"Minimum radius of curvature: {min_radius:.2f}\")\n", + "print(f\"Length: {length:.2f}\")\n", + "print(c0.info)\n", + "pl.plot_radius(p)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], "source": [] } ],