<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>TinyComputers.io (Posts about benchmark)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/benchmark.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 A.C. Jokela 
&lt;!-- div style="width: 100%" --&gt;
&lt;a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"&gt;&lt;img alt="" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png" /&gt; Creative Commons Attribution-ShareAlike&lt;/a&gt;&amp;nbsp;|&amp;nbsp;
&lt;!-- /div --&gt;
</copyright><lastBuildDate>Fri, 24 Apr 2026 17:16:01 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>What a Commercial PCB Placer Does That My Open-Source One Can't</title><link>https://tinycomputers.io/posts/what-a-commercial-pcb-placer-does-that-my-open-source-one-cant.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/what-a-commercial-pcb-placer-does-that-my-open-source-one-cant_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;28 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;div class="sponsor-widget"&gt;
&lt;div class="sponsor-widget-header"&gt;&lt;a href="https://baud.rs/wdr0dP"&gt;Quilter&lt;/a&gt; · Partner&lt;/div&gt;
&lt;p&gt;I partnered with &lt;a href="https://baud.rs/wdr0dP"&gt;Quilter.ai&lt;/a&gt; on this post. They provided access to their commercial placement service at no cost so I could run this comparison. The open-source placer I built to benchmark against them, the evaluation methodology, and every number in this post are mine.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;This is an interlude in a running series on designing a level-shifter shield for the &lt;a href="https://baud.rs/poSQeo"&gt;Arduino Giga R1&lt;/a&gt;. The main arc is about the design itself — how to get 72 channels of logic-level translation across a 155mm x 90mm board using ten SN74LVC8T245 shifters, in a repeatable Python-scripted workflow. Previous posts covered the &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;original Fiverr design&lt;/a&gt;, the &lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;Claude Code redesign&lt;/a&gt;, &lt;a href="https://tinycomputers.io/posts/how-a-pin-numbering-bug-killed-a-pcb.html"&gt;a pin-numbering bug&lt;/a&gt;, and the &lt;a href="https://tinycomputers.io/posts/what-routing-314-nets-taught-me-about-ai-assisted-pcb-design.html"&gt;v0.4 respin&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This post is an honest comparison of two ways to place the components on that board: Quilter, a commercial service aimed at exactly this problem, and pyplacer, an open-source simulated-annealing placer I wrote over two weekends specifically to have something to benchmark Quilter against. The results are not flattering to pyplacer, but they are interesting. They also surface a concrete and narrow thing that commercial placement services do right that a naive first pass does not.&lt;/p&gt;
&lt;h3&gt;Why Write a Placer At All&lt;/h3&gt;
&lt;p&gt;A PCB has two distinct layout problems. The first is &lt;em&gt;placement&lt;/em&gt;: deciding where each component goes on the board. The second is &lt;em&gt;routing&lt;/em&gt;: deciding how to connect the pins once the components are placed. These are usually solved by different tools, and the quality of the placement determines how well the routing can go. A bad placement makes a good routing impossible; a good placement makes routing almost boring.&lt;/p&gt;
&lt;p&gt;There is no good open-source placement tool. Freerouting, which is genuinely excellent as an autorouter, does not do placement — it takes placed components as input. KiCad has a rudimentary "move to group" feature and nothing else. Eagle has nothing. pcb-rnd has nothing. For decades, PCB placement has been a human-only activity, and the industry's answer to "automate placement" has been to employ better humans.&lt;/p&gt;
&lt;p&gt;A few commercial services are now genuinely trying to change that. &lt;a href="https://baud.rs/wdr0dP"&gt;Quilter&lt;/a&gt; is one of them: you upload a KiCad board with component libraries and a netlist, it returns a placed-and-routed board. I approached Quilter about partnering on a post and I decided that a straight review would be uninteresting. What I actually wanted to know was: how much of the gap between Quilter and "do nothing" could be closed by a motivated hobbyist with a weekend, some Python, and a simulated annealer? If the gap is small, Quilter is an incremental improvement. If the gap is large, Quilter is doing something specific that is hard to reproduce.&lt;/p&gt;
&lt;p&gt;So I built &lt;a href="https://baud.rs/tcrfa0"&gt;pyplacer&lt;/a&gt;. The goal was not to beat Quilter — that would have been foolish. The goal was to produce a credible open-source baseline, run both tools on the same board under the same constraints, and see how the numbers fell.&lt;/p&gt;
&lt;h3&gt;The Board&lt;/h3&gt;
&lt;p&gt;The benchmark is &lt;a href="https://baud.rs/pOawfA"&gt;giga_shield v0.4&lt;/a&gt; — the same board discussed in the previous post. Relevant specs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;155mm x 90mm, 4-layer stack (two signal, two plane)&lt;/li&gt;
&lt;li&gt;161 nets across 64 components&lt;/li&gt;
&lt;li&gt;Ten SN74LVC8T245PW level-shifter ICs in TSSOP-24 packages&lt;/li&gt;
&lt;li&gt;Two 36-pin 2x18 headers (J9 and J10) along the right edge for the Z80 side&lt;/li&gt;
&lt;li&gt;Several 8-pin and 26-pin single-row headers for the Arduino Giga side&lt;/li&gt;
&lt;li&gt;Forty 0603 bypass and DIR-control caps/resistors&lt;/li&gt;
&lt;li&gt;All components on the top side of the board&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a &lt;em&gt;dense&lt;/em&gt; layout for 161 nets on 155x90mm. The level shifters are the routing chokepoint: each one has 8 A-side pins that connect to one header and 8 B-side pins that connect to another, plus VCCA/VCCB/GND/DIR. Get the shifters wrong relative to the headers and the board is unroutable at 4 layers.&lt;/p&gt;
&lt;p&gt;The honest baseline for comparison is not "random placement." It is &lt;em&gt;my&lt;/em&gt; hand-placed v0.4 layout, which Freerouting routes to 100% at 0.3mm clearance in about 45 minutes of wall-clock time. I know that because I spent several weeks iterating to produce it. The goal of any automated placer is to do that work so I do not have to.&lt;/p&gt;
&lt;h3&gt;Quilter: The Commercial Option&lt;/h3&gt;
&lt;p&gt;Quilter presents as a web service: you upload your &lt;code&gt;.kicad_pcb&lt;/code&gt; (unplaced) with its component libraries and netlist, set a few parameters (board outline, fixed components, target layer count), and they generate candidate placements.&lt;/p&gt;
&lt;p&gt;For this comparison I submitted giga_shield v0.4 with the ten shifters and all passives unplaced, the four headers and mounting holes locked in position (because those are mechanical constraints tied to the Arduino Giga's hardware), and the board outline fixed. Quilter returned &lt;code&gt;Candidate_1&lt;/code&gt; after 1 hour 10 minutes 48 seconds of compute.&lt;/p&gt;
&lt;div style="text-align: center; margin: 30px 0;"&gt;
&lt;img src="https://tinycomputers.io/images/giga-shield/quilter-top.png" alt="Top-down render of Quilter's Candidate_1 placement. Ten level shifters distributed across the board: three in a row along the top edge near J5, two in the bottom third near J8, and five clustered between J9 and J10 on the right-hand side. Passives are scattered across the routing channels in short local clusters. The board has routing traces visible on the top and inner layers." style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px;"&gt;
&lt;p style="color: #666; font-size: 12px; margin-top: 10px;"&gt;Quilter Candidate_1, rendered via &lt;code&gt;kicad-cli pcb render&lt;/code&gt;. Ten shifters spread into three clusters, each one closest to the connectors it serves. Routing threads through every available channel.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Freerouting, given Quilter's placement and the same 0.3mm clearance rule I used for my hand-placement, routed 99.4% of the board: 1 net unrouted out of 161. The single unrouted net was a non-critical DIR control signal that could have been fixed with a manual jumper or a small placement tweak. For practical purposes, the board is fabricatable as Quilter produced it.&lt;/p&gt;
&lt;p&gt;That is a strong result. It is in the same quality neighborhood as my hand-placement, produced in about seventy minutes of their wall-clock time versus my several weeks of iteration.&lt;/p&gt;
&lt;h3&gt;pyplacer: The Open-Source Attempt&lt;/h3&gt;
&lt;p&gt;pyplacer is about 2,000 lines of Python. It does &lt;a href="https://baud.rs/rZysbW"&gt;simulated annealing&lt;/a&gt; over component positions on a fixed board outline, with a cost function that combines half-perimeter wirelength, bounding-box overlap penalties, an out-of-bounds penalty, a grid-cell congestion estimate from probing L-shaped routes across a coarse mesh, and a small pad-exit direction bias that tries to align TSSOP-24 pads with their natural routing sides.&lt;/p&gt;
&lt;p&gt;The overall architecture is pragmatic rather than clever:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A KiCad board loader that reads footprints, pads, and netlist&lt;/li&gt;
&lt;li&gt;A heuristic seed placement that puts each shifter at the midpoint between its two dominant connectors (the connectors that account for the most of its signal pins), with special handling for clusters of shifters sharing the same connector pair&lt;/li&gt;
&lt;li&gt;An SA loop: propose a move (shift, swap, or component-to-component swap), evaluate the cost delta, accept with &lt;a href="https://baud.rs/09b03A"&gt;Metropolis probability&lt;/a&gt; at the current temperature, cool&lt;/li&gt;
&lt;li&gt;A DSN exporter that writes a Specctra file with the placed positions and the netlist, ready to feed to Freerouting&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The cost function I arrived at after maybe a dozen tuning rounds:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;W_HPWL&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;wirelength&lt;/span&gt;
&lt;span class="n"&gt;W_OVERLAP_HARD&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1000.0&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;may&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;overlap&lt;/span&gt;
&lt;span class="n"&gt;W_OVERLAP_SOFT&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;50.0&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keepout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;zones&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;around&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;components&lt;/span&gt;
&lt;span class="n"&gt;W_OUT_OF_BOUNDS&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;200.0&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;
&lt;span class="n"&gt;W_CONGESTION&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;40.0&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;based&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;congestion&lt;/span&gt;
&lt;span class="n"&gt;W_PAD_EXIT&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pads&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;toward&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;natural&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;routing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sides&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Everything else — rotation moves, attachment-to-dominant-connector, displacement-from-heuristic — I tried and disabled, because each one either produced worse results or did not produce a measurable improvement.&lt;/p&gt;
&lt;p&gt;The SA run itself is 5,000 iterations at 0.96 cooling with a 1e-4 final-temperature ratio, taking about 90 seconds on a single core of a Mac M3 Pro. That is fast enough that I could do broad seed sweeps cheaply.&lt;/p&gt;
&lt;p&gt;For the benchmark, I ran pyplacer on 64 distinct random seeds across three machines (my Mac, a 32-core Linux host, a 64-core Linux host), routed every resulting placement through Freerouting at the same 0.3mm clearance, and recorded the number of unrouted nets. The seed distribution matters because SA is stochastic: different seeds give different local minima, and the spread of outcomes is informative.&lt;/p&gt;
&lt;h3&gt;The Numbers&lt;/h3&gt;
&lt;p&gt;Over 64 seeds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Best result: 10 unrouted nets (93.8% routed) — this was the early best from the first batch of runs&lt;/li&gt;
&lt;li&gt;Typical plateau result: 16–20 unrouted nets (87.6%–90.1% routed)&lt;/li&gt;
&lt;li&gt;Worst result: 114 unrouted nets (29.2% routed)&lt;/li&gt;
&lt;li&gt;Median: 21 unrouted nets (87.0% routed)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every single pyplacer run was worse than Quilter's &lt;code&gt;Candidate_1&lt;/code&gt;. The gap between the best pyplacer seed and Quilter is about six percentage points of routing completion, which on a 161-net board is nine to fifteen missing traces. In practice, a board with 10–16 unrouted nets is not fabricatable as-is. It needs hand-fixing, which is exactly the work the placer was supposed to save.&lt;/p&gt;
&lt;div style="text-align: center; margin: 30px 0;"&gt;
&lt;img src="https://tinycomputers.io/images/giga-shield/pyplacer-top-routed.png" alt="Top-down render of a pyplacer placement after Freerouting. All ten level shifters clustered tightly in a horizontal band across the middle of the board, immediately to the left of J9. Routing traces visible, but the shifter cluster is visibly cramped and many nets run long distances to reach it." style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px;"&gt;
&lt;p style="color: #666; font-size: 12px; margin-top: 10px;"&gt;A pyplacer placement after a Freerouting pass. Notice how the shifters are crammed into a single cluster hugging the left edge of J9, and the routing channel on that edge is visibly dense. This layout routes to about 90%; the unrouted nets are all in the congested zone on the left side of J9.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Compare that image to the Quilter render above. The topological difference is immediate: Quilter spreads its shifters into several clusters, each close to the connector or connector-pair it serves. pyplacer piles them all into the shortest-wirelength position, which happens to be a narrow strip where routes cannot fan out. The visual story matches the numbers.&lt;/p&gt;
&lt;h3&gt;Which Nets Failed&lt;/h3&gt;
&lt;p&gt;I logged Freerouting's warning-level output across the entire 64-seed sweep and extracted the nets that consistently failed to route on the best seed's plateau. Seventeen candidate nets, of which sixteen appear in any given plateau pass. The surprising thing is not the count — it is the topology:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Net&lt;/th&gt;
&lt;th&gt;Pins&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;D23&lt;/td&gt;
&lt;td&gt;J9-4 ↔ U8-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D25&lt;/td&gt;
&lt;td&gt;J9-6 ↔ U8-4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D27&lt;/td&gt;
&lt;td&gt;J9-8 ↔ U8-5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D29&lt;/td&gt;
&lt;td&gt;J9-10 ↔ U8-7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D31&lt;/td&gt;
&lt;td&gt;J9-12 ↔ U8-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D33&lt;/td&gt;
&lt;td&gt;J9-14 ↔ U8-9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D35&lt;/td&gt;
&lt;td&gt;J9-16 ↔ U8-10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D37&lt;/td&gt;
&lt;td&gt;J9-18 ↔ U9-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D39&lt;/td&gt;
&lt;td&gt;J9-20 ↔ U9-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D41&lt;/td&gt;
&lt;td&gt;J9-22 ↔ U9-5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D43&lt;/td&gt;
&lt;td&gt;J9-24 ↔ U7-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D45&lt;/td&gt;
&lt;td&gt;J9-26 ↔ U7-5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D47&lt;/td&gt;
&lt;td&gt;J9-28 ↔ U7-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D49&lt;/td&gt;
&lt;td&gt;J9-30 ↔ U7-10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D51&lt;/td&gt;
&lt;td&gt;J9-32 ↔ U10-4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PJ5&lt;/td&gt;
&lt;td&gt;J10-16 ↔ U8-13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PB1&lt;/td&gt;
&lt;td&gt;J1-4  ↔ U2-5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Fifteen of the seventeen are the exact same topological class: a short digital signal from a level shifter to a sequential pin on the J9 double-row header. These are the easiest routes on the board. They fail because pyplacer has piled all four shifters (U7, U8, U9, U10) up against J9's left edge, and sixteen signals are trying to enter J9 through a 10mm-wide routing channel that can physically only accommodate maybe eight parallel traces.&lt;/p&gt;
&lt;p&gt;This is a connector-edge congestion failure. It is not a routing problem. It is a placement problem that the router cannot undo.&lt;/p&gt;
&lt;h3&gt;Why pyplacer Gets This Wrong&lt;/h3&gt;
&lt;p&gt;Half-perimeter wirelength rewards placing U7/U8/U9/U10 as close to J9 as possible, because every one of their B-side pins connects to J9 and the distance matters. The bounding-box penalty keeps them from overlapping each other. The congestion estimate &lt;em&gt;does&lt;/em&gt; catch some of this — it is why pyplacer's placements are not as awful as they would be without it — but the resolution of the congestion grid is too coarse to catch what is happening at J9's edge specifically. The cost function sees "OK, the shifters are stacked but they are not literally on top of each other, and the average 2x2mm cell is not saturated." The router sees "sixteen traces trying to enter the same five-millimeter gap, no thanks."&lt;/p&gt;
&lt;p&gt;Fixing this correctly is not just a matter of tuning weights. The cost function is missing a dimension. Specifically, it needs a term that penalizes &lt;em&gt;incoming pin density at a fixed connector's edge&lt;/em&gt; — a count of how many signal nets want to enter a given side of each fixed header per millimeter of edge, with a soft threshold above which the penalty climbs sharply. A naive implementation of this adds about 50 lines to the cost function and a per-move incremental update. I have not yet written it because the interesting finding of this benchmark is not "how do I close the gap" — that is the next post. The interesting finding is &lt;em&gt;where&lt;/em&gt; the gap lives.&lt;/p&gt;
&lt;p&gt;Quilter, whatever it is doing internally, is not piling shifters up against J9. It spreads them. U7, U8, U9, and U10 in Quilter's placement are in a 4x1 tight column arrangement a few millimeters away from J9's edge, with gaps between each chip that are deliberately large enough for routes to fan out between them. That does not minimize wirelength. It maximizes &lt;em&gt;routability&lt;/em&gt;. The two are not the same objective, and pyplacer only optimizes the first.&lt;/p&gt;
&lt;h3&gt;Speed and Cost&lt;/h3&gt;
&lt;p&gt;A fair comparison also needs to account for what each approach costs to produce.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quilter&lt;/strong&gt;: &lt;code&gt;Candidate_1&lt;/code&gt; took 1h 10m 48s of their wall-clock compute. Pricing is on their website; I received this candidate through my partnership with Quilter and did not personally pay. One good candidate is enough for a finished board, so the total spend for a project like this is whatever a single candidate costs, not a subscription or a retainer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pyplacer&lt;/strong&gt;: A single seed takes about 90 seconds of single-core wall-clock time. A 64-seed sweep with partial parallelism across three machines took about 35 minutes of real time and consumed roughly three CPU-hours. The Freerouting pass that evaluates each placement takes 10–15 minutes per candidate, so evaluating all 64 placements took an additional ~15 CPU-hours, mostly on Linux hosts. Electricity cost is negligible — pennies. But the human-hours cost of writing, tuning, and debugging pyplacer was maybe 25 hours of my weekend time spread across two weeks.&lt;/p&gt;
&lt;p&gt;So: Quilter produces a usable placement in about seventy minutes of their compute, at whatever their per-candidate list price is. pyplacer cost 25 hours of engineering time, some CPU, and still does not produce a usable placement. The break-even point, assuming my engineering time is valued at even a modest hourly rate, favors Quilter by a wide margin for single-project work. The calculus only flips if you are going to place hundreds of boards and amortize the pyplacer development cost across all of them. For a hobby project or a one-off prototype, it is not close.&lt;/p&gt;
&lt;h3&gt;What About Future Improvements?&lt;/h3&gt;
&lt;p&gt;The obvious counter is that pyplacer is a weekend project and Quilter has been in development for years with a team. If I added the connector-edge congestion term, fixed the coarse grid resolution, added rotation moves, and let the SA run for 10x longer, would I close the gap?&lt;/p&gt;
&lt;p&gt;Probably partially. Maybe to 95–97%. I do not think I would close it fully. Here is why.&lt;/p&gt;
&lt;p&gt;Quilter is doing things that are not obvious from its output but are visible in the spacing patterns: it seems to have a model of the router's behavior, of how close is "too close" for vias to cluster, of how routing channels compose across layers. A simulated annealer can be taught any cost function you can express, but the cost function has to know to include these things. Writing a cost function that captures what Quilter captures is not a weekend project. It is months of work by someone who understands routing at a deep level, which I do not.&lt;/p&gt;
&lt;p&gt;I could also throw more compute. The 64-seed sweep took ~18 CPU-hours total. If I ran 1,000 seeds on a cluster, I would likely find a placement better than the best seed from 64, by selection pressure alone. But the ceiling set by the cost function's limitations is not breached by more samples; it is breached by a better objective function. And that objective function is where the commercial tool is earning its fee.&lt;/p&gt;
&lt;h3&gt;What This Means for the Series&lt;/h3&gt;
&lt;p&gt;The immediate practical takeaway for the GigaShield series is that if I wanted to save time on future revisions of this board, running it through Quilter and accepting &lt;code&gt;Candidate_1&lt;/code&gt; would be a sensible default — especially for the v1.0 redesign where I want to add thermal pads, improve the layer stack, and shuffle the power rails. That is a genuine upgrade over the hand-placement workflow, which, while competent, took me multiple weeks of iteration to get right.&lt;/p&gt;
&lt;p&gt;The broader takeaway is that placement is a harder problem than routing. Freerouting is an excellent autorouter because routing has a clean, well-posed objective: find paths that respect clearance rules and minimize length. Placement has no such clean objective. A "good" placement is one the router can finish, which is not something you can compute directly from the placement — you only find out by running the router. That feedback loop makes placement the sort of problem where stochastic search without a good model does not close the gap with humans or with informed commercial tools.&lt;/p&gt;
&lt;p&gt;I am going to keep iterating on pyplacer because I find the problem interesting and because an open-source baseline has value even when it is worse than the commercial alternative. The connector-edge congestion term is the first thing I will add. After that, I want to look at rotation moves in the SA, because the ability to flip a shifter's A/B-side orientation is exactly the sort of thing that can unclog a congested connector edge at the cost of a small HPWL penalty.&lt;/p&gt;
&lt;p&gt;But I am not going to pretend I am closing the gap to Quilter through cleverness alone. Whatever Quilter is doing, it is working. For a dense board with real constraints and a real fabrication deadline, their output is better than mine.&lt;/p&gt;
&lt;h3&gt;The Honest Review&lt;/h3&gt;
&lt;p&gt;Companies that partner with me on posts sometimes expect the post to soften the criticism of their tool. Quilter, to their credit, did not. I uploaded my own board, they ran it through their service, and they let me analyze the output however I wanted. The result is that this is a post about an open-source placer that loses to a commercial one, not a post about a commercial tool defeating a straw man.&lt;/p&gt;
&lt;p&gt;For the specific use case I built this around — dense four-layer mixed-signal shields with 150+ nets and tight routing channels — Quilter's &lt;code&gt;Candidate_1&lt;/code&gt; is better than what I can produce with a weekend placer. The gap is not close. The gap is specifically at connector-edge congestion, which is a place where my naive objective function fails and theirs does not. That is a real and informative finding, and the right conclusion is that whatever Quilter charges per candidate is a reasonable price for what you get.&lt;/p&gt;
&lt;p&gt;I would recommend them for boards like this one. I would not recommend them for dead-simple boards where a hand placement in KiCad takes twenty minutes — that is still the cheapest path for trivial work. The interesting threshold is somewhere in the range of 50–100 nets and a non-trivial connector layout; below that, do it yourself; above that, pay the pros.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Previous posts in the GigaShield series: &lt;a href="https://tinycomputers.io/posts/what-routing-314-nets-taught-me-about-ai-assisted-pcb-design.html"&gt;What Routing 314 Nets Taught Me About AI-Assisted PCB Design&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/how-a-pin-numbering-bug-killed-a-pcb.html"&gt;How a Pin Numbering Bug Killed a PCB&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;Redesigning with Claude Code (Part 1)&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;Fiverr PCB Design (\$468)&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</description><category>arduino giga</category><category>benchmark</category><category>freerouting</category><category>level shifter</category><category>open-source</category><category>pcb design</category><category>placement</category><category>pyplacer</category><category>quilter</category><category>retroshield</category><category>simulated annealing</category><category>z80</category><guid>https://tinycomputers.io/posts/what-a-commercial-pcb-placer-does-that-my-open-source-one-cant.html</guid><pubDate>Fri, 24 Apr 2026 15:30:00 GMT</pubDate></item></channel></rss>