Skip to content

✨ Add the planning stages and mqt-scpd plan - #140

Open
FeldmeierMichael wants to merge 2 commits into
mainfrom
phase-3-partition-assignment
Open

FeldmeierMichael wants to merge 2 commits into
mainfrom
phase-3-partition-assignment

Conversation

@FeldmeierMichael

@FeldmeierMichael FeldmeierMichael commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖

Description

Until now the tool could read, check, draw and export a chip, and the grid and router modules of #134 could route one wire. Nothing decided yet which wire goes where. This pull request adds the four planning steps of the pipeline and the command that runs them. Each step writes one artifact:

Step What the step does Artifact
capacity Partitions the free space at its bottlenecks and budgets the wires of every capacity chain 01-capacity.fb
global Solves the inner circuit and the port ring that the assignment works on 02-global.fb
assign Assigns the ring ports to launchers and feedline chains 03-assign.fb
corridor Routes every connection through the partitions 04-corridor.fb

mqt-scpd plan runs the steps into a run directory. On the 17-qubit chip, a run prints:

$ mqt-scpd plan -c benchmarks/17q/config.toml \
    --chip planar-superconducting-pd/inputs/17q/routing_config.json -o runs/17q
��mqt-scpd plan 17q → runs/17q  ·  HiGHS 1.11.0

🧩 capacity
   361 partitions · 62 bottlenecks · 69 chains                                                 1.12s
   ✓ 01-capacity.fb  1.3 MB
🔳 global
   14 inner wires · 8 lattices · length 23419.67                                               0.02s
   ✓ 02-global.fb  14.1 kB · fails 0
📌 assign
   58 ports on 7 launchers · objective 27.84 · optimal                                         0.18s
   ✓ 03-assign.fb  2.8 kB · fails 0
🧭 corridor
   round 1 forward    routed 56/58 · fails 2                                                   0.05s
   round 2 backward   routed 58/58 · fails 0                                                   0.05s
   routed 58/58 · 2 rounds                                                                     0.05s
   ✓ 04-corridor.fb  60.5 kB · fails 0

✓ 4 stages · 1.37s · stopped after corridor · fails 0

The detail and final routing follow in the next phase.

The steps were built and measured on the development branch. This pull request ports them onto the reviewed grid of main. On seven of the eight benchmark chips, the four artifacts equal those of the development branch in every field. The 4-qubit chip differs by one bug fix (see "Differences from the development branch").

The planning steps

  • Capacity (watershed). The step rasterizes the chip and takes the distance transform of the free space. It finds the bottlenecks along the medial axis of the free space, which a Voronoi diagram over the boundary cells of the obstacles gives (Boost.Polygon). From each target it walks the bottlenecks outward into capacity chains. The chambers that the chains cross become partitions, and the rest of the free space grows into partitions from the middle of every clear capacity cell. Every partition border gets a budget: the number of wires that may cross it. A band in front of every port keeps the straight approach of its wire clear.
  • Global (hanan-milp). The step routes the inner circuit: the wires to the ports that the configured ring does not carry. It solves a binary flow model on the Hanan lattice of each capacity chain as one mixed-integer program. The bridge rules of the configuration name the two ports of a coupler that a wire passes between. The result includes the ring of outer ports that the assignment works on.
  • Assignment (ordered-milp). One mixed-integer program assigns the ring ports to launchers and feedline chains and keeps the feedlines from crossing.
  • Corridor (partition-astar). The router searches the partition graph instead of the cells. A node is a crossing slot on a partition border. A connection therefore learns which partitions it runs through and where it crosses from one into the next. The detail routing of the next phase draws the wire inside each partition. No two wires take the same slot, and no two wires cross inside one partition. The router works in rounds that sweep forward and backward over the connections. A slot that a wire wanted and could not have gets dearer for the wire on it, so two wires do not trade one slot back and forth.

The global step runs before the assignment, because the inner circuit decides at which coupler ports its wires surface, and the assignment works on the ring that those ports extend. The roadmap lists the two steps the other way round.

The MILP module (MQT::ScpdMilp)

  • Model holds variables, linear constraints and one linear objective.
  • The HiGHS backend solves a model with HiGHS 1.11.0. The version is pinned, because another version can return another optimum among equal ones.
  • toMps writes a model as MPS text, and an external backend solves that text. The Python package registers Gurobi this way when gurobipy is installed and licensed. [stages.solver] backend or SCPD_SOLVER chooses auto, highs or gurobi.
  • A SolveObserver receives the solver log and the progress of the search (objective, bound, gap and nodes). An exception that the observer throws stops HiGHS through its interrupt flag and is thrown again after the solve, so it never passes through HiGHS.

The pipeline module (MQT::ScpdPipeline)

  • Every step has an interface and a registry of named algorithms. mqt-scpd list-algorithms lists them, and the configuration can choose one per step.
  • A Report carries what a step reports: free lines, entries such as one round, the result and the progress, each at one of four levels. The core never prints; the Python package renders the report.

Command line

  • mqt-scpd plan -c <config> -o <run directory> copies the configuration and the chip input into the run directory and runs the steps. --stop-after <step>, or [run] stop_after, ends the run after a step. --stage <step> runs one step again on the copies in the run directory and deletes the artifacts of the later steps. Each artifact is written to a temporary file and then renamed, so a run never leaves half a file. A run that ends with fails exits with code 1. Ctrl-C stops a running solve and exits with code 130.
  • A run prints one block per step. -v, -vv and -vvv add the parameters and sub-steps, one line per item, and the solver log. On a terminal, a live line through rich shows the running step. docs/terminal_output.md fixes this format for the steps of later phases, and tests hold it: a golden output of plan on the test fixture, a grammar test of every line at -vvv, and a test that every step has a mark, an ASCII form and a colour.
  • plot and render draw a planning step of a run with --run-dir and --stage. In GDS, the planning shapes use the layers 20 to 31.
  • --chip replaces the chip input of the configuration for doctor, plot, render and plan. benchmarks/<chip>/config.toml holds a configuration for each of the eight benchmark chips; the chip inputs stay in planar-superconducting-pd.

Configuration

  • The component pattern groups the ports by component. The bridge_pair pattern and the [[ports.bridge_pairs]] rules pair the two ports of a coupler that a wire passes between.
  • The sections [stages.capacity], [stages.global], [stages.assignment], [stages.corridor] and [stages.solver] hold the parameters of the steps. [run] holds stop_after.
  • [stages.assignment] launcher_target has no default, because it is a figure of the chip. UPGRADING.md lists what a configuration needs for plan.

Differences from the development branch

The reference was the development branch at commit 712cf7a, with fix 1 applied.

  1. Polygon 0. The development branch took the first obstacle polygon for the chip outline and skipped it in two raster loops and in the check of the inner circuit against the chip artwork. On every benchmark chip, that polygon is a pad. The raster of main starts at polygon 0 since ✨ Add the routing grid and the Dubins router #134, and the Global step of this pull request does the same.
  2. Cells reserved for a port approach. On the 4-qubit chip, the development branch traced the cells reserved for a port approach as a partition with label 1, which means "reserved". This pseudo-partition had borders into real partitions and offered the Corridor step crossing slots through a port approach. The partition outlines now leave out every label below the first partition label. This removes one partition from the 4-qubit plan (55 to 54) and changes its 01-capacity.fb and 04-corridor.fb. The other chips have no such cells.
  3. Objective of a maximization through MPS. MPS states a minimization, so toMps negates a maximization. The external backend now negates the objective that it gets back. Both planning models minimize, so no artifact changes.

The grid of main also differs from the development branch in three rules from the review of #134: a watershed tie goes to the lower seed, the border smoothing keeps every seed cell, and the polygon fill computes each crossing from the nearer end of an edge. None of these rules changes an artifact of the eight benchmark chips.

Known limits

  • The partition outlines start at the first entry of a std::unordered_map. The shapes are the same everywhere, but the start corner and the order of the rings in 01-capacity.fb can differ between standard libraries. Every later use of the outlines ignores that order. A sorted order would change the artifact on every chip, so this pull request keeps the order of the development branch.
  • The assignment expects the launchers to be numbered against the direction of the port ring, as on every benchmark chip. A chip numbered the other way gets feeds across the chip and unrouted corridors, without a message. UPGRADING.md states the rule; a check in doctor could follow.
  • A missing launcher_target or an unknown algorithm name fails only when its step runs. On the 69-qubit chip, that can be a minute into the run.
  • This pull request does not compare the Gurobi backend with HiGHS, because gurobipy is not installed on the test machine. With auto, a licensed gurobipy takes precedence over HiGHS.
  • The configurations of planar-superconducting-pd do not carry the new keys yet, so plan needs the configurations in benchmarks/.
  • docs/terminal_output.md documents warnings, but no step produces one yet.

Determinism

The same input gives the same artifacts within one build, and a resumed run equals an uninterrupted one. The project targets build without floating-point contraction, as since #134. HiGHS keeps the compiler defaults and is always built without link-time optimization. Main configures the dependencies before the project settings, so HiGHS used to get link-time optimization only on a second configure, and that changed the last bits of the assignment objectives on the 45- and 69-qubit chips. The comparisons with the development branch ran on macOS arm64.

Notes for review

  • The artifacts keep the binary layout of the development branch: the same envelope, the SCP1 identifier and the same union order, with CorridorRouting as the seventh member. The later stages of the development branch therefore read them, and the acceptance check below relies on that.
  • HiGHS 1.11.0 and Boost 1.89.0 (only Boost.Polygon, which is header-only) come through FetchContent, so every C++ job now builds HiGHS. rich>=14 is a new runtime dependency of the Python package.
  • A step binding releases the GIL only around the C++ call, and a Python reporter takes the GIL back for each call. The live line therefore keeps running during the 30-second assignment solve of the 69-qubit chip. On the 69-qubit chip, plan takes about 1.5 minutes, 56 seconds of it in the Capacity step.
  • The test fixture test/fixtures/mini gets launcher offsets of 0, a launcher target of 1 and one feedline termination, and its launchers are numbered clockwise. With the old values, the assignment of the fixture has no solution.
  • There are five NOLINTs: two implicit conversions to LinearExpr, which keep model expressions short; a copy in a test that checks that a copied report shares its state; and setenv and unsetenv in a test, which include-cleaner does not map to a header.
  • The changelog uses ⬆️🪝 update pre-commit hooks #139 as the number of this pull request.

Verification

  • Equality with the development branch. mqt-scpd plan --stop-after corridor ran on the eight benchmark chips, and a script compared the artifacts with those of the development branch field by field, floating-point values bit for bit. All four artifacts are equal on the chips with 9 to 69 qubits. On the 4-qubit chip, only fix 2 makes a difference.
  • Acceptance. The detail and final routing of the development branch, stopped after the outer routing, ran on the artifacts of this pull request. On all eight chips, the last line of the final stage reports no failing wire.
  • C++. 518 tests pass in release and debug builds. The new tests cover the grid additions (port bands, Voronoi diagram, bottlenecks, partitions and chamber moves), the MILP module (HiGHS, MPS, backend choice, observer and interrupt), the report, the registry and every step on the test fixture, with a determinism test per step.
  • Python. 175 tests pass on Python 3.13, plus 66 benchmark checks with MQT_SCPD_BENCHMARKS. The benchmark checks plan every chip and test what the later stages rely on: every inner port is reached, every ring port has a launcher, every connection has a corridor, a crossing slot carries one wire, and no two wires meet inside a partition. Every planning step also draws an SVG under 10 MB and a GDS file. The benchmarks workflow runs these checks.
  • clang-tidy 22 with the repository configuration reports no finding in the new and changed files. uvx nox -s lint passes. The Python stubs and the schema code are regenerated.

AI assistance. The port, the tests, the documentation and this description were written with Claude Opus 5.5 via Claude Code. The author reviewed them.

Checklist

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

If PR contains AI-assisted content:

  • Any agent that created, edited, or submitted GitHub content was explicitly authorized for that scope, as required by our AI Usage Guidelines.
  • Every agent-authored or agent-edited public text body begins with the visible disclosure 🤖 *AI text below* 🤖 (titles are exempt).
  • I have disclosed AI assistance in the PR description.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

🤖 Generated with Claude Code

- Add the Capacity, Global, Assignment and Corridor stages, ported from
  the development branch onto the grid of main; they write the artifacts
  01-capacity.fb to 04-corridor.fb
- Add port bands, the Voronoi medial axis, bottlenecks, partitions and
  chamber moves to the grid module, and bridge pairs and components to
  the design module
- Add MQT::ScpdMilp: a linear model, a HiGHS 1.11.0 backend, MPS output
  and an external backend that reaches Gurobi through gurobipy
- Add the stage interfaces, the algorithm registry and the stage report
  to the pipeline module
- Add mqt-scpd plan with a run directory, --stop-after, --stage, -v to
  -vvv and a live progress line through rich, list-algorithms, and
  --run-dir and --stage for plot and render
- Define the terminal output in docs/terminal_output.md and test it
- Add the configurations of the eight benchmark chips and the --chip
  option
- Leave labels below the first partition label out of the partition
  outlines, so the cells reserved for a port approach form no partition
- Negate the objective of a maximization that an external backend solves
  through MPS
- Build HiGHS without link-time optimization, so a solve does not depend
  on how often the build directory was configured

Assisted-by: Claude Opus 5.5 via Claude Code
@FeldmeierMichael

Copy link
Copy Markdown
Contributor Author

Reminder for me: Changes on the benchmarks configs need to be moved to the benchmark repo

@codecov

codecov Bot commented Oct 9, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Cpp-Linter Report ⚠️

Some files did not pass the configured checks!

clang-tidy (v22.1.8) reports: 3 concern(s)
  • test/pipeline/test_solver.cpp:23:1: warning: [misc-include-cleaner]

    included header cstdlib is not used directly

       23 | #include <cstdlib>
          | ^~~~~~~~~~~~~~~~~~
       24 | #include <memory>
  • test/pipeline/test_report.cpp:142:16: warning: [performance-unnecessary-copy-initialization]

    local copy 'copy' of the variable 'report' of type 'const Report' is never modified; consider avoiding the copy

      142 |   const Report copy =
          |                ^
          |               &
  • test/pipeline/test_capacity_planner.cpp:26:1: warning: [misc-include-cleaner]

    included header cstddef is not used directly

       26 | #include <cstddef>
          | ^~~~~~~~~~~~~~~~~~
       27 | #include <cstdint>

Have any feedback or feature suggestions? Share it here.

- Draw every feedline chain of the assignment as a line from its first
  launcher through the feeds of its resonators to its last launcher,
  with a dot at every feed and a square where it ends at a termination
- Give every chain a color that differs from those of its neighbours
  along the ring, and name its launchers and resonators in a tooltip
- Draw the chains in the corridor picture as well, from the assignment
  of the run, and write them to GDS layer 32 (plan.feedline)

Assisted-by: Claude Opus 5.5 via Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant