What CP-SAT and HiGHS actually do in the whole route

The whole route (awx/whole_route.py, run on a real board by the bus step awx/route_bus.py, on bus622-take5) plans every lane's complete path between two ball grid arrays before it routes anything. That plan is built by two solvers with different jobs. CP-SAT settles discrete questions: who crosses whom, and on which layer. HiGHS then solves a continuous question: how far sideways each lane sits at every point along the way. Between them sits the frame, which turns the board into one road per lane, so both solvers can work along a single coordinate. Most sections below have an interactive toy. The toys use the same ideas and constraint shapes as the code, at toy scale.

The words you will meet

lane
one net's planned path through the bus, from the source BGA to the destination BGA. A differential pair is one lane.
tooth / berth
where a lane escapes at the source (tooth) and arrives at the destination (berth): the end of its escape stub, just outside the array. A surface stub fixes the layer the lane starts or ends on. A stub that ends in a via (a via end) leaves the layer to the solve.
trunk
a straight line from the source array's pad box through the destination's. Lanes that arrive on the destination's facing side end on it.
ring
a road round the destination, outside its pads and the parts hugging it. A lane whose berth is on another side of the destination leaves the trunk and rides a ring to it.
cut
(two meanings) the point where an array's outline is unrolled into one order: the destination's, away from its facing side, where its two rings meet and which decides which way round each lane goes, and the source's, on its far face (the frame); and a "not this combination" rule sent back to CP-SAT after a later stage found a plan that cannot be drawn (the loop).
u
a lane's route coordinate: distance along the trunk from its tooth (or from the end of its lead-in, for a tooth on the source's far side), then along its ring to its berth. Every crossing and layer change is a position in u.
crossing
a pair of lanes whose order is swapped between source and destination. They must pass through each other, so at that point they sit on different layers.
via
a layer change. Vias are what the discrete plan tries to minimise. Every via is a through via.
convex
a region with no dents, holes or separate pieces: every point between two allowed points is allowed too. An LP's region always is, which is why the LP finds the best answer overall and not just the best nearby (section 2).
F.Cu (top layer)B.Cu (bottom layer)via
One correction to the framing. In the whole route HiGHS is a plain LP (continuous, via scipy.optimize.linprog in whole_geo.py), not a MILP. HiGHS is used as a MILP (scipy.optimize.milp) in the older braid-chain code and its instruments: schedule.exact_pages, joint_floor.py, synth_bus.cap_floor. Those are covered at the end.

Watch a whole run

Two films of real runs of the whole route on a two-layer bench (a processor's BGA to a DDR3 memory), made by awx/whole_movie.py in September. Each walks the stages in order: the ends, the frame, the solve, the geometry, the polish, the snap, the audit and the route. The stages are the same today; what has been added since (more than two layers, the escape fanout, the winding cut) is described below. Beside the board the film draws what each solver decides:

Neither solver is re-implemented for the film. Each is run again under the chain's own settings and watched, and the film is refused unless the re-run writes what the chain wrote, byte for byte.

K28: the plan passes on the first round

25 lanes (28 nets, 3 of them differential pairs). CP-SAT finds three plans in 0.14 s and proves the last one optimal, and the plan passes every audit check on the first loop round. Routed: every net connected, DRC clean, 30 vias and 589 mm, against the human's 48 vias and 678 mm on the same nets (the human's copper includes its length-matching meanders).

2 min 38 s, 1080p. Open in Google Drive

K51: one round of negotiation

48 lanes (51 nets, 3 of them differential pairs). CP-SAT proves its plan optimal in 19 s, but the first loop round does not pass: the audit finds 3 places short, and the polish finds 2 lanes with no room on their side of an island. The film stops to show what goes back (section 3): side flips to the geometry, and cuts and history to the solve, which solves again with the same vias and the crossings moved out of the short places. Routed: every net connected, DRC clean, 80 vias and 1220 mm, against the human's 88 vias and 1337 mm.

2 min 50 s, 1080p. Open in Google Drive

0   The pipeline, and where the two solvers sit

Click a stage. Dashed arrows are the feedback that makes this a negotiation instead of a one-way pipe. whole_route.py repeats the fanout for up to 3 rounds (--rounds; the bus step in the routing chain runs 2), and within a round its loop repeats solve, geometry, polish and audit for up to 6 rounds (LOOP_ROUNDS).

0a   Ends and fanout: the ends decide the braid (fanout_from_plan.py, whole_ends.py)

Before any solver runs, the fanout decides where every lane leaves the source BGA (its tooth) and where it arrives at the destination (its berth), including which layer it is on at each end. Those choices fix the whole puzzle: the order of teeth around the source and of berths around the destination decides which pairs must cross, and the end layers decide what each crossing costs. That is why the fanout is not a separate chore. whole_ends scores candidate ends by the vias the resulting braid would need (an estimate from the ends alone), and the fanout lays the cheapest, then judges what it actually laid. Crowded ends found later by the audit come back here as feedback.

The ends are chosen from a menu of ways out of each ball. With the escape fanout (route_bus's escape mode) the menu is the human's: every netted ball of both arrays is first dog-boned (a short stub to a via on a diagonal gap point, dogbone_arrays.py), an outer ball with a clear way out is left on the surface, and a lane may then leave its ball's via on any routing layer: half a pitch onto a ball line, a sidestep to a line beside it, or onto an empty street through the array (escape_plan.py). An array an earlier bus already routed through is planned whole each round with the new bus, its other nets and plane balls included (joint_escape.py). The bus step has two other modes. joint also fans out the arrays' other nets and plane balls: the bus is laid round a via site kept free in every other ball, and the rest are planned together round it in one CP-SAT model, plane balls sharing drop vias or strapped to a neighbour. plain fans out the bus alone.

Five lanes leave the source (left, fixed order) and must be given berths at the destination (right). Press a ⇅ button to swap two neighbouring berths, and click a layer badge to flip an end's layer. The counters give the crossings your choice forces and the minimum vias for them (found exactly, as in 1a). Then press Best ends: it tries all 120 berth orders for the layers you set. That is a brute-force version of the choice the ends model makes from its escape menus, which are far too big to try in full (0b).

crossings: 0minimum vias: 0

The real menu is far too big to try every combination, so whole_ends uses a local search. One move changes one lane's tooth or berth to another option. If that option is already taken by one other lane, that lane is ejected to its best free option, and the two changes are scored together (an exchange). A move is taken as soon as it lowers the objective, and it sweeps until no single move helps. That reaches a local optimum, which is not always the best. So it is iterated: a few lanes are kicked to random options, searched again, and the result kept only if better. The kicks use a fixed seed, so every machine gets the same answer.

Five lanes must each take one of seven exits at the destination (right). An exit is fixed to a layer, so the choice of exit also chooses the end layer. The number is the minimum vias (found exactly, as before) plus a small tie-break for crossings. Try it yourself: click a lane on the right, then click an exit to move it there (the lane in the way is ejected to its best free exit). Or let the search work: Take a move once at a time, then Kick when it is stuck.

vias now -moves taken 0kicks: 0 tried, 0 in a row failed
How this maps onto the real search
  • The real objective is one sum in vias: every via of the ends and the estimated route (a pair's change is two, its two barrels); five more for every via a net carries past two; each lane's ride at a fixed exchange rate per via; congestion on the trunk; stacking (two lanes' ends at one point on different layers where either changes layer); what blocks a lane's straight way out of its exit; a plane ball the ends wall in; and the audits' feedback, priced by place. The crossings' count is the last tie-break, exactly as here.
  • Options that conflict are never taken (select_moves._conflict). An option with two or more conflicts is skipped, not ejected.
  • Here the teeth are fixed. In the real run the search is done twice: berths against the teeth as laid, then with the teeth free too (a tooth move must save more than half a via, because it costs a re-fanout). On the escape fanout nothing is laid yet, so both ends are free from the first search.
  • The kick moves 3 lanes to random options and stops after 4 kicks in a row that find nothing (at most 12). Then ban kicks: the three most crossed lanes have their current ends banned, the search runs with and without the bans, and the better state stands. The finalists are re-ranked by the exact CP-SAT route.
  • On more than two layers the estimate and the search loop run in the router's Rust core (ends_kernel.py), forming the same numbers in the same order as the Python, so both walk the same states.

Why a local search here, when the rest of the pipeline uses CP-SAT and an LP. The search starts from the fanout's own greedy choice of berths (every berth clear of the others), from CP-SAT's joint ends on an escape fanout (below), or on a re-plan from the previous round's, and improves it by the moves above. CP-SAT is still used, but as the final judge rather than the search: the best few ends the search reaches are re-ranked on the exact route (best_exact). The ends are hard to put in one exact model, for three reasons:

What it gives up is a proof. CP-SAT proves the fewest vias for the ends it is given, and the LP the best geometry for that plan, but nothing proves the ends themselves are the best. The search stops at a local optimum. The kicks and the exact re-ranking make a poor one less likely, but do not rule it out.

Where CP-SAT does choose the ends. On the escape fanout, before any local search, one CP-SAT model picks every lane's tooth and berth together from the same menus (the joint ends): one choice per lane and end, the same conflicts, and two lanes whose orders differ round the boxes crossing on different layers. It counts the fanout's vias and ten vias a crossing, so it puts crossings first: the local search's objective prices a crossing at a hundredth of a via, and on a dense bus it bought stacks back with crossings the solve could then not place. Whichever ends are chosen are then put to the solve's check: the bench they would make is built in memory and the whole solve (section 1) is run on it under its strict rules. If it finds no plan, the crowded lanes are priced and the ends are searched again (up to four times). The ends model also chooses the cut on the destination's far side, which is where the frame takes over.

0c   The frame: one road per lane, both arrays unrolled (whole_frame.py)

The ends are points round two arrays, but both solvers want each lane to be one road with one coordinate. The frame builds those roads from the board alone.

One lane, worked through. Take a lane whose berth is on the destination's far side, facing away from the source. It leaves its tooth and runs along the trunk, the straight line from the source's pad box to the destination's. In front of the destination it reaches the arrival line (H0). There it leaves the trunk onto the north ring, a road round the destination outside its pads, follows it round the corner and down the far side, and drops into its berth (lane D in the figure below). Its route coordinate u is how far it has come: 32 mm along the trunk, then 23.7 mm along the ring, so u runs from 0 to 55.7. A lane whose berth faces the source never leaves the trunk at all. Every crossing and every via the solve places is a position in u.

Which way round is decided by the cut, one point on the destination's outline away from the facing side. Berths on one side of the cut go round the north, the others round the south. Now unroll the destination: cut its outline there and straighten it into a line. The berths stand in one order, north to south. Two lanes cross exactly when that order and the teeth's disagree.

The source is unrolled too, from a cut on its own far face, but it has no rings. A tooth on its facing face starts on the trunk. A tooth on its north or south face runs along that face and joins the trunk past the corner, and its u counts that run. A tooth on its far face cannot be carried by the trunk at all (the trunk's columns stand across that stretch), so it goes straight out of the face and round the corner on a lead-in: north of the source's cut round the north, south of it round the south, then on as a side face's lane. On each side the lane launched first rides outermost. So the teeth stand in one order too, north to south, and the source's cut decides which way a far-face tooth goes round. Everything the solve needs to know about who crosses whom is in those two lists: moving either cut changes the crossings, and with them the vias. It also changes how far each lane rides round.

Nine lanes leave all four faces of the source (left) and berth on all four sides of the destination (right). Click a grey dot on the destination's outline, or on the source's far face, to put that array's cut there. A dashed stretch is a lead-in. Below the board, the two unrolled orders: a pair of lines that crosses there is a pair of lanes that crosses on the board. Click a lane (or its chip below) to see its u.

crossings -ride - mmnorth ring - · trunk - · south ring -

Default cuts puts them where the code does without the ends model's say: the destination's at the middle of the widest gap between its far side's berths, the source's at the gap between its far-face teeth with the fewest crossings. In the real run the ends model chooses the destination's cut: it scores the ends at each candidate cut and keeps the best (0a). Crossings are cheap only when the two lanes can be on different layers there, so fewer crossings usually means fewer vias, and a cut that winds lanes round the far side can win even though they ride further. A lane winding round the destination to arrive from the far side is what a human layout often does too.

What the real frame does beyond this toy

1   CP-SAT: the discrete plan (whole_solve.py)

Bus lanes leave the source BGA in one order (the launch order: the teeth, unrolled) and must arrive at the destination in another (the berth order: the destination unrolled from the cut, 0c). Every pair that is inverted between the two has to cross, once, at a position in u both lanes share. Two copper lanes cannot cross on the same layer, so at each crossing they are on different layers: on two layers one is on F.Cu and the other on B.Cu. Each layer change costs a via. A lane whose stub ends on the surface starts (or ends) on that layer; a lane whose stub ends in a via may leave it on any layer its run fits on.

The CP-SAT model is exactly that puzzle, for every lane at once: booleans for "which lane is on which layer at this crossing", integers for "where along u the crossing happens" and "where each lane changes layer" (up to three changes a lane), and constraints tying them together. It minimises vias and proves the minimum. Below, an instance of that puzzle with the numbers shrunk to 5 lanes on two layers.

1a   The layer puzzle, and how a solver searches it

Click a × to swap which lane is on which layer at that crossing and watch the via count. Then press Solve and watch the search. Coloured lane pieces show the layer; a white circle marks a via. The boxes at the ends are the fixed tooth layer (left) and berth layer (right).

vias now: 0 best found: - proven lower bound: - nodes visited: 0 branches pruned: 0 of 0 full-tree nodes

The tree: each level decides one crossing (left branch = lane A of the pair on F.Cu, right = on B.Cu). Grey × = a whole subtree thrown away without looking, because its cost so far plus the cheapest possible finish already cannot beat the best plan found. Green = a new best plan.

What the real solver does beyond this toy

The toy is a depth-first branch & bound with one bound (each lane still on the wrong layer needs at least one more via). OR-tools CP-SAT runs the same idea but much harder:

  • Propagation. Every constraint prunes domains as soon as a variable is fixed. Two crossing lanes on opposite layers is a boolean XOR; a lane's change count has to match its start/end layer mismatch (AddBoolXOr(act + [1 if tooth==berth])), so fixing one boolean can force many others with no search.
  • Conflict learning. When a branch fails, it works out why and adds a new clause ruling out every assignment with that same reason, not just this one. The same dead end is never entered twice.
  • LP relaxation and core-based bounds. Extra sub-solvers (default_lp, max_lp, core, objective_lb_search) compute lower bounds on vias, which is what lets it stop with a proof. whole_solve stops as soon as floor(best/W_V) <= floor(bound/W_V): the via count is proved, and only the smaller tie-break term is left.
  • Its work by need. The bound-raising workers run first, a probe of 10 batches: if their plan meets their bound, that is the solve. Otherwise the plan-finding workers (quick restarts, search without the LP, core-based search) run on from the probe's plan and bound, at most 100 batches. A re-solve starts from the round before's plan, laid out whole as a hint.
  • Determinism. Budgets are counted in work (interleaved batches), never in seconds; the workers share no clauses. The same model gives the same plan on every run.
  • Never nothing. A round's first solve keeps the best plan it found even when it cannot prove it (SOLVE_UNPROVED), marked unproved, and the nets that plan leaves over two vias go back to the ends. A model with no plan at all is solved again as a crowd diagnosis: every lane free to break what its crossings need, the fewest such lanes the only price. The lanes it names go back to the fanout to be given other ends.

1b   Where along u: time-window constraints

Booleans decide who crosses. Integers decide where. Each crossing gets an integer position t in u (on a grid of a fifth of a lane pitch), limited to a window: the stretch where both lanes exist, clear of a pair's end connector, and for two lanes on the same ring short of the earlier one's berth. A lane's crossings along it must be at least a lane pitch apart (less for a lane sweeping steeply across the others). That is the AddNoOverlap constraint on optional intervals in whole_solve.py. Layer changes are positions in u too, with rooms of their own:

Inside its window, nothing in the objective prices where a crossing or a change stands: every place costs the same once the vias are minimal. So CP-SAT tends to leave them where its search first put them, often at the window's start, just past the teeth, and the geometry and the audits then judge whether there is room there.

Drag the blue bars. Green rails are the allowed windows. The bottom lane is the trunk, where overlaps show up red. Solve tries the orderings of the four crossings and places each as early as its window allows. Raise the pitch until no ordering fits: that is what "infeasible" means, and it is the situation that becomes a cut in section 3.

1c   What "best" means: a lexicographic objective

The objective is one sum, in vias: W_V·(vias + 5·(each via a net carries past two) + (each run on a plane's layer)) + (history congestion). A net with three vias on the board costs eight, so the solve spreads changes across nets, but this is a price, not a cap. A run on an inner layer that carries a power or ground pour costs one via more, so a lane takes a plane's layer only where it saves a via. The weights are chosen so that each term dominates the next. Fewer vias always beats less congestion, and congestion only breaks ties between plans with equal vias. The congestion term is PathFinder-style history: places where earlier audits found trouble get a price, so the solver steers around them. Last of all, between plans of equal cost, each run's layer is chosen again by the plane it would split (a ground plane's dearer than a power plane's), so a plan and its mirror on the inner layers always come out the same way.

Plan X: 12 vias  ·  congestion
Plan Y: 13 vias, congestion 0
X = Y =
W_V = 1,000,000 per via. Slide the congestion all the way up: X still wins as long as congestion stays under one via's weight. The tie-break never buys an extra via.

1d   More than two layers (route_layers.py, relayer.py)

ROUTE_LAYERS names the layers the lanes may run on: F.Cu and B.Cu, then any inner layers (F.Cu,B.Cu,In1.Cu,In2.Cu), planes' layers included. On two layers a lane's layer is one bit, flipped by each change. On more, each run between two changes is an integer, its layer, and a change goes to any other layer. Two crossing lanes must be on different layers, which now leaves the solve a choice of which two.

2   HiGHS: the continuous geometry (whole_geo.py)

CP-SAT has fixed the topology: the order of lanes at every column, where each pair crosses, and each lane's layer, so where along its route each via sits. What is still unknown is real numbers: how far off its frame's spine each lane sits at each column (one column every 4 router grid steps, along the trunk and along both rings). That question is a linear program, one for the whole bus.

The unknown is one number per (lane, column): its sideways offset o[lane][col]. The rows are:

Every "hard" row is really elastic: it carries a slack variable priced at W_HARD=10⁴. So the LP is always feasible, and if the plan cannot be drawn, the LP does not fail. It pays, and the rows it paid on are reported. Those become cuts for CP-SAT. The LP runs twice: the second pass holds each lane to one side of every island near it, and solves the many tangent rows lazily (only those near binding, then any its answer breaks, until it breaks none).

Why every either-or choice is made before the LP. A lane can pass a pad box below it or above it, and those are two separate pieces of space: between a position below the box and one above it lies the box itself. An LP cannot say "below or above". Every row is a straight wall that says "stay on this side of this line", so an LP's region is always convex: one piece, with no dents. That is what makes its answer trustworthy. In one piece, a lane whose forces balance is at the best answer overall. With both sides of a box allowed, a lane could settle against the wrong side with every force balanced, and never see the better answer on the far side. So CP-SAT fixes the order and the crossings, and static_sides fixes each lane's side of each island (the split below), before the LP runs. The dual page draws it: why the region must be convex.

Three lanes run left to right (order fixed: bottom to top). Lanes are pinned at both ends. A pad box (grey) sits in the middle, and each lane must pass it above or below. Drag the interior dots to route by hand and see what it costs. Solve LP runs a real simplex solver (my own port, a few dozen lines) on the same rows and moves the lanes to the optimum. Between lanes, the coloured ticks show pitch: red below the minimum, amber below comfortable, green fine.

your cost - LP optimum - length -   bends -   comfort -   hard -
Try this
  • Press Solve LP, then squeeze the board height. The optimum starts to pay on a hard row, and the log names which. That is the message that goes back to CP-SAT as a cut.
  • Move the split: it changes which side of the box each lane is forced to. Some splits are cheap, some force awful detours. In the code this is static_sides, one split per island, pinned by the lanes' own ends.
  • Drag a lane into a wrong-side pose, then Solve. The LP always gets a cost at or below yours. It finds the global minimum because, with the split fixed, its region is convex (one piece), so there are no local minima to fall into.

2b   How an LP is solved: walking the corners, or cutting through the middle

Picture an LP with only two unknowns (x, y) so it fits on a screen. Every row is a straight wall, and together the walls fence in a polygon of allowed answers. The polygon is convex, with no dents, because straight walls cannot make one. The cost is a slope: the polygon is shaded from expensive to cheap. The best answer is always at a corner (or along a whole side, if the slope happens to line up with it). The real LP is the same picture with tens of thousands of unknowns. HiGHS has two ways to find that corner:

  1. Pick a direction with the slider (or press Tie to see the special case below).
  2. Press Simplex: next step a few times. Read the log under the picture after each press.
  3. Press Interior point: next step the same way, then compare. Or press Play both to watch them race.
simplex path (corners)interior point path (centre outward)★ = the true optimumthe numbered walls are the LP's rows
Why this matters here. Press Tie, then run both methods to the end. When the cost slope lines up with a side of the polygon, every point on that side is equally good. Simplex stops at whichever end it reaches first. Interior point stops in the middle of the side. Which answer you get can change with the platform's floating-point rounding, but the plan must be identical on every machine. So detmath.lp_tie_break adds a tiny distinct cost to every column, making the optimum a single corner. detmath.lp_round then snaps the answer. Now tick the tie-break box and run again: both methods land on the same corner.

whole_geo.lp_by_dual has one further trick: it solves the dual LP. This LP has many more rows than columns, so its dual has a row per column and every slack is a plain inequality. HiGHS's interior point runs it about 6× faster (on a 51-net bench's second pass: 31 s instead of 188 s). The lane offsets come back in the same answer: HiGHS returns a multiplier for every balance equation of the dual, and those multipliers are the offsets. The interior point is capped at 500 iterations (it once ran 2329 on an 18-net bench); past that the dual simplex solves it, and lands on the same point. What that dual is: its prices are the forces that hold each lane where it is, and a rule that pushes must be touching, which is how the forces place the lanes. See Shadow prices as forces.

3   The two solvers negotiate

CP-SAT proves a plan is optimal in vias, but it knows nothing about millimetres. The LP knows the millimetres but can't change topology. When the LP has to pay on a hard row, it says which lane and which place. That becomes a cut (a boolean disjunction) added to the CP-SAT model, and CP-SAT solves again. Audits after the polish and snap do the same and also feed the congestion history. The loop (whole_route.py) runs up to 6 rounds.

4   After the plan: polish, snap, audit, route

The solvers give a plan that is correct on paper. The remaining stages make it something the router can lay, and check it against the router's own rules before a single track is routed.

4a   Polish: meet the audit's numbers exactly (whole_polish.py)

The geometry LP measures in its own frames and linearises slopes, so its lanes come out a few micrometres short of the audit's bars in dozens of places. The polish works on the lanes' real board polylines. Every near-violation becomes a linear constraint on small vertex moves, a small LP finds the least movement that meets them all, and it repeats (re-linearised) until the audit's measures hold. Each bar is the snap's plus a grid step, so every gap the snap later splits holds a grid row. The toy below uses a simple push in place of that LP, but shows the same behaviour: tiny targeted moves, a few rounds, ends pinned.

Two lanes and a round pad. Red rings mark vertices that are closer than the bar (the lane-to-lane pitch, or the clearance to the pad). The LP's shortfall is exaggerated here so you can see it. Press One polish round repeatedly, or Run to done.

round 0worst shortfall -moved so far 0

4b   Snap: land the line on the router's grid (whole_snap.py)

The router draws only on its grid, in 0°, 45° and 90° segments. The snap takes each lane's smooth line and finds an octilinear path on grid points in a band around it, one lane at a time, with hard clearance to everything already placed. The cost is length + bends + distance from the smooth line. Because the polish left every lane a grid step of room, the snap of one lane never steals the room the next needs.

The dashed grey line is the smooth plan and the pale strip is the allowed band. The coloured path is the snapped result. Click any grid point to add or remove an obstacle (already-placed copper) and watch the path go around it. The sliders are the cost terms.

length -bends -furthest from the smooth line -

4c   Audit, gate and feedback (whole_audit.py, whole_gate.py, whole_lint.py, whole_feedback.py)

The plan is installed in the whole route's own lanes exactly as the router will get it, and the audit runs every one of the router's own checks on it: pitch, dive room, clearance to static copper as KiCad draws the pads, a pair's end connectors and crossover as exact copper, shape, and that every lane's band joins its tooth to its berth. A shortfall of a quarter of the router's grid step or less is a near miss, not a finding: the grid snap moves a lane that near off its bar. The gate passes a plan only when every lane is laid and every check is clean, with nothing waived, and writes every finding's position. The lint checks what the snap promises: grid points, 0/45/90 pieces, a via at every layer change. Where a finding is decides who can fix it:

Feedback reaches the fanout by four channels: crowded ends from the audits; the nets an unproved plan left over two vias; a fanout that was refused (a pair it split); and, after a round is laid, every net it left open, named at the end where it fails. A named end is priced by place (in full where the lane stands on its layer, half on the other layer, fading over 0.76 mm around it), and its price doubles each later round it is named again. A fanout that lays the same ends again has its feedback raised and runs again.

Two lanes run from a source array (left) to a destination array (right). Move the pinch to squeeze the gap between them, then Run audit. Try it once in the middle and once near an end. Then use the two fix buttons and audit again.

gate: not run

4d   Route all lanes, each in its band (route_lanes.py)

Finally the production router lays every lane from the audited plan (--plan): pairs first, then the singles, each in the berths' order round the destination, each confined to its own band around the plan and routed against every earlier lane's copper. A pair's end connectors and crossover are laid as planned and the pair router runs between them. A pair refused in its band is tried in a wider one, and free of the plan only last, after every lane has had its band. Then check_connected and check_drc grade the board (a lane reported "routed" is not proof its net connects). The plan is what makes the order unimportant: it already decided which lanes pass which side of an obstacle.

Four lanes must get past a block. Each is routed in turn as a shortest path that keeps clear of what is already laid. With follow the plan's bands off, the router is on its own: whether all four fit depends on the order. Turn it on and every order works.

routed 0 / 4

4e   Rounds, and the board handed on (whole_route.py, route_bus.py)

A run hands back the best board it laid, and lays something whenever a plan for some of the lanes exists:

On a real board the bus step (route_bus.py) wraps this. It finds the buses between pairs of ball arrays (or takes them from --bus SRC:DEST[:MODE[:LAYERS]], each with its own fanout mode, joint, escape or plain, and its own routing layers), strips the bus's old copper, fans out, turns the board into the flow frame, runs the whole route and turns the copper back. It then grades the board where it is written: every bus net connected, no DRC violation on a bus net, the whole board no worse than it came. The board's length and skew rules are named but not counted, because the bus is laid untuned. A net left open, or named in a violation, goes back as it came, for the rest of the routing chain. The nets handed on are protected in the board's project and their tracks and vias locked, so the later routing steps route round them and never rip them up. Several buses are routed in turn, each on the board the one before handed on.

5   Side by side

CP-SAT whole_solve.pyHiGHS LP whole_geo.py
QuestionWhich pairs cross, at which position along the route (u), and on which layer does each lane sit at each crossing? How many vias are unavoidable?Given that order and those layers, what sideways offset does each lane have at each column of the trunk and the rings?
UnknownsBooleans (which lane above, change active), small integers (crossing position, up to 3 changes per lane, and on more than two layers each run's layer)Real offsets (tens of thousands of columns of them), plus elastic slacks
StructureCombinatorial. Search + propagation + clause learning + LP bounds, in parallel sub-solvers, deterministic.Convex: one piece, because CP-SAT and static_sides have made every either-or choice. Simplex or interior point on a sparse system (often dual), with a tie-break for a unique optimum.
ObjectiveOne sum in vias: vias, five more for each via a net carries past two, one more for each run on a plane's layer; then history congestion; then the planes, as a tie-break.Length + bends + squeeze below comfortable pitch, plus 10⁴× anything hard it had to break.
GuaranteeProves the via minimum where it can. A round's first solve keeps the best plan it found even unproved, so a round never ends with nothing; a model with no plan names the crowded lanes instead.Global optimum of its LP, always feasible. Whatever it pays on hard rows is reported.
Failure feedbackReceives cuts (soft, or firm for another net's track or a keep-out), layer cuts, and history (audit found this place hot).Produces cuts, and flips a static side when the polish found no room.
ScaleCost tracks the number of crossings: about 20 crossings proved in 2 s, 135 in 20 s, 280 in about 4 minutes; one bus of 317 crossings was kept unproved after 518 s.One LP per geometry pass; seconds to tens of seconds.

Where else these solvers appear (older braid chain and instruments)