diff --git a/CLAUDE.md b/CLAUDE.md index c157739..49d58ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,12 +1,12 @@ # CLAUDE.md -Guidance for Claude Code working in the `freeroute` repository. +Guidance for Claude Code working in the `rpmroute` repository. ## What this is A native Python PCB autorouter: a Java-free reimplementation of the [FreeRouting](https://github.com/freerouting/freerouting) engine. Specctra DSN -in, Specctra SES out, no JVM anywhere. Published on PyPI as `freeroute` +in, Specctra SES out, no JVM anywhere. Published on PyPI as `rpmroute` (GPL-3.0-or-later, CalVer). ## Commands @@ -16,7 +16,7 @@ uv sync uv run pytest -m "not oracle" # the fast suite (this is what you run) uv run pytest -m oracle # differential tests against the real JAR (needs Java) uv run ruff check src/ tests/ -uv run freeroute board.dsn --engine room --pack -do board.ses +uv run rpmroute board.dsn --engine room --pack -do board.ses ``` ## The three non-negotiable invariants @@ -67,7 +67,7 @@ route/ grid_router | exact_router | room_router, shove, pipeline, cli decomposed into convex rooms joined by doors, so it routes off-grid channels the grid literally cannot see. DRC-clean. Supports `--pack` and `--shove`. -The engine/option matrix lives in `freeroute.cli.ENGINE_OPTIONS` and is +The engine/option matrix lives in `rpmroute.cli.ENGINE_OPTIONS` and is importable, so consumers can assert against it. **An unsupported combination is a hard error (exit 2), never a silent no-op.** Downstream code (`mckicad`) reads this matrix to avoid sending an invalid pair. diff --git a/README.md b/README.md index e7e47f6..c129dc7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,23 @@ -# freeroute +# rpmroute -A native Python PCB autorouter: a Java-free reimplementation of the -[FreeRouting](https://github.com/freerouting/freerouting) engine. Specctra DSN -in, Specctra SES out, no JVM anywhere in the pipeline. +A native Python PCB autorouter. Specctra DSN in, Specctra SES out, no JVM +anywhere in the pipeline. + +It began as a Java-free port of the +[FreeRouting](https://github.com/freerouting/freerouting) engine and has grown +its own router since: negotiated congestion before routing, multi-pin nets +routed as trees, every route checked against exact pad and copper shapes, and +pin escapes along the pad's own axis for parts placed at an angle. On dense +boards it now finishes more connections than the FreeRouting JAR, with no +rule violations. + +It was called `freeroute` until 2026-10-07; that command still works. ## Why Toolchains that autoroute (`kicad-mcp` among them) do it by shelling out to `freerouting.jar`, which drags a JVM into every install and container image. -`freeroute` reimplements the routing engine in pure Python, with no dependencies +`rpmroute` reimplements the routing engine in pure Python, with no dependencies beyond the standard library, so the JAR step becomes a `pip install`. ## The contract @@ -16,31 +25,31 @@ beyond the standard library, so the JAR step becomes a `pip install`. The entire integration surface is two Specctra files: ``` -board.dsn ──▶ freeroute ──▶ board.ses +board.dsn ──▶ rpmroute ──▶ board.ses (unrouted) (routed session: traces + vias) ``` -`kicad-cli` already exports `.dsn` and imports `.ses`, so `freeroute` drops in +`kicad-cli` already exports `.dsn` and imports `.ses`, so `rpmroute` drops in wherever `java -jar freerouting.jar` was invoked, and nothing else in the pipeline changes. The `-de` / `-do` / `-mp` flags are the JAR's, on purpose. ## Install ```bash -uv tool install freeroute # or: pipx install freeroute +uv tool install rpmroute # or: pipx install rpmroute ``` ## Usage ```bash # a KiCad board: the exact engine, with the board's copper-to-edge rule -freeroute board.dsn --engine exact --shove --edge-clearance 0.5 -do board.ses +rpmroute board.dsn --engine exact --shove --edge-clearance 0.5 -do board.ses -freeroute -de board.dsn -do board.ses # the FreeRouting JAR spelling (grid engine) -freeroute board.dsn --engine room --pack --shove -do board.ses +rpmroute -de board.dsn -do board.ses # the FreeRouting JAR spelling (grid engine) +rpmroute board.dsn --engine room --pack --shove -do board.ses ``` -With no `-do` / `-o`, the SES goes to stdout. Connections freeroute could not +With no `-do` / `-o`, the SES goes to stdout. Connections rpmroute could not make are listed on stderr; the SES is still written and the exit code stays 0, as with the JAR. Connections whose two ends sit in one zone of their net are listed separately, as left to that zone's fill. @@ -99,7 +108,7 @@ What the opt-in passes buy you: - **`--diagonal`** (exact): a still-dropped 2-pin net is retried as a 45° trace covered by an exact integer octagon. -## What freeroute reads from a KiCad DSN +## What rpmroute reads from a KiCad DSN These were each checked against KiCad itself, pad by pad or with its DRC: @@ -125,7 +134,7 @@ SES coordinates are written in resolution units, exactly as the JAR writes them. ## Library use ```python -from freeroute.route import route +from rpmroute.route import route ses_text = route(dsn_text, engine="exact", shove=True, edge_clearance_mm=0.5) ``` @@ -136,13 +145,13 @@ Alpha. Measured on two real KiCad 10 boards (the TerraHawk charge node's power stage and sense board, 2026-09-27), with every track stripped, then routed, then checked with `kicad-cli pcb drc --refill-zones`: -| board | freeroute exact | FreeRouting JAR | DRC errors from freeroute | +| board | rpmroute exact | FreeRouting JAR | DRC errors from rpmroute | |---|---|---|---| | power stage (40 x 30 mm) | 7 unconnected, ~5 s | 12 | 0 | | sense board (55 x 25 mm, 0.5 mm pitch MCU) | 41 unconnected, ~2.5 min | 32 | 0 | In the board owner's production flow (hand-routed power lanes kept, pours added -after routing), freeroute completed the power stage with no unconnected items. +after routing), rpmroute completed the power stage with no unconnected items. What it is **not**, stated plainly: @@ -164,7 +173,7 @@ What it is **not**, stated plainly: For maximum completion on a dense board today, the JAR is still the stronger router. For routing without a JVM (CI, containers, an MCP server, a library -call), freeroute routes legally and says what it could not do. +call), rpmroute routes legally and says what it could not do. ## Development diff --git a/bench/bench.py b/bench/bench.py index ab8669d..6f70354 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -37,8 +37,8 @@ import tempfile import threading import time -NOT_ROUTED = re.compile(r"freeroute: (\d+) connection\(s\) not routed") -TO_ZONE = re.compile(r"freeroute: (\d+) more connection\(s\) left to a zone fill") +NOT_ROUTED = re.compile(r"rpmroute: (\d+) connection\(s\) not routed") +TO_ZONE = re.compile(r"rpmroute: (\d+) more connection\(s\) left to a zone fill") def parse_boards(path: Path) -> list[tuple[str, Path, list[str]]]: @@ -61,8 +61,8 @@ def parse_variant(spec: str) -> tuple[str, str | None, list[str]]: if not sep or not head: raise SystemExit(f"variant {spec!r}: want NAME=FLAGS or NAME@SRC=FLAGS") name, _, src = head.partition("@") - if src and not (Path(src) / "freeroute").is_dir(): - raise SystemExit(f"variant {name}: {src} has no freeroute package") + if src and not (Path(src) / "rpmroute").is_dir(): + raise SystemExit(f"variant {name}: {src} has no rpmroute package") return name, src or None, shlex.split(flags) @@ -74,7 +74,7 @@ def run_one(board, variant, seed: int, timeout: float) -> dict: env["PYTHONPATH"] = src + os.pathsep + env.get("PYTHONPATH", "") with tempfile.TemporaryDirectory() as tmp: cmd = [ - sys.executable, "-m", "freeroute.cli", str(dsn), "-do", f"{tmp}/out.ses", + sys.executable, "-m", "rpmroute.cli", str(dsn), "-do", f"{tmp}/out.ses", *board_flags, *flags, "--seed", str(seed), ] start = time.monotonic() diff --git a/bench/diagnose.py b/bench/diagnose.py index 9464e2a..5a92b09 100644 --- a/bench/diagnose.py +++ b/bench/diagnose.py @@ -23,14 +23,14 @@ import json from pathlib import Path import sys -from freeroute.board import build_board -from freeroute.board.search_tree import ShapeSearchTree -from freeroute.dsn import parse_dsn -from freeroute.geometry.octagon import overlaps_2d -from freeroute.route import grid_router -from freeroute.route.exact_router import route_board_exact -from freeroute.route.grid_router import GridRouter, _ends_of -from freeroute.route.pipeline import ( +from rpmroute.board import build_board +from rpmroute.board.search_tree import ShapeSearchTree +from rpmroute.dsn import parse_dsn +from rpmroute.geometry.octagon import overlaps_2d +from rpmroute.route import grid_router +from rpmroute.route.exact_router import route_board_exact +from rpmroute.route.grid_router import GridRouter, _ends_of +from rpmroute.route.pipeline import ( _mm_to_board, _net_widths_board, _rule_clearance_dsn, diff --git a/bench/drc_check.sh b/bench/drc_check.sh index db2e9ec..2392546 100755 --- a/bench/drc_check.sh +++ b/bench/drc_check.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# Route one KiCad board with freeroute and judge the result with KiCad's DRC. +# Route one KiCad board with rpmroute and judge the result with KiCad's DRC. # -# bench/drc_check.sh [--keep] [freeroute args...] +# bench/drc_check.sh [--keep] [rpmroute args...] # # Copies the board (and its .kicad_pro) into , strips its tracks and # vias unless --keep is given (then they reach the router as fixed wiring), @@ -10,14 +10,14 @@ # shorts, opens, hole problems (whatever severity the board gives them) and # the other errors. The source board is never touched. # -# FREEROUTE (default: this checkout's .venv/bin/freeroute) and MCK (default +# RPMROUTE (or the older FREEROUTE; default: this checkout's .venv/bin/rpmroute) and MCK (default # ~/claude/mckicad) pick the builds to judge. set -euo pipefail keep=0 if [[ "${1:-}" == "--keep" ]]; then keep=1; shift; fi src="$1"; out="$2"; shift 2 here="$(cd "$(dirname "$0")/.." && pwd)" -FREEROUTE="${FREEROUTE:-$here/.venv/bin/freeroute}" +RPMROUTE="${RPMROUTE:-${FREEROUTE:-$here/.venv/bin/rpmroute}}" MCK="${MCK:-$HOME/claude/mckicad}" rm -rf "$out"; mkdir -p "$out" cp "${src%.kicad_pcb}.kicad_pro" "$out/board.kicad_pro" 2>/dev/null || true @@ -43,7 +43,7 @@ assert FreeRoutingEngine().export_dsn_from_kicad('$out/board.kicad_pcb', '$out/b >/dev/null 2>&1 || { echo "$out: DSN export failed"; exit 1; } start=$(date +%s.%N) -"$FREEROUTE" "$out/board.dsn" -do "$out/board.ses" "$@" >"$out/route.log" 2>&1 || true +"$RPMROUTE" "$out/board.dsn" -do "$out/board.ses" "$@" >"$out/route.log" 2>&1 || true secs=$(python3 -c "print(round($(date +%s.%N)-$start,1))") [[ -s "$out/board.ses" ]] || { echo "$out: no session ($(tail -1 "$out/route.log"))"; exit 1; } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7d4cb01..c55475c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -90,7 +90,7 @@ this is the structure the Python `reader.py` mirrors): **Python port status:** items 1 (tokenizer), the shape grammar, and every structure/library/network/placement/parser/resolution scope above are -implemented in `src/freeroute/dsn/` this session (see below). +implemented in `src/rpmroute/dsn/` this session (see below). ## 2. Board data model — `board/` @@ -175,7 +175,7 @@ converge toward FreeRouting's free-space rooms. Not started this session. into the read result). ## Port order (this file drives the roadmap) -1. **DSN parser** — done this session (`src/freeroute/dsn/`). +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 (base_design ) @@ -184,6 +184,6 @@ converge toward FreeRouting's free-space rooms. Not started this session. 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** — `freeroute board.dsn -o board.ses`. +6. **CLI + kicad-mcp integration** — `rpmroute board.dsn -o board.ses`. diff --git a/docs/PORTING_PLAN.md b/docs/PORTING_PLAN.md index a85f8a4..767d9f3 100644 --- a/docs/PORTING_PLAN.md +++ b/docs/PORTING_PLAN.md @@ -43,7 +43,7 @@ the output. Match that I/O and the rest of `kicad-mcp` is unchanged. 4. **Autorouter core** — maze routing with rip-up-and-retry. Start grid-based Manhattan/45° for an MVP; converge toward FreeRouting's approach. _(TBD.)_ 5. **Optimize pass** — via minimization, trace smoothing. _(TBD.)_ -6. **CLI + kicad-mcp integration** — `freeroute board.dsn -o board.ses`, then +6. **CLI + kicad-mcp integration** — `rpmroute board.dsn -o board.ses`, then swap it in behind `kicad_mcp/utils/freerouting_engine.py`'s JAR step. ## Validation strategy diff --git a/pyproject.toml b/pyproject.toml index 8a5c101..8110a3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,9 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "freeroute" -version = "2026.07.14" -description = "Native Python PCB autorouter — a Java-free port of the FreeRouting engine (Specctra DSN in, SES out)" +name = "rpmroute" +version = "2026.10.07" +description = "Native Python PCB autorouter with negotiated congestion and tree routing (Specctra DSN in, SES out)" readme = "README.md" license = "GPL-3.0-or-later" license-files = ["LICENSE"] @@ -40,14 +40,17 @@ requires-python = ">=3.10" dependencies = [] [project.urls] -"Homepage" = "https://git.supported.systems/warehack.ing/freeroute" -"Repository" = "https://git.supported.systems/warehack.ing/freeroute" -"Issues" = "https://git.supported.systems/warehack.ing/freeroute/issues" -# The Java engine this is a port of; freeroute ships no upstream code. -"Upstream" = "https://github.com/freerouting/freerouting" +"Homepage" = "https://git.supported.systems/warehack.ing/rpmroute" +"Repository" = "https://git.supported.systems/warehack.ing/rpmroute" +"Issues" = "https://git.supported.systems/warehack.ing/rpmroute/issues" +# Where it started: a port of the Java FreeRouting engine. It has since grown +# its own router (negotiation, tree nets, exact checks) and ships no upstream code. +"Origin" = "https://github.com/freerouting/freerouting" [project.scripts] -freeroute = "freeroute.cli:main" +rpmroute = "rpmroute.cli:main" +# the name it had until 2026-10-07, kept so existing scripts keep working +freeroute = "rpmroute.cli:main" [dependency-groups] dev = [ @@ -57,7 +60,7 @@ dev = [ ] [tool.hatch.build.targets.wheel] -packages = ["src/freeroute"] +packages = ["src/rpmroute"] [tool.hatch.build.targets.sdist] # Ship the package, the licence, and the docs — nothing else. `reference/` is a @@ -95,7 +98,7 @@ select = ["E", "W", "F", "I", "B", "C4", "UP", "SIM"] ignore = ["E501"] [tool.ruff.lint.isort] -known-first-party = ["freeroute"] +known-first-party = ["rpmroute"] force-sort-within-sections = true [tool.pytest.ini_options] diff --git a/src/freeroute/__init__.py b/src/freeroute/__init__.py deleted file mode 100644 index 258aa19..0000000 --- a/src/freeroute/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""freeroute — native Python PCB autorouter (Java-free FreeRouting port).""" - -__version__ = "0.1.0" diff --git a/src/rpmroute/__init__.py b/src/rpmroute/__init__.py new file mode 100644 index 0000000..7e0b750 --- /dev/null +++ b/src/rpmroute/__init__.py @@ -0,0 +1,3 @@ +"""rpmroute — native Python PCB autorouter (Java-free FreeRouting port).""" + +__version__ = "0.1.0" diff --git a/src/freeroute/board/__init__.py b/src/rpmroute/board/__init__.py similarity index 92% rename from src/freeroute/board/__init__.py rename to src/rpmroute/board/__init__.py index e1a06a0..fbd957c 100644 --- a/src/freeroute/board/__init__.py +++ b/src/rpmroute/board/__init__.py @@ -1,9 +1,9 @@ -"""Board data model for freeroute. +"""Board data model for rpmroute. The data model a parsed DSN produces and the (future) router operates on: layers, units, coordinate transform, nets, clearance matrix, the item hierarchy, and the :class:`BasicBoard` container. :func:`build_board` constructs a board -from a :class:`~freeroute.dsn.model.DsnBoard`. +from a :class:`~rpmroute.dsn.model.DsnBoard`. The routing operations (``RoutingBoard``) and the spatial ``ShapeSearchTree`` belong to the router phase. diff --git a/src/freeroute/board/board.py b/src/rpmroute/board/board.py similarity index 96% rename from src/freeroute/board/board.py rename to src/rpmroute/board/board.py index a49effa..b0bdb9c 100644 --- a/src/freeroute/board/board.py +++ b/src/rpmroute/board/board.py @@ -9,7 +9,7 @@ board-model invariants check. from __future__ import annotations -from freeroute.geometry import IntBox +from rpmroute.geometry import IntBox from .clearance import ClearanceMatrix from .items import ConductionArea, Item, ObstacleArea, Pin, Trace, Via @@ -74,7 +74,7 @@ class BasicBoard: """Copper already routed on the board (the DSN's ``wiring``), as ``(net, layers, box)``. A track is its segments, its corners and the round caps at its ends (as squares); a via is its largest pad.""" - from freeroute.geometry.polyline import Polyline, PolylineShape, corner_box + from rpmroute.geometry.polyline import Polyline, PolylineShape, corner_box out: list[tuple[int, frozenset[int], IntBox]] = [] for item in self._items: diff --git a/src/freeroute/board/build.py b/src/rpmroute/board/build.py similarity index 98% rename from src/freeroute/board/build.py rename to src/rpmroute/board/build.py index 507e558..e617c35 100644 --- a/src/freeroute/board/build.py +++ b/src/rpmroute/board/build.py @@ -1,6 +1,6 @@ """Build a :class:`BasicBoard` from a parsed DSN :class:`DsnBoard`. -This is the load-bearing integration between :mod:`freeroute.dsn` and the board +This is the load-bearing integration between :mod:`rpmroute.dsn` and the board model. It mirrors what FreeRouting's ``Structure.create_board`` / ``Library`` / ``Component`` / ``Network`` readers do collectively, but on the already-parsed ``DsnBoard``: @@ -26,10 +26,10 @@ from __future__ import annotations import math import re -from freeroute.dsn.model import DsnBoard -from freeroute.dsn.shapes import Circle, Path, Polygon, Rectangle, Shape -from freeroute.geometry import FloatPoint, IntBox, IntPoint, IntVector, Simplex -from freeroute.geometry.polygon_shape import PolygonShape +from rpmroute.dsn.model import DsnBoard +from rpmroute.dsn.shapes import Circle, Path, Polygon, Rectangle, Shape +from rpmroute.geometry import FloatPoint, IntBox, IntPoint, IntVector, Simplex +from rpmroute.geometry.polygon_shape import PolygonShape from .board import BasicBoard from .clearance import ClearanceMatrix diff --git a/src/freeroute/board/clearance.py b/src/rpmroute/board/clearance.py similarity index 100% rename from src/freeroute/board/clearance.py rename to src/rpmroute/board/clearance.py diff --git a/src/freeroute/board/items.py b/src/rpmroute/board/items.py similarity index 93% rename from src/freeroute/board/items.py rename to src/rpmroute/board/items.py index 8a0319c..26b335f 100644 --- a/src/freeroute/board/items.py +++ b/src/rpmroute/board/items.py @@ -3,22 +3,22 @@ Ports the *data model* of ``board/Item.java`` and its subclasses (``Pin``, ``ObstacleArea``/``ConductionArea``, ``Via``, ``PolylineTrace``). The routing and search-tree machinery in the Java classes is out of scope here — these hold -the geometry and connectivity a :class:`~freeroute.board.board.BasicBoard` +the geometry and connectivity a :class:`~rpmroute.board.board.BasicBoard` needs and that the DSN-import invariants check. Coordinates are board units (DSN units scaled by the resolution). Shapes are the -exact convex tiles / boxes from :mod:`freeroute.geometry`. +exact convex tiles / boxes from :mod:`rpmroute.geometry`. ``Trace``/``Via`` are only produced by routing (an imported unrouted board has none); they are lightweight here and the router phase fleshes out -``Trace`` with a proper :class:`~freeroute.geometry.Polyline` shape. +``Trace`` with a proper :class:`~rpmroute.geometry.Polyline` shape. """ from __future__ import annotations from dataclasses import dataclass, field -from freeroute.geometry import IntBox, IntPoint +from rpmroute.geometry import IntBox, IntPoint __all__ = ["Item", "Pin", "ObstacleArea", "ConductionArea", "Via", "Trace"] @@ -126,7 +126,7 @@ class Trace(Item): """A routed trace (router-produced). Held as its corner points plus a half-width; the router phase attaches the - full :class:`~freeroute.geometry.Polyline` swept shape. + full :class:`~rpmroute.geometry.Polyline` swept shape. """ layer: int = 0 diff --git a/src/freeroute/board/layer.py b/src/rpmroute/board/layer.py similarity index 100% rename from src/freeroute/board/layer.py rename to src/rpmroute/board/layer.py diff --git a/src/freeroute/board/net.py b/src/rpmroute/board/net.py similarity index 100% rename from src/freeroute/board/net.py rename to src/rpmroute/board/net.py diff --git a/src/freeroute/board/outline.py b/src/rpmroute/board/outline.py similarity index 98% rename from src/freeroute/board/outline.py rename to src/rpmroute/board/outline.py index 025978c..cf0cb5c 100644 --- a/src/freeroute/board/outline.py +++ b/src/rpmroute/board/outline.py @@ -9,7 +9,7 @@ from __future__ import annotations import math -from freeroute.geometry import IntBox, IntPoint +from rpmroute.geometry import IntBox, IntPoint class BoardOutline: diff --git a/src/freeroute/board/search_tree.py b/src/rpmroute/board/search_tree.py similarity index 97% rename from src/freeroute/board/search_tree.py rename to src/rpmroute/board/search_tree.py index 4bf17e5..db48847 100644 --- a/src/freeroute/board/search_tree.py +++ b/src/rpmroute/board/search_tree.py @@ -2,7 +2,7 @@ Ports the query role of ``board/ShapeSearchTree*.java``. It replaces the exact router's occupancy grid: instead of rasterizing shapes into cells, it indexes -each item's convex tiles (:class:`~freeroute.geometry.IntBox`) and answers +each item's convex tiles (:class:`~rpmroute.geometry.IntBox`) and answers overlap / region queries with a broad phase (a bounding-box spatial hash) and an **exact** narrow phase (exact ``IntBox`` intersection). Insert and remove keep the index in step as traces are added. @@ -18,7 +18,7 @@ from __future__ import annotations from dataclasses import dataclass -from freeroute.geometry import IntBox, IntPoint, Simplex, overlaps_2d, segment_octagon, segment_slab +from rpmroute.geometry import IntBox, IntPoint, Simplex, overlaps_2d, segment_octagon, segment_slab __all__ = ["TreeShape", "ShapeSearchTree"] @@ -34,7 +34,7 @@ class TreeShape: ``exact`` / ``seg`` are set only for a 45-degree (diagonal) trace tile: ``tile`` is then the bounding box (broad-phase only), ``exact`` is the tight octagon - copper :class:`~freeroute.geometry.Simplex`, and ``seg`` is ``(a, b, + copper :class:`~rpmroute.geometry.Simplex`, and ``seg`` is ``(a, b, half_width)`` so the clearance region can be rebuilt exactly on demand. For every orthogonal tile both are ``None`` and all code paths are byte-identical to the box-only tree. diff --git a/src/freeroute/board/transform.py b/src/rpmroute/board/transform.py similarity index 97% rename from src/freeroute/board/transform.py rename to src/rpmroute/board/transform.py index cc3101f..6987bc5 100644 --- a/src/freeroute/board/transform.py +++ b/src/rpmroute/board/transform.py @@ -8,7 +8,7 @@ resolution`` and zero offset (see ``Structure.create_board``). from __future__ import annotations -from freeroute.geometry import FloatPoint, IntPoint +from rpmroute.geometry import FloatPoint, IntPoint __all__ = ["CoordinateTransform"] diff --git a/src/freeroute/board/unit.py b/src/rpmroute/board/unit.py similarity index 100% rename from src/freeroute/board/unit.py rename to src/rpmroute/board/unit.py diff --git a/src/freeroute/cli.py b/src/rpmroute/cli.py similarity index 94% rename from src/freeroute/cli.py rename to src/rpmroute/cli.py index d21c8fa..468d929 100644 --- a/src/freeroute/cli.py +++ b/src/rpmroute/cli.py @@ -1,8 +1,8 @@ -"""freeroute command-line interface: Specctra DSN in, routed SES out. +"""rpmroute command-line interface: Specctra DSN in, routed SES out. Flag-compatible with the FreeRouting JAR (``-de`` / ``-do`` / ``-mp``) so it can be dropped in wherever ``java -jar freerouting.jar`` was invoked, plus the -freeroute-specific engine selection (``--engine``) and its opt-in passes. +rpmroute-specific engine selection (``--engine``) and its opt-in passes. Not every engine supports every pass. The matrix below is the single source of truth; an unsupported combination is an error, never a silent no-op:: @@ -18,14 +18,14 @@ from __future__ import annotations import argparse import sys -from freeroute.dsn.reader import parse_dsn -from freeroute.route.orders import best_of_orders, promote_failures -from freeroute.route.pipeline import ( +from rpmroute.dsn.reader import parse_dsn +from rpmroute.route.orders import best_of_orders, promote_failures +from rpmroute.route.pipeline import ( build_exact_routing_result, build_rooms_routing_result, build_routing_result, ) -from freeroute.ses import write_ses +from rpmroute.ses import write_ses #: which optional flags each engine actually implements. Anything not listed for #: an engine is rejected by :func:`_validate` rather than quietly ignored. @@ -74,7 +74,7 @@ def _validate(parser: argparse.ArgumentParser, engine: str, requested: list[str] def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - prog="freeroute", + prog="rpmroute", description="Native PCB autorouter — route a Specctra DSN and write an SES session file.", ) parser.add_argument("dsn", nargs="?", help="input Specctra .dsn file") @@ -291,7 +291,7 @@ def main(argv: list[str] | None = None) -> int: with open(dsn_path, encoding="utf-8") as f: dsn_text = f.read() except OSError as e: - print(f"freeroute: cannot read {dsn_path}: {e}", file=sys.stderr) + print(f"rpmroute: cannot read {dsn_path}: {e}", file=sys.stderr) return 2 dsn = parse_dsn(dsn_text) @@ -346,14 +346,14 @@ def main(argv: list[str] | None = None) -> int: dsn_text, args.engine, options, rounds=args.promote_failures, seed=args.seed ) counts = ", ".join(str(m) for m, _z in rounds) - print(f"freeroute: {len(rounds)} rounds, unrouted {counts}; kept the best", file=sys.stderr) + print(f"rpmroute: {len(rounds)} rounds, unrouted {counts}; kept the best", file=sys.stderr) elif args.orders and args.orders > 1: result, kept, tried = best_of_orders( dsn_text, args.engine, options, orders=args.orders, seed=args.seed, jobs=args.jobs ) counts = ", ".join(f"{'default' if s is None else s}: {m}" for s, (m, _z) in tried) print( - f"freeroute: tried {len(tried)} orders, kept " + f"rpmroute: tried {len(tried)} orders, kept " f"{'the default order' if kept is None else f'--seed {kept}'} (unrouted {counts})", file=sys.stderr, ) @@ -367,12 +367,12 @@ def main(argv: list[str] | None = None) -> int: in_zone = [u for u in result.unrouted if u[3]] if missing: # the SES is still written; say plainly what it is missing - print(f"freeroute: {len(missing)} connection(s) not routed:", file=sys.stderr) + print(f"rpmroute: {len(missing)} connection(s) not routed:", file=sys.stderr) for net, a, b, _zone in missing: print(f" {net}: {a} to {b}", file=sys.stderr) if result.narrowed: print( - f"freeroute: {len(result.narrowed)} connection(s) of a wider net class only fitted " + f"rpmroute: {len(result.narrowed)} connection(s) of a wider net class only fitted " "at the default width; their nets are written at the default width:", file=sys.stderr, ) @@ -380,7 +380,7 @@ def main(argv: list[str] | None = None) -> int: print(f" {net}: {a} to {b}", file=sys.stderr) if in_zone: print( - f"freeroute: {len(in_zone)} more connection(s) left to a zone fill that may join them:", + f"rpmroute: {len(in_zone)} more connection(s) left to a zone fill that may join them:", file=sys.stderr, ) for net, a, b, zone in in_zone: diff --git a/src/freeroute/dsn/__init__.py b/src/rpmroute/dsn/__init__.py similarity index 79% rename from src/freeroute/dsn/__init__.py rename to src/rpmroute/dsn/__init__.py index c6364f2..3a1385c 100644 --- a/src/freeroute/dsn/__init__.py +++ b/src/rpmroute/dsn/__init__.py @@ -1,13 +1,13 @@ -"""Specctra DSN parsing for freeroute. +"""Specctra DSN parsing for rpmroute. Public API:: - from freeroute.dsn import parse_dsn + from rpmroute.dsn import parse_dsn board = parse_dsn(dsn_text) -``parse_dsn`` returns a typed :class:`~freeroute.dsn.model.DsnBoard`. The -lower-level :func:`~freeroute.dsn.tokenizer.tokenize` and -:func:`~freeroute.dsn.sexp.parse` are exposed for tooling and tests. +``parse_dsn`` returns a typed :class:`~rpmroute.dsn.model.DsnBoard`. The +lower-level :func:`~rpmroute.dsn.tokenizer.tokenize` and +:func:`~rpmroute.dsn.sexp.parse` are exposed for tooling and tests. """ from __future__ import annotations diff --git a/src/freeroute/dsn/model.py b/src/rpmroute/dsn/model.py similarity index 100% rename from src/freeroute/dsn/model.py rename to src/rpmroute/dsn/model.py diff --git a/src/freeroute/dsn/reader.py b/src/rpmroute/dsn/reader.py similarity index 100% rename from src/freeroute/dsn/reader.py rename to src/rpmroute/dsn/reader.py diff --git a/src/freeroute/dsn/sexp.py b/src/rpmroute/dsn/sexp.py similarity index 97% rename from src/freeroute/dsn/sexp.py rename to src/rpmroute/dsn/sexp.py index 8481d4b..4e1591c 100644 --- a/src/freeroute/dsn/sexp.py +++ b/src/rpmroute/dsn/sexp.py @@ -22,7 +22,7 @@ class SExp: ``head`` is the text of the first token when it is an atom (e.g. ``pcb``, ``layer``, ``net``), else ``None``. ``items`` holds the remaining elements, - each either a :class:`~freeroute.dsn.tokenizer.Token` (a leaf atom/string) + each either a :class:`~rpmroute.dsn.tokenizer.Token` (a leaf atom/string) or a nested :class:`SExp`. """ diff --git a/src/freeroute/dsn/shapes.py b/src/rpmroute/dsn/shapes.py similarity index 100% rename from src/freeroute/dsn/shapes.py rename to src/rpmroute/dsn/shapes.py diff --git a/src/freeroute/dsn/tokenizer.py b/src/rpmroute/dsn/tokenizer.py similarity index 100% rename from src/freeroute/dsn/tokenizer.py rename to src/rpmroute/dsn/tokenizer.py diff --git a/src/freeroute/geometry/__init__.py b/src/rpmroute/geometry/__init__.py similarity index 97% rename from src/freeroute/geometry/__init__.py rename to src/rpmroute/geometry/__init__.py index 2b259b3..8b65132 100644 --- a/src/freeroute/geometry/__init__.py +++ b/src/rpmroute/geometry/__init__.py @@ -1,4 +1,4 @@ -"""Planar geometry primitives for freeroute. +"""Planar geometry primitives for rpmroute. A port of the subset of FreeRouting's ``geometry/planar`` package the router needs, with the exact-arithmetic types preserved. Arithmetic model per type: diff --git a/src/freeroute/geometry/box.py b/src/rpmroute/geometry/box.py similarity index 99% rename from src/freeroute/geometry/box.py rename to src/rpmroute/geometry/box.py index fea215a..25705dd 100644 --- a/src/freeroute/geometry/box.py +++ b/src/rpmroute/geometry/box.py @@ -231,7 +231,7 @@ class IntBox: return [FloatPoint(c.x, c.y) for c in self.corners()] def to_simplex(self): - """Return this box as a :class:`~freeroute.geometry.simplex.Simplex`. + """Return this box as a :class:`~rpmroute.geometry.simplex.Simplex`. Four directed border lines whose common right side is the box interior (ports ``IntBox.to_Simplex``). diff --git a/src/freeroute/geometry/direction.py b/src/rpmroute/geometry/direction.py similarity index 100% rename from src/freeroute/geometry/direction.py rename to src/rpmroute/geometry/direction.py diff --git a/src/freeroute/geometry/float_point.py b/src/rpmroute/geometry/float_point.py similarity index 95% rename from src/freeroute/geometry/float_point.py rename to src/rpmroute/geometry/float_point.py index 31c0028..eccb408 100644 --- a/src/freeroute/geometry/float_point.py +++ b/src/rpmroute/geometry/float_point.py @@ -6,7 +6,7 @@ Ports ``geometry/planar/FloatPoint.java``. keeps ``FloatPoint`` *outside* the exact ``Point`` hierarchy because float math is inexact; it is used for distances, rounding, and speed-over-accuracy paths. Its :meth:`side_of` has no reliable ``COLLINEAR`` result (numerical noise), so -exact side tests must go through :class:`~freeroute.geometry.point.Point`. +exact side tests must go through :class:`~rpmroute.geometry.point.Point`. """ from __future__ import annotations @@ -137,13 +137,13 @@ class FloatPoint: # --- rounding to integer points ---------------------------------------- def round(self): - """Round to the nearest :class:`~freeroute.geometry.point.IntPoint`.""" + """Round to the nearest :class:`~rpmroute.geometry.point.IntPoint`.""" from .point import IntPoint return IntPoint(_round_half_up(self.x), _round_half_up(self.y)) def bounding_box(self): - """Smallest :class:`~freeroute.geometry.box.IntBox` containing this point.""" + """Smallest :class:`~rpmroute.geometry.box.IntBox` containing this point.""" from .box import IntBox return IntBox( diff --git a/src/freeroute/geometry/limits.py b/src/rpmroute/geometry/limits.py similarity index 100% rename from src/freeroute/geometry/limits.py rename to src/rpmroute/geometry/limits.py diff --git a/src/freeroute/geometry/line.py b/src/rpmroute/geometry/line.py similarity index 98% rename from src/freeroute/geometry/line.py rename to src/rpmroute/geometry/line.py index 41ae3bd..5b6041e 100644 --- a/src/freeroute/geometry/line.py +++ b/src/rpmroute/geometry/line.py @@ -2,12 +2,12 @@ Ports ``geometry/planar/Line.java`` (the subset needed for the router's half-plane work). Like the upstream class, a ``Line`` is defined by two -:class:`~freeroute.geometry.point.IntPoint` endpoints. +:class:`~rpmroute.geometry.point.IntPoint` endpoints. **Arithmetic model: exact.** :meth:`side_of` uses an exact integer determinant, -and :meth:`intersection` returns an :class:`~freeroute.geometry.point.IntPoint` +and :meth:`intersection` returns an :class:`~rpmroute.geometry.point.IntPoint` when the crossing is integral and a -:class:`~freeroute.geometry.point.RationalPoint` otherwise (``z == 0`` when the +:class:`~rpmroute.geometry.point.RationalPoint` otherwise (``z == 0`` when the lines are parallel — the "point at infinity"). :meth:`intersection_approx` is the fast ``float`` path. """ diff --git a/src/freeroute/geometry/octagon.py b/src/rpmroute/geometry/octagon.py similarity index 100% rename from src/freeroute/geometry/octagon.py rename to src/rpmroute/geometry/octagon.py diff --git a/src/freeroute/geometry/point.py b/src/rpmroute/geometry/point.py similarity index 100% rename from src/freeroute/geometry/point.py rename to src/rpmroute/geometry/point.py diff --git a/src/freeroute/geometry/polygon.py b/src/rpmroute/geometry/polygon.py similarity index 97% rename from src/freeroute/geometry/polygon.py rename to src/rpmroute/geometry/polygon.py index adb0795..50439a6 100644 --- a/src/freeroute/geometry/polygon.py +++ b/src/rpmroute/geometry/polygon.py @@ -1,7 +1,7 @@ """``Polygon`` — a normalized corner list (no duplicate or collinear corners). Ports ``geometry/planar/Polygon.java``. Used as the corner normalizer behind -:class:`~freeroute.geometry.polygon_shape.PolygonShape`. +:class:`~rpmroute.geometry.polygon_shape.PolygonShape`. **Arithmetic model:** corner de-duplication and collinearity removal are **exact** (exact ``Point.side_of``); the winding-number test is approximate diff --git a/src/freeroute/geometry/polygon_shape.py b/src/rpmroute/geometry/polygon_shape.py similarity index 99% rename from src/freeroute/geometry/polygon_shape.py rename to src/rpmroute/geometry/polygon_shape.py index 5ee36a6..9d8905b 100644 --- a/src/freeroute/geometry/polygon_shape.py +++ b/src/rpmroute/geometry/polygon_shape.py @@ -2,7 +2,7 @@ Ports ``geometry/planar/PolygonShape.java``, in particular the recursive ``split_to_convex`` that decomposes a simple polygon into convex -:class:`~freeroute.geometry.simplex.Simplex` tiles — the decomposition the +:class:`~rpmroute.geometry.simplex.Simplex` tiles — the decomposition the router's search tree relies on. **Arithmetic model:** corner orientation / convexity tests are **exact** diff --git a/src/freeroute/geometry/polyline.py b/src/rpmroute/geometry/polyline.py similarity index 98% rename from src/freeroute/geometry/polyline.py rename to src/rpmroute/geometry/polyline.py index b5ccbb4..7f36de0 100644 --- a/src/freeroute/geometry/polyline.py +++ b/src/rpmroute/geometry/polyline.py @@ -7,7 +7,7 @@ trace half-width — the copper region — as a set of exact convex tiles. **Arithmetic model: exact, integer.** The router that uses this routes *orthogonally* (axis-aligned segments), so each swept segment is an exact -:class:`~freeroute.geometry.box.IntBox` (centreline offset by the half-width, +:class:`~rpmroute.geometry.box.IntBox` (centreline offset by the half-width, squared at the ends). The 45-degree case (which needs ``IntOctagon`` caps to stay exact) is deferred; ``segment_box`` returns the axis-aligned bounding box for a diagonal segment, which is a *conservative* cover (it never under-reports diff --git a/src/freeroute/geometry/side.py b/src/rpmroute/geometry/side.py similarity index 100% rename from src/freeroute/geometry/side.py rename to src/rpmroute/geometry/side.py diff --git a/src/freeroute/geometry/simplex.py b/src/rpmroute/geometry/simplex.py similarity index 98% rename from src/freeroute/geometry/simplex.py rename to src/rpmroute/geometry/simplex.py index 844387a..d6840b8 100644 --- a/src/freeroute/geometry/simplex.py +++ b/src/rpmroute/geometry/simplex.py @@ -1,10 +1,10 @@ """``Simplex`` — a convex region as the intersection of directed half-planes. Ports ``geometry/planar/Simplex.java``. A simplex is defined by an array of -directed :class:`~freeroute.geometry.line.Line`s sorted by direction angle; the +directed :class:`~rpmroute.geometry.line.Line`s sorted by direction angle; the region is the intersection of the *right* half-plane of each line. Corners are the exact intersections of consecutive border lines -(:meth:`~freeroute.geometry.line.Line.intersection`). +(:meth:`~rpmroute.geometry.line.Line.intersection`). **Arithmetic model: exact where it matters.** Corners and all point-containment tests are exact (exact line intersection + exact ``side_of``). ``offset`` is diff --git a/src/freeroute/geometry/tile.py b/src/rpmroute/geometry/tile.py similarity index 97% rename from src/freeroute/geometry/tile.py rename to src/rpmroute/geometry/tile.py index 7e45ee0..918c02d 100644 --- a/src/freeroute/geometry/tile.py +++ b/src/rpmroute/geometry/tile.py @@ -3,11 +3,11 @@ Ports the concrete (non-abstract) behaviour of ``geometry/planar/TileShape.java`` that is defined purely in terms of the border lines and corners: point containment, area, centre of gravity, and border tests. Concrete subclasses -(:class:`~freeroute.geometry.simplex.Simplex`) supply ``border_line_count``, +(:class:`~rpmroute.geometry.simplex.Simplex`) supply ``border_line_count``, ``border_line``, ``corner`` and ``corner_approx``. **Arithmetic model:** containment and ``contains_on_border`` are **exact** -(they use the exact :meth:`~freeroute.geometry.line.Line.side_of`); ``area`` and +(they use the exact :meth:`~rpmroute.geometry.line.Line.side_of`); ``area`` and ``centre_of_gravity`` are approximate (``float``), as upstream. A convex tile is the intersection of the right half-planes of its directed diff --git a/src/freeroute/geometry/vector.py b/src/rpmroute/geometry/vector.py similarity index 99% rename from src/freeroute/geometry/vector.py rename to src/rpmroute/geometry/vector.py index 507b585..153a548 100644 --- a/src/freeroute/geometry/vector.py +++ b/src/rpmroute/geometry/vector.py @@ -13,7 +13,7 @@ Ports ``geometry/planar/{Vector,IntVector,RationalVector}.java``. representing the affine vector ``(x/z, y/z)``; ``z`` is kept non-negative. Mirrors the upstream ``BigInteger`` triple. ``BigIntDirection`` is not needed because ``int`` is unbounded, so normalized directions are always - :class:`~freeroute.geometry.direction.IntDirection`. + :class:`~rpmroute.geometry.direction.IntDirection`. """ from __future__ import annotations diff --git a/src/freeroute/py.typed b/src/rpmroute/py.typed similarity index 100% rename from src/freeroute/py.typed rename to src/rpmroute/py.typed diff --git a/src/freeroute/route/__init__.py b/src/rpmroute/route/__init__.py similarity index 89% rename from src/freeroute/route/__init__.py rename to src/rpmroute/route/__init__.py index 368dc95..dd8c40c 100644 --- a/src/freeroute/route/__init__.py +++ b/src/rpmroute/route/__init__.py @@ -1,9 +1,9 @@ -"""Autorouting for freeroute. +"""Autorouting for rpmroute. An MVP grid-based maze router and the DSN-in / SES-out pipeline that makes -freeroute a Java-free replacement for the ``freerouting.jar`` step:: +rpmroute a Java-free replacement for the ``freerouting.jar`` step:: - from freeroute.route import route + from rpmroute.route import route ses_text = route(dsn_text) The router (:class:`GridRouter`) is intentionally simple — single-layer A* over diff --git a/src/freeroute/route/exact_router.py b/src/rpmroute/route/exact_router.py similarity index 97% rename from src/freeroute/route/exact_router.py rename to src/rpmroute/route/exact_router.py index f842b71..eef2f88 100644 --- a/src/freeroute/route/exact_router.py +++ b/src/rpmroute/route/exact_router.py @@ -1,18 +1,18 @@ """Exact-geometry routing track (orthogonal), built on the search tree. -The grid MVP (:mod:`freeroute.route.grid_router`) approximates clearance with +The grid MVP (:mod:`rpmroute.route.grid_router`) approximates clearance with cell occupancy and emits centrelines with no width-aware DRC check. This track keeps the grid's rip-up/multi-layer/via *topology* search (in orthogonal mode, so segments are axis-aligned) but replaces the occupancy check with an **exact** -one: every item's copper is an exact :class:`~freeroute.geometry.IntBox` tile -indexed in a :class:`~freeroute.board.search_tree.ShapeSearchTree`, and the +one: every item's copper is an exact :class:`~rpmroute.geometry.IntBox` tile +indexed in a :class:`~rpmroute.board.search_tree.ShapeSearchTree`, and the routed board is verified to have **no clearance violation** by exact tile intersection. A net whose copper would violate is dropped, so the output is DRC-clean by construction. **Shove (opt-in, ``shove=True``).** Before a dropped net is abandoned, the router tries to make room by *moving an existing trace aside* geometrically -(:func:`~freeroute.geometry.shove_segment`) rather than dropping the incoming +(:func:`~rpmroute.geometry.shove_segment`) rather than dropping the incoming net. A shove is accepted only if the moved trace still connects its pads, clears every other item exactly (via the tree), and stays in the board outline — so cleanliness is preserved. Shoves are bounded (``max_shove_depth`` and a @@ -30,10 +30,10 @@ from __future__ import annotations from dataclasses import dataclass, field import math -from freeroute.board.board import BasicBoard -from freeroute.board.items import Pin, Via -from freeroute.board.search_tree import ShapeSearchTree, TreeShape -from freeroute.geometry import ( +from rpmroute.board.board import BasicBoard +from rpmroute.board.items import Pin, Via +from rpmroute.board.search_tree import ShapeSearchTree, TreeShape +from rpmroute.geometry import ( IntBox, IntPoint, Polyline, @@ -44,7 +44,7 @@ from freeroute.geometry import ( segment_slab, shove_segment, ) -from freeroute.geometry.polyline import corner_box +from rpmroute.geometry.polyline import corner_box from .grid_router import RouteResult, route_board @@ -633,7 +633,7 @@ def _conflicting_segment(corners, d_boxes, half_width, clearance): horizontal = a.y == b.y if not horizontal and a.x != b.x: continue # skip diagonal grid stubs - from freeroute.geometry import segment_box + from rpmroute.geometry import segment_box seg = segment_box(a, b, half_width).offset(clearance) if any(seg.overlaps(box) for box in d_boxes): diff --git a/src/freeroute/route/grid_router.py b/src/rpmroute/route/grid_router.py similarity index 99% rename from src/freeroute/route/grid_router.py rename to src/rpmroute/route/grid_router.py index 8c27dbc..3da055d 100644 --- a/src/freeroute/route/grid_router.py +++ b/src/rpmroute/route/grid_router.py @@ -31,11 +31,11 @@ import itertools import math import random -from freeroute.board.board import BasicBoard -from freeroute.board.items import ConductionArea, ObstacleArea, Pin, Via -from freeroute.geometry import IntBox, IntPoint, Simplex -from freeroute.geometry.octagon import overlaps_2d, segment_slab -from freeroute.geometry.polyline import corner_box, segment_box +from rpmroute.board.board import BasicBoard +from rpmroute.board.items import ConductionArea, ObstacleArea, Pin, Via +from rpmroute.geometry import IntBox, IntPoint, Simplex +from rpmroute.geometry.octagon import overlaps_2d, segment_slab +from rpmroute.geometry.polyline import corner_box, segment_box __all__ = ["GridRouter", "RouteResult", "route_board"] @@ -1096,7 +1096,7 @@ class GridRouter: def _stub_cells(self, pin_location: IntPoint, cell: Cell, half: int | None = None) -> list[Cell]: """Cells where another net's trace would come too close to the stub.""" - from freeroute.board.outline import _point_segment + from rpmroute.board.outline import _point_segment points = self._stub_points(pin_location, cell) reach = (half or self._cur_half) + self.half_width + self.clearance @@ -1889,8 +1889,8 @@ class GridRouter: """The net's tracks and vias already on the board, as connectivity.""" cache = getattr(self, "_fixed_cache", None) if cache is None: - from freeroute.board.items import Trace - from freeroute.geometry.polyline import Polyline, PolylineShape, corner_box + from rpmroute.board.items import Trace + from rpmroute.geometry.polyline import Polyline, PolylineShape, corner_box cache = self._fixed_cache = {} for item in self.board.get_items(): diff --git a/src/freeroute/route/orders.py b/src/rpmroute/route/orders.py similarity index 98% rename from src/freeroute/route/orders.py rename to src/rpmroute/route/orders.py index 54a31e4..cf4b616 100644 --- a/src/freeroute/route/orders.py +++ b/src/rpmroute/route/orders.py @@ -15,8 +15,8 @@ from __future__ import annotations from concurrent.futures import ProcessPoolExecutor import os -from freeroute.dsn import parse_dsn -from freeroute.ses import RoutingResult +from rpmroute.dsn import parse_dsn +from rpmroute.ses import RoutingResult from .pipeline import build_exact_routing_result, build_routing_result diff --git a/src/freeroute/route/pipeline.py b/src/rpmroute/route/pipeline.py similarity index 99% rename from src/freeroute/route/pipeline.py rename to src/rpmroute/route/pipeline.py index 83f9068..6f376b4 100644 --- a/src/freeroute/route/pipeline.py +++ b/src/rpmroute/route/pipeline.py @@ -15,9 +15,9 @@ from __future__ import annotations import re import time -from freeroute.board import build_board -from freeroute.dsn import DsnBoard, parse_dsn -from freeroute.ses import RoutedVia, RoutedWire, RoutingResult, write_ses +from rpmroute.board import build_board +from rpmroute.dsn import DsnBoard, parse_dsn +from rpmroute.ses import RoutedVia, RoutedWire, RoutingResult, write_ses from .exact_router import ExactRouteResult, route_board_exact from .grid_router import RouteResult, route_board diff --git a/src/freeroute/route/room_router.py b/src/rpmroute/route/room_router.py similarity index 99% rename from src/freeroute/route/room_router.py rename to src/rpmroute/route/room_router.py index 04254ac..aa89161 100644 --- a/src/freeroute/route/room_router.py +++ b/src/rpmroute/route/room_router.py @@ -30,10 +30,10 @@ from __future__ import annotations from dataclasses import dataclass, field import heapq -from freeroute.board.board import BasicBoard -from freeroute.board.items import Pin -from freeroute.board.search_tree import ShapeSearchTree, TreeShape -from freeroute.geometry import IntBox, IntPoint, Polyline, PolylineShape, shove_segment +from rpmroute.board.board import BasicBoard +from rpmroute.board.items import Pin +from rpmroute.board.search_tree import ShapeSearchTree, TreeShape +from rpmroute.geometry import IntBox, IntPoint, Polyline, PolylineShape, shove_segment from .exact_router import ExactRouteResult, _routable_nets from .grid_router import RouteResult @@ -56,7 +56,7 @@ class _Accepted: ``wires`` / ``vias`` are the *same* list objects as ``RouteResult.wires`` / ``.vias`` for this net, so rewriting a shoved wire in place also updates the routed output. ``wire_owners`` is parallel to ``wires``: each entry is the - list of :class:`~freeroute.board.search_tree.TreeShape` owner ids for that + list of :class:`~rpmroute.board.search_tree.TreeShape` owner ids for that wire's copper tiles, so a later shove can locate, remove, and roll back a specific wire without disturbing a multi-wire net's other connections. """ diff --git a/src/freeroute/route/shove.py b/src/rpmroute/route/shove.py similarity index 95% rename from src/freeroute/route/shove.py rename to src/rpmroute/route/shove.py index 76a2ca3..5dad084 100644 --- a/src/freeroute/route/shove.py +++ b/src/rpmroute/route/shove.py @@ -1,6 +1,6 @@ """Shared orthogonal shove primitives for the exact and room routers. -The exact-geometry router (:mod:`freeroute.route.exact_router`) grew the first +The exact-geometry router (:mod:`rpmroute.route.exact_router`) grew the first shove implementation; the continuous room router reuses its geometry so both tracks agree on how a blocking trace's conflicting segment is found, how the perpendicular displacement ladder is generated, and how a reshaped trace is @@ -19,8 +19,8 @@ from __future__ import annotations from collections.abc import Iterator -from freeroute.board.search_tree import ShapeSearchTree, TreeShape -from freeroute.geometry import IntBox, IntPoint, Polyline, PolylineShape +from rpmroute.board.search_tree import ShapeSearchTree, TreeShape +from rpmroute.geometry import IntBox, IntPoint, Polyline, PolylineShape from .exact_router import _conflicting_segment as conflicting_segment diff --git a/src/freeroute/route/window.py b/src/rpmroute/route/window.py similarity index 98% rename from src/freeroute/route/window.py rename to src/rpmroute/route/window.py index 7fb0b47..b625025 100644 --- a/src/freeroute/route/window.py +++ b/src/rpmroute/route/window.py @@ -26,10 +26,10 @@ from __future__ import annotations import time from typing import Any -from freeroute.board import BasicBoard, build_board -from freeroute.board.items import ObstacleArea, Pin, Trace, Via -from freeroute.dsn import DsnBoard -from freeroute.geometry import IntBox, IntPoint +from rpmroute.board import BasicBoard, build_board +from rpmroute.board.items import ObstacleArea, Pin, Trace, Via +from rpmroute.dsn import DsnBoard +from rpmroute.geometry import IntBox, IntPoint from .exact_router import route_board_exact from .grid_router import RouteResult diff --git a/src/freeroute/ses/__init__.py b/src/rpmroute/ses/__init__.py similarity index 79% rename from src/freeroute/ses/__init__.py rename to src/rpmroute/ses/__init__.py index 9519ffd..caa7c3c 100644 --- a/src/freeroute/ses/__init__.py +++ b/src/rpmroute/ses/__init__.py @@ -1,9 +1,9 @@ -"""Specctra SES (session) writing for freeroute. +"""Specctra SES (session) writing for rpmroute. Public API:: - from freeroute.dsn import parse_dsn - from freeroute.ses import write_ses, RoutingResult, RoutedWire, RoutedVia + from rpmroute.dsn import parse_dsn + from rpmroute.ses import write_ses, RoutingResult, RoutedWire, RoutedVia board = parse_dsn(dsn_text) ses_text = write_ses(board) # valid no-op session diff --git a/src/freeroute/ses/indent.py b/src/rpmroute/ses/indent.py similarity index 100% rename from src/freeroute/ses/indent.py rename to src/rpmroute/ses/indent.py diff --git a/src/freeroute/ses/model.py b/src/rpmroute/ses/model.py similarity index 100% rename from src/freeroute/ses/model.py rename to src/rpmroute/ses/model.py diff --git a/src/freeroute/ses/writer.py b/src/rpmroute/ses/writer.py similarity index 97% rename from src/freeroute/ses/writer.py rename to src/rpmroute/ses/writer.py index 8c601d0..f82e5ea 100644 --- a/src/freeroute/ses/writer.py +++ b/src/rpmroute/ses/writer.py @@ -1,8 +1,8 @@ """Specctra SES session-file writer. Ports the write path of FreeRouting's ``io/specctra/SesWriter.java`` to operate -on this project's parsed :class:`~freeroute.dsn.model.DsnBoard` plus an -in-memory :class:`~freeroute.ses.model.RoutingResult`. The emitted structure is:: +on this project's parsed :class:`~rpmroute.dsn.model.DsnBoard` plus an +in-memory :class:`~rpmroute.ses.model.RoutingResult`. The emitted structure is:: (session (base_design ) @@ -30,8 +30,8 @@ from __future__ import annotations import math -from freeroute.dsn.model import DsnBoard -from freeroute.dsn.shapes import Circle, Path, Polygon, Rectangle, Shape +from rpmroute.dsn.model import DsnBoard +from rpmroute.dsn.shapes import Circle, Path, Polygon, Rectangle, Shape from .indent import Identifier, IndentWriter from .model import RoutedNet, RoutedVia, RoutedWire, RoutingResult diff --git a/tests/board/test_board.py b/tests/board/test_board.py index 96e37be..7cf799d 100644 --- a/tests/board/test_board.py +++ b/tests/board/test_board.py @@ -2,7 +2,7 @@ from __future__ import annotations -from freeroute.board import ( +from rpmroute.board import ( ClearanceMatrix, CoordinateTransform, Layer, diff --git a/tests/board/test_build.py b/tests/board/test_build.py index e6d246a..4b9c076 100644 --- a/tests/board/test_build.py +++ b/tests/board/test_build.py @@ -15,9 +15,9 @@ import sys import pytest -from freeroute.board import BasicBoard, ObstacleArea, Pin, build_board -from freeroute.dsn import parse_dsn -from freeroute.geometry import IntPoint +from rpmroute.board import BasicBoard, ObstacleArea, Pin, build_board +from rpmroute.dsn import parse_dsn +from rpmroute.geometry import IntPoint sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) @@ -184,7 +184,7 @@ def test_place_keepout_does_not_block_routing(): def test_grid_router_leaves_the_interior_of_a_ring_keepout_open(): - from freeroute.route.pipeline import route_dsn_board + from rpmroute.route.pipeline import route_dsn_board ring_only = "\n".join( line for line in _RING_DSN.splitlines() if "wire_keepout" not in line @@ -251,7 +251,7 @@ def test_oval_pad_keeps_its_width(): def test_via_size_comes_from_the_via_padstack(): - from freeroute.route.pipeline import _via_half_board + from rpmroute.route.pipeline import _via_half_board assert _via_half_board(parse_dsn(_PAD_DSN), 10) == 3000 # 0.6 mm via, board units @@ -279,7 +279,7 @@ def test_a_keepout_named_rule_area_takes_no_clearance(): def _route_channel(name): - from freeroute.route.pipeline import build_exact_routing_result + from rpmroute.route.pipeline import build_exact_routing_result return build_exact_routing_result(parse_dsn(_CHANNEL_DSN.format(name=name)), resolution=2) diff --git a/tests/board/test_search_tree.py b/tests/board/test_search_tree.py index 748a7fb..fc31be0 100644 --- a/tests/board/test_search_tree.py +++ b/tests/board/test_search_tree.py @@ -2,8 +2,8 @@ from __future__ import annotations -from freeroute.board import ShapeSearchTree, TreeShape -from freeroute.geometry import IntBox +from rpmroute.board import ShapeSearchTree, TreeShape +from rpmroute.geometry import IntBox def shape(owner, net, layer, box, routed=False): diff --git a/tests/dsn/test_reader.py b/tests/dsn/test_reader.py index 3574f96..df6ad16 100644 --- a/tests/dsn/test_reader.py +++ b/tests/dsn/test_reader.py @@ -6,10 +6,10 @@ from pathlib import Path import pytest -from freeroute.dsn import parse_dsn -from freeroute.dsn.reader import DsnParseError -from freeroute.dsn.shapes import Circle, Rectangle -from freeroute.dsn.shapes import Path as DsnPath +from rpmroute.dsn import parse_dsn +from rpmroute.dsn.reader import DsnParseError +from rpmroute.dsn.shapes import Circle, Rectangle +from rpmroute.dsn.shapes import Path as DsnPath FIXTURES = Path(__file__).parent / "fixtures" diff --git a/tests/dsn/test_sexp.py b/tests/dsn/test_sexp.py index 14214e4..db46b91 100644 --- a/tests/dsn/test_sexp.py +++ b/tests/dsn/test_sexp.py @@ -4,8 +4,8 @@ from __future__ import annotations import pytest -from freeroute.dsn.sexp import SExp, parse -from freeroute.dsn.tokenizer import DsnSyntaxError, Token +from rpmroute.dsn.sexp import SExp, parse +from rpmroute.dsn.tokenizer import DsnSyntaxError, Token def test_parse_simple_head_and_values(): diff --git a/tests/dsn/test_shapes.py b/tests/dsn/test_shapes.py index 3858de9..648ee74 100644 --- a/tests/dsn/test_shapes.py +++ b/tests/dsn/test_shapes.py @@ -2,8 +2,8 @@ from __future__ import annotations -from freeroute.dsn.sexp import parse -from freeroute.dsn.shapes import ( +from rpmroute.dsn.sexp import parse +from rpmroute.dsn.shapes import ( Circle, Path, Polygon, diff --git a/tests/dsn/test_tokenizer.py b/tests/dsn/test_tokenizer.py index cb01314..d41a85d 100644 --- a/tests/dsn/test_tokenizer.py +++ b/tests/dsn/test_tokenizer.py @@ -4,7 +4,7 @@ from __future__ import annotations import pytest -from freeroute.dsn.tokenizer import DsnSyntaxError, TokenKind, tokenize +from rpmroute.dsn.tokenizer import DsnSyntaxError, TokenKind, tokenize def kinds(text: str) -> list[TokenKind]: diff --git a/tests/geometry/test_box.py b/tests/geometry/test_box.py index 7dd777b..ffb0b3c 100644 --- a/tests/geometry/test_box.py +++ b/tests/geometry/test_box.py @@ -6,7 +6,7 @@ containment and degenerate (empty / point / segment) tiles. from __future__ import annotations -from freeroute.geometry import IntBox, IntPoint, IntVector, RationalPoint +from rpmroute.geometry import IntBox, IntPoint, IntVector, RationalPoint def test_measures(): diff --git a/tests/geometry/test_direction.py b/tests/geometry/test_direction.py index 892003c..3d1b168 100644 --- a/tests/geometry/test_direction.py +++ b/tests/geometry/test_direction.py @@ -5,7 +5,7 @@ Oracles hand-derived from ``geometry/planar/{Direction,IntDirection}.java``. from __future__ import annotations -from freeroute.geometry import Direction, IntDirection, IntPoint, IntVector, Side +from rpmroute.geometry import Direction, IntDirection, IntPoint, IntVector, Side def test_normalized_direction_reduces_by_gcd(): diff --git a/tests/geometry/test_line.py b/tests/geometry/test_line.py index a4f8ed3..a6713cc 100644 --- a/tests/geometry/test_line.py +++ b/tests/geometry/test_line.py @@ -5,7 +5,7 @@ Oracles hand-derived from ``geometry/planar/Line.java``. from __future__ import annotations -from freeroute.geometry import IntPoint, Line, RationalPoint, Side +from rpmroute.geometry import IntPoint, Line, RationalPoint, Side def test_side_of_point(): @@ -79,7 +79,7 @@ def test_intersection_approx_matches_exact(): def test_translate_and_opposite(): line = Line.from_coords(0, 0, 4, 0) assert line.opposite().direction().equals(line.direction()) is False - from freeroute.geometry import IntVector + from rpmroute.geometry import IntVector moved = line.translate_by(IntVector(0, 5)) assert moved.side_of(IntPoint(2, 5)) == Side.COLLINEAR diff --git a/tests/geometry/test_octagon.py b/tests/geometry/test_octagon.py index 795b46f..69c5adb 100644 --- a/tests/geometry/test_octagon.py +++ b/tests/geometry/test_octagon.py @@ -7,7 +7,7 @@ import random import pytest -from freeroute.geometry import IntBox, IntPoint, ceil_sqrt2, overlaps_2d, segment_octagon +from rpmroute.geometry import IntBox, IntPoint, ceil_sqrt2, overlaps_2d, segment_octagon @pytest.mark.parametrize("r", [0, 1, 2, 3, 1000, 2000, 3000, 12345, 999999]) diff --git a/tests/geometry/test_point_vector.py b/tests/geometry/test_point_vector.py index 2760da1..1f27ec0 100644 --- a/tests/geometry/test_point_vector.py +++ b/tests/geometry/test_point_vector.py @@ -8,7 +8,7 @@ Java ``long``, and rational equality. from __future__ import annotations -from freeroute.geometry import ( +from rpmroute.geometry import ( IntPoint, IntVector, RationalPoint, @@ -139,7 +139,7 @@ def test_rational_point_infinite(): def test_rational_point_contained_in_box_exact(): - from freeroute.geometry import IntBox + from rpmroute.geometry import IntBox box = IntBox(0, 0, 2, 2) assert RationalPoint(3, 2, 2).is_contained_in(box) # (1.5, 1.0) inside diff --git a/tests/geometry/test_polygon.py b/tests/geometry/test_polygon.py index eccbf2d..36eadda 100644 --- a/tests/geometry/test_polygon.py +++ b/tests/geometry/test_polygon.py @@ -13,9 +13,9 @@ from __future__ import annotations import pytest -from freeroute.geometry import IntPoint, Side -from freeroute.geometry.polygon import Polygon -from freeroute.geometry.polygon_shape import PolygonShape +from rpmroute.geometry import IntPoint, Side +from rpmroute.geometry.polygon import Polygon +from rpmroute.geometry.polygon_shape import PolygonShape def pts(*coords): diff --git a/tests/geometry/test_polyline.py b/tests/geometry/test_polyline.py index c6fc5b9..1ef807d 100644 --- a/tests/geometry/test_polyline.py +++ b/tests/geometry/test_polyline.py @@ -2,7 +2,7 @@ from __future__ import annotations -from freeroute.geometry import ( +from rpmroute.geometry import ( IntBox, IntPoint, Polyline, diff --git a/tests/geometry/test_simplex.py b/tests/geometry/test_simplex.py index bc58ca1..bd1e703 100644 --- a/tests/geometry/test_simplex.py +++ b/tests/geometry/test_simplex.py @@ -16,7 +16,7 @@ Source: ``geometry/planar/{TileShape,Simplex}.java``. from __future__ import annotations -from freeroute.geometry import IntBox, IntPoint, Line, Side, Simplex +from rpmroute.geometry import IntBox, IntPoint, Line, Side, Simplex def _grid(lo: int, hi: int): @@ -162,7 +162,7 @@ def test_get_instance_detects_empty_from_opposing_halfplanes(): def test_translate_preserves_shape(): tri = right_triangle() - from freeroute.geometry import IntVector + from rpmroute.geometry import IntVector moved = tri.translate_by(IntVector(100, 50)) for p in _grid(-3, 13): diff --git a/tests/oracle.py b/tests/oracle.py index 74bdb56..63ff47f 100644 --- a/tests/oracle.py +++ b/tests/oracle.py @@ -1,8 +1,8 @@ """FreeRouting JAR oracle for behaviour-level validation. -**Dev/test only — not part of the shipped package** (freeroute is Java-free by +**Dev/test only — not part of the shipped package** (rpmroute is Java-free by design). This runs the reference FreeRouting JAR to route a Specctra DSN so that -freeroute's own output can be diffed against the reference implementation. There +rpmroute's own output can be diffed against the reference implementation. There are no unit-test oracles for FreeRouting's geometry/router, so the oracle is how we validate at the behaviour level: same DSN in, compare routed connectivity. @@ -10,7 +10,7 @@ Tests that use this skip automatically when a Java 21+ runtime or the JAR is not available (e.g. CI without a JVM), so the normal suite stays Java-free. Overrides: -- ``FREEROUTE_ORACLE_JAVA`` — explicit path to a ``java`` binary. +- ``RPMROUTE_ORACLE_JAVA`` (or the older ``FREEROUTE_ORACLE_JAVA``) — explicit path to a ``java`` binary. - ``FREEROUTING_JAR`` — explicit path to a freerouting JAR. """ @@ -27,7 +27,7 @@ import tempfile import pytest -from freeroute.dsn.sexp import parse +from rpmroute.dsn.sexp import parse _MIN_JAVA_MAJOR = 21 _REPO_ROOT = Path(__file__).resolve().parents[1] @@ -60,7 +60,7 @@ def _java_major(java: str) -> int: def find_java() -> str | None: """Return a path to a Java >= 21 runtime, or None.""" candidates: list[str] = [] - override = os.environ.get("FREEROUTE_ORACLE_JAVA") + override = os.environ.get("RPMROUTE_ORACLE_JAVA") or os.environ.get("FREEROUTE_ORACLE_JAVA") if override: candidates.append(override) java_home = os.environ.get("JAVA_HOME") diff --git a/tests/route/test_axis_escapes.py b/tests/route/test_axis_escapes.py index c9a0918..71e55f1 100644 --- a/tests/route/test_axis_escapes.py +++ b/tests/route/test_axis_escapes.py @@ -14,11 +14,11 @@ import math import pytest -from freeroute.board import build_board -from freeroute.dsn import parse_dsn -from freeroute.geometry import IntPoint, overlaps_2d, segment_slab -from freeroute.route.grid_router import GridRouter -from freeroute.route.pipeline import ( +from rpmroute.board import build_board +from rpmroute.dsn import parse_dsn +from rpmroute.geometry import IntPoint, overlaps_2d, segment_slab +from rpmroute.route.grid_router import GridRouter +from rpmroute.route.pipeline import ( _rule_clearance_dsn, _rule_width_dsn, _via_half_board, @@ -107,7 +107,7 @@ def test_a_pad_on_the_grid_axes_keeps_the_usual_exits(): def test_a_slanted_slab_is_tight_where_a_bounding_box_is_not(): slab = segment_slab(IntPoint(0, 0), IntPoint(10000, 10000), 750) # a point beside the diagonal but inside its bounding box - from freeroute.geometry import IntBox + from rpmroute.geometry import IntBox assert not overlaps_2d(slab, IntBox(8000, 0, 8100, 100)) assert overlaps_2d(slab, IntBox(4900, 4900, 5100, 5100)) diff --git a/tests/route/test_diagonal.py b/tests/route/test_diagonal.py index 738a481..ab6aba4 100644 --- a/tests/route/test_diagonal.py +++ b/tests/route/test_diagonal.py @@ -16,11 +16,11 @@ from pathlib import Path import pytest -from freeroute.board import build_board -from freeroute.dsn import parse_dsn -from freeroute.dsn.sexp import parse -from freeroute.geometry import IntBox, IntPoint, overlaps_2d, segment_octagon -from freeroute.route import route, route_dsn_board_exact +from rpmroute.board import build_board +from rpmroute.dsn import parse_dsn +from rpmroute.dsn.sexp import parse +from rpmroute.geometry import IntBox, IntPoint, overlaps_2d, segment_octagon +from rpmroute.route import route, route_dsn_board_exact FIXTURES = Path(__file__).resolve().parent.parent / "dsn" / "fixtures" DIAGONAL_WIN = FIXTURES / "diagonal_win.dsn" @@ -55,7 +55,7 @@ def _pins_by_net(fixture): def _rule_clearance_board(fixture): - from freeroute.route.pipeline import _rule_clearance_dsn + from rpmroute.route.pipeline import _rule_clearance_dsn dsn = parse_dsn(fixture.read_text()) return round(_rule_clearance_dsn(dsn) * max(dsn.resolution.value, 1)) diff --git a/tests/route/test_exact_router.py b/tests/route/test_exact_router.py index efa4b0f..ca53d50 100644 --- a/tests/route/test_exact_router.py +++ b/tests/route/test_exact_router.py @@ -15,12 +15,12 @@ import sys import pytest -from freeroute.board import ShapeSearchTree, TreeShape, build_board -from freeroute.dsn import parse_dsn -from freeroute.dsn.sexp import parse -from freeroute.geometry import IntBox, Polyline, PolylineShape -from freeroute.route import route, route_dsn_board, route_dsn_board_exact -from freeroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn +from rpmroute.board import ShapeSearchTree, TreeShape, build_board +from rpmroute.dsn import parse_dsn +from rpmroute.dsn.sexp import parse +from rpmroute.geometry import IntBox, Polyline, PolylineShape +from rpmroute.route import route, route_dsn_board, route_dsn_board_exact +from rpmroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) diff --git a/tests/route/test_fixed_wiring.py b/tests/route/test_fixed_wiring.py index d5bdfbe..4fab109 100644 --- a/tests/route/test_fixed_wiring.py +++ b/tests/route/test_fixed_wiring.py @@ -2,9 +2,9 @@ from __future__ import annotations -from freeroute.board import build_board -from freeroute.dsn import parse_dsn -from freeroute.route.pipeline import route_dsn_board_exact +from rpmroute.board import build_board +from rpmroute.dsn import parse_dsn +from rpmroute.route.pipeline import route_dsn_board_exact _DSN = """(pcb wired (parser) (resolution um 10) (unit um) (structure @@ -95,9 +95,9 @@ def test_a_fanned_out_signal_is_routed_from_its_via(): def test_the_session_never_repeats_copper_already_on_the_board(): # the JAR writes the DSN's wiring back into its session, and an applier - # then adds it a second time on top of itself; freeroute writes only + # then adds it a second time on top of itself; rpmroute writes only # what it routed - from freeroute.route.pipeline import build_exact_routing_result + from rpmroute.route.pipeline import build_exact_routing_result fixed_vias = {(5000.0, -11500.0), (10000.0, -11500.0)} fixed_paths = {(5000.0, -10000.0, 5000.0, -11500.0), (10000.0, -10000.0, 10000.0, -11500.0), diff --git a/tests/route/test_grid_exact_agreement.py b/tests/route/test_grid_exact_agreement.py index af234f7..f61093b 100644 --- a/tests/route/test_grid_exact_agreement.py +++ b/tests/route/test_grid_exact_agreement.py @@ -12,11 +12,11 @@ import math import pytest -from freeroute.board import build_board -from freeroute.dsn import parse_dsn -from freeroute.geometry import IntBox, Simplex -from freeroute.route.grid_router import GridRouter -from freeroute.route.pipeline import ( +from rpmroute.board import build_board +from rpmroute.dsn import parse_dsn +from rpmroute.geometry import IntBox, Simplex +from rpmroute.route.grid_router import GridRouter +from rpmroute.route.pipeline import ( _rule_clearance_dsn, _rule_width_dsn, _via_half_board, @@ -268,9 +268,9 @@ def test_a_via_by_a_chamfered_corner_is_judged_round(): """The grid keeps a via's centre its radius plus the edge clearance from the outline. The exact check used the via's square, whose corner reaches past a 45-degree chamfer where the round via does not.""" - from freeroute.board.outline import BoardOutline - from freeroute.geometry import IntBox, IntPoint - from freeroute.route.exact_router import _inside_outline + from rpmroute.board.outline import BoardOutline + from rpmroute.geometry import IntBox, IntPoint + from rpmroute.route.exact_router import _inside_outline # a 10 mm square with its lower-left corner cut at 45 degrees outline = BoardOutline([IntPoint(*p) for p in diff --git a/tests/route/test_negotiate.py b/tests/route/test_negotiate.py index e2d588a..03d8d27 100644 --- a/tests/route/test_negotiate.py +++ b/tests/route/test_negotiate.py @@ -6,10 +6,10 @@ from pathlib import Path import pytest -from freeroute.board import build_board -from freeroute.dsn import parse_dsn -from freeroute.route.grid_router import GridRouter -from freeroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn, route_dsn_board +from rpmroute.board import build_board +from rpmroute.dsn import parse_dsn +from rpmroute.route.grid_router import GridRouter +from rpmroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn, route_dsn_board FIXTURES = Path(__file__).resolve().parent.parent / "dsn" / "fixtures" diff --git a/tests/route/test_net_widths.py b/tests/route/test_net_widths.py index 0096a2a..76d7944 100644 --- a/tests/route/test_net_widths.py +++ b/tests/route/test_net_widths.py @@ -2,8 +2,8 @@ from __future__ import annotations -from freeroute.dsn import parse_dsn -from freeroute.route.pipeline import build_exact_routing_result +from rpmroute.dsn import parse_dsn +from rpmroute.route.pipeline import build_exact_routing_result _BOARD = """(pcb widths (parser) (resolution um 10) (unit um) (structure diff --git a/tests/route/test_orders.py b/tests/route/test_orders.py index 38c285d..7130159 100644 --- a/tests/route/test_orders.py +++ b/tests/route/test_orders.py @@ -4,8 +4,8 @@ from __future__ import annotations from pathlib import Path -from freeroute.cli import main -from freeroute.route.orders import ( +from rpmroute.cli import main +from rpmroute.route.orders import ( best_of_orders, candidate_seeds, promote_failures, diff --git a/tests/route/test_real_board_geometry.py b/tests/route/test_real_board_geometry.py index c146370..1039999 100644 --- a/tests/route/test_real_board_geometry.py +++ b/tests/route/test_real_board_geometry.py @@ -6,9 +6,9 @@ import math import pytest -from freeroute.board import build_board -from freeroute.dsn import parse_dsn -from freeroute.route.pipeline import route_dsn_board_exact +from rpmroute.board import build_board +from rpmroute.dsn import parse_dsn +from rpmroute.route.pipeline import route_dsn_board_exact # 20 x 20 mm board whose corners are rounded with a 3 mm radius, as polylines _R = 3000.0 @@ -70,7 +70,7 @@ def test_every_pin_of_a_half_millimetre_pitch_row_escapes(resolution): def test_resolution_one_has_no_halo_so_the_grid_is_unchanged(): - from freeroute.route.grid_router import GridRouter + from rpmroute.route.grid_router import GridRouter board = build_board(parse_dsn(_dsn("", ""))) assert GridRouter(board, trace_width=2500, clearance=1500)._halo_offsets == [] @@ -101,7 +101,7 @@ def test_heuristic_weight_bounds_route_length(): # --- copper zones (DSN planes) ----------------------------------------------- # rc4 power stage: pads inside "keep router off" pours connect through the pour, -# and freeroute, not knowing zones, dropped every such connection. +# and rpmroute, not knowing zones, dropped every such connection. _ZONE = "(plane N (polygon F.Cu 0 4000 -4000 10000 -4000 10000 -10000 4000 -10000 4000 -4000))" _NO_TRACKS = "(wire_keepout \"pour\" (polygon F.Cu 0 4000 -4000 10000 -4000 10000 -10000 4000 -10000))" @@ -121,7 +121,7 @@ def test_planes_are_read_and_are_not_obstacles(): def test_pads_in_one_zone_need_no_route_between_them(): - from freeroute.route.grid_router import _connected_groups + from rpmroute.route.grid_router import _connected_groups board = build_board(_zone_board()) pins = sorted(board.get_pins(), key=lambda p: p.component_ref) @@ -155,7 +155,7 @@ def test_a_pour_other_nets_can_cross_is_not_trusted_to_connect(): def test_cli_names_the_connections_it_could_not_route(tmp_path, capsys): # a keepout wall across the board on both layers: A and B cannot be joined - from freeroute.cli import main + from rpmroute.cli import main wall = " ".join( f"(keepout \"wall\" (polygon {layer} 0 9000 1000 11000 1000 11000 -21000 9000 -21000))" @@ -218,7 +218,7 @@ def test_a_pad_outside_the_plane_routes_to_it(): def test_a_through_hole_pad_inside_a_plane_needs_no_route(): - from freeroute.route.grid_router import _connected_groups + from rpmroute.route.grid_router import _connected_groups board = build_board(parse_dsn(_four_layer_dsn())) plane = [z for z in board.get_conduction_areas() if z.name == "GND"] diff --git a/tests/route/test_rollback.py b/tests/route/test_rollback.py index 1d3edd6..7b48157 100644 --- a/tests/route/test_rollback.py +++ b/tests/route/test_rollback.py @@ -6,10 +6,10 @@ from pathlib import Path import pytest -from freeroute.board import build_board -from freeroute.dsn import parse_dsn -from freeroute.route.grid_router import GridRouter -from freeroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn, route_dsn_board +from rpmroute.board import build_board +from rpmroute.dsn import parse_dsn +from rpmroute.route.grid_router import GridRouter +from rpmroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn, route_dsn_board FIXTURES = Path(__file__).resolve().parent.parent / "dsn" / "fixtures" diff --git a/tests/route/test_room_pack.py b/tests/route/test_room_pack.py index 18e07a1..7e2d950 100644 --- a/tests/route/test_room_pack.py +++ b/tests/route/test_room_pack.py @@ -23,11 +23,11 @@ from pathlib import Path import pytest -from freeroute.dsn import parse_dsn -from freeroute.dsn.sexp import parse -from freeroute.geometry import Polyline, PolylineShape -from freeroute.route import route, route_dsn_board_rooms -from freeroute.route.pipeline import _rule_clearance_dsn +from rpmroute.dsn import parse_dsn +from rpmroute.dsn.sexp import parse +from rpmroute.geometry import Polyline, PolylineShape +from rpmroute.route import route, route_dsn_board_rooms +from rpmroute.route.pipeline import _rule_clearance_dsn FIXTURES = Path(__file__).resolve().parent.parent / "dsn" / "fixtures" CHANNEL_PACK = FIXTURES / "channel_pack.dsn" @@ -134,7 +134,7 @@ def test_channel_pack_is_deterministic(): def test_channel_pack_endpoints_stay_on_pads(): dsn = parse_dsn(CHANNEL_PACK.read_text()) board_pins: dict[int, set[tuple[int, int]]] = {} - from freeroute.board import build_board + from rpmroute.board import build_board for pin in build_board(dsn).get_pins(): for net_no in pin.net_nos: diff --git a/tests/route/test_room_router.py b/tests/route/test_room_router.py index 9292c4a..d18a58a 100644 --- a/tests/route/test_room_router.py +++ b/tests/route/test_room_router.py @@ -13,11 +13,11 @@ import sys import pytest -from freeroute.board import build_board -from freeroute.dsn import parse_dsn -from freeroute.dsn.sexp import parse -from freeroute.geometry import Polyline -from freeroute.route import route, route_dsn_board_exact, route_dsn_board_rooms +from rpmroute.board import build_board +from rpmroute.dsn import parse_dsn +from rpmroute.dsn.sexp import parse +from rpmroute.geometry import Polyline +from rpmroute.route import route, route_dsn_board_exact, route_dsn_board_rooms sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) @@ -31,7 +31,7 @@ WIDE_DOOR = FIXTURES / "rooms_wide_door.dsn" def _clearance(dsn, scale): - from freeroute.route.pipeline import _rule_clearance_dsn + from rpmroute.route.pipeline import _rule_clearance_dsn return round(_rule_clearance_dsn(dsn) * scale) diff --git a/tests/route/test_room_shove.py b/tests/route/test_room_shove.py index b127286..14c6b09 100644 --- a/tests/route/test_room_shove.py +++ b/tests/route/test_room_shove.py @@ -32,12 +32,12 @@ import sys import pytest -from freeroute.board import build_board -from freeroute.dsn import parse_dsn -from freeroute.dsn.sexp import parse -from freeroute.geometry import IntBox, IntPoint, Polyline, PolylineShape -from freeroute.route import route, route_dsn_board_exact, route_dsn_board_rooms -from freeroute.route.pipeline import _rule_clearance_dsn +from rpmroute.board import build_board +from rpmroute.dsn import parse_dsn +from rpmroute.dsn.sexp import parse +from rpmroute.geometry import IntBox, IntPoint, Polyline, PolylineShape +from rpmroute.route import route, route_dsn_board_exact, route_dsn_board_rooms +from rpmroute.route.pipeline import _rule_clearance_dsn def _emitted_drc_violations(result, clearance): @@ -265,8 +265,8 @@ def test_shove_is_byte_identical_on_foundation_fixtures(fixture): def test_project_gate_reduces_to_clamp_without_occupancy(): - from freeroute.board.search_tree import ShapeSearchTree - from freeroute.route.room_router import _Door, _edge_gate, _project_gate + from rpmroute.board.search_tree import ShapeSearchTree + from rpmroute.route.room_router import _Door, _edge_gate, _project_gate door = _Door(other=1, kind="edge", lo=IntPoint(0, 100), hi=IntPoint(200, 100)) tree = ShapeSearchTree() @@ -277,8 +277,8 @@ def test_project_gate_reduces_to_clamp_without_occupancy(): def test_project_gate_avoids_other_net_copper_on_the_door(): - from freeroute.board.search_tree import ShapeSearchTree, TreeShape - from freeroute.route.room_router import _Door, _project_gate + from rpmroute.board.search_tree import ShapeSearchTree, TreeShape + from rpmroute.route.room_router import _Door, _project_gate door = _Door(other=1, kind="edge", lo=IntPoint(0, 100), hi=IntPoint(200, 100)) tree = ShapeSearchTree() @@ -293,7 +293,7 @@ def test_project_gate_avoids_other_net_copper_on_the_door(): def test_free_intervals_splits_around_forbidden_band(): - from freeroute.route.room_router import _best_free_point, _free_intervals + from rpmroute.route.room_router import _best_free_point, _free_intervals free = _free_intervals(0, 200, [(87, 113)]) assert free == [(0, 87), (113, 200)] diff --git a/tests/route/test_router.py b/tests/route/test_router.py index 6f67806..2f51149 100644 --- a/tests/route/test_router.py +++ b/tests/route/test_router.py @@ -16,11 +16,11 @@ import sys import pytest -from freeroute.board import build_board -from freeroute.dsn import parse_dsn -from freeroute.dsn.sexp import parse -from freeroute.route import route, route_dsn_board -from freeroute.ses.writer import ses_scale +from rpmroute.board import build_board +from rpmroute.dsn import parse_dsn +from rpmroute.dsn.sexp import parse +from rpmroute.route import route, route_dsn_board +from rpmroute.ses.writer import ses_scale sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) @@ -287,11 +287,11 @@ def test_connectivity_parity_on_congested_board(): if not HAS_ORACLE: pytest.skip("FreeRouting oracle unavailable") our_nets = routed_net_set(route(RIPUP.read_text())) - assert our_nets == {"NET_A", "NET_B"} # freeroute connects both via rip-up + assert our_nets == {"NET_A", "NET_B"} # rpmroute connects both via rip-up oracle_nets = routed_net_set(route_dsn(RIPUP, max_passes=3, timeout=300)) if not oracle_nets: # the reference JAR does not route this synthetic single-signal-layer - # board (it expects a routing/via layer); freeroute's rip-up connecting + # board (it expects a routing/via layer); rpmroute's rip-up connecting # both nets is already asserted above and by the fast tests. pytest.skip("reference JAR routed nothing on the synthetic single-layer board") assert oracle_nets <= our_nets # rip-up matches the JAR's connectivity @@ -306,7 +306,7 @@ def test_connectivity_parity_on_crossing_board(): our_nets = routed_net_set(route(CROSSING.read_text())) oracle_nets = routed_net_set(route_dsn(CROSSING, max_passes=3, timeout=300)) assert oracle_nets, "oracle routed nothing on a routable board" - assert oracle_nets <= our_nets # freeroute connects every net the JAR does + assert oracle_nets <= our_nets # rpmroute connects every net the JAR does @pytest.mark.oracle diff --git a/tests/route/test_shove.py b/tests/route/test_shove.py index 54bbed6..78491b2 100644 --- a/tests/route/test_shove.py +++ b/tests/route/test_shove.py @@ -17,10 +17,10 @@ import sys import pytest -from freeroute.dsn import parse_dsn -from freeroute.dsn.sexp import parse -from freeroute.route import route, route_dsn_board_exact -from freeroute.route.pipeline import _rule_clearance_dsn +from rpmroute.dsn import parse_dsn +from rpmroute.dsn.sexp import parse +from rpmroute.route import route, route_dsn_board_exact +from rpmroute.route.pipeline import _rule_clearance_dsn sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) @@ -66,7 +66,7 @@ def test_shove_result_is_deterministic(): def test_shoved_trace_endpoints_still_on_pads(): - from freeroute.board import build_board + from rpmroute.board import build_board on, _, _ = route_dsn_board_exact(parse_dsn(SHOVE.read_text()), rip_up=False, shove=True) board = build_board(parse_dsn(SHOVE.read_text())) diff --git a/tests/route/test_straight_pin_exits.py b/tests/route/test_straight_pin_exits.py index 89020ab..acce3c8 100644 --- a/tests/route/test_straight_pin_exits.py +++ b/tests/route/test_straight_pin_exits.py @@ -8,12 +8,12 @@ from __future__ import annotations import pytest -from freeroute.board import build_board -from freeroute.board.items import Pin -from freeroute.dsn import parse_dsn -from freeroute.geometry import IntPoint -from freeroute.route.grid_router import GridRouter -from freeroute.route.pipeline import route_dsn_board_exact +from rpmroute.board import build_board +from rpmroute.board.items import Pin +from rpmroute.dsn import parse_dsn +from rpmroute.geometry import IntPoint +from rpmroute.route.grid_router import GridRouter +from rpmroute.route.pipeline import route_dsn_board_exact def _row(x0: int, pins: int = 6, skew: int = 3000) -> str: diff --git a/tests/route/test_tree_nets.py b/tests/route/test_tree_nets.py index 74c197a..bd3cf06 100644 --- a/tests/route/test_tree_nets.py +++ b/tests/route/test_tree_nets.py @@ -5,11 +5,11 @@ from __future__ import annotations import pytest -from freeroute.board import build_board -from freeroute.dsn import parse_dsn -from freeroute.route import exact_router -from freeroute.route.grid_router import GridRouter, RouteResult -from freeroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn +from rpmroute.board import build_board +from rpmroute.dsn import parse_dsn +from rpmroute.route import exact_router +from rpmroute.route.grid_router import GridRouter, RouteResult +from rpmroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn # P1 and P2 are nearest each other, so the tree pairs them first; P3 sits # under the middle of that leg, 50 mm below it and 90 mm from either pin. diff --git a/tests/route/test_via_spacing.py b/tests/route/test_via_spacing.py index 7f7cfdf..59de024 100644 --- a/tests/route/test_via_spacing.py +++ b/tests/route/test_via_spacing.py @@ -5,8 +5,8 @@ from __future__ import annotations import itertools import math -from freeroute.dsn import parse_dsn -from freeroute.route.pipeline import route_dsn_board_exact +from rpmroute.dsn import parse_dsn +from rpmroute.route.pipeline import route_dsn_board_exact # four GND pads 0.5 mm apart over a GND plane: each drops a via to the plane _DSN = """(pcb holes (parser) (resolution um 10) (unit um) @@ -57,9 +57,9 @@ def test_connections_sharing_a_plane_via_write_it_once(): def _router(dsn_text, via_drill_um=300): - from freeroute.board import build_board - from freeroute.route.grid_router import GridRouter - from freeroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn + from rpmroute.board import build_board + from rpmroute.route.grid_router import GridRouter + from rpmroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn dsn = parse_dsn(dsn_text) s = dsn.resolution.value @@ -90,7 +90,7 @@ def test_no_via_goes_near_a_through_hole_pads_hole_even_of_its_own_net(): def test_a_via_in_a_pad_keeps_hole_spacing_from_a_fixed_via(): - from freeroute.geometry import IntPoint + from rpmroute.geometry import IntPoint text = _THT.replace("(network (net SIG (pins J1-1))))", "(network (net SIG (pins J1-1)))\n" diff --git a/tests/route/test_window_reroute.py b/tests/route/test_window_reroute.py index 9584535..e166eed 100644 --- a/tests/route/test_window_reroute.py +++ b/tests/route/test_window_reroute.py @@ -3,11 +3,11 @@ it leaves fewer connections unrouted.""" from __future__ import annotations -from freeroute.board import build_board -from freeroute.dsn import parse_dsn -from freeroute.geometry import IntBox -from freeroute.route.grid_router import GridRouter -from freeroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn, route_dsn_board_exact +from rpmroute.board import build_board +from rpmroute.dsn import parse_dsn +from rpmroute.geometry import IntBox +from rpmroute.route.grid_router import GridRouter +from rpmroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn, route_dsn_board_exact def _slot(gap: int = 640, far: bool = False) -> str: @@ -101,7 +101,7 @@ def test_a_spent_budget_starts_no_window(): def test_window_board_keeps_far_items_of_the_rerouted_nets_only(): - from freeroute.route.window import _item_box, _window_board + from rpmroute.route.window import _item_box, _window_board board = build_board(parse_dsn(_slot(far=True))) items = [(item, _item_box(item)) for item in board.get_items()] @@ -115,7 +115,7 @@ def test_window_board_keeps_far_items_of_the_rerouted_nets_only(): def test_a_connection_ending_outside_the_window_gets_no_copper(): """Its far end has no grid cells; a stub out to it would cross items the window board never saw.""" - from freeroute.route.exact_router import route_board_exact + from rpmroute.route.exact_router import route_board_exact dsn = parse_dsn(_slot(far=True)) s = dsn.resolution.value diff --git a/tests/ses/test_indent.py b/tests/ses/test_indent.py index 2f91630..8c73fc1 100644 --- a/tests/ses/test_indent.py +++ b/tests/ses/test_indent.py @@ -2,7 +2,7 @@ from __future__ import annotations -from freeroute.ses.indent import Identifier, IndentWriter +from rpmroute.ses.indent import Identifier, IndentWriter def test_nested_scopes_indent_by_two_spaces(): diff --git a/tests/ses/test_writer.py b/tests/ses/test_writer.py index 68727c9..5404451 100644 --- a/tests/ses/test_writer.py +++ b/tests/ses/test_writer.py @@ -1,7 +1,7 @@ """End-to-end tests for the SES writer. Where possible these re-parse the emitted SES with the project's own tokenizer / -S-expression parser (:mod:`freeroute.dsn.sexp`) and assert structural +S-expression parser (:mod:`rpmroute.dsn.sexp`) and assert structural invariants, mirroring what FreeRouting's ``SesReader`` and ``kicad-cli pcb import specctra-ses`` walk: ``session > routes > network_out > net > wire(path)/via``. @@ -13,9 +13,9 @@ from pathlib import Path import pytest -from freeroute.dsn import parse_dsn -from freeroute.dsn.sexp import SExp, parse -from freeroute.ses import RoutedVia, RoutedWire, RoutingResult, write_ses +from rpmroute.dsn import parse_dsn +from rpmroute.dsn.sexp import SExp, parse +from rpmroute.ses import RoutedVia, RoutedWire, RoutingResult, write_ses FIXTURES = Path(__file__).parent.parent / "dsn" / "fixtures" @@ -142,7 +142,7 @@ def test_round_trip_from_empty_board_fixture(): # --- SES coordinate units --------------------------------------------------- # FreeRouting's SesWriter writes coordinates in resolution units, and KiCad's -# importer and mckicad's ses_apply read them that way. On 2026-09-26 freeroute +# importer and mckicad's ses_apply read them that way. On 2026-09-26 rpmroute # was found writing DSN units instead, so every routed board came back 10x small. @@ -155,8 +155,8 @@ def test_round_trip_from_empty_board_fixture(): ], ) def test_ses_scale(resolution, unit, expected): - from freeroute.dsn import parse_dsn - from freeroute.ses.writer import ses_scale + from rpmroute.dsn import parse_dsn + from rpmroute.ses.writer import ses_scale dsn = parse_dsn(f"(pcb t (parser) {resolution} {unit} (structure) (placement) (library) (network))") assert ses_scale(dsn) == pytest.approx(expected) diff --git a/tests/test_cli.py b/tests/test_cli.py index a01e587..cc64dc7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,4 @@ -"""Tests for the freeroute CLI (Specctra DSN in, SES out). +"""Tests for the rpmroute CLI (Specctra DSN in, SES out). Covers the engine selection contract: every engine must route a fixture from the command line, and every option the selected engine does not implement must fail @@ -11,7 +11,7 @@ from pathlib import Path import pytest -from freeroute.cli import ENGINE_OPTIONS, main +from rpmroute.cli import ENGINE_OPTIONS, main FIXTURES = Path(__file__).parent / "dsn" / "fixtures" FIXTURE = FIXTURES / "simple_2net.dsn" diff --git a/uv.lock b/uv.lock index 47333a8..4e59725 100644 --- a/uv.lock +++ b/uv.lock @@ -126,27 +126,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] -[[package]] -name = "freeroute" -version = "2026.7.14" -source = { editable = "." } - -[package.dev-dependencies] -dev = [ - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "ruff" }, -] - -[package.metadata] - -[package.metadata.requires-dev] -dev = [ - { name = "pytest", specifier = ">=7.0.0" }, - { name = "pytest-cov", specifier = ">=4.0.0" }, - { name = "ruff", specifier = ">=0.1.0" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -215,6 +194,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] +[[package]] +name = "rpmroute" +version = "2026.10.7" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=7.0.0" }, + { name = "pytest-cov", specifier = ">=4.0.0" }, + { name = "ruff", specifier = ">=0.1.0" }, +] + [[package]] name = "ruff" version = "0.15.21"