Files
rpmroute/docs/ARCHITECTURE.md
rsp2k a6aa1991a1 Rename to rpmroute
The router began as a port of FreeRouting and has grown its own engine
(negotiated congestion, tree nets, exact checks, axis escapes), and
'freeroute' was one letter from the project it no longer follows. The
package, module and command are now rpmroute; the freeroute command stays
as an alias, and the RPMROUTE / RPMROUTE_ORACLE_JAVA variables fall back
to the old FREEROUTE names. Version 2026.10.07. Routing is unchanged: a
PCBench board routes to a byte-identical session either way.
2026-10-07 14:10:36 -06:00

12 KiB
Raw Permalink Blame History

Upstream FreeRouting architecture — a port map

This maps the parts of the FreeRouting Java source (cloned into the gitignored reference/freerouting/) that matter for a Java-free Python port. It was written by reading the actual source, not the Specctra spec. Package paths here are verified against the clone (commit fetched via --depth 1 on 2026-07-11) and differ from the seed plan's guesses — the DSN/SES code lives under io/specctra/, not designforms/specctra/.

Java root in the clone: src/main/java/app/freerouting/.

1. Specctra DSN / SES I/O — io/specctra/

This is the I/O boundary the port must match: kicad-cli produces the .dsn input and consumes the .ses output.

Entry points (io/specctra/)

  • DsnReader.java (294 lines) — the modern read entry point. readBoard(stream,…) validates the (pcb <name> header with a literal three-token check (( , pcb scope keyword, name string), then calls Keyword.PCB_SCOPE.read_scope(par) to parse the body. readMetadata(stream) is a fast path that parses only parser/resolution/structure and stops. Returns a typed BoardReadResult (Success / OutlineMissing / ParseError / IoError). Defaults captured here: it flips the scanner to NAME state to read the pcb name cleanly.
  • SesReader.java (392) / SesWriter.java (430) — session-file read/write.
  • RulesReader.java / RulesWriter.java — .rules sidecar files.
  • DsnWriter.java (97) — DSN writer entry.

Tokenizer + scope readers (io/specctra/parser/)

The lexer is JFlex-generated — SpecctraDsnStreamReader.java (1467 lines) is a packed DFA; do not port the DFA. The human-readable grammar is in SpecctraFileDescription.flex (312 lines) and that is what to reimplement. Key lexical facts extracted from the flex file:

  • Whitespace: \r \n \f \t space. Comments: #… to EOL, and /* … */.
  • Two quote chars: " (STRING1) and ' (STRING2). No escaping — inside a quoted string \ is a literal backslash; the matching quote ends it.
  • Keywords are matched case-insensitively (%ignorecase) and returned as singleton Keyword objects; ( → OPEN_BRACKET, ) → CLOSED_BRACKET.
  • Non-keyword atoms are returned as Integer, Double, or String.
  • Lexical states are the tricky part. Several keywords call yybegin(NAME) (or LAYER_NAME, COMPONENT_NAME, SPEC_CHAR, IGNORE_QUOTE) so the next token is forced to be read as a string even when it looks like a number (a net or layer literally named 0). The Python port reconciles this by keeping every unquoted atom as a token that preserves both its raw text and a best-effort numeric value, and letting each scope reader decide which to use — exactly the decision the lexical state encodes.
  • High-level scanner helpers (bottom of SpecctraDsnStreamReader.java): next_string(ignoreNewline, leadingSep), next_string_list(sep), next_double(), next_closing_bracket(). next_string skips leading whitespace, handles a leading ", and stops at whitespace / ( / ) (or a caller-supplied separator such as - for Comp-Pin splitting).

Scope-reader classes (each a recursive-descent reader over the token stream — this is the structure the Python reader.py mirrors):

  • ScopeKeyword.java — base class. read_scope loop: on ( followed by a known ScopeKeyword, recurse; unknown scope → skip_scope (bracket counting). This tolerant skip is the backbone of the whole reader.
  • Parser.java — (parser (string_quote ") (host_cad …) (host_version …) (constant …) (write_resolution …) (generated_by_freerouting)).
  • Resolution.java — (resolution <unit> <int>). Defaults: unit mil, resolution 100 (set in ReadScopeParameter).
  • Structure.java (1174) — the big one. Reads layer, boundary, via (routing via padstack names, incl. (spare …)), rule (default width/ clearance), keepout / via_keepout / place_keepout, plane, control (via_at_smd on|off), snap_angle, autoroute_settings, flip_style. Then create_board turns it into geometry. layer: (layer <name> (type signal|power|jumper) (use_net …) (rule …)). boundary: a shape on layer pcb is the bounding box; shapes on layer signal are the outline.
  • Shape.java (575) — shape grammar shared everywhere: (rect <layer> x1 y1 x2 y2), (circle <layer> dia [x y]), (polygon <layer> aperture x1 y1 …), (path <layer> width x1 y1 …). read_area_scope reads an optional name + a border shape + (window …) holes
    • optional (clearance_class …).
  • Library.java (323) — (library (padstack …) (image …)). padstack: name, one or more (shape (<shape>)), (attach on|off), (absolute on|off). Package.java (391) reads (image <name> (side front|back) (pin <padstack> [ (rotate d) ] <pinname> x y) (outline …) (keepout …) …).
  • Placement.java / Component.java (315) — (placement (component <libname> (place <refdes> x y front|back rot [ (lock_type position) ] [ (PN part) ] …))). A place with no coords means "not yet placed".
  • Network.java (1193) — (network (net <name> [subnet] (pins Comp-Pin …) (fromto …) (rule …)) (class <name> net… (circuit (use_via …)) (rule …)) (class_class …)). Pin refs split component/pin on the first -.
  • NetClass.java, Net.java, Rule.java (width/clearance rules), AutorouteSettings.java — supporting readers.
  • Data holders: Layer, Padstack(core), PinInfo, ComponentPlacement, Circle/Rectangle/Polygon/PolygonPath/PolylinePath.

Python port status: items 1 (tokenizer), the shape grammar, and every structure/library/network/placement/parser/resolution scope above are implemented in src/rpmroute/dsn/ this session (see below).

2. Board data model — board/

Constructed by Structure.create_board. Central classes:

  • BasicBoard.java — geometric item container: insert/delete/pick items, layer structure, bounding box, insert_obstacle, insert_via_obstacle, insert_conduction_area, insert_component_obstacle. RoutingBoard.java extends it with the routing operations the autorouter drives.
  • Item.java + subclasses: Trace/PolylineTrace (a routed wire as a Polyline), Via, DrillItem, Pin, ObstacleArea/ConductionArea/ ComponentObstacleArea/ViaObstacleArea (keepouts & planes), BoardOutline.
  • Component.java / Components.java — placed component instances.
  • Layer.java / LayerStructure.java — signal/power layers, index 0 = top.
  • SearchTreeManager.java + ShapeSearchTree*.java — the spatial index (MinAreaTree R-tree variants, one per angle restriction) used for fast obstacle queries during routing.
  • Shoving/tightening: ShoveTraceAlgo, ForcedPadAlgo, ForcedViaAlgo, PullTightAlgo{,45,90,AnyAngle}.
  • Unit.java (mil/inch/mm/um), CoordinateTransform (dsn↔board scaling), AngleRestriction (NINETY / FORTYFIVE / NONE).

Python translation: a board package of dataclasses + a spatial index. Item hierarchy → dataclasses with a shared base; the search tree → an rtree/ STRtree (shapely) or a hand-rolled bbox tree. Not started this session.

3. Planar geometry — geometry/planar/ (34 files)

The math the router stands on. Key types and their Python analogues:

  • IntPoint / IntVector — integer point/vector (board coords). FloatPoint (double tuple, not derived from Point because float math is inexact) for approximate work. RationalPoint/RationalVector — exact rational intersections of lines.
  • Line / LineSegment / Polyline — a Polyline is a sequence of lines whose n lines define n−1 corners; traces are polylines. Direction, FortyfiveDegreeDirection encode the angle grid.
  • Shape (interface) → PolylineShape (straight-line borders) → TileShape (convex, half-plane intersection) → Simplex (general convex) and IntBox/IntOctagon (axis- and 45°-aligned boxes). ConvexShape, RegularTileShape interfaces. Circle, Ellipse for round pads.
  • Area / PolylineArea — a shape possibly with holes (border + hole shapes).
  • split_to_convex() (decompose into TileShape[]), convex_hull(), offset(), contains(), intersection() are the workhorse ops.
  • Limits.java (CRIT_INT overflow guard used when picking the coord scale).

Python translation: this is the highest-risk port (exact rational geometry, convex decomposition). Options: lean on shapely for polygon ops and add a thin 45°/octagon layer, or port Simplex/TileShape directly for bit-exact parity with the JAR oracle. Decide when the board model lands. Not started this session.

4. Autorouter core — autoroute/

  • BatchAutorouter.java / BatchAutorouterThread.java — the top loop: pick the next unrouted connection, route it, rip up and retry on failure, repeat over passes. BatchAutorouterV19.java is a newer variant; BatchFanout, BatchOptimizer{,MultiThreaded} are the fanout/optimize passes.
  • AutorouteEngine.java — per-board routing state (expansion rooms, drill pages, the maze search driver). Holds the CompleteFreeSpaceExpansionRoom graph.
  • MazeSearchAlgo.java — the A*/Dijkstra maze search over expansion rooms ("route an incomplete connection via a maze search"). MazeSearchElement, MazeListElement, MazeShoveTraceAlgo (shove obstacles while searching).
  • LocateFoundConnectionAlgo{,45Degree,AnyAngle}.java — turn a found maze path back into concrete trace geometry at the right angle restriction.
  • ExpansionRoom / FreeSpaceExpansionRoom / ObstacleExpansionRoom / ExpansionDoor / ExpansionDrill — the free-space decomposition the maze search walks. SortedRoomNeighbours{,45Degree,Orthogonal} order neighbours.
  • AutorouteControl.java — cost function parameters (via costs, preferred- direction trace costs, ripup costs, start pass number).
  • InsertFoundConnectionAlgo.java — commit the located connection to the board.

Python translation: the last and largest piece. An MVP can start grid/ Manhattan-45° maze routing decoupled from the exact expansion-room model, then converge toward FreeRouting's free-space rooms. Not started this session.

5. Supporting packages (context)

  • rules/ — BoardRules, ClearanceMatrix, NetClass, Net, ViaInfo, DefaultItemClearanceClasses. The clearance matrix is class×class×layer.
  • core/ — Padstack, Padstacks, Package, Packages, BoardLibrary, RoutingJob. settings/RouterSettings holds autoroute config.
  • datastructures/ — UndoableObjects, IndentFileWriter (the DSN/SES pretty printer), IdentifierType (quoting on write), identification-number generators.
  • drc/ — design-rule checks. logger/FRLogger — logging (warnings collected into the read result).

Port order (this file drives the roadmap)

  1. DSN parser — done this session (src/rpmroute/dsn/).
  2. SES writer — next. Start with an empty/no-op session to prove the loop end-to-end through kicad-cli pcb import specctra-ses. See SesWriter.java and SpecctraSesFileWriter.java: (session <name> (base_design <name>) (placement …) (was_is) (routes (resolution …) (network_out (net <name> (wire (path <layer> <width> x1 y1 …)) (via <padstack> x y))))).
  3. Geometry primitives — geometry/planar subset needed by the board model.
  4. Board model — board items + spatial index.
  5. Autorouter — maze/rip-up core, then optimize pass.
  6. CLI + kicad-mcp integration — rpmroute board.dsn -o board.ses.