feat(autoroute): add best-of-N support (attempts, targetNets, passSchedule) (#190)

The existing single-shot autoroute leaves 1-7 nets unrouted on dense
boards in my testing. Best-of-N drives that to 0 most of the time by
running Freerouting a few times with varied --max-passes and keeping
the SES with the best routing score.

New optional parameters (all backward-compatible):

  attempts:     int, default 1 (unchanged behaviour). When > 1, run
                Freerouting N times and pick the highest-scoring SES.
  targetNets:   list of critical net names. An attempt that routes all
                of them earns a 50,000-point scoring bonus.
  passSchedule: list of --max-passes values to cycle through across
                attempts. Default: [50, 60, 65, 70, 75, 80, 85, 90, 55,
                95] (wraps if attempts > len). Ignored when attempts=1
                (legacy maxPasses still used).

Scoring contract (pinned by tests):
  score = nets_routed * 1000 + segments
  if targetNets and all routed: score += 50_000

  - +1 net always beats any segment-count delta (1000 pt step).
  - Segments break ties at equal net count.
  - Target bonus dominates net-count gains from unrelated nets.

## Implementation notes

  - When attempts > 1, each attempt runs with `-mt 1` (single-thread
    optimisation). Freerouting 2.x's multi-threaded optimiser is
    documented to introduce clearance violations, so forcing
    single-thread during scoring keeps the comparison apples-to-apples.
  - One failed attempt does not abort the whole best-of-N run. The
    failure is recorded in the response under attempts[] with ok=False,
    and the remaining attempts compete for best. If every attempt fails
    the response surfaces a clear error.
  - The winning SES is preserved as <stem>_best.ses next to the
    canonical <stem>.ses so the caller can inspect it after the run.
  - Response shape:
      attempts == 1:  unchanged (no attempts/best_attempt fields)
      attempts > 1:   adds attempts[], best_attempt, best_score,
                      best_ses_path

## Attribution

Scoring approach and default pass schedule ported from
morningfire-pcb-automation
(https://github.com/NiNjA-CodE/morningfire-pcb-automation,
scripts/routing/freeroute_runner.py). Credited in the function
docstring, the TypeScript wrapper comment, the tool description (visible
to MCP clients), and the CHANGELOG entry.

The MCP version adds: cleaner per-attempt result reporting, automatic
single-thread optimisation, graceful degradation on partial failure,
and explicit validation that surfaces clean error payloads for invalid
attempts values.

## Tests

  tests/test_autoroute_score.py             8 cases, scoring contract
  tests/test_autoroute_best_of_n.py         6 cases, orchestration logic

All 14 passing. Tests are pure-Python: subprocess is mocked so the
suite runs in any environment (no Java / Freerouting / KiCad required).

  - Single-attempt response shape unchanged
  - Best-of-three picks the highest-scoring SES
  - One nonzero exit attempt doesn't abort the run
  - passSchedule wraps when attempts exceeds len
  - targetNets bonus wins over higher raw net count
  - attempts=0 rejected with clean error before DSN export
  - +1 net (1000 pts) dominates any segment delta
  - Segments tiebreak at equal net count
  - Quoted net names in SES are normalised vs unquoted targets

TypeScript builds clean.
This commit is contained in:
NiNjA-CodE
2026-05-18 21:03:38 -06:00
committed by GitHub
parent 983ffc3793
commit e2941631c1
5 changed files with 733 additions and 51 deletions

View File

@@ -80,6 +80,28 @@ All notable changes to the KiCAD MCP Server project are documented here.
### Tool Enhancements
- `autoroute`: best-of-N support. New optional parameters `attempts`,
`targetNets`, and `passSchedule`. When `attempts > 1`, Freerouting is
invoked multiple times with varied `--max-passes` values, each result
is scored by `(nets_routed * 1000) + segments` plus a 50,000-point
bonus when every `targetNets` entry is routed, and the winning SES is
imported into the board. Single-attempt behaviour is unchanged when
`attempts` is omitted, so existing callers don't need updates.
Motivation: on dense boards a single Freerouting run routinely leaves
17 nets unrouted. Cycling through a few `-mp` values typically drives
the unrouted count to zero. Empirically, 3 attempts is usually enough
for 4-layer designs; 58 for stubborn cases.
The scoring approach and the default `passSchedule` are ported from
[morningfire-pcb-automation](https://github.com/NiNjA-CodE/morningfire-pcb-automation)
(`scripts/routing/freeroute_runner.py`). The MCP version adds:
cleaner per-attempt result reporting, automatic single-thread
optimisation (`-mt 1`) during scored attempts so the multi-threaded
optimiser's known clearance-violation bug doesn't distort the
comparison, and graceful degradation when one attempt errors out
(the run continues and the best of the remainder wins).
- `edit_schematic_component`: extended with two new optional parameters that
promote arbitrary custom properties to first-class citizens:
- **`properties`** — map of property name to either a string value or a full