diff --git a/.github/write_layer_stack.py b/.github/write_layer_stack.py new file mode 100644 index 0000000..82cbaad --- /dev/null +++ b/.github/write_layer_stack.py @@ -0,0 +1,569 @@ +"""Generate interactive layer_stack.md for cspdk. + +Reads layer_stack and cross_sections from each band PDK and writes +docs/layer_stack.md with interactive SVG/JS visualizations. +""" + +from __future__ import annotations + +import json +import math +from pathlib import Path + +from cspdk.si220.cband import PDK as PDK_SI220_CBAND +from cspdk.si220.cband.config import PATH +from cspdk.si220.oband import PDK as PDK_SI220_OBAND + +BANDS = [ + ("Si220 C-band", PDK_SI220_CBAND), + ("Si220 O-band", PDK_SI220_OBAND), +] + +_optional = [ + ("Si500", "cspdk.si500"), + ("SiN300", "cspdk.sin300"), + ("Si340", "cspdk.si340"), + ("SiN200", "cspdk.sin200"), + ("Ge-on-Si", "cspdk.ge_on_si"), + ("Si Suspended", "cspdk.si_sus"), +] + +for _label, _mod_path in _optional: + try: + _mod = __import__(_mod_path, fromlist=["PDK"]) + BANDS.append((_label, _mod.PDK)) + except ImportError: + pass + +# --------------------------------------------------------------------------- +# Colour palette +# --------------------------------------------------------------------------- + +MATERIAL_COLORS = { + "si": "#A0A0A0", "silicon": "#A0A0A0", + "sio2": "#FFF8DC", "oxide": "#D0D0D0", + "sin": "#8FBC8F", "sion": "#8FBC8F", + "ge": "#4682B4", "al": "#4CAF50", "cu": "#D0A050", + "w": "#9E9E9E", "tin": "#DDA0DD", "air": "#E8F4FD", + "nclad": "#FFF8DC", "nbox": "#FFF8DC", +} + +XS_MATERIAL_COLORS = { + "si": "#5B8CBE", "silicon": "#5B8CBE", + "sin": "#2E8B57", "sion": "#2E8B57", + "ge": "#4682B4", "al": "#CCC", "cu": "#D0A050", + "tin": "#CD853F", "w": "#9E9E9E", +} + + +def _color_for_layer(material): + mat = (material or "").lower().strip() + return MATERIAL_COLORS.get(mat, "#D0D0D0") + + +def _color_for_xs(material): + mat = (material or "").lower().strip() + return XS_MATERIAL_COLORS.get(mat, "#999") + + +def _infer_layer_type(material, name): + mat = (material or "").lower() + n = (name or "").lower() + if any(m in mat for m in ("al", "cu", "w", "tin", "nickel", "metal")): + return "conductor" + if "via" in n: + return "via" + if "sub" in n or "box" in n: + return "substrate" + return "dielectric" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _gds_layer_number(layer_attr): + if layer_attr is None: + return None + inner = getattr(layer_attr, "layer", layer_attr) + if isinstance(inner, (tuple, list)) and len(inner) >= 1: + try: + return int(inner[0]) + except (ValueError, TypeError): + return None + try: + return int(inner) + except (ValueError, TypeError): + return None + + +def _nice_step(range_val, target_ticks=10): + raw = range_val / target_ticks + mag = 10 ** math.floor(math.log10(raw)) + residual = raw / mag + if residual <= 1.5: + return mag + elif residual <= 3.5: + return 2 * mag + elif residual <= 7.5: + return 5 * mag + return 10 * mag + + +# --------------------------------------------------------------------------- +# Data extraction +# --------------------------------------------------------------------------- + + +def _extract_layers(layer_stack): + layers = [] + for name, level in layer_stack.layers.items(): + thickness = getattr(level, "thickness", 0) or 0 + zmin = getattr(level, "zmin", 0) or 0 + material = getattr(level, "material", "") or "" + gds = _gds_layer_number(getattr(level, "layer", None)) + layers.append({ + "name": name, "gds": gds, "material": material, + "type": _infer_layer_type(material, name), + "zmin": round(zmin, 4), "zmax": round(zmin + thickness, 4), + "thickness": round(thickness, 4), + "color": _color_for_layer(material), + }) + layers.sort(key=lambda l: l["zmin"]) + return layers + + +def _compute_layout(layers, svg_w=550, svg_h=750): + margin = {"top": 60, "bottom": 30, "left": 70, "right": 100} + plot_h = svg_h - margin["top"] - margin["bottom"] + + real = [l for l in layers if abs(l["thickness"]) > 1e-9] + if not real: + return layers, [] + + n = len(real) + row_h = plot_h / max(n, 1) + for i, l in enumerate(real): + l["uy"] = round(margin["top"] + (n - 1 - i) * row_h, 1) + l["uh"] = round(row_h * 0.93, 1) + + all_z = [] + for l in real: + all_z.extend([l["zmin"], l["zmax"]]) + z_min_g = min(all_z) + z_max_g = max(all_z) + z_range = z_max_g - z_min_g or 1.0 + + def z_to_y(z): + frac = (z - z_min_g) / z_range + return margin["top"] + plot_h - frac * plot_h + + for l in real: + overlapping = [ol for ol in real if + ol["zmin"] < l["zmax"] - 1e-6 and ol["zmax"] > l["zmin"] + 1e-6] + l["ncols"] = len(overlapping) + l["col"] = overlapping.index(l) if l in overlapping else 0 + sy_top = z_to_y(l["zmax"]) + sy_bot = z_to_y(l["zmin"]) + l["sy"] = round(sy_top, 1) + l["sh"] = round(max(sy_bot - sy_top, 2), 1) + + ticks = [] + step = _nice_step(z_range) + z = math.floor(z_min_g / step) * step + while z <= z_max_g + step * 0.5: + ticks.append({"z": round(z, 2), "py": round(z_to_y(z), 1)}) + z += step + + return real, ticks + + +def _extract_cross_sections(pdk, layer_stack): + xs_dict = getattr(pdk, "cross_sections", {}) + if not xs_dict: + return [] + + layer_z_by_gds = {} + layer_z_by_name = {} + for name, level in layer_stack.layers.items(): + thickness = getattr(level, "thickness", 0) or 0 + zmin = getattr(level, "zmin", 0) or 0 + material = getattr(level, "material", "") or "" + gds = _gds_layer_number(getattr(level, "layer", None)) + info = {"name": name, "zmin": zmin, "thickness": thickness, "material": material, "gds": gds} + if gds is not None: + layer_z_by_gds[gds] = info + layer_z_by_name[name] = info + + layer_name_to_gds = {} + layer_map = getattr(pdk, "layers", None) + if layer_map is not None: + try: + for entry in layer_map: + lname = getattr(entry, "name", None) + val = getattr(entry, "value", None) + gds = _gds_layer_number(val) if isinstance(val, (tuple, list)) else (val if isinstance(val, int) else _gds_layer_number(val)) + if lname and gds is not None: + layer_name_to_gds[lname] = gds + except TypeError: + pass + + def resolve(layer_ref): + gds = _gds_layer_number(layer_ref) + if gds is not None and gds in layer_z_by_gds: + return layer_z_by_gds[gds] + if isinstance(layer_ref, str): + if layer_ref in layer_z_by_name: + return layer_z_by_name[layer_ref] + g = layer_name_to_gds.get(layer_ref) + if g is not None and g in layer_z_by_gds: + return layer_z_by_gds[g] + return None + + results = [] + for xs_name, xs_func in xs_dict.items(): + try: + xs = xs_func() if callable(xs_func) else xs_func + except Exception: + continue + sections = getattr(xs, "sections", []) + main_layer = getattr(xs, "layer", None) + main_width = getattr(xs, "width", None) + xs_layers = [] + + if main_layer is not None and main_width is not None: + info = resolve(main_layer) + if info: + xs_layers.append({ + "name": info["name"], "material": info["material"], + "zmin": round(info["zmin"], 4), + "zmax": round(info["zmin"] + info["thickness"], 4), + "thickness": round(info["thickness"], 4), + "width": round(float(main_width), 2), "gds": info["gds"], + }) + + for section in sections: + sec_layer = getattr(section, "layer", None) + sec_width = getattr(section, "width", None) + if sec_layer is None or sec_width is None: + continue + info = resolve(sec_layer) + if info: + xs_layers.append({ + "name": info["name"], "material": info["material"], + "zmin": round(info["zmin"], 4), + "zmax": round(info["zmin"] + info["thickness"], 4), + "thickness": round(info["thickness"], 4), + "width": round(float(sec_width), 2), "gds": info["gds"], + }) + + if xs_layers: + results.append({"name": xs_name, "layers": xs_layers}) + + return results + + +# --------------------------------------------------------------------------- +# SVG rendering +# --------------------------------------------------------------------------- + +_svg_counter = 100 + + +def _next_id(): + global _svg_counter + _svg_counter += 1 + return f"sv_{_svg_counter}" + + +def _render_layer_stack(layers, ticks, svg_id, svg_w=550, svg_h=750): + margin = {"top": 60, "bottom": 30, "left": 70, "right": 100} + plot_w = svg_w - margin["left"] - margin["right"] + plot_h = svg_h - margin["top"] - margin["bottom"] + d = json.dumps(layers, separators=(",", ": ")) + t = json.dumps(ticks, separators=(",", ": ")) + m = json.dumps(margin, separators=(",", ": ")) + + return f'''