A field guide to quantum circuit compilers

What quantum compilers and transpilers do to a circuit, how they decide one circuit is better than another, the main families of methods and tools, and where research is heading, including compilers for fault-tolerant machines. Part 1 of a series on quantum circuit optimization.

A quantum compiler takes the circuit you wrote and returns one that a particular machine can run, usually with fewer gates. This post is a field guide to what happens in between, for readers who know what qubits, gates and circuit diagrams are but have never looked inside a compiler.

I wrote it while working on the Classiq Quantum Circuit Challenge, the subject of Part 2. The task was a phase oracle, a circuit that puts a minus sign on the basis states that stand for the black pixels of the Classiq logo. When I ran the best off-the-shelf compilers on the challenge’s baseline circuit, they made it about 6 percent shorter, while a redesign of the same oracle was 56 times shorter. The reason is that a compiler must keep the circuit’s behaviour on every input and is never told the facts that would allow a much shorter circuit, as a later section explains. Before that come the jobs a compiler does, the costs it optimises, the main families of methods, the tools you can install today, and where research is going, including compilers for fault-tolerant machines, which count costs very differently.

A tree diagram: quantum compiler branches into six jobs: translate into native gates, synthesise from a specification, improve a circuit, map to the hardware, check the result, and a fault-tolerant back end. Each job has one or two leaves naming its main methods and tools.
The jobs of a quantum compiler. A compiler is several kinds of work, each with its own methods and tools. The sections below take them roughly in this order.

From a program to a runnable circuit

Borrowed from classical compilers

A classical compiler reads source code, turns it into an intermediate representation (IR) that is easy to analyse, runs a sequence of passes that each rewrite or analyse the IR, and finally emits instructions for one target processor. Quantum toolchains copied this shape. The paper describing Qiskit calls its design “similar to classical compiler infrastructures such as LLVM” : a circuit is held as a graph of gates, and a pass manager runs passes over it in stages.

The IR is usually the circuit itself, as a list or a dependency graph of gates. The common text format for exchanging circuits is OpenQASM. Its third version adds classical control flow and timing, so a program can measure a qubit mid-circuit and branch on the result . Compiling such dynamic circuits is a research topic of its own . Other IRs borrow directly from classical compilers; QIR, for example, builds on the LLVM infrastructure . A 2025 review compares these IRs .

You will also meet the word transpiler. Qiskit uses it for the part of the compiler that takes a circuit and returns a circuit, as opposed to the part that turns a high-level program into a first circuit. Most of this post is about transpilers, because that is where most optimisation happens.

The target matters at every step. A real device runs only a few native gates, typically one kind of two-qubit gate (CZ, for example) plus a handful of one-qubit rotations, and on most superconducting chips a two-qubit gate is possible only between physically neighbouring qubits. Gate errors, coherence times and gate durations differ from qubit to qubit and are recalibrated often.

Inside one transpiler

The Qiskit documentation lists six stages for Qiskit’s preset pass managers, which I also read off the installed library (Qiskit 2.5.2):

A vertical pipeline of six stages from an input circuit to a device-ready circuit: init, layout, routing, translation, optimization and scheduling, each with a one-line description. Layout and routing are bracketed as existing only because not every pair of qubits is wired; the optimization stage has a loop arrow labelled repeat.
The stages of Qiskit's transpiler. Each stage has one job. Layout and routing exist only because the device does not connect every pair of qubits. The optimization stage repeats its passes while depth and gate count keep falling. Scheduling does not run unless you ask for it.

Only one stage is called optimization, but every stage makes choices that change the result. Layout picks which physical qubit holds each of the program’s qubits. Routing then inserts SWAP gates, which exchange the states of two neighbouring qubits, until every two-qubit gate acts on neighbours. A poor layout forces routing to add many SWAPs, and no later pass removes all of them. The preset pipelines also come in optimization levels, from 0 to 3, which trade compile time for circuit quality; level 3 tries more passes and more layouts. Compile time is a real cost for large circuits and for workflows that compile many circuits.

tket organises its compiler differently but does the same jobs , and so do the other tools in the table further down.

What counts as better

A compiler needs a number to minimise, and there are several candidates.

These numbers can point in different directions. I ran three tools on the baseline circuit from Part 2, an 18-qubit circuit with depth 5329 and 3502 CNOT gates (CX in the figures). I used my own reproduction of it, which scores the same, allowed every pair of qubits to interact, and checked every output for correctness:

Three bar charts side by side, for depth, CX count and T-count, each with six rows: the baseline circuit, one-qubit gates merged only, Qiskit level 3, tket then Qiskit, PyZX teleport_reduce and PyZX full_reduce. tket then Qiskit is best for depth (5004) and CX (3347); the two PyZX rows tie for the lowest T-count (3167 and 3160, within the counter's tolerance), and full_reduce is worst for depth (8950) and CX (8576).
Which tool wins depends on the cost. Six versions of one 18-qubit circuit, all checked correct. Qiskit and tket lower depth by about 6 percent and the CNOT count by about 4. Both PyZX methods lower the T-count by a quarter; full_reduce also more than doubles the CNOTs. Merging neighbouring one-qubit gates, which every tool does, accounts for much of the T-count drop of the first rows. Versions: Qiskit 2.5.2, pytket 2.18.1, PyZX 0.10.6.

Qiskit at level 3 and tket followed by Qiskit improve depth and CNOT count a little. PyZX, a tool based on the ZX-calculus described below, cuts the T-count from 4247 to about 3160 with either of two methods, and one of them, full_reduce, pays for it with 8576 CNOTs instead of 3502. Under a cost model that counts only T gates, the two PyZX results are the best of the six; under one that counts CNOTs, full_reduce is the worst. For today’s noisy hardware the tket result is the one to run. PyZX counts every phase that is not a multiple of \(\pi/2\) as one T gate. Eleven of the circuit’s rotations have angles that are not multiples of \(\pi/4\), and a fault-tolerant compiler would expand each of those into many T gates, so compiled for such a machine every circuit here would need more T gates than the chart shows.

Building a circuit from a specification

Sometimes the input is a description of what the circuit should do: a matrix, a state to prepare, a Boolean function, or a list of Pauli rotations from a Hamiltonian. Turning that into gates is called synthesis. It is also a tool inside optimisation, because a block of an existing circuit can be cut out, described as a matrix, and synthesised again.

Two qubits are solved

Any two-qubit gate can be built from at most three CNOTs and some one-qubit gates, and some two-qubit gates need all three . Because of this, a transpiler can collect any run of gates that acts on the same two qubits, multiply them into one \(4\times4\) matrix, and rebuild the block with at most three CNOTs. Qiskit does this in its optimization stage.

Larger unitaries

For \(n\) qubits, exact decompositions of an arbitrary unitary exist, but the number of CNOTs they need grows like \(4^n\) , so they are useful only for a few qubits. In that range, numerical synthesis often finds much shorter circuits. QSearch, for example, grows a circuit structure one two-qubit gate at a time and, for each structure, fits the one-qubit angles by numerical optimisation until the circuit matches the target . BQSKit packages methods of this kind and scales them to large circuits by cutting the circuit into small blocks and resynthesising each one .

Numerical synthesis also makes approximate compilation natural. If the target is a distance \(\varepsilon\) instead of exact equality, a shorter circuit is often possible. Madden and Simonetto compress a standard decomposition of arbitrary unitaries by a factor of two “without practical loss of fidelity” . On noisy hardware a slightly wrong but much shorter circuit can even give better answers than an exact long one, and QUEST exploits this by running several different approximations of one circuit and combining their outputs .

Structured inputs

When the specification has structure, a method that knows the structure does far better than generic unitary synthesis.

Oracles from Boolean functions

Many algorithms need an oracle: a circuit that computes a classical function \(f(x)\) of the input bits, either into an extra qubit or as a sign on each basis state, as in Grover’s algorithm. Part 2 builds one. Computing \(f\) usually needs intermediate results, which go on helper qubits (ancillas) that start at 0. Once the answer has been used, the circuit runs the same steps backwards to return the helpers to 0, which is called uncomputation. Without it the helpers stay entangled with the input and spoil the interference the algorithm relies on.

Logic synthesis, borrowed from classical chip design, supplies the forms. One writes \(f\) as an exclusive sum of products (ESOP), an XOR of AND terms, each of which becomes a multi-controlled NOT gate . Another breaks \(f\) into small lookup tables with classical logic-synthesis tools and builds a reversible gate for each . A third writes \(f\) as a network of XOR and AND gates. XORs cost only CNOTs, and a circuit for \(f\) needs at most four T gates and one helper qubit per AND gate, so for a fault-tolerant target the number of ANDs matters most . A Toffoli gate that is correct except for extra phases on some inputs is cheaper still, and the phases cancel when the same gate is undone during uncomputation . Part 2’s oracle has this shape: helpers computed and later uncomputed, around a middle that is an XOR of AND terms.

Improving a circuit you already have

Most of what a transpiler calls optimisation takes a correct circuit and returns a shorter one that implements the same unitary. The methods fall into four families, which differ in how much of the circuit they look at.

Four rows, each with a before and after example. 1, local rewrites: Rz alpha, CX, Rz beta, CX becomes Rz of alpha plus beta. 2, re-synthesise a block: a two-qubit block with four CX becomes a block with three CX. 3, change the language: a graph of eight green and red ZX nodes becomes a graph of three. 4, search over rewrites: a tree of circuit costs where a step from 12 to the worse 13 leads to 8 and then 7, while always taking the best next step stops at 10.
Four ways to improve a circuit. They differ in how far they look: one pair of gates, one small block, the whole circuit in another notation, or a search over many rewrites. Row 4's costs are made up to show why a search sometimes accepts a worse circuit.

Local rewrites

The oldest methods are peephole rewrites borrowed from classical compilers. Two CNOTs in a row cancel, two rotations about the same axis merge into one, and a gate can often move past another (a \(Z\) rotation commutes with the control of a CNOT) to bring a cancelling pair together. Nam and co-authors built a fast optimiser for large circuits from rules of this kind, including a rotation-merging pass that finds rotations far apart in the circuit that act on the same parity, the XOR of the same set of qubit values . Template matching generalises the idea: it searches the circuit for any subcircuit equal to part of a known identity and replaces it with the cheaper remainder .

Local rewrites are fast and safe, and they are the bulk of every optimisation stage. They also stop early, at the first circuit where no single rule applies.

Resynthesis

The second family cuts out a block and synthesises it again from its matrix. The two-qubit case above is the everyday example. BQSKit does the same for blocks of three or four qubits with numerical synthesis, which finds savings no rule list contains but costs much more time .

Changing the language

The third family translates the circuit into a different notation, simplifies it there, and translates back. The best-known example is the ZX-calculus, a graphical language in which a circuit becomes a graph of green and red nodes (“spiders”) carrying phases . The graph obeys rewrite rules that circuits do not have, so it can shrink in ways no gate-level rule would find . PyZX implements this , and its methods are especially good at lowering the T-count .

The hard step is turning the simplified graph back into a circuit, called extraction . It can add many CNOTs. That is what happened on the baseline circuit above: the graph was simpler, but the extracted circuit had more than twice the CNOTs. PyZX also has a gentler method, teleport_reduce, that moves phases through the graph and keeps the original circuit’s structure; on the same input it lowered the T-count just as far and left the CNOT count unchanged. A review from 2025 surveys the many variants .

Search and learning

The fourth family treats optimisation as a search. Instead of applying whichever rule helps right now, it tries many sequences of rewrites and may accept a worse circuit on the way to a better one, as in row 4 of the figure. Tools of this kind are sometimes called superoptimisers, after classical tools that search for the shortest program equivalent to a given one. Quartz generates every small rewrite rule that is valid for a given gate set and verifies each one . QUESO synthesises rules whose angles are symbols rather than numbers, and checks them with a randomised test that is right with high probability . GUOQ, by QUESO’s authors, mixes cheap rewrite rules with occasional expensive resynthesis in a search similar to simulated annealing. On its authors’ benchmarks it removes 28 percent of two-qubit gates on average, against 18 percent for Quarl, which trains a reinforcement-learning agent to choose the next rule , and 7 percent for tket . QUASAR, presented at PLDI 2026, borrows the e-graph from classical compilers: a data structure that stores many equivalent circuits at once, so the search does not have to commit to one rewrite at a time .

These tools spend more compile time, from minutes to hours, for better circuits, and each paper reports results on its own benchmark set, so the percentages are not directly comparable. In production, learning so far shows up in narrow passes. IBM’s transpiler service, for example, uses reinforcement learning to synthesise small Clifford, linear (CNOT-only) and permutation blocks and to route . Large language models are newer still. In a 2025 experiment on Google’s Willow processor, the AlphaEvolve agent, which uses a large language model to write code, evolved the programs that generate the experiment’s time-evolution circuits. The authors note that the search scored candidates against a complete, classically computed set of answers, which will not exist for problems beyond classical simulation .

Layout, routing and scheduling

On most superconducting chips a two-qubit gate is possible only between neighbouring qubits. The compiler has to choose which physical qubit holds each of the program’s qubits (layout) and insert SWAP gates to bring distant pairs together (routing). A SWAP costs three CNOTs, so routing can multiply a circuit’s size.

Three panels. 1: four qubits a, b, c, d on a line of physical qubits P0 to P3, and a wanted CX between a and d, which are not connected. 2: a circuit with two SWAPs that move d next to a, then the CX; one CX became seven. 3: a better starting layout a, d, b, c where the CX needs no SWAP.
Routing in miniature. A CX between two qubits that are not wired together needs SWAPs, each costing three CX. A better starting layout can avoid some of them, but with many gates no single layout suits them all.

Choosing layout and SWAPs optimally is NP-complete. Siraichi and co-authors, writing at a classical compiler conference, called it qubit allocation, by analogy with register allocation, the classical compiler’s job of assigning variables to a processor’s few registers . Production compilers therefore use heuristics. SABRE, the most widely used, looks ahead at the next layer of gates, picks the SWAP that brings them closest together, and runs over the circuit forwards and backwards to choose a good starting layout . Its successor LightSABRE, written in Rust inside Qiskit, is about 200 times faster than Qiskit’s 2020 implementation and uses 18.9 percent fewer SWAPs on average than the original SABRE .

Exact methods based on SAT and SMT solvers, general-purpose solvers for logical constraints, can find optimal layouts for small circuits . Their main use is to measure the heuristics. On benchmark circuits built to have a known optimum, 2020-era tools were on average 1.5 to 12 times deeper than optimal on a small device and 5 to 45 times on a larger one . A 2025 preprint that counts SWAPs instead finds LightSABRE 63 times above the optimum and tket 330 times . These circuits are built so that the optimum is known, not to resemble real programs, so the gap on real workloads is an open question.

On real devices, a noise-aware compiler chooses the layout and schedule from the latest calibration data, steering gates away from the noisiest qubits . The scheduling stage, which assigns each gate a start time, now also suppresses errors, for example by filling idle periods with dynamical-decoupling pulse sequences, which cancel slow noise on qubits that are waiting .

How much of this you need depends on the hardware. Trapped ions and neutral atoms can connect any pair of qubits, either directly or by moving atoms, so SWAP routing largely disappears and is replaced by scheduling the moves . The challenge in Part 2 also allowed any pair, so routing played no role there.

The tools, and how to trust them

The main tools

Most of the methods above are available in a handful of packages. These are the ones you are most likely to meet:

tool what it is strong at
Qiskit IBM’s SDK; staged transpiler with a Rust core the default for most users; fast routing (LightSABRE)
tket Quantinuum’s retargetable compiler peephole optimisation, many back ends
BQSKit Berkeley’s synthesis-first toolkit numerical resynthesis, approximate compilation
PyZX ZX-calculus library T-count reduction, experiments with ZX rules
VOQC optimiser with machine-checked proofs passes proved to preserve the circuit’s meaning
Quartz, QUESO, GUOQ research superoptimisers squeezing a circuit hard, given time
MQT (QMAP, QCEC) Munich toolkit exact mapping, equivalence checking

The tools can be chained. On the circuits of Part 2 my best gate-level results came from running tket first and Qiskit after it.

Checking the output

Compilers have bugs, and a buggy optimisation pass returns a circuit that is wrong but looks good. Giallar, a project that verified the passes of Qiskit with an automated prover, covered 44 of 56 passes and found 3 bugs along the way . VOQC goes further and proves its passes correct . For everything else there is equivalence checking: compare the compiled circuit with the original. QCEC uses the fact that one circuit followed by the inverse of the other must give the identity if the two are equal, and adds simulations on random inputs; its authors report that “in many cases just a single simulation run is sufficient” .

I learned to check every output end to end. In the off-the-shelf runs above, one PyZX recipe returned depth 4668, better than every correct result, and that circuit was wrong: the helper qubits did not return cleanly to 0, and some pixels got the wrong sign. During the contest, a tket setting that lets the compiler permute qubits at the end, and Qiskit’s default assumption that every qubit starts at 0, also gave me wrong oracles. In each case the tool did what it was told, under an assumption I had not checked.

Benchmarks

QASMBench and MQT Bench collect shared benchmark circuits for comparing tools, and Benchpress runs over a thousand tests across seven SDKs . In its 2024 results, measured against Qiskit as the baseline, tket used 1.31 times as many two-qubit gates and 13.3 times the compile time on the geometric mean, and BQSKit 1.26 times the gates and 108 times the time. The authors, who all work for IBM, also write that there is “no clear winner when looking at each test in isolation”. Rankings like these move with every release, so treat them as a snapshot.

The fault-tolerant frontier

Everything so far assumes today’s noisy hardware, often called NISQ (noisy intermediate-scale quantum), where each gate adds a little error and the compiler’s job is to use fewer gates. A large share of current compiler research targets a different machine: one with error-corrected logical qubits. On such a machine each logical qubit is a patch of hundreds of physical qubits running an error-correcting code, usually the surface code. The cost model changes completely .

A two-column comparison of a NISQ compiler and a fault-tolerant compiler across six jobs: gates the machine runs, what costs most, rotations, optimise, place and route, and final score. A note under the fault-tolerant column says that magic-state cultivation, proposed in 2024, puts a T state at about the cost of a lattice-surgery CNOT.
Noisy and fault-tolerant compilers compared. A fault-tolerant compiler does what a NISQ compiler does, but two-qubit Clifford gates become cheap, T gates become the expensive resource, and routing returns at the level of error-corrected patches.

Rotations become T gates

An error-corrected machine runs a small fixed gate set exactly. The usual choice is the Clifford gates (H, S, CNOT) plus the T gate, a rotation by \(\pi/4\) about \(Z\). Clifford gates are comparatively cheap to run fault-tolerantly; each T gate needs a magic state, described below. Any other rotation must be approximated by a sequence of these gates. The generic Solovay–Kitaev algorithm does it with a number of gates that grows polylogarithmically in \(1/\varepsilon\) . The number-theoretic method gridsynth needs, for a \(Z\) rotation, typically about \(3\log_2(1/\varepsilon)\) T gates , so a rotation accurate to \(10^{-10}\) costs around a hundred T gates. A 2026 preprint shows that small rotations can be made much cheaper than this angle-independent count suggests . A circuit that a NISQ compiler considers cheap because its rotations are free can be expensive here.

Fewer T gates

Because T gates dominate the cost, T-count and T-depth became optimisation targets of their own. A circuit made of CNOT and T gates can be written as a phase polynomial, a sum of terms that each say which parity of the qubits receives which phase. Re-synthesising the polynomial gives a polynomial-time method for T-count and T-depth reduction . For circuits of CNOT and T gates, minimising the T-count turns out to be equivalent to decoding a Reed–Muller code , which led to the TODD optimiser . The ZX-calculus methods above reach the same goal by graph rewriting , and AlphaTensor-Quantum casts T-count reduction as a tensor decomposition and searches with reinforcement learning .

Fault-tolerant compilers also have to use measurement, which an optimiser that sees only unitaries cannot. A Toffoli gate can be done with four T gates instead of seven if the circuit may measure and correct . A temporary logical AND costs four T gates to compute and none to erase, because the erasure is a measurement followed by a classically controlled Clifford fix-up; this halves the T-count of quantum addition .

Lattice surgery and magic states

On a surface-code machine, logical operations between patches are done by lattice surgery, which merges and splits patches along paths through free space on the chip . Laying out patches and scheduling those paths is the layout and routing problem again, one level up.

Optimal lattice-surgery compilation is NP-hard , so tools use heuristics, such as routing along edge-disjoint paths or SAT solvers for small, heavily reused subroutines , and open end-to-end compilers now exist . There is no agreement on the best overall strategy. One family rewrites the whole computation as a sequence of measurements of multi-qubit Pauli operators, products of X, Y and Z on several qubits, which removes every Clifford gate; another compiles Clifford and T gates directly and keeps more parallelism. A 2026 comparison finds that each wins on different programs .

Each T gate consumes a magic state, a special state prepared on the side, and preparing magic states has long been the dominant cost in estimates of what a computation needs. Distillation, which turns many noisy copies into fewer good ones, became cheaper than it was assumed to be . In 2024 Gidney, Shutty and Jones proposed magic-state cultivation, which grows a good T state inside a single patch and, in its authors’ words, “uses roughly the same number of physical gates as a lattice surgery CNOT gate of equivalent reliability” . A 2025 experiment on a superconducting processor, reported in a preprint, cut the error of a T state by a factor of 40, keeping 8 percent of attempts and discarding the rest . If T states become about as cheap as CNOTs, T-count stops being the whole bill. Routing starts to matter as much, and so does timing: cultivation succeeds at random, so a schedule fixed in advance cannot say when a T state will be ready, and part of compilation moves to run time .

A resource estimate is the final score

A NISQ compiler’s output can be run today. A fault-tolerant compiler’s output mostly cannot yet, so its final score is a resource estimate: how many physical qubits and how much time the computation would need on a stated hardware model. Estimators such as Microsoft’s Azure Resource Estimator and Google’s Qualtran turn logical gate counts into those numbers. The headline estimates have moved fast. Factoring a 2048-bit RSA key was estimated at 20 million noisy qubits for 8 hours in 2021 . In 2025 the estimate fell to under a million qubits for under a week, under the same hardware assumptions . The author credits approximate arithmetic, denser storage of idle qubits and cultivation. None of the three is a change a gate-level optimiser could make.

Not everything has to be rebuilt for the new costs. A 2025 study found that ordinary NISQ optimisation passes do lower fault-tolerant resource estimates, if chosen per application . The newest survey of fault-tolerant compilers, from September 2026, lists the open problems: optimising across layers of the stack, compiling for codes other than the surface code, designing compilers and decoders (the classical software that turns error-correction measurements into corrections) together, adapting at runtime, and building shared benchmarks .

What gate-level passes cannot see

The off-the-shelf tools gained only about 6 percent on the challenge circuit, and I think the main reason is what a transpiler is given. It receives a list of gates and must return a list that implements the same unitary. It is not told that some helper qubits always start in 0, that a block of gates computes a particular Boolean function, or that the program only needs the result on some inputs, so it cannot use any of these facts. In the challenge, six helper qubits start at 0, so 63 of every 64 inputs a transpiler must preserve are never used. Structure can also be lost before the transpiler sees it. A 2026 preprint measured this across Qiskit, tket, Cirq and the MQT tools. After a circuit made a round trip through OpenQASM 3, high-level operations such as multi-controlled gates had become plain gates, and asking the next compiler to re-synthesise them had no effect in any of the 360 cases tested. In one pipeline, passing an eight-qubit Grover circuit through OpenQASM 2 raised its two-qubit gate count by 37.2 percent .

A compiler that knows such facts works at a higher level, and these levels have their own tools. High-level synthesis systems, Classiq’s among them, take a functional description of a program, together with constraints and goals, and search for a circuit that meets them . Silq makes uncomputation part of the language and checks it with types , and Unqomp and its successor Reqomp add uncomputation to circuits automatically, Reqomp within a budget of helper qubits . Logic synthesis, mentioned above, works on the Boolean function.

A table-like diagram with four rows: Problem, Boolean function, Gate list and Hardware. For each row it names who works there and the depth reached on the logo challenge: 95 from algorithm design, 1886 from logic synthesis (345 with networks shaped by hand), 4665 from my rescheduler starting at 5329, and not scored for hardware mapping. Arrows between the rows say what each step down forgets or adds.
Who can change what. Each row is a level at which a circuit can be described, and the arrows say what each step down forgets or adds. A gate-level tool cannot use facts that only the Boolean function or the problem shows. The depths are from the challenge in Part 2.

In the challenge in Part 2, off-the-shelf gate-level tools took the baseline circuit from depth 5329 to 5004, and a rescheduler I wrote, which only reorders gates, took it to 4665. Logic synthesis from the logo’s truth table reached 1886. Designing the computation by hand, then searching over the designs, reached 95, with gate-level polishing doing the last stretch from 107.

In practice I would still run an off-the-shelf transpiler on any circuit. It is fast and removes routine waste, and its output can be checked against the input. When I needed far more than a few percent, the savings came from changing the description of the problem, and Part 2 shows how that went for the logo.

Further reading

Surveys and one tutorial paper, roughly from broad to narrow: