Add SES writer helpers: indent writer and routing-result model
Ports two FreeRouting building blocks for session output: - indent.py: IndentWriter (2-space indented S-expression output) and Identifier (reserved-char/non-ASCII/leading-digit quoting), mirroring datastructures/IndentFileWriter and IdentifierType. The SES reserved set includes '-' and '_', so net and padstack names get quoted. - model.py: RoutedWire/RoutedVia/RoutedNet/RoutingResult — the minimal in-memory routing structure the maze router will populate and the SES writer serializes. Coordinates are in DSN units.
This commit is contained in:
parent
3bf4f6bed2
commit
650c732a30
103
src/freeroute/ses/indent.py
Normal file
103
src/freeroute/ses/indent.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
"""Indented S-expression output and identifier quoting for SES writing.
|
||||||
|
|
||||||
|
Ports two FreeRouting helpers used by the session writer:
|
||||||
|
|
||||||
|
* :class:`IndentWriter` mirrors ``datastructures/IndentFileWriter`` — a 2-space
|
||||||
|
indenting writer with ``start_scope`` / ``end_scope`` / ``new_line``.
|
||||||
|
* :class:`Identifier` mirrors ``datastructures/IdentifierType`` — quotes a name
|
||||||
|
when it contains a reserved character, a non-ASCII character, or starts with a
|
||||||
|
digit; strips surrounding quotes and any embedded quote character first.
|
||||||
|
|
||||||
|
The SES reserved-character set is the one ``SesWriter`` constructs:
|
||||||
|
``( ) <space> ; - _ / ~ { }``. Note that ``-`` and ``_`` are reserved, so net
|
||||||
|
names like ``NET_A`` and padstacks like ``Via[0-1]_600:300_um`` are quoted.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from io import StringIO
|
||||||
|
import re
|
||||||
|
|
||||||
|
__all__ = ["IndentWriter", "Identifier", "SES_RESERVED_CHARS"]
|
||||||
|
|
||||||
|
_INDENT = " "
|
||||||
|
|
||||||
|
#: Reserved characters for SES identifiers (from SesWriter.write).
|
||||||
|
SES_RESERVED_CHARS: tuple[str, ...] = (
|
||||||
|
"(",
|
||||||
|
")",
|
||||||
|
" ",
|
||||||
|
";",
|
||||||
|
"-",
|
||||||
|
"_",
|
||||||
|
"/",
|
||||||
|
"~",
|
||||||
|
"{",
|
||||||
|
"}",
|
||||||
|
)
|
||||||
|
|
||||||
|
_LEADING_DIGIT = re.compile(r"-?\d")
|
||||||
|
|
||||||
|
|
||||||
|
class IndentWriter:
|
||||||
|
"""Accumulates indented S-expression text, mirroring ``IndentFileWriter``."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._buf = StringIO()
|
||||||
|
self._level = 0
|
||||||
|
|
||||||
|
def write(self, text: str) -> None:
|
||||||
|
self._buf.write(text)
|
||||||
|
|
||||||
|
def new_line(self) -> None:
|
||||||
|
"""Start a new line at the current indent level."""
|
||||||
|
self._buf.write("\n")
|
||||||
|
self._buf.write(_INDENT * self._level)
|
||||||
|
|
||||||
|
def start_scope(self, new_line: bool = True) -> None:
|
||||||
|
"""Open a ``(`` scope, optionally on a fresh line, and indent."""
|
||||||
|
if new_line:
|
||||||
|
self.new_line()
|
||||||
|
self._buf.write("(")
|
||||||
|
self._level += 1
|
||||||
|
|
||||||
|
def end_scope(self) -> None:
|
||||||
|
"""Close the innermost scope with ``)`` on its own aligned line."""
|
||||||
|
self._level -= 1
|
||||||
|
self.new_line()
|
||||||
|
self._buf.write(")")
|
||||||
|
|
||||||
|
def getvalue(self) -> str:
|
||||||
|
return self._buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
class Identifier:
|
||||||
|
"""Formats a name as a DSN/SES token, quoting it when required."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, string_quote: str = '"', reserved: tuple[str, ...] = SES_RESERVED_CHARS
|
||||||
|
) -> None:
|
||||||
|
self.quote = string_quote or '"'
|
||||||
|
self.reserved = tuple(reserved)
|
||||||
|
|
||||||
|
def format(self, name: str) -> str:
|
||||||
|
"""Return ``name`` quoted if it needs quoting, else unchanged."""
|
||||||
|
# Strip a surrounding matching pair of quote characters.
|
||||||
|
while len(name) >= 2 and name[0] == self.quote and name[-1] == self.quote:
|
||||||
|
name = name[1:-1]
|
||||||
|
# Remove any embedded quote character (it cannot be escaped).
|
||||||
|
if self.quote in name:
|
||||||
|
name = name.replace(self.quote, "")
|
||||||
|
|
||||||
|
need_quotes = any(rc in name for rc in self.reserved)
|
||||||
|
if not need_quotes and any(ord(ch) > 127 for ch in name):
|
||||||
|
need_quotes = True
|
||||||
|
if not need_quotes and _LEADING_DIGIT.match(name):
|
||||||
|
need_quotes = True
|
||||||
|
|
||||||
|
if need_quotes:
|
||||||
|
return f"{self.quote}{name}{self.quote}"
|
||||||
|
return name
|
||||||
|
|
||||||
|
def write(self, name: str, out: IndentWriter) -> None:
|
||||||
|
out.write(self.format(name))
|
||||||
80
src/freeroute/ses/model.py
Normal file
80
src/freeroute/ses/model.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
"""In-memory routing result — the input the SES writer serializes.
|
||||||
|
|
||||||
|
The maze router (a later stage) populates a :class:`RoutingResult`; the SES
|
||||||
|
writer turns it into the ``(routes (network_out ...))`` scope. Coordinates and
|
||||||
|
widths are in DSN units (the same units the DSN was parsed in); the writer
|
||||||
|
rounds them to integers, matching FreeRouting's ``SesWriter`` which emits
|
||||||
|
integer session coordinates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
__all__ = ["RoutedWire", "RoutedVia", "RoutedNet", "RoutingResult"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RoutedWire:
|
||||||
|
"""A routed trace segment on one layer.
|
||||||
|
|
||||||
|
``coords`` is a flat ``[x1, y1, x2, y2, ...]`` list (DSN units); ``width`` is
|
||||||
|
the full trace width (DSN units). ``fixed`` maps to an emitted
|
||||||
|
``(type fix)`` marker when set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
layer: str
|
||||||
|
width: float
|
||||||
|
coords: list[float]
|
||||||
|
fixed: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RoutedVia:
|
||||||
|
"""A routed via at ``(x, y)`` (DSN units) using a named padstack."""
|
||||||
|
|
||||||
|
padstack: str
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
fixed: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RoutedNet:
|
||||||
|
"""The routed wires and vias belonging to one net."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
wires: list[RoutedWire] = field(default_factory=list)
|
||||||
|
vias: list[RoutedVia] = field(default_factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_items(self) -> bool:
|
||||||
|
return bool(self.wires or self.vias)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RoutingResult:
|
||||||
|
"""The complete routing output for a board, grouped by net."""
|
||||||
|
|
||||||
|
nets: list[RoutedNet] = field(default_factory=list)
|
||||||
|
|
||||||
|
def net(self, name: str, *, create: bool = False) -> RoutedNet | None:
|
||||||
|
"""Return the :class:`RoutedNet` with ``name`` (creating it if asked)."""
|
||||||
|
for n in self.nets:
|
||||||
|
if n.name == name:
|
||||||
|
return n
|
||||||
|
if create:
|
||||||
|
n = RoutedNet(name=name)
|
||||||
|
self.nets.append(n)
|
||||||
|
return n
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_wire(self, net_name: str, wire: RoutedWire) -> None:
|
||||||
|
self.net(net_name, create=True).wires.append(wire)
|
||||||
|
|
||||||
|
def add_via(self, net_name: str, via: RoutedVia) -> None:
|
||||||
|
self.net(net_name, create=True).vias.append(via)
|
||||||
|
|
||||||
|
def routed_nets(self) -> list[RoutedNet]:
|
||||||
|
"""Nets that actually carry routed geometry (in insertion order)."""
|
||||||
|
return [n for n in self.nets if n.has_items]
|
||||||
Loading…
x
Reference in New Issue
Block a user