Source code for reasitic.exports.sonnet

"""Sonnet-em ``.son`` exporter (subset).

The binary's ``SonnetSave`` command (case 302) writes a Sonnet
project file that loads directly into Sonnet's full-wave EM
solver. Sonnet's format is verbose and mostly metadata; we emit the
subset needed to reproduce the spiral's geometry and layer stack
within a Sonnet project.

The header layout is::

    FTYP SONPROJ 16 ! Sonnet Project File
    HEADER
    LIC ...
    DAT ...
    BUILT_BY_AUTHOR <user>
    MDATE ...
    HDATE ...
    END HEADER
    DIM
    FREQUENCY GHZ
    INDUCTANCE NH
    LENGTH UM
    ...
    END DIM
    GEO
      ... per-layer geometry ...
    END GEO
    END

This module emits a working but minimal subset: the geometry block,
plus a few required metadata lines. Sonnet itself fills in defaults
for omitted sections.

Mirrors ``cmd_sonnet_emit`` (cases referencing the
``SONNETSAVE`` command).
"""

from __future__ import annotations

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

from reasitic.geometry import Point, Polygon, Shape
from reasitic.tech import Tech


[docs] def write_sonnet(shapes: Iterable[Shape], tech: Tech) -> str: """Emit a minimal Sonnet ``.son`` string for ``shapes``.""" out = StringIO() out.write("FTYP SONPROJ 16 ! Sonnet Project File\n") out.write("HEADER\n") out.write("BUILT_BY_AUTHOR reASITIC\n") out.write("END HEADER\n") out.write("DIM\n") out.write("FREQUENCY GHZ\n") out.write("INDUCTANCE NH\n") out.write("LENGTH UM\n") out.write("ANGLE DEG\n") out.write("CONDUCTIVITY SIEMENS/M\n") out.write("RESISTIVITY OHM CM\n") out.write("RESISTANCE OHM\n") out.write("CAPACITANCE PF\n") out.write("END DIM\n") out.write("GEO\n") out.write(f"BOX 1 {tech.chip.chipx:g} {tech.chip.chipy:g}\n") for sh in shapes: for p in sh.polygons: out.write(f"NUM {len(p.vertices)}\n") out.write(f"LAYER {p.metal}\n") for v in p.vertices: out.write(f"{v.x:g} {v.y:g}\n") out.write("END\n") out.write("END GEO\n") out.write("END\n") return out.getvalue()
[docs] def write_sonnet_file(path: str | Path, shapes: Iterable[Shape], tech: Tech) -> None: """Write the Sonnet rendering to ``path``.""" Path(path).write_text(write_sonnet(shapes, tech))
# Reader -----------------------------------------------------------------
[docs] def read_sonnet(text: str, tech: Tech) -> list[Shape]: """Parse the GEO block of a Sonnet ``.son`` file into Shapes. Supports the subset emitted by :func:`write_sonnet`: ``NUM`` polygon-size lines followed by ``LAYER`` and vertex coordinates, terminated by ``END``. Other Sonnet directives are skipped. Returns one Shape per file (combining all polygons). """ shape = Shape(name="sonnet_imported") in_geo = False pending: list[tuple[float, float]] | None = None pending_layer = 0 expected_n = 0 for raw in text.splitlines(): line = raw.strip() if not line: continue if line == "GEO": in_geo = True continue if line == "END GEO": in_geo = False continue if not in_geo: continue if line.startswith("NUM "): try: expected_n = int(line.split()[1]) except (ValueError, IndexError): continue pending = [] continue if line.startswith("LAYER "): try: pending_layer = int(line.split()[1]) except (ValueError, IndexError): pending_layer = 0 continue if line == "END": if pending is not None and len(pending) == expected_n: z = 0.0 w = 0.0 t = 0.0 if 0 <= pending_layer < len(tech.metals): m = tech.metals[pending_layer] z = m.d + m.t * 0.5 w = 0.0 t = m.t shape.polygons.append( Polygon( vertices=[Point(x, y, z) for (x, y) in pending], metal=pending_layer, width=w, thickness=t, ) ) pending = None continue # Vertex line if pending is not None: parts = line.split() if len(parts) >= 2: import contextlib with contextlib.suppress(ValueError): pending.append((float(parts[0]), float(parts[1]))) return [shape]
[docs] def read_sonnet_file(path: str | Path, tech: Tech) -> list[Shape]: """Read a Sonnet ``.son`` file and parse the GEO block.""" return read_sonnet(Path(path).read_text(), tech)