Source code for reasitic.exports.cif

"""CIF (Caltech Intermediate Format) exporter.

CIF is a 1970s-era ASCII layout format still in use as the lingua
franca for academic IC tooling and as a Sonnet/Mosis input. The
binary's ``CIFSAVE`` command (case 300) emits this format.

The format is line-oriented with semicolon terminators::

    DS 1 1 1;             // start a symbol, scale=1/1
    L NM3;                // switch to layer NM3
    B 100 50 0 0;         // box of width 100, height 50, centred at (0,0)
    P 0 0 100 0 100 50 0 50 0 0;   // polygon path
    DF;                   // end symbol
    C 1;                  // call symbol 1
    E                     // end of file

We use the polygon ``P`` form because it preserves arbitrary spiral
geometry. Each metal layer becomes a separate ``L`` directive named
after the metal in the tech file (uppercased, prefixed with ``N``).

Coordinates default to centi-microns (1 unit = 0.01 μm), matching
the convention most downstream tools expect.
"""

from __future__ import annotations

from collections.abc import Iterable
from io import StringIO
from pathlib import Path

from reasitic.geometry import Shape
from reasitic.tech import Tech

_CENTI_UM_PER_UM = 100  # 1 unit in CIF = 0.01 μm


[docs] def write_cif( shapes: Iterable[Shape], tech: Tech, *, symbol_id: int = 1, scale_a: int = 1, scale_b: int = 1, units_per_um: float = _CENTI_UM_PER_UM, ) -> str: """Render ``shapes`` to a CIF string.""" out = StringIO() out.write(f"DS {symbol_id} {scale_a} {scale_b};\n") last_layer: str | None = None for sh in shapes: for p in sh.polygons: metal_idx = p.metal if 0 <= metal_idx < len(tech.metals): layer_name = "N" + tech.metals[metal_idx].name.upper() else: layer_name = f"NL{metal_idx}" if layer_name != last_layer: out.write(f"L {layer_name};\n") last_layer = layer_name coords = " ".join( f"{round(v.x * units_per_um)} {round(v.y * units_per_um)}" for v in p.vertices ) out.write(f"P {coords};\n") out.write("DF;\n") out.write(f"C {symbol_id};\n") out.write("E\n") return out.getvalue()
[docs] def write_cif_file( path: str | Path, shapes: Iterable[Shape], tech: Tech, **kwargs: object, ) -> None: """Write the CIF rendering of ``shapes`` to ``path``.""" text = write_cif(shapes, tech, **kwargs) # type: ignore[arg-type] Path(path).write_text(text)
# Reader ------------------------------------------------------------------
[docs] def read_cif( text: str, tech: Tech, *, units_per_um: float = _CENTI_UM_PER_UM, ) -> list[Shape]: """Parse a CIF string back into reasitic Shapes. Supports the subset emitted by :func:`write_cif`: ``DS`` symbol open, ``L <name>`` layer switch, ``P <coords>`` polygon, ``DF`` symbol close, ``C <id>`` symbol call, ``E`` end of file. All other CIF directives are silently skipped. The polygon's metal layer is resolved from the L name by matching against the tech file's metal names (case-insensitive, with a leading ``N`` stripped). Unknown layers fall back to metal 0. """ from reasitic.geometry import Point, Polygon, Shape shape = Shape(name="cif_imported") metal_idx = 0 in_symbol = False for raw in text.split(";"): line = raw.strip() if not line or line == "E": continue if line.startswith("DS"): in_symbol = True continue if line.startswith("DF"): in_symbol = False continue if line.startswith("C "): continue if not in_symbol: continue if line.startswith("L "): layer_name = line[2:].strip() # Strip leading N if present if layer_name.startswith("N"): layer_name = layer_name[1:] metal_idx = 0 for i, m in enumerate(tech.metals): if m.name.upper() == layer_name.upper(): metal_idx = i break continue if line.startswith("P "): tokens = line[2:].split() if len(tokens) % 2 != 0: continue coords = [int(t) for t in tokens] verts = [ Point(coords[i] / units_per_um, coords[i + 1] / units_per_um, 0.0) for i in range(0, len(coords), 2) ] if metal_idx < len(tech.metals): m = tech.metals[metal_idx] w = m.t # placeholder; CIF doesn't store conductor width t = m.t # Reset Z to the metal layer height z = m.d + m.t * 0.5 verts = [Point(v.x, v.y, z) for v in verts] else: w = 0.0 t = 0.0 shape.polygons.append( Polygon(vertices=verts, metal=metal_idx, width=w, thickness=t) ) continue return [shape]
[docs] def read_cif_file(path: str | Path, tech: Tech, **kwargs: object) -> list[Shape]: """Read a CIF file and parse into Shapes.""" return read_cif(Path(path).read_text(), tech, **kwargs) # type: ignore[arg-type]