Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

XQuad is a hardware-agnostic toolchain for expressing and running quadratic optimisation problems – QUBO, Ising, and integer formulations – on quantum annealers and classical solvers. A problem is written once against the XQVM instruction set and runs unchanged on any backend the toolchain supports.

Think of it as LLVM for quantum computing: write a problem once, compile it to XQVM bytecode, and run it on any supported backend.

What XQuad solves

Combinatorial optimisation problems – travelling salesman, graph colouring, knapsack, set cover, and their relatives – can be expressed as quadratic binary models and handed to a quantum annealer or a classical sampler. XQuad gives that pipeline a single intermediate representation, XQVM bytecode, and a reference virtual machine, so the same compiled program runs on a D-Wave QPU, a local simulated-annealing sampler, or the Quip network without a rewrite. See Quadratic Models for what a QUBO/Ising model actually is, and Backends for the solvers XQuad targets.

The real components

The toolchain is dual-language: a Rust core (VM, assembler, bytecode) with Python interfaces (constraint-programming DSL, solver adapters, FFI bindings, and the xquad CLI).

Two Rust crates, published to crates.io:

CrateRole
xqvmBytecode definitions, opcode table, instruction builder, binary codec, an incremental instruction-stream reader, the VM interpreter, and a disassembler
xqasmText assembler: .xqasm source to bytecode

A third crate, xqffi, is a PyO3 bridge that exposes xqvm and xqasm to Python. It ships as a Python wheel rather than a crates.io library.

Four Python distributions, published to PyPI:

PackageRole
xqcpHigh-level constraint-programming DSL that compiles to XQVM assembly
xqsaSolver adapters for XQMX models: local simulated annealing, D-Wave, and the Quip network
xqffiThe PyO3 FFI bindings crate above, packaged as a wheel
xquadUmbrella package re-exporting xqffi, xqcp, and xqsa under one namespace, with an interactive Program / Session / RunResult API

spec/xqvm/ defines the VM, and the committed conformance vectors hold xqvm to it: each vector runs in CI against the outcome the spec prescribes, and a mismatch fails the build. See Conformance for what a vector does and does not cover.

Architecture at a glance

XQVM is a stack-based interpreter with a 256-slot register file. The value stack holds i64 integers; registers hold typed values (RegVal): integers, integer vectors, QUBO/Ising/integer models (XqmxModel), model vectors, and candidate solutions (XqmxSample). A dedicated loop stack drives RANGE and ITER iteration.

The instruction set comprises 93 instructions, declared once in the opcodes! table at xqvm/src/bytecode/types/table.rs. The Opcode enum, the Instruction enum, mnemonic strings, and operand arity are all derived from that single table.

Every .xqb file opens with a fixed 15-byte XQBC header – magic bytes, format version, calldata/output-slot counts, instruction-stream length, and a CRC-32 checksum – followed by the raw instruction stream: an opcode byte followed by its operands in big-endian byte order. See Bytecode Format for the full field layout.

Where to go next

  • Getting Started – install the toolchain and run your first program.
  • Concepts – what a QUBO/Ising model is, the ways to use XQuad, and the backends it targets.
  • Modelling with XQCP – write a problem with the constraint-programming DSL.
  • Running Programs and Solving – execute a compiled program and hand it to a solver.
  • XQVM Reference – the machine model, assembly language, instruction set, and bytecode format.
  • Embedding – using the Rust crates directly, no_std support, and conformance to the specification.
  • Examples – worked problems including a Travelling Salesman Problem.

License

Licensed under the GNU Affero General Public License v3.0 or later.

Install XQuad

This page installs XQuad, confirms the install works, and gets the runnable examples this book uses. It also covers what a GPU or QPU solver needs beyond the base install. First Problem picks up from here with a real run.

Install

The root README’s Install section is the canonical install reference for both ecosystems this project ships into – prebuilt wheel platforms, the Rust toolchain required to build the xqffi extension from source, and installing individual Python packages instead of the umbrella.

Install XQuad into a project environment first: a virtual environment, or a uv project.

python -m venv .venv && source .venv/bin/activate
pip install xquad
# or, in a uv project:
uv add xquad

For the xquad command alone, outside any project, install it as a uv tool, with the quip extra for the xquad quip commands:

uv tool install xquad            # the xquad command
uv tool install 'xquad[quip]'    # with the xquad quip commands

On macOS, a user install (pip install --user, or pip falling back to one) puts the xquad command in ~/Library/Python/3.x/bin, which is not on PATH; python -m xquad runs the same command without it.

Verify the Install

xquad --version prints the installed version, confirming the command is on PATH and runs. If your shell reports xquad: command not found, that Python environment’s script directory is not on PATH; python -m xquad --version runs the same command without it. Importing the umbrella package confirms the Python API:

$ python -c "import xquad; print('ok')"
ok

Get the Examples

Every uv run python examples/... command in this book, including on the next page, runs against a checkout of this repository – the examples are not part of any published package, so pip install above does not put them on disk. Get one and set up the Python workspace once:

git clone https://gitlab.com/quip.network/xquad.git
cd xquad
make deps-py

make deps-py needs uv and a Rust toolchain, which builds the xqffi extension from source. It syncs the Python workspace and wires up cross-package imports, so uv run python examples/<name>/runner.py then works from the repository root – the same command every example and cookbook page in this book uses.

GPU and QPU Install Prerequisites

The base install runs CPU simulated annealing only, through xqsa’s cpu backend. Every other solver – a local GPU or a real D-Wave QPU – needs an extra on top, and the extra needs hardware or credentials this page cannot install for you. xqsa/pyproject.toml defines the extras, and xquad/pyproject.toml forwards to the ones it re-exports:

  • cuda – cupy-cuda12x>=13.0, nvidia-cuda-nvrtc-cu12, and nvidia-cuda-runtime-cu12, for a local NVIDIA GPU.
  • dwave – dwave-system>=1.0, for the real D-Wave QPU.
  • metal – pyobjc-framework-Metal>=11.0, marked sys_platform == 'darwin', for a local Apple GPU. The marker means pip install xquad[metal] succeeds on Linux and installs nothing.
  • quip – substrate-interface>=1.7.4,<2 plus quip-signer>=0.3.0,<0.4, for the Quip network solver, which needs a QUIP_RPC_URL and a configured signer; see Quip Network for what that solver does.

xqcp and xqffi define no optional dependencies at all.

The only extra this page can fully specify is [dwave]: it needs a D-Wave Leap account and a DWAVE_API_TOKEN, and dwave ping confirms both. A local GPU ([cuda] or [metal]) is another option; see Local Solvers for those two extras and their driver checks.

pip install "xquad[cuda]", xquad[metal], xquad[dwave], and xquad[quip] each forward to the matching xqsa extra, and extras are composable: pip install "xquad[cuda,dwave]".

A missing extra does not break the base install: import xqsa never fails just because an optional extra is absent. Only constructing the solver class that needs it does, raising ImportError with a pip install xqsa[...] hint naming the extra to add.

Per-solver parameters, driver-level troubleshooting, and what a QPU result contains that a CPU one does not are covered in Solving Overview, Local Solvers, and D-Wave QPU. This section only covers what to install before you get there.

Where to Go From Here

  • First Problem – run a complete problem end to end and get a real answer back.
  • What Happened – the explanation of what that run just did.
  • XQVM Reference – a minimal hand-written .xqasm program, if you want to see the machine underneath before running anything larger.
  • Toolchain Map – the pieces XQuad is built from, and how they hand off to each other.

First Problem

This page runs one problem from nothing to a real answer. It does not explain how the pieces work – What Happened does that once you have seen a result to explain.

Max-Cut

Max-Cut splits a graph’s nodes into two groups to maximise the total weight of edges crossing between them. Quadratic Models derives its formulation by hand on a three-node graph; examples/maxcut/ is the same problem, runnable, on a graph you pick the size of.

Run It

With XQuad installed and the examples checked out, per Install XQuad:

$ uv run python examples/maxcut/runner.py --seed 42
{
  "_note": "canonical CI golden",
  "_seed": 42,
  "cut_weight": 354,
  "energy": -354,
  "n": 5,
  "partition": [
    0,
    1,
    0,
    0,
    1
  ],
  "valid": 1
}

No flags beyond --seed are required: --n defaults to 5, and --solver defaults to cpu, the baseline that needs no hardware or credentials.

Reading the Result

partition is the answer: one 0 or 1 per node, five nodes here, naming which side of the cut each node is on. cut_weight is the total weight of edges connecting a 0-side node to a 1-side node – the quantity Max-Cut maximises. energy is -354, the negative of cut_weight, because the model minimises -1 times the cut weight to turn maximising into the minimising every XQuad model does; see Quadratic Models for why that sign flip is exact. valid: 1 confirms every value in partition is 0 or 1 – the only thing this problem’s verifier checks, since Max-Cut declares no constraint beyond that.

Next: What Happened names each step this run took, in order, and links to where each one is covered in full.

What Happened

First Problem ran examples/maxcut/runner.py and printed a partition, a cut weight, and an energy. This page is an index: it names the five moves that run made, in order, and points at the page that covers each one in full, rather than explaining them again here. Toolchain Map has its own diagram of the same pipeline, if a picture is what you want first.

Five Moves, One Command

  1. A problem became three programs. build_problem in examples/maxcut/runner.py describes Max-Cut once, in Python, with the xqcp DSL; problem.compile() turned it into an encoder, a verifier, and a decoder, sharing no state. Three Programs covers why there are three and not one, and walks this exact run step by step.
  2. The encoder ran and built a model. Quadratic Models covers the Hamiltonian it built, specialised to this graph’s weights.
  3. A solver minimised it. cpu, the default backend, ran simulated annealing over the model and returned a sample. Backends covers the other four solvers this same model could have gone to instead, unchanged.
  4. The verifier checked the sample and computed its energy independently. energy: -354 was not relayed from the solver; see Quadratic Models: Energy for that computation.
  5. The decoder turned the sample into partition. See Outputs and Decoding for what a decoder can read and what it hands back.

Where to Go From Here

  • Toolchain Map – every piece named above, in one table, with what each hands to the next.
  • Modelling – write your own Problem instead of reading someone else’s; builds on a second running example, Knapsack, from the first input declaration onward.
  • Running Programs – drive an already-compiled program from Python yourself, the way runner.py does internally.
  • Solving Overview – the five solvers behind step 3, and what each needs installed before you can reach it.
  • Using the Examples – turn Max-Cut, or any of the other thirteen examples, into a problem of your own.

Toolchain Map

XQuad turns one problem description into bytecode that runs unchanged on any backend it supports. This page names the pieces that make that true, and what each one consumes and produces.

flowchart LR
    P["DSL (xqcp) or hand-written .xqasm"] --> ASM[Assembler]
    ASM --> B[XQVM bytecode]
    B --> V["VM: xqvm"]
    H[Host program] -->|calldata| V
    V -->|model-building program| M[XQMX model]
    M --> H
    H --> SLV[xqsa solver]
    SLV -->|sample| H
    H -->|sample as calldata| V
    V -->|verifier / decoder programs| H
ComponentRoleHands off
DSL (xqcp) or .xqasm sourceDescribes the problem: a program whose job is to build a model, in Python or in assembly text.xqasm source
Assembler (xqasm)Parses .xqasm text and resolves labelsXQVM bytecode (.xqb)
BytecodeThe portable artifact: the same bytes run on the xqvm interpreter, wherever it is embeddedInput to the VM
VM (xqvm)Executes bytecode against calldata the host program suppliesAn XqmxModel, when the program’s job is building one; decoded output, when its job is reading a sample back
Model (XqmxModel)The Hamiltonian a model-building program assembled: the model’s energy function, covered in Quadratic ModelsInput to a solver
Solver (xqsa)Minimises the model’s HamiltonianAn XqmxSample: the best assignment found
Host programThe script or CLI session driving every stage aboveReads the model out of the VM and hands it to the solver, then feeds the sample back in as calldata (the numbered input slots a host fills before each VM run) for the next run

Following the Pipeline

A hand-written program can skip most of this. Save the following as add.xqasm:

; push two integers and add them
PUSH 10
PUSH 32
ADD
HALT
$ xquad vm asm add.xqasm -o add.xqb
assembled 4 instructions (21 bytes) -> add.xqb
$ xquad vm run add.xqb
stack (bottom to top):
  42

Here the pipeline is just two rows: the assembler and the VM. The xquad CLI plays the host program’s role, reading add.xqb from disk and printing the result, and that already runs the real toolchain end to end. XQVM Reference walks through this same add.xqasm again, from the machine’s side: what each instruction does to the stack as it runs.

A problem worth handing to a solver uses every row in the table above. xqcp compiles a Problem into three separate .xqasm programs, not one – see Three Programs for what those three are and why. The first one builds a model; xqsa solves it; the other two check and decode the answer. A Python host program – xquad.program, xqffi.vm, or a full xqcp pipeline – drives it, injecting calldata before each VM run and reading outputs back after. See Ways to Use XQuad for which surface plays that host-program role in each of the six ways to work with XQuad.

Where to Go From Here

  • Ways to Use XQuad – the six surfaces that build a model and drive execution, and how to pick one.
  • Quadratic Models – what the model in the middle of this pipeline actually is, and why constraints become part of it.
  • Three Programs – why xqcp compiles a problem into three separate programs instead of one.
  • Backends – the solvers that sit in the gap between encoder and verifier.

Past this chapter: Modelling builds a model with the DSL, Running Programs and Solving Overview execute it and hand it to a solver, and XQVM Reference is the machine this whole pipeline compiles down to.

Ways to Use XQuad

Six separate surfaces get a problem into XQuad and a result back out. Pick the wrong one and you find out several pages later. This page is the map.

SurfaceWhat it isWho it is for
xqcp DSLPython constraint-programming layer; compiles a problem to XQVM assemblyModelling a new combinatorial problem without hand-writing assembly
xquad Program/Session APIProgram / Session / RunResult: load a program once, run it repeatedly with fresh calldataDriving an existing compiled program from Python – a REPL, a notebook, a script
xqffi.vm FFIVm: a thin wrapper over the Rust interpreterPython code building its own convenience layer on top of the raw Rust VM
xquad.vm VM wrapperVM: runs .xqasm source text or bytecode on the Rust interpreter, keeping calldata, output slots and limits across runsScripts that run several programs in a row with the same settings; the surface every example runner under examples/ uses
xquad CLIxquad vm asm / run / dis / checkAssembling, running, inspecting, or verifying one .xqasm/.xqb file, no Python involved
Rust embeddingxqvm::InstructionBuilder, building a Program directly, no text assembly stepEmbedding XQVM in a Rust host: no_std target, WASM runtime, on-chain pallet

xqcp – The DSL

You write a Problem in Python: declare inputs, define the model, add objective terms and constraints, declare outputs. problem.compile() returns CompiledPrograms(encoder, verifier, decoder) – three .xqasm programs, ready to run. See Three Programs for what those three are and why there are three. Every example under examples/ is built this way; examples/maxcut/runner.py is a complete pipeline you can read start to finish.

Stop here if you have a combinatorial problem to model and no existing .xqasm source. Keep reading if you already have compiled bytecode and need to run or inspect it. Full coverage: Modelling.

xquad.program – Program and Session

Program.from_source(src) or Program.load(bytecode) loads a program once; program.session(output_slots=n) gives you a Session you call .run() on repeatedly, each time with fresh calldata via session.set_calldata(...). Each run returns a RunResult with dict-keyed outputs (an unset slot reads as None), the residual stack, and the step count. Program.from_source plus Session.run() round-trips 40 + 2 through calldata and an output slot to {0: 42}, and running the same session again returns {0: 42} again: each run() starts clean.

Stop here if you are driving an already-compiled program from Python and want calldata handling and output decoding done for you. Keep reading if you are choosing between this and the raw FFI below. Full coverage: Running Programs.

xqffi.vm – The Raw FFI

xqffi.vm.Vm is a thin wrapper over the Rust interpreter: construct a Vm, call set_calldata/set_output_slots, call .run(bytecode), read .outputs(). No Program/Session layer on top, and unlike Session, a Vm holds its stack and registers across runs: call .run() twice on the same Vm without an intervening .reset() and the second run’s results sit on top of the first’s. reset() clears the stack, registers, and loop state; calldata and outputs both survive a reset() untouched, and only a fresh set_output_slots(n) call replaces the output slots. Session sidesteps the question entirely: Session.run() builds a fresh Vm per call, so no state survives between runs. That is the real reason to prefer it for anything but a single one-off program.

Reach for Vm directly only if you are building your own convenience layer on top, or the Program/Session assumptions do not fit – most Python users want xquad.program or xquad.vm instead. Full coverage: Running Programs.

xquad.vm – The Source-Level VM

xquad.vm.VM wraps xqffi.vm.Vm. run(source) assembles .xqasm text and runs it; run_bytecode(bytecode) runs pre-assembled bytes. Calldata, output slots, and the step and memory limits persist across runs until reset(), while every run starts from a fresh stack and register file. Outputs come back as the xqffi types unchanged: int, list[int], XqmxModel, XqmxSample, or None. Every example runner under examples/ imports VM from here, not xqffi.vm directly.

Stop here if you run .xqasm source from a script and want the settings to carry from one run to the next. Keep reading if you need the raw FFI underneath instead. Full coverage: Running Programs.

xquad CLI

xquad vm asm, xquad vm run, xquad vm dis, xquad vm check – four subcommands, entirely in the shell. Assemble a .xqasm file to bytecode, run a .xqb file (or a .xqasm file with --text) against calldata, disassemble bytecode back to a readable listing, or verify a compiled program before running it. xquad vm asm add.xqasm -o add.xqb followed by xquad vm run add.xqb, xquad vm dis add.xqb, and xquad vm check add.xqb assembles, runs, disassembles, and verifies the four-instruction add.xqasm from Toolchain Map.

Stop here if you are working from a shell or from CI. Keep reading if you need to drive many runs programmatically rather than one at a time. Full coverage: CLI.

Rust Embedding

xqvm::InstructionBuilder builds a Program directly from Rust, with no .xqasm text step at all:

#![allow(unused)]
fn main() {
use xqvm::{InstructionBuilder, Vm};

let mut builder = InstructionBuilder::new();
builder.emit_push(10).emit_push(32).emit_add().emit_halt();
let program = builder.build().unwrap();

let mut vm = Vm::new();
vm.run(&program).unwrap();
assert_eq!(vm.stack(), &[42]);
}

The xqvm crate is no_std + alloc-compatible, so this is the surface for embedding XQVM where Python, or even a filesystem, is not available.

Stop here if you are embedding the VM inside another Rust program. Everyone else wants one of the five surfaces above. Full coverage: Builder API.

The Orthogonal Choice: Where It Solves

Whichever surface builds your model, you still choose separately where that model gets solved: local CPU or GPU simulated annealing, a D-Wave QPU, or the Quip network. xqsa.build_solver(name, seed=...) selects a backend by name regardless of which surface above produced the model, and every example runner exposes this as a --solver flag. See Backends.

Quadratic Models

Every backend XQuad targets agrees on one thing: a single number, computed from a candidate assignment, that the solver tries to make as small as possible. That number is the model’s energy, and a quadratic model is the recipe for computing it:

$$H(x) = \sum_i \text{linear}[i] \cdot x_i + \sum_{i \le j} \text{quadratic}[i,j] \cdot x_i \cdot x_j$$

x is a vector of variables. linear[i] scales with variable i’s own value, and quadratic[i,j] scales with the product of i and j’s values – a cost, or a saving if negative. Read those as “the cost of turning i on” and “the extra cost of turning i and j on together” when x is binary; Three Domains below covers two domains where nothing is “on.” No term touches three variables at once – that is what “quadratic” means here. The sum runs over \(i \le j\), not \(i < j\): the diagonal is legal, and quadratic[i,i] scales with \(x_i^2\). What that means depends on the domain – on binary variables \(x^2 = x\), so it acts as a linear bias written through the quadratic table; on spin variables \(x^2 = 1\), so it is a constant offset. The VM stores and evaluates such a term without interpreting it. H is called the model’s Hamiltonian, borrowing the term physicists use for a system’s total energy, because the annealing hardware XQuad can target literally is a physical system settling toward a low-energy state.

XQVM builds this structure directly: BQMX/SQMX/XQMX allocate a model register, and SETLINE/SETQUAD/ADDLINE/ADDQUAD write the two coefficient maps one term at a time. See Allocators and Coefficient Access for the instructions.

Why Optimisation Problems Become One of These

A combinatorial problem – which cities to visit in what order, or which items fit in the knapsack – usually starts life as a set of decisions plus rules those decisions must obey. A quadratic model has no separate mechanism for rules: it is one function, and a solver’s only operation is minimising it. Every constraint has to become part of the same H, or the solver never sees it. The Penalties section below is that mechanism, and the concept this page most wants you to leave with.

A Worked Example: Max-Cut

Take Max-Cut: split a graph’s nodes into two groups to maximise the total weight of edges that cross between them. \(x_i \in \{0, 1\}\) marks which group node \(i\) is in. For each weighted edge \((i, j, w)\):

$$\text{linear}[i] \mathrel{+}= -w, \qquad \text{linear}[j] \mathrel{+}= -w, \qquad \text{quadratic}[i,j] \mathrel{+}= 2w$$

That edge’s contribution to \(H\) is \(-w(x_i + x_j - 2x_ix_j)\), and \(x_i + x_j - 2x_ix_j\) is the binary XOR of \(x_i\) and \(x_j\): it is \(1\) when \(x_i \neq x_j\) and \(0\) when they match. That edge contributes \(-w\) exactly when it crosses the partition, and \(0\) otherwise. Summed over every edge, \(H\) is the negative of the cut weight, so minimising \(H\) maximises the cut. examples/maxcut/runner.py builds exactly this model.

For three nodes and edges \((0,1,3)\), \((1,2,5)\), \((0,2,1)\), the rule above gives:

$$\text{linear} = \{0: -4,\ 1: -8,\ 2: -6\}, \qquad \text{quadratic} = \{(0,1): 6,\ (1,2): 10,\ (0,2): 2\}$$

Two assignments, worked by hand:

  • \(x = (1, 1, 0)\): \(H = (-4)(1) + (-8)(1) + (-6)(0) + 6(1)(1) + 10(1)(0) + 2(1)(0) = -12 + 6 = -6\). Nodes 0 and 1 share a group, so only the two edges crossing to node 2 count: \(5 + 1 = 6\), and \(-6\) is the negative of that, matching.
  • \(x = (1, 0, 1)\): \(H = (-4)(1) + (-8)(0) + (-6)(1) + 6(1)(0) + 10(0)(1) + 2(1)(1) = -10 + 2 = -8\). Every edge except \((0,2)\) crosses: \(3 + 5 = 8\), matching. This is the lowest of the four distinct cuts – a partition and its complement always have the same energy, so the eight assignments give four cuts – so it is the Max-Cut optimum for this graph.

Three Domains

x_i is not always a 0/1 bit. XQVM supports three domains, and the choice affects one thing above all: whether a backend can solve the result.

DomainValuesModel allocatorSample allocator
Binary\(\{0, 1\}\)BQMXBSMX
Spin\(\{-1, 1\}\)SQMXSSMX
Integer(\(k\))\(\{0, \ldots, k{-}1\}\), \(k \ge 2\)XQMXXSMX

Binary is QUBO – Quadratic Unconstrained Binary Optimisation – the domain most combinatorial formulations target directly: a variable is either selected or not. The Max-Cut model above is binary.

Spin is the Ising model: each variable is a magnetic moment pointing up or down, \(-1\) or \(+1\), which is the domain quantum annealing hardware minimises natively. A spin variable’s linear coefficient rewards one orientation and penalises the other by the same amount, rather than switching a cost on or off. Binary and spin describe the same choices: substituting \(x_i = (s_i + 1)/2\) (or \(s_i = 2x_i - 1\)) turns any binary Hamiltonian into a spin Hamiltonian over the same variables, with rescaled coefficients and one additive constant that does not change which assignment is optimal. Build a model in one domain and you can rebuild it in the other; between these two the choice is free, since every current xqsa solver accepts both, so pick whichever domain the problem is natural in.

Integer generalises past two states to give a variable \(k\) integer values directly, suited to a quantity with a natural ordering or magnitude – a position in a short list – without one-hot encoding it into several binary variables first. Unlike binary and spin, it is not a relabelling of the other two: it cannot encode an unordered categorical choice, because a quadratic form over integer variables cannot express that two values merely differ without also expressing by how much.

Integer is the domain with a real consequence: every current xqsa solver rejects it. An integer XqmxModel is a real thing you can build in XQVM bytecode today, but there is no backend yet that can solve one. See Backends.

A model and a sample share a domain and a variable count, but not a shape: a model is two sparse coefficient maps, linear and quadratic, while a sample is one value per variable and nothing else. Allocators covers both families and their default values in full.

Energy

Given a model and a candidate assignment (a sample), ENERGY computes \(H(x)\) for that specific x and pushes the result – the same formula as the model’s Hamiltonian above, evaluated at one point. “Energy” and “Hamiltonian value at x” mean the same thing throughout XQuad’s documentation. A solver’s job is to search for the x that minimises this value; XQVM’s job is only to compute it, which is what lets a program check a solver’s answer independently rather than trust it. See Energy Evaluation.

Penalties: Folding a Constraint into the Objective

The constraint instructions below expand assuming binary variables, where \(x^2 = x\). That identity does not hold for spin variables, where \(x^2 = 1\) always: apply these instructions to a BQMX model, and convert a spin model to binary first if you need one of them (see Three Domains).

Take the simplest useful constraint: exactly one of a set of variables should be 1 (a one-hot choice – pick exactly one city to visit first, one colour for this node). Written as a penalty term:

$$H \mathrel{+}= P \cdot \left(\sum_i x_i - 1\right)^2$$

The squared term is zero exactly when the constraint holds (the sum is 1) and strictly positive for every assignment that violates it – pick zero variables and the sum is 0, pick two and the sum is 2, either way \((\sum x_i - 1)^2 \ge 1\). Adding this to \(H\) means violating the constraint always costs at least \(P\) units of energy on top of whatever the rest of the objective says. A solver minimising the combined \(H\) has a direct incentive to satisfy the constraint, without any code path in the solver that knows constraints exist – it is minimising one number, the same way it always does.

This is the general shape behind most of XQVM’s high-level constraint instructions: ONEHOTR/ONEHOTC are exactly the sum above over a grid row or column, and EXCLUDE, IMPLIES, EQUALITY, ATLEAST, and ATLEASTW are the same idea applied to different rules: mutual exclusion, implication, weighted equality, at-least-k with unit weights, and at-least-k with arbitrary weights. The instruction reference also documents REDUCE, which uses the same technique – a term that is zero exactly when a condition holds – but to enforce an algebraic identity between variables rather than a rule from the problem; that is a different enough job to read about on its own. The full expansion for each instruction – which linear and quadratic coefficients change by how much – is reference material, not a concept, and lives on High-Level Constraints and in the normative HLF specification. This page is not repeating that table; the mechanism above is what to carry forward from it.

Choosing a Penalty Weight

\(P\) is not a detail to default and forget – it is the real engineering decision in penalty-based modelling, and it fails in both directions.

Too low, and the solver buys its way out of the constraint: \(P\) must exceed the largest objective improvement any violating assignment can buy over the best feasible one, or the minimiser takes that trade, because nothing in \(H\) told it not to. Suppose the Max-Cut model above also carried a one-hot constraint over nodes 0 and 2 only, with weight \(P\). The unconstrained optimum \(x = (1, 0, 1)\) worked out above violates it; the best assignments that satisfy it are \(x = (1, 1, 0)\), worked out above, and \(x = (0, 0, 1)\), both at \(H = -6\). Violating the constraint lowers the energy from \(-6\) to \(-8\), an improvement of \(2\), so penalty = 1 costs less than it saves and the minimiser still prefers the infeasible optimum. penalty = 2 ties the two, and \(P\) must exceed \(2\) before satisfying the constraint wins outright.

Too high does not corrupt the model itself. For any two assignments that both satisfy the constraint, the penalty term is 0 on both, so their energy difference is exactly the objective’s difference, independent of \(P\); ENERGY computes that difference with exact i64 arithmetic, so nothing in \(H\) degrades. What a large \(P\) costs happens outside the model. On a fixed-precision device, an objective difference much smaller than \(P\) can fall below the device’s resolution once the combined \(H\) is scaled to fit it, so the device stops representing that difference at all. And a large \(P\) raises the energy barrier between feasible regions, so a heuristic such as simulated annealing gets trapped in whichever feasible region it reaches first and stops exploring for a better one elsewhere.

Getting this right in practice – how to size \(P\) relative to your objective, and how to tell from a solver’s output which failure mode you hit – belongs to Constraints. What to take from this page is that the weight is a parameter you choose, not one XQVM chooses for you: every constraint instruction pops penalty off the stack as an ordinary operand.

Three Programs

An optimisation problem in XQuad is three independent programs sharing one instruction set: an encoder, a verifier, and a decoder. They communicate only through calldata in and outputs out. xqvm’s Three-Program Architecture defines this shape, so it is not something xqcp adds on top. Nothing in the instruction set enforces it – see Beyond xqcp – but every tool in the toolchain assumes it.

xqcp generates all three for you from one Problem definition: problem.compile() returns CompiledPrograms(encoder, verifier, decoder), three independent .xqasm sources, each its own complete program with its own fixed inputs and outputs. A hand-written program can follow the same shape with no DSL involved: xqvm/examples/tsp/main.rs builds and runs a three-program TSP pipeline directly against the Rust xqvm crate.

Why Three, Not One

XQVM has no instruction that calls a solver. Its 93 instructions cover control flow, stack and register I/O, arithmetic, comparison, vectors, grid operations, the constraint and energy family – nothing that reaches outside the VM to an annealer or a sampler. Solving happens in host code, between two runs of the VM, using whichever backend Backends describes. A model-building program cannot also be the program that checks and decodes the answer, because the answer does not exist yet when that program halts.

flowchart LR
    Q[Problem definition] -->|compile| ENC[Encoder]
    Q -->|compile| VER[Verifier]
    Q -->|compile| DEC[Decoder]
    ENC -->|XQMX model| H[Host program]
    H -->|model| SLV[xqsa solver]
    SLV -->|XQMX sample| H
    H -->|model, sample| VER
    H -->|sample| DEC
    VER -->|energy, valid| H
    DEC -->|decoded result| H

What Each Program Does

Encoder. Reads the problem’s runtime inputs from calldata, allocates the XQMX model, and emits every objective term and constraint penalty the DSL recorded. Its one output is the model, on slot 0. This is “a program whose job is to construct a model” – running it does not solve anything, it only builds the thing a solver will minimise.

Verifier. Takes the encoder’s own inputs, then the model and the sample, and checks whether the sample satisfies every constraint the encoder applied, then computes the sample’s energy with the ENERGY opcode. It outputs (energy, valid). It needs the encoder’s inputs because it replays the encoder to rebuild the constraint data, which lives in registers at VM runtime rather than in the model. This is how a sample’s feasibility gets checked independently of whatever backend produced it.

Decoder. Takes a sample and N, and extracts the answer in the problem’s own terms – a tour, a partition, a set of selected items – into one or more output vectors. It does not know or care whether the sample it was given is valid; that is the verifier’s job, not the decoder’s.

Independent, Not Sequential

The three programs share no state. Communication between them happens only through calldata in and outputs out – there is no hidden channel, and no program reads another program’s internals. The verifier and the decoder are not a pipeline: both read the same sample straight from the solver, at the same point, side by side. A host program can decode a sample the verifier just rejected, which is useful for inspecting what a bad solution actually looks like.

A Concrete Run

examples/maxcut/runner.py is exactly this shape. Max-Cut declares no constraints, so its verifier has none to check: the loop under ; === Validity checks === in the compiled verifier only confirms each sample value is 0 or 1. The ENERGY recomputation still runs and is real independent verification – the energy the verifier reports is computed fresh from the model and the sample, not relayed from the solver.

Its run() function makes four calls in sequence, three of them through the VM:

  1. vm.run(programs.encoder) with calldata [n, flat_edges] and one output slot, producing an XQMX model.
  2. solver.solve(model), entirely outside the VM, producing a sample.
  3. vm.run(programs.verifier) with calldata [n, flat_edges, model, sample] and two output slots, producing (energy, valid).
  4. vm.run(programs.decoder) with calldata [sample, n] and one output slot, producing the decoded partition.

Running uv run python examples/maxcut/runner.py --n 5 --seed 42 prints:

$ uv run python examples/maxcut/runner.py --n 5 --seed 42
{
  "_note": "canonical CI golden",
  "_seed": 42,
  "cut_weight": 354,
  "energy": -354,
  "n": 5,
  "partition": [
    0,
    1,
    0,
    0,
    1
  ],
  "valid": 1
}

The encoder, verifier, and decoder are deterministic – same bytecode and calldata in, same output out. The solve sitting between them is only pinned to the seed and the dwave-samplers version, since SA is sensitive to BQM construction order, so a different version of that library can return a different valid sample for the same seed. See below for what make example-smoke checks instead of the exact output.

_note and _seed are the runner’s own bookkeeping, not part of the result: _note’s value, "canonical CI golden", describes what the runner calls this invocation, not a guarantee that a test pins against it. energy is the negative of cut_weight because the encoder minimises -weight per crossing edge to make the objective function, matching the derivation in Quadratic Models. valid: 1 here only confirms every sample value is in {0, 1}, which is all this problem’s verifier checks. make example-smoke is what actually guards this example: it runs it once and checks valid == 1, and does not compare cut_weight, energy, or partition against a stored answer. These numbers depend on the dwave-samplers version behind cpu; a different version can return a different valid sample with a different cut weight.

The three VM calls above, and the solve between them, are the host program driving all three compiled programs and the solver together – see Ways to Use XQuad for the surfaces that can play that role.

Beyond xqcp

A hand-assembled .xqasm program is free to read inputs, build a model, and produce output in one file, the way Toolchain Map’s minimal add.xqasm example does – the interpreter does not enforce the three-program split. The split earns its cost once an external solver sits in the loop and a sample needs independent checking, which is exactly when reaching for xqcp, or hand-writing the same three-program shape, starts to pay off. See Modelling: Compiling for how the compiler produces the three programs, and Running Programs and Solving Overview for executing each stage.

Backends

“Hardware-agnostic” means one concrete thing: build a model once, then choose where it solves, from five backends behind a single interface. Nothing about the model, the encoder, or the verifier changes with that choice.

The Solver Set

NameClassWhere it solvesInstall
cpu (default)SolverCPULocally, on the CPU: simulated annealingbase xqsa
dwaveSolverDWaveD-Wave Leap cloud: physical quantum annealing hardwarexqsa[dwave]
cudaSolverGPU(engine="cuda")Locally, on an NVIDIA GPU: simulated annealingxqsa[cuda]
metalSolverGPU(engine="metal")Locally, on an Apple GPU: simulated annealing or block Gibbs samplingxqsa[metal]
quipSolverQuipThe Quip network: submitted to a decentralised compute market, solved by a miner you do not controlxqsa[quip]

build_solver(name, seed=...) constructs any of the five from this name, so a caller can stay backend-agnostic. seed reaches the three classical simulated-annealing backends and is ignored by dwave (physical hardware has no seed) and quip (env-configured, no local randomness).

What Actually Distinguishes Them

Where the computation happens. cpu, cuda, and metal all run the identical algorithm family – simulated annealing – just on different local processors; picking between them is a performance question, not a correctness one. dwave hands the problem to real annealing hardware over the network. quip does not solve anything itself: it posts the model as a job to the network’s mempool, a miner solves it on hardware of its own choosing, and the adapter decodes whatever comes back.

Domain support. Every backend above accepts a binary or spin model and rejects anything else: Solver._validate_model() raises ValueError for any domain other than BINARY or SPIN. XQVM’s third domain, integer (XQMX), has no backend yet. See Quadratic Models for what the three domains are.

The result contract is identical regardless of backend. Every solve() call returns a SolverResult(sample, energy, timing, metadata). energy is not simply relayed from the device: XQuad’s own verifier program recomputes it independently with the ENERGY opcode, so you never have to take a backend’s word for its own energy – run the verifier and recompute it. See Three Programs.

Before You Choose

cpu needs no hardware and no credentials, so it is the default: the reproducible baseline every other backend is compared against. The other four need something this book cannot check for you – a GPU for cuda or metal, D-Wave Leap credentials for dwave, network configuration for quip – and setting each of those up belongs to the pages below.

Depth Lives in Solving

This page is orientation, not a manual. Backend-specific setup, parameter tuning, and result interpretation belong to Solving Overview, Local Solvers, D-Wave QPU, Quip Network, and Energy and Precision.

Modelling Lifecycle

XQCP is a Python-embedded DSL that turns a problem description into three XQVM assembly programs. You declare a Problem’s runtime inputs, allocate the quadratic model those inputs fill, add objective terms and constraints, declare what you want back, then call compile(). XQCP records every call you make against Problem as you make it and only turns that recording into .xqasm when compile() runs – see Compiling for how the recording becomes three programs.

A problem definition follows one call sequence, in order:

Problem(name) -> input()* -> define_model() -> body* -> output()* -> compile()

input() calls come first: Inputs and Model Shape covers why, and what a declared input becomes at run time. define_model() runs exactly once and makes problem.model available for the rest of the body: objective terms, constraints, and loops, in any order, covered across Expressions, Objectives, Constraints, and Control Flow. Your first output() call marks the start of the decoder section, covered in Outputs and Decoding.

Why XQCP, Not Assembly

Ways to Use XQuad names six surfaces for getting a problem into XQuad; XQCP is one of them. Reach for it when you are modelling a combinatorial problem from scratch and would otherwise hand-write the same loop-over-variables, add-a-coefficient pattern in .xqasm three times over, once per program – Problem records that pattern once, and the compiler emits all three. Write .xqasm directly instead when a program is small enough that the three-program duplication costs nothing to write by hand. Reach for xqvm::InstructionBuilder instead when a Rust host is generating programs and adding a Python step to emit them buys nothing.

One Problem, Once

Every page in this chapter builds on the same running example: examples/knapsack/runner.py. It takes item weights, item values, and a capacity, and chooses the subset of items maximising total value without exceeding the capacity. build_problem is the whole of it:

def build_problem(n: int, weights: list[int], values: list[int], capacity: int) -> Problem:
    problem = Problem("Knapsack")

    num_items = problem.input("num_items", type=Types.Int)
    weights_in = problem.input("weights", type=Types.Vec)
    values_in = problem.input("values", type=Types.Vec)
    capacity_in = problem.input("capacity", type=Types.Int)

    problem.define_model(size=num_items, domain=Domain.BINARY)

    # Objective: minimise -sum(v_i * x_i)
    with problem.range(0, num_items) as i:
        vi = problem.stow("vi", values_in.get(i))
        problem.model.linear[i].add(-vi)

    # Constraint: sum(w_i * x_i) <= W
    indices = problem.vec()
    coeffs = problem.vec()
    with problem.range(0, num_items) as i:
        indices.push(i)
        coeffs.push(weights_in.get(i))

    problem.slack(indices, coeffs, num_items, capacity_in)
    problem.model.apply_equality(indices, coeffs, capacity_in, 100)

    selected = problem.output("selected", type=Types.Vec)
    with problem.range(0, num_items) as i:
        selected.append(problem.sample.getline(i))

    return problem

One binary variable per item: x_i = 1 means item i is selected. Four inputs, one 1D binary model sized to num_items, a range loop building the objective, a second range loop paired with slack and apply_equality encoding the capacity constraint, and a decoder loop reading the solved sample back into a selected vector. Running it end to end:

$ uv run python examples/knapsack/runner.py --seed 42
{
  "_note": "canonical CI golden",
  "_seed": 42,
  "capacity": 18,
  "energy": -32446,
  "n": 5,
  "selection": [
    1,
    1,
    1,
    0,
    1
  ],
  "total_value": 46,
  "total_weight": 12,
  "valid": 1,
  "values": [
    5,
    4,
    18,
    3,
    19
  ],
  "weights": [
    2,
    1,
    5,
    4,
    4
  ]
}

_note and _seed are the runner’s own bookkeeping, the same as in Three Programs. energy is the full model’s Hamiltonian, capacity constraint included, so it is not simply -total_value here the way Max-Cut’s energy is -cut_weight – Objectives isolates just the objective term’s contribution, and Constraints covers what the rest of energy is paying for.

This run is not the optimum: the five weights sum to 16, under the capacity of 18, so every item fits and the true best value is 49, not the 46 printed above. A heuristic solver returns a good sample, not a proven best one. valid: 1 reports only the verifier’s binary-domain check here, not the capacity constraint; see Compiling for what a verifier actually checks.

Inputs and Model Shape starts with the four problem.input() calls and the define_model() line above; the rest of the chapter works through the remaining lines in the order they appear.

Where to Go From Here

  • Inputs and Model Shape – runtime inputs and the 1D/2D model they fill.
  • Expressions – the operators and functions that build a coefficient or a loop bound.
  • Objectives – turning “minimise this” into linear/quadratic coefficients, sign included.
  • Constraints – folding a rule into the objective as a penalty.
  • Control Flow – range, iter, and branch.
  • Outputs and Decoding – reading a solved sample back into your problem’s own terms.
  • Compiling – what problem.compile() actually produces.

Inputs and Model Shape

A Problem definition starts with two declarations: the runtime inputs it reads, and the model those inputs fill. Both happen before anything else – spec/xqcp/SPEC.md fixes the order as Problem(name) -> input()* -> define_model() -> body* -> output()* -> compile(), and input() calls after define_model() raise RuntimeError.

Every code sample in this chapter, and the rest of Modelling and Solving, assumes the same import:

from xquad import cp

It is shown once, here, and omitted everywhere after.

Inputs

problem.input(name, type=cp.Types.Int) or type=cp.Types.Vec declares one runtime value and returns an InputRef you use for the rest of the problem body. examples/knapsack/runner.py declares four:

num_items = problem.input("num_items", type=cp.Types.Int)
weights_in = problem.input("weights", type=cp.Types.Vec)
values_in = problem.input("values", type=cp.Types.Vec)
capacity_in = problem.input("capacity", type=cp.Types.Int)

Types.Int is a scalar integer, and Types.Vec is a vector of integers. Every runtime input is one of these two types.

Declaration Order Is Calldata Order

Compiling the fragment above emits this for each input:

PUSH 0
INPUT r0
PUSH 1
INPUT r1

INPUT pops a calldata slot index and clones calldata[slot] into the target register. It does not check the calldata value’s type against anything (Register I/O covers INPUT in full). XQCP’s register allocator hands out registers in call order starting at r0, and because input() must run before define_model(), the first input() call is allocated r0, the second r1, each next call the next register, with nothing else allocated between them. That is why the slot index XQCP pushes before each INPUT matches the input’s position in your problem.input() call sequence: your Nth input() call reads calldata[N-1].

The host has to supply calldata in that same order. examples/knapsack/runner.py calls vm.set_calldata([n, weights, values, capacity]), matching num_items, weights_in, values_in, capacity_in above one for one. Nothing checks this correspondence: INPUT clones whatever sits at that slot, regardless of the input’s declared type. Swap the position of weights and values on either side and both are Types.Vec, so no RegisterType fault stops you – the encoder runs to completion and reads weights where it wanted values. See Calldata and Outputs for how a host supplies calldata to any XQVM program, XQCP-generated or hand-written.

Reading a Vec Input

A Types.Vec input supports two operations no Types.Int input has:

values_in.get(i)      # VECGET: element at index i
values_in.veclen()    # VECLEN: the vector's length

Both return an expression, not a value – see Expressions for what you can build with the result. Calling either on a Types.Int input raises TypeError immediately, at problem-definition time, not at compile time:

>>> num_items.get(0)
TypeError: Cannot index into int input 'num_items'

The Model

problem.define_model(size, domain, rows=None, cols=None) allocates the quadratic model the encoder builds and runs exactly once per problem. Before it runs, problem.model and problem.sample raise RuntimeError; after, both are available for the rest of the chapter. size is the total variable count, as an int or an expression built from your inputs – knapsack sizes its model directly off an input:

problem.define_model(size=num_items, domain=cp.Domain.BINARY)

domain is a Domain member; see Quadratic Models for what each domain means and how to choose between them, since that choice does not belong to this page. Binary is the domain the running examples in this book use. It must be xqcp.Domain, which xquad.cp re-exports: xquad.types.Domain is the VM’s own domain type under the same name, and define_model() raises TypeError if handed it.

Integer Variables

An integer variable takes one of k values rather than two. Give the width directly:

problem.define_model(size=num_assets, domain=cp.Domain.INTEGER, k=4)

That allocates num_assets variables over {0, 1, 2, 3}. k is an expression like size, so it may come from calldata.

Where the values you are modelling are not zero-based, give bounds instead:

problem.define_model(size=num_assets, domain=cp.Domain.INTEGER, lo=-5, hi=5)

You then write coefficients over x in [-5, 5] while the model holds y = x - lo in {0, ..., 10}, and sample.value(i) shifts back on the way out. XQCP rewrites each quadratic write for you: w * x_i * x_j expands to w*y_i*y_j + w*lo*y_i + w*lo*y_j + w*lo^2 once x = y + lo is substituted, so the write records w*lo against the linear coefficient of both indices as well. The w*lo^2 constant is dropped, because XQMX has no offset field. Energies shift by the same amount for every assignment, so the minimum is still in the same place; the number is not the objective’s true value. Linear writes need no correction.

Two consequences worth knowing before you reach for the ranged form. Setting a coefficient is refused on it, on quadratic[i, j] = w and linear[i] = w alike, because setting replaces a coefficient while the corrections can only accumulate: two writes to the same pair would disagree, and a linear set would drop whatever corrections earlier quadratic writes had left on that index. Use .add(), which loses nothing, since a coefficient starts at zero. And a runtime lo competes with an output loop bound for the decoder’s single calldata scalar, so compile() raises naming both. Literal bounds sidestep it.

Constraints are binary-only. On a spin or integer model every apply_* method is refused and only coefficient writes are supported, because each expansion in the VM is derived under x^2 = x. Write the penalty out by hand, as Portfolio Rebalance does for its budget.

Categorical Variables

A categorical variable takes one of k unordered cases. There is no VM domain for that, so XQCP records the standard encoding:

problem.define_model(size=num_nodes, domain=cp.Domain.CATEGORICAL, k=num_colors, penalty=200)

That is a num_nodes x num_colors binary grid with one ONEHOTR per row, built through the same calls you would have written yourself. The model is binary afterwards, so constraints work on it as usual and coefficient access is (variable, case). Read the answer back with sample.case(v), which gives the case a variable took or -1 if its row came back empty.

1D and 2D Models

A model is either flat (1D, indexed 0..size) or a grid (2D, indexed by (row, col)). Every model in this chapter is 1D: knapsack’s x_i is one binary decision per item, with no row/column structure to it. Pass rows and cols to get a grid instead:

problem.define_model(size=rows * cols, domain=cp.Domain.BINARY, rows=rows, cols=cols)

rows and cols are both-or-neither: define_model() raises ValueError when given exactly one, rather than building a 1D model that fails later.

Once a model has a shape, coefficient access accepts a (row, col) tuple in place of a flat index, and XQCP flattens it for you. Compiling

n = problem.input("n", type=cp.Types.Int)
problem.define_model(size=n * n, domain=cp.Domain.BINARY, rows=n, cols=n)
problem.model.linear[(1, 2)] = 99

emits the block below, once n has claimed r0 and define_model() has claimed r1 for cols and r2 for the model:

PUSH 1
PUSH 2
LOAD r1
IDXGRID
PUSH 99
SETLINE r2

r1 is the cols register define_model() allocated; this block does not depend on n’s runtime value at all, only on how many calls came before it. IDXGRID computes row * cols + col, so at run time, with n = 3, the coordinate (1, 2) flattens to 1 * 3 + 2 = 5, and every coordinate-accepting call on a 2D model (coefficient access, apply_exclude, apply_implies) goes through the same flattening. size still has to equal rows * cols yourself; XQCP does not derive one from the other. It does optimise the size expression itself: size=n*n emits a single SQR instead of LOAD, LOAD, MUL whenever both multiplicands are the same register, 2D model or not. Grid allocation itself – RESIZE and the rest of what BQMX r2 plus a shape actually builds – is Allocators and Grid Operations territory, not this page’s.

Symbolic Reference Types Introduced Here

spec/xqcp/TYPES.md is the normative reference for every symbolic type XQCP hands back. The two this page introduces:

TypeCreated byRegister type
InputRefproblem.input(name, type)int or vec
ModelRefproblem.define_model(...)xqmx

SampleRef – problem.sample, the model’s read-only counterpart used in the decoder – is created together with ModelRef but belongs to Outputs and Decoding, where it is actually used. Expressions covers LoopVar and RegLoad, the two symbolic types this page’s fragments do not need yet; the full type table, including register-allocation and error-condition details past what this page restates, is in spec/xqcp/TYPES.md.

With inputs declared and a model allocated, the next thing every problem body needs is a way to compute with them – see Expressions.

Expressions

Every symbolic value XQCP hands you – an InputRef from problem.input(), a LoopVar from problem.range()/problem.iter(), a RegLoad from problem.stow(), a coefficient read back from model.linear[i] – builds the same kind of expression tree when you combine it with an operator or an xq_* function. A plain Python int used anywhere one of these is expected gets coerced to a literal automatically; anything else raises TypeError. This page groups the resulting vocabulary by what you are trying to compute, not by class hierarchy.

Every xq_* function below lives in the same namespace as Problem and Types:

from xquad import cp

Arithmetic: Computing a Value

+ - * // % and unary - work exactly as Python’s operators do on integers, and compile to ADD SUB MUL DIV MOD NEG. Knapsack’s objective loop uses three of them in three lines:

with problem.range(0, num_items) as i:
    vi = problem.stow("vi", values_in.get(i))
    problem.model.linear[i].add(-vi)

-vi is unary negation on a RegLoad; problem.range and problem.iter yield LoopVars (i here) with the same arithmetic, used as loop-bound math in Max-Cut’s offset = e * 3. // is integer division, matching XQVM’s DIV, not Python’s floating-point / – XQCP has no /, since every value in this system is an i64.

Adding or subtracting exactly the literal 1 is special-cased: a + 1 compiles to LOAD, INC and a - 1 to LOAD, DEC, skipping the PUSH 1 a general add or subtract would need. Nothing about how you write it changes – a + 1 and a - 1 are the natural way to write these, and the compiler does the substitution for you.

Comparisons: Producing 0 or 1

== < > <= >= compile to EQ LT GT LTE GTE, each pushing 1 for true and 0 for false rather than a Python bool. That makes a comparison’s result usable anywhere an integer is: as a coefficient directly, as the condition argument to problem.branch() (see Control Flow), or combined further with arithmetic.

Python has no != here. There is no XQVM opcode for it, so != on an XQCP expression raises immediately, at problem-definition time:

TypeError: '!=' is not supported on XQCP expressions; use xq_not(a == b) instead

_ExprOps overloads __ne__ purely to raise that. Left to Python’s default, __ne__ would call __eq__ and invert the result, and inverting a CompareOp object with not only asks whether the object itself is falsy – which it never is – so a != b would evaluate to the constant False regardless of a and b. Write xq_not(a == b) instead:

model.linear[0] = cp.xq_not(a == b)

compiles to LOAD, LOAD, EQ, NOT, and gives 1 when a and b differ, 0 when they match.

Bitwise: Bit-Level Values

& | ^ ~ << >> compile to BAND BOR BXOR BNOT SHL SHR, useful wherever a problem packs several small values into one integer, or – as in knapsack’s slack step – builds a binary-weighted sum out of individual bits. These are ordinary Python operators, not xq_* functions, because Python lets you overload them.

Index Math: One Number From Several

Two functions turn a pair (or triple) of coordinates into a single flat index:

cp.xq_triu(i, j)            # IDXTRIU: upper-triangular packed index for (i, j)
cp.xq_grid(row, col, cols)  # IDXGRID: row * cols + col, grid flat index

xq_grid is what a 2D model’s tuple-coordinate coefficient access uses internally – see Inputs and Model Shape for that path; call it directly when you need the flat index as a value in its own right rather than as a coordinate to model.linear[...]. xq_triu(i, j) swaps i and j when i > j, then computes j * (j - 1) // 2 + i on the swapped pair, so the packed index does not depend on argument order, per the swap rule spec/xqvm/ISA.md gives for IDXTRIU. xq_triu(2, 5) returns 12 (5 * 4 // 2 + 2; no swap needed, since 2 <= 5).

Logical and Other Free Functions

Python’s and, or, and not keywords cannot be overloaded. XQCP gives you functions in their place. The first four work on the 0/1 convention comparisons use, not on Python truthiness:

cp.xq_not(x)                # NOT:  0 -> 1, non-zero -> 0
cp.xq_and(a, b)             # AND
cp.xq_or(a, b)              # OR
cp.xq_xor(a, b)             # XOR
cp.xq_bnot(x)               # BNOT: bitwise complement, the same opcode as ~ above,
                            # kept as a function for symmetry -- not the same as xq_not

Writing a and b, a or b, not a, or using an expression as an if condition instead of calling one of these functions raises immediately, the same way != does above:

TypeError: XQCP expressions cannot be used in a boolean context ('and', 'or', 'not', 'if'); use xq_and(a, b), xq_or(a, b) or xq_not(a) instead

Python’s and/or/not/if all decide on the truthiness of the object, not on the value it represents, so XQCP has no way to give them the right answer – only to refuse before a Python habit produces the wrong operand or the constant False in place of a real expression.

Five more free functions round out the arithmetic vocabulary that has no Python operator:

cp.xq_sqr(x)                # SQR:  x * x
cp.xq_abs(x)                # ABS:  absolute value
cp.xq_min(a, b)             # MIN
cp.xq_max(a, b)             # MAX
cp.xq_bitlen(x)             # BITLEN: floor(log2(x)) + 1, and 0 for x <= 0

Naming an Expression: stow

problem.stow(name, expr) evaluates an expression once and stores it in a register, returning a RegLoad you can reuse without re-emitting the computation. Knapsack’s vi = problem.stow("vi", values_in.get(i)) is this: values_in.get(i) is a VECGET, emitted once per loop iteration and bound to vi, and every later use of vi in that iteration is a cheap LOAD instead of repeating the VECGET. Passing an existing RegLoad back into stow overwrites that register instead of allocating a new one – useful for an accumulator threaded through a loop.

The operators and free functions above are the DSL’s whole vocabulary; spec/xqcp/TYPES.md lists nothing this page omits. With that in hand, the next question is what you do with a value once you have one: Objectives covers turning an expression into a term the solver minimises.

Objectives

An objective is the part of a model’s Hamiltonian you write directly, as opposed to the part a constraint instruction adds on your behalf – see Quadratic Models for what the Hamiltonian \(H\) is and why a solver minimising it is the whole mechanism. In XQCP terms, an objective is whatever you assign or add to model.linear[i] and model.quadratic[i, j] before any constraint method runs.

Solvers Minimise. Say So.

Every backend XQuad targets minimises. If your problem is a maximisation, you have to negate it, and getting this backwards produces a model that runs, verifies, and confidently returns the worst answer instead of the best one – nothing checks that you meant to maximise.

Knapsack maximises total value. examples/knapsack/runner.py writes the objective as:

# Objective: minimise -sum(v_i * x_i)
with problem.range(0, num_items) as i:
    vi = problem.stow("vi", values_in.get(i))
    problem.model.linear[i].add(-vi)

x_i = 1 means item i is selected, and -vi is the sign flip: instead of rewarding selection with +v_i, the objective penalises it with -v_i, so minimising H picks large values, not small ones. Isolating just this loop – no capacity constraint – and running the encoder with num_items=5, values=[5, 4, 18, 3, 19], then computing ENERGY against the sample [1, 1, 1, 0, 1] (items 0, 1, 2, 4 selected, values 5 + 4 + 18 + 19 = 46) gives:

objective-only energy: -46
expected: -(sum of selected values) = -46

Energy is exactly the negative of total value, for any selection, because that is what linear[i] = -v_i means: drop the - and the same solver would minimise total value instead, selecting nothing.

Assign or Accumulate

model.linear[i] and model.quadratic[i, j] are CoefficientRef proxies, not values. Three things you can do with one:

SyntaxOpcodeEffect
model.linear[i] = wSETLINECoefficient becomes w, replacing whatever was there
model.linear[i].add(w)ADDLINECoefficient becomes current + w
x = model.linear[i]GETLINERead the current coefficient as an expression

The same three exist for model.quadratic[i, j], via SETQUAD/ADDQUAD/ GETQUAD. The distinction matters the moment more than one term touches the same coefficient – knapsack’s .add(-vi) runs once per item, each touching a different linear[i], so assign and accumulate would agree there. They stop agreeing as soon as two terms share a coefficient: two .add() calls against the same linear[0] with weights 5 and 7 leave it at 12; two = assignments with the same weights leave it at 7, the second overwriting the first outright. Reach for .add() any time a coefficient might accumulate contributions from more than one place, which for an objective built inside a loop – the usual shape – is the common case; reach for = when you know this is the coefficient’s only source.

Quadratic Terms

A quadratic coefficient couples two variables, and the same accumulate pattern applies. Max-Cut’s objective is the DSL rendition of the Max-Cut coefficient rule Quadratic Models derives by hand – for each weighted edge (i, j, w):

problem.model.linear[i].add(-w)
problem.model.linear[j].add(-w)
problem.model.quadratic[i, j].add(w * 2)

three .add() calls, one per term of \(\text{linear}[i] \mathrel{+}= -w,\ \text{linear}[j] \mathrel{+}= -w,\ \text{quadratic}[i,j] \mathrel{+}= 2w\). .add() is what makes this loop correct: a node touched by several edges accumulates a -w contribution from each one, and = would let the last edge silently overwrite every edge before it. Running examples/maxcut/runner.py --n 5 --seed 42 confirms the sign on the result – energy: -354 against cut_weight: 354 – matching the same “minimise the negative” pattern knapsack uses, for the same reason.

Coefficient access on a 2D model accepts (row, col) tuples in place of flat indices, for both linear and quadratic coordinates independently – see Inputs and Model Shape for the flattening XQCP applies; nothing about objective assignment changes once a coordinate is a tuple instead of an int.

Beyond Quadratic: model.reduce()

Everything above stops at degree two: one or two variables per term. model.reduce(var_a, var_b, p_aux) -> RegLoad is the only way past that. It performs a Rosenberg degree reduction: allocate one fresh auxiliary variable w at model.size, grow model.size by one, and add enforcement terms so w behaves as var_a AND var_b: p_aux * (x_a*x_b - 2*x_a*w - 2*x_b*w + 3*w), which is 0 when w == x_a AND x_b and strictly positive otherwise. It returns w as a RegLoad, an ordinary variable index you can use in a further quadratic term, or feed into a second reduce() call to reach one degree higher still:

w = problem.model.reduce(ti, tj, p_aux)
problem.model.quadratic[w, tk].add(coeff)   # coeff * x_i * x_j * x_k

examples/cubic_opt/runner.py builds a cubic term this way, one reduce() call per term inside a problem.range() loop; max3sat and portfolio_opt use the same one-call pattern for their own cubic terms. examples/quartic_opt/runner.py chains two reduce() calls to reach a quartic term – the reason reduce() returns a RegLoad rather than nothing.

p_aux is a penalty in its own right, separate from any constraint penalty in the same model: it has to be large enough that violating the w == x_a AND x_b relationship is never worth it, the same reasoning Constraints applies to constraint penalties. Each reduce() call that actually executes grows model.size by one, so the final variable count is size plus the number of reduce() calls executed – loops included. cubic_opt allocates one auxiliary variable per cubic term inside its loop, not one for the whole model.

reduce() hangs off model, like every constraint form, but it is not one: XQCP keeps it out of the DSL’s constraint bookkeeping deliberately, since it is a structural transformation of the model, not a domain rule about a solution. That is also why it lives on this page and not Constraints.

How Large an Objective Gets

An objective’s magnitude is bounded by what its coefficients can sum to. Knapsack’s linear coefficients are -values, so its objective ranges from 0 (nothing selected) down to -49 for values = [5, 4, 18, 3, 19] (everything selected, which for this instance fits the capacity: the weights [2, 1, 5, 4, 4] sum to 16, under the capacity of 18) – a range fixed entirely by the problem’s own numbers, before any constraint enters the picture. Constraints picks a penalty weight relative to a range like this one; this page stops at producing the range, not sizing anything against it.

With an objective in place, the next step most problems need is a rule the objective alone cannot express – see Constraints for folding one into the same Hamiltonian.

Constraints

A constraint is a rule a solution must obey. XQCP has no separate mechanism for rules – every constraint call turns into a penalty term added to the model’s Hamiltonian, the same H Quadratic Models describes. This page covers the constraint forms the DSL exposes, when to reach for each, and how to size the penalty weight in practice. For the linear/quadratic coefficient deltas each opcode produces, see High-Level Constraints; this page does not repeat that table.

Every constraint method hangs off problem.model, the ModelRef that define_model() makes available, and every one takes a penalty argument.

The Constraint Forms

DSL callEnforcesReach for it when
model.apply_onehot_row(row, penalty)Exactly one variable in grid row row is 1A 2D model needs “exactly one choice per row” – one city per tour position. Requires a grid; row must name one the grid declares
model.apply_onehot_col(col, penalty)Exactly one variable in grid column col is 1The column-wise mirror – one position per city. Same grid requirement
model.apply_exclude(a, b, penalty)a and b are not both 1Two choices conflict and picking both is meaningless or invalid
model.apply_implies(a, b, penalty)If a is 1, b is 1One choice requires another – selecting a route requires its start node open
model.apply_equality(indices, coeffs, target, penalty)sum(coeffs[k] * x[indices[k]]) == targetA weighted sum equals an exact value – one-hot is a special case of this (all-1 coefficients, target 1); exclude is not, despite the resemblance
model.apply_atleast(indices, k, penalty)At least k of indices are 1A minimum count, unweighted – cover at least k elements
model.apply_atleastw(indices, coeffs, k, penalty)sum(coeffs[j] * x[indices[j]]) >= kA minimum weighted sum – cover at least k units of capacity
model.apply_inequality(indices, coeffs, target, capacity, penalty)sum(coeffs[k] * x[indices[k]]) <= capacityA capacity-style bound in one call – composes problem.slack() and apply_equality(), shown separately below. The third parameter is named target in the source but is not a target value: it is where slack variables begin, normally the count of real variables. capacity is the bound. Pass it positionally

onehot_row/onehot_col, exclude and implies take coordinates directly; on a 2D model those coordinates can be (row, col) tuples, flattened automatically. equality, atleast, atleastw and inequality instead take two vector registers built with problem.vec() and .push(): indices names which variables participate, coeffs (where present) weights them.

A minimal \(2 \times 2\) grid problem exercises the row/column/pairwise forms together:

problem.define_model(size=n * n, domain=Domain.BINARY, rows=n, cols=n)
with problem.range(0, n) as r:
    problem.model.apply_onehot_row(r, penalty=50)
with problem.range(0, n) as c:
    problem.model.apply_onehot_col(c, penalty=50)
problem.model.apply_exclude((0, 0), (1, 1), penalty=50)
problem.model.apply_implies((0, 1), (1, 0), penalty=50)

rows and cols on define_model are what make the two one-hot forms legal. Omit them and every apply_onehot_row/apply_onehot_col call raises InvalidGridDimensions at run time – xquad vm check cannot catch it, because grid extents are runtime values. The product rows * cols must also fit inside size; a grid cannot describe cells the model never declared. atleast, atleastw and inequality append slack variables past the grid and only ever grow size, so they never invalidate a grid that was legal when it was set.

Compiling and running this for n = 2 produces a model whose linear and quadratic maps match the row/column penalties plus the two pairwise terms:

linear:    {0: -100, 1: -50, 2: -100, 3: -100}
quadratic: {(0, 1): 100, (0, 2): 100, (0, 3): 50, (1, 2): -50, (1, 3): 100, (2, 3): 100}

atleast and atleastw allocate slack variables and grow model.size directly, as part of their own expansion – ATLEAST’s own expansion in HLF.md builds the slack terms itself rather than composing with the SLACK instruction below. That is a real difference, not an implementation detail: SLACK takes no model operand and only appends to the indices/coeffs vectors it is given, so it never touches model.size by itself. A problem.slack() call grows the model only later and indirectly, when a subsequent apply_equality() widens it to max(indices) + 1 – a problem.slack() result that apply_equality() never consumes leaves the model unchanged. Each atleast/atleastw call, by contrast, allocates floor(log2(max_excess)) + 1 slack variables, where max_excess is N - k for atleast and sum(coeffs) - k for atleastw, and zero slack variables when max_excess is zero or less. A 3-item apply_atleast(idx, 2, penalty=30) allocates one slack variable, since max_excess = 3 - 2 = 1. apply_atleastw(idx, coeffs, 3, penalty=30) on the same three indices then allocates zero more for coeffs = [1, 1, 1], one more for coeffs = [1, 1, 2], or two more for coeffs = [1, 2, 3].

Turning an Inequality into an Equality

None of the forms above is <= or >= on a plain sum – atleast/atleastw cover >=, but a capacity constraint like knapsack’s (“total weight at most W”) needs the other direction. problem.slack() bridges the gap: it appends binary-weighted slack variables to an indices/coeffs pair so that an EQUALITY constraint over the combined vector is satisfiable exactly when the original inequality holds. See SLACK for the bit-width formula.

examples/knapsack/runner.py builds its capacity constraint this way:

indices = problem.vec()
coeffs = problem.vec()
with problem.range(0, num_items) as i:
    indices.push(i)
    coeffs.push(weights_in.get(i))

problem.slack(indices, coeffs, num_items, capacity_in)
problem.model.apply_equality(indices, coeffs, capacity_in, 100)

indices/coeffs start as the item weights, one entry per item. slack appends slack variables starting at index num_items (the model’s current size) sized to absorb up to capacity_in of unused capacity. apply_equality then constrains sum(weight_i * x_i) + sum(slack bits) == capacity. If the items sum to less than capacity, some slack combination fills the gap exactly. If they sum to more, no slack combination can, because slack only adds. The compiled encoder shows the two instructions back to back:

LOAD r0
LOAD r3
SLACK r7 r8

LOAD r3
PUSH 0x64
EQUALITY r4 r7 r8

model.apply_inequality(indices, coeffs, num_items, capacity_in, 100) composes exactly these two calls into one and produces the same model. Reach for the two-call form when you need indices/coeffs for something else afterward, or want the two steps visible; reach for apply_inequality otherwise.

Why the Reported Energy Is Not Just -total_value

EQUALITY’s expansion adds penalty * a_k * (a_k - 2b) and 2 * penalty * a_k * a_m terms but drops the constant penalty * b^2, since it shifts every assignment’s energy equally and does not change which one is optimal – see HLF.md. Dropping a constant from the model does not drop it from ENERGY’s result: a feasible sample still carries -penalty * b^2 as a fixed offset, because the terms that remain evaluate to exactly that once the constraint holds.

Running examples/knapsack/runner.py --seed 42 selects items worth 46 in total against a capacity of 18, at penalty = 100, and reports energy = -32446. That is -46 - 100 * 18^2 = -46 - 32400 = -32446: the objective contribution and the constant the equality constraint leaves behind, added together. Objectives isolates the -46 on its own; this is where the other -32400 comes from.

Choosing a Penalty Weight

Quadratic Models gives a safe upper bound: penalty must exceed the largest objective improvement any violating assignment can buy over the best feasible one. It is not the tightest bound – an equality-shaped violation (equality, one-hot, atleast, atleastw) with integer excess d actually pays penalty * d^2, and exclude/implies pay penalty outright, the d = 1 case. The rest of this section finds that tighter threshold by accounting for d directly.

Enumerate the Failure, Not the Intuition

Take a 4-item knapsack: weights [3, 4, 5, 2], values [4, 5, 6, 3], capacity 7. The best feasible selection is items {0, 1} or {2, 3}, tied at value 9 and weight 7. Every selection that exceeds capacity is a candidate the solver might prefer instead, if the penalty is too cheap. Seven of the sixteen selections exceed it:

ItemsWeightValueExcess over capacityValue gap over 9Rejection threshold
{0, 2}810111.00
{1, 2}911220.50
{0, 1, 3}912230.75
{0, 2, 3}1013340.44
{1, 2, 3}1114450.31
{0, 1, 2}1215560.24
{0, 1, 2, 3}1418790.18

The last column is value gap / excess^2, because EQUALITY’s penalty term is penalty * (sum - target)^2: a selection exceeding capacity by d pays penalty * d^2, not penalty. The largest entry in that column, not the largest value gap, sets the threshold: {0, 2} needs penalty > 1, the largest threshold in the table, while {0, 1, 2, 3} – the biggest value gain – needs only penalty > 0.18, because its excess of 7 is squared away. Reasoning from the value gap alone (9, from the worst offender by value) is safe, but it oversizes the weight here: the raw gap suggests penalty > 9, nine times the penalty > 1 this problem actually needs.

Compiling this exact problem and brute-forcing every assignment of the real QUBO – 4 item variables plus the 3 slack bits SLACK allocates for a capacity of 7 – confirms the table: at penalty = 1, the true optimum is a three-way tie at H = -58 between the two feasible sets and the infeasible {0, 2}; at penalty = 2, only the two feasible sets remain, both at H = -107. penalty = 1 is the exact boundary the table predicts, not an approximation of it.

A tie at the boundary is not a tie a sampler breaks the same way every seed. Sampling this exact model at penalty = 1 with SolverCPU (200 reads) across eight seeds returns the infeasible {0, 2} on two of the eight, at the identical energy, -58, as every feasible seed; the other six split across the two feasible ties. A single feasible sample at a borderline weight is not evidence the weight is safe – the tie means either outcome is a true optimum, and only the rate across repeated solves, each with a different solver seed, separates a weight sitting on the boundary from one below it. A penalty that is actually below its threshold, rather than sitting on it, behaves differently: building a second 4-item instance (weights [5, 4, 2, 5], values [7, 4, 3, 9], capacity 6) whose tightest threshold works out to penalty > 3, the identical penalty = 1 is now strictly below it, and all eight seeds return the infeasible pick, every time, because there the infeasible assignment is the unique optimum rather than one member of a tie.

When You Cannot Enumerate

Most problems are too large to brute-force. Enumerate a small instance of the same shape if you can, the way the table above does, since the qualitative lesson – the tightest violator, not the most valuable one, sets the threshold – carries over. When even that is impractical, a safe but loose fallback exists for a purely linear objective: since each variable contributes to it at most once, no two full assignments can differ by more than the sum of the absolute values of every linear coefficient, and any nonzero integer excess is at least 1, so penalty greater than that sum always rejects every violation. A quadratic objective needs the same sum extended to include the absolute value of every quadratic coefficient too, since a quadratic[i,j] term can also flip between two assignments. For the table above that sum is 4 + 5 + 6 + 3 = 18, eighteen times the actual boundary of 1 – a real bound, not the largest single coefficient, and worth using only until you can afford to enumerate a representative case.

The seed-42 knapsack run printed in One Problem, Once has values [5, 4, 18, 3, 19], and knapsack’s objective is -values (see Objectives), so the same fallback sum is 5 + 4 + 18 + 3 + 19 = 49. The runner’s own penalty = 100 clears that sum outright, so it is safe by this loose bound alone, without needing the per-instance enumeration this page otherwise recommends.

Too High Does Not Corrupt the Model

Raising penalty from 2 to 1000 on the same problem leaves the gap between the best and second-best true optimum at exactly 1 in both cases, for the reason Quadratic Models gives: the penalty term is zero on every feasible assignment, so it cannot change their relative order.

What a large penalty costs instead happens outside the model, on the hardware that has to represent it – see Energy and Precision for that trade-off, and Selection Under Budget for a worked recipe built on this page.

Control Flow

XQCP gives you four constructs for anything beyond a flat sequence of calls: problem.range(), problem.iter(), problem.branch(), and problem.stow(). Each is a Python API you call while building a Problem, and each records something into the action list that compile() later turns into real XQVM instructions. That gap between the two is the first thing to get straight, because it decides what each construct actually buys you.

A Python Loop and a Compiled Loop Are Different Things

xqcp is a Python program that runs once, when you call problem.compile(), to produce an .xqasm string. An ordinary Python for loop inside your problem definition runs during that one recording pass – it does not exist in the output at all. Three plain calls to add(1) inside for i in range(3) record three separate add_linear actions, one per concrete index, and the compiled encoder shows exactly that: no loop instruction, three ADDLINE blocks back to back.

for i in range(3):
    problem.model.linear[i].add(1)
PUSH 0
PUSH 1
ADDLINE r1
PUSH 1
PUSH 1
ADDLINE r1
PUSH 2
PUSH 1
ADDLINE r1

problem.range() is different: it records one RANGE loop that the VM executes at run time, over a count that need not be known until calldata arrives. with problem.range(0, num_items) as i compiles to a single RANGE/NEXT pair regardless of how large num_items turns out to be, because num_items is a runtime input, not a Python integer the recorder can unroll:

with problem.range(0, num_items) as i:
    vi = problem.stow("vi", values_in.get(i))
    problem.model.linear[i].add(-vi)
PUSH 0
LOAD r0
RANGE
  LVAL r5
  LOAD r5
  VECGET r2
  STOW r6
  LOAD r5
  LOAD r6
  NEG
  ADDLINE r4
NEXT

This is examples/knapsack/runner.py’s objective loop, unaltered. Use a plain Python loop only when the count is fixed at problem-definition time and small; use problem.range() whenever the count depends on an input, which is the common case, since num_items in this example is not known until calldata is set.

range – Counted Loops

problem.range(start, end) is a context manager yielding a LoopVar. It records a range_start action on entry and a range_end action on exit, and compiles to RANGE/NEXT around whatever the body records. The bounds can be any expression, not just literals or inputs – problem.range(i + 1, n) inside another range(0, n) builds the upper-triangular pattern a pairwise comparison needs, one RANGE nested inside another:

with problem.range(0, n) as i:
    with problem.range(i + 1, n) as j:
        problem.model.quadratic[i, j].add(1)

For n = 3 this adds quadratic[i, j] += 1 for every pair with i < j: (0, 1), (0, 2), (1, 2), each exactly once, matching what running the compiled encoder against n = 3 produces. RANGE is documented at the instruction level in Loops, where it pops count and start, not start and an exclusive end. problem.range(start, end) takes the exclusive end a Python range would; the compiler emits the subtraction that turns it into a count, so a non-zero start shows a SUB before RANGE – compiling problem.range(2, 5) emits PUSH 2 / PUSH 5 / PUSH 2 / SUB / RANGE. A start of 0 hides this, because end and count coincide there, so every RANGE listing so far in this chapter shows no SUB.

iter – Looping Over a Vector’s Elements

problem.iter(vec, start, end) loops over a slice of a vector’s actual elements rather than a count, compiling to ITER/NEXT. It always yields a pair, (idx, val): the element’s absolute position in the vector and its value. Unpack whichever you need and discard the other with _:

with problem.iter(weights, 0, n) as (idx, val):
    problem.model.linear[idx].add(val)

Running this against weights = [7, 8, 9] produces linear = {0: 7, 1: 8, 2: 9} – idx supplies the coordinate, val the weight added there. The compiled form pairs LIDX/LVAL inside the loop body with exactly this reading:

PUSH 0
LOAD r0
ITER r1
  LIDX r3
  LVAL r4
  LOAD r3
  LOAD r4
  ADDLINE r2
NEXT

Reach for iter when the loop body needs the element’s value directly and would otherwise call vec.get(idx) on every iteration; reach for range when the body needs an index into something other than the vector being walked, the way knapsack’s objective loop above indexes values_in by position rather than walking it as a slice.

stow – Carrying State Across Iterations

Expressions covers what problem.stow(name, expr) does and why knapsack’s objective loop uses it once per iteration. Inside range and iter, stow has a second job: passing an existing RegLoad back into it, instead of a name, reuses that RegLoad’s register rather than allocating a new one, which is how a loop carries an accumulator from one iteration to the next:

acc = problem.stow("acc", 0)
with problem.range(0, n) as i:
    problem.stow(acc, acc + i)
problem.model.linear[0].add(acc)

The compiled loop body reads and writes the same register, r2, on every iteration, and running it for n = 4 leaves linear[0] = 6 – 0 + 1 + 2 + 3:

PUSH 0
STOW r2
PUSH 0
LOAD r0
RANGE
  LVAL r3
  LOAD r2
  LOAD r3
  ADD
  STOW r2
NEXT

branch – Conditional Terms

problem.branch(cond1, body1, cond2, body2, ..., default) compiles to a chain of JUMPI/JUMP/TARGET with first-match semantics. branch tests conditions in order and runs the first true arm’s body; no later arm runs, even one whose condition is also true. The final argument is mandatory and has no condition – pass a callable for a default action, or None to do nothing when no arm matches.

with problem.range(0, n) as i:
    w = problem.stow("w", weights.get(i))
    problem.branch(
        w > 5, lambda: problem.model.linear[i].add(w * 10),
        w > 0, lambda: problem.model.linear[i].add(w),
        None,
    )
LOAD r4
PUSH 5
GT
NOT
JUMPI .1
  LOAD r3
  LOAD r4
  PUSH 10
  MUL
  ADDLINE r2
  JUMP .0
TARGET .1
LOAD r4
PUSH 0
GT
NOT
JUMPI .2
  LOAD r3
  LOAD r4
  ADDLINE r2
  JUMP .0
TARGET .2
TARGET .0

Each condition compiles to itself negated, then a JUMPI past its arm: w > 5 becomes NOT then JUMPI .1, so the arm runs when the condition is true and is skipped straight to TARGET .1 when it is false. An arm that runs ends with JUMP .0, past every remaining arm, which is what makes the semantics first-match rather than last-match. The default arm has no condition and no skip logic – it just falls through to TARGET .0 if reached.

Each body is a zero-argument callable, called once while recording, under the same recording pass a range body runs under, so it can reference i and w from the enclosing scope. Running this for weights = [7, 3, 0] adds 70 at index 0 (7 > 5), 3 at index 1 (3 > 5 is false, 3 > 0 is true), and nothing at index 2 (neither condition holds, and the default is None): linear = {0: 70, 1: 3}, with index 2 absent rather than zero.

Nesting and Ordering

range and iter nest to arbitrary depth, matching the nesting Loops documents at the instruction level: LVAL, LIDX and NEXT always act on the innermost active loop. branch arms can themselves contain range, iter, or further branch calls, since a body callable can record anything a top-level problem definition can.

The encoder splits body actions into an objective block and a constraint block by scanning for constraint calls – see Compiling for that partition. A range or iter body is recorded flatly, so a nested constraint call is still visible to the scan. A branch arm is captured separately: a constraint call made only inside one still constrains the model, but prints under ; === Objective === instead of ; === Constraints ===. Call constraint methods directly inside the enclosing range instead, the way examples/set_cover/runner.py does, if you want the section comment to match.

Outputs and Decoding

A solver hands back a sample: one raw value per variable. Decoding turns that assignment into the answer your problem is actually about – a chosen item set, for knapsack. This page covers a decoder program’s outputs, its read access to the sample, and when decoding outside the VM makes more sense.

Declaring an Output

problem.output(name, type=Types.Vec, length=None) declares one decoder output and returns an OutputRef. type must be Types.Vec – every pipeline output is a vector, and problem.output() raises TypeError for anything else. An output is filled one of two ways, chosen by length=.

Appending

Without length=, an output starts empty and .append(value) grows it by one element:

selected = problem.output("selected", type=Types.Vec)
with problem.range(0, num_items) as i:
    selected.append(problem.sample.getline(i))

This is examples/knapsack/runner.py’s decoder in full: one output, selected, filled by reading the sample’s per-item assignment directly. problem.sample.getline(i) reads sample[i]: 1 if the solver selected item i, 0 otherwise. selected ends up the same shape as the input item list, a 0/1 flag per item rather than a list of chosen indices. Compiled, this is the decoder assembly Compiling shows in full: VECI r2 ; selected, a RANGE loop reading GETLINE r0 and pushing with VECPUSH r2, then PUSH 0 / OUTPUT r2.

Writing and Reading by Index

With length=, the decoder starts the output as length zeros, and you write and read it by index: out[i] = value compiles to VECSET, and out[i] to VECGET. Use it when a value’s position is computed rather than reached in order. On the grid examples/tsp/runner.py builds, one row per city and one column per position, rowfind gives the position a city was placed at, so the tour can be written city by city:

tour = problem.output("tour", type=Types.Vec, length=n)
with problem.range(0, n) as city:
    tour[problem.sample.rowfind(row=city, value=1)] = city

length is anything the decoder can evaluate before its first loop: a literal, the scalar on calldata slot 1, arithmetic over those, or a sample read. Slots you never write stay 0. An out[i] read returns an expression, so one output can feed another:

flags = problem.output("flags", type=Types.Vec, length=n)
scores = problem.output("scores", type=Types.Vec)
with problem.range(0, n) as i:
    flags[i] = problem.sample.getline(i)
with problem.range(0, n) as i:
    scores.append(flags[i] * 10 + i)

Record-Time Checks

The two modes do not mix, and both are checked when you write the problem, not when the decoder runs. out[i] = value or out[i] on an output without length= raises

TypeError: Writing to output 'selected' by index needs a declared length; declare it with problem.output('selected', length=n), or fill it with selected.append(value)

(Reading from for a read), and .append() on an output with length= raises

TypeError: Output 'tour' was declared with length= and is filled by index; write tour[i] = value instead of tour.append(value)

A literal length= below 1 raises ValueError, and so does a literal index outside [0, length) when length= is a literal too. A length= that reads another output, such as length=counts[0], raises ValueError as well: every length is evaluated before any output is filled, so it would read 0. Any other index is bounds-checked by the VM when the decoder runs, which faults with IndexOutOfBounds. A length that evaluates to 0 or less at decode time leaves the output empty, so the first index write faults the same way.

problem.output() must be called at the top level. Inside a problem.range() or problem.iter() block, or inside a branch() arm, it raises RuntimeError at once:

RuntimeError: problem.output('o') is called inside a range() or iter() block; declare every output at the top level, outside every loop and branch() arm

Reading a Sample

problem.sample is available once define_model() has run, and exposes seven read methods built on the Grid Operations and Coefficient Access instructions a Sample register supports:

CallReads
sample.getline(i)Variable i’s assignment (1D or flat)
sample.value(i)Variable i in the domain you declared it over
sample.case(v)Case variable v took, or -1 (categorical)
sample.rowfind(row, value)Column of the first match for value in row (2D)
sample.colfind(col, value)Row of the first match for value in col (2D)
sample.rowsum(row)Sum of every value in row (2D)
sample.colsum(col)Sum of every value in col (2D)

getline and value differ only on a model declared with lo=/hi=, where the stored variable is y = x - lo and value(i) adds lo back to give the x you wrote coefficients over. Everywhere else they are the same call. getline is the only raw reader and value the only shifted one, so nothing shifts unless you ask.

case(v) is rowfind(v, 1) under a name that says what it means on a categorical model: the case variable v took, or -1 where the solver left that row empty. It reads the same on any 2D binary grid, whether or not Domain.CATEGORICAL built it.

Knapsack’s flat model only needs getline. A 2D grid model – one variable per (row, col) pair, the way a one-hot assignment problem is usually shaped – more often needs colfind: “which row has a 1 in this column” reads directly as “which choice was made for this slot.” On a \(2 \times 2\) grid sample with rows [0, 1] and [1, 0], sample.colfind(col=0, value=1) returns 1 (row 1 has the 1 in column 0) and sample.colfind(col=1, value=1) returns 0, each call compiling to the column pushed, then the value to match, then COLFIND r{sample}.

What a Decoder Block May Reference

Everything recorded after the first problem.output() call goes to the decoder and to nothing else, and the decoder is a separate program with a separate register file. It runs on two pieces of calldata: the sample on slot 0, and one scalar on slot 1. Anything a decoder block names that is not reachable from those two is refused at compile() rather than compiled into a read of whatever register happens to hold something.

A block may reference the sample through the read methods above, its own loop variables, one scalar, and out[i] on an output declared with length=. That scalar does not have to be N: examples/graph_coloring/runner.py stows a total variable count before declaring its output and passes that on slot 1. The emitted decoder names which one it resolved, so INPUT r1 ; total_vars tells you what the caller has to supply. Naming a second one raises, because the decoder is handed exactly one and cannot say which of the two you meant:

RuntimeError: xqcp: a decoder block references two scalars, 'n' and 'acc', but the decoder is handed exactly one on calldata slot 1

What a block may not reference is anything the decoder was never given: a vector input or a problem.vec() allocation, a model coefficient (problem.model.linear[i] – the decoder holds the sample, not the model), or a loop variable from a loop that has closed. problem.iter() is refused for the same reason: ITER walks a vector register, and no input vector reaches the decoder. Walk the indices with problem.range() and read each one from problem.sample.

Filling Outputs in Any Order

The decoder allocates every output before it runs anything else and ships every output to its slot after everything else has run. Between those, it walks every action recorded after the first problem.output() once, in the order you recorded them. An output can therefore be filled from any point after its declaration: two outputs from one loop, or an earlier output after a later one has been declared.

chosen = problem.output("chosen", type=Types.Vec)
weights = problem.output("weights", type=Types.Vec)
with problem.range(0, num_items) as i:
    chosen.append(problem.sample.getline(i))
    weights.append(problem.sample.getline(i) * 2)

Conditional Outputs

problem.branch() works after the first problem.output(), with the same first-match arms it has in the encoder. Conditions read the sample, loop variables and the scalar, like any other decoder expression. This keeps only the indices of the selected items instead of a flag per item:

picked = problem.output("picked", type=Types.Vec)
with problem.range(0, num_items) as i:
    problem.branch(
        problem.sample.getline(i) == 1, lambda: picked.append(i),
        None,
    )

With items 0, 3 and 4 selected out of five, picked decodes to [0, 3, 4]. An arm takes everything else a decoder block takes: .append(), out[i] = value, a problem.range() loop, and a nested problem.branch().

The Decoder Program vs. Decoding in the Host

Once a solver returns a sample, two different things can decode it, and they are not the same operation.

The decoder program is XQVM bytecode: vm.run(programs.decoder) with the sample and N on calldata, exactly like running the encoder or the verifier. It only ever sees the sample through the instructions above, so it runs on any surface that can execute XQVM bytecode – Ways to Use XQuad lists six – with no Python or Rust object model required at the call site.

Decoding in the host means reading the sample object directly in whatever language is driving the pipeline, without running the decoder program at all. In Python, a solved sample is an XqmxSample, so sample.get_linear(i) for each item index reads the same assignments sample.getline(i) does inside the decoder, just from host code instead of from bytecode:

vm.set_calldata([sample, n])
vm.set_output_slots(1)
vm.run(programs.decoder)
result = vm.outputs()[0]

decoder_selection = list(result)                            # via programs.decoder
host_selection = [sample.get_linear(i) for i in range(n)]   # direct read

VM imports from xquad.vm, alongside the xquad.cp/xquad.types imports Inputs and Model Shape opens this chapter with. set_output_slots has to run before vm.run, since the slot count defaults to 0. OUTPUT against a slot that was never allocated raises OutputIndex while the decoder runs.

vm.outputs()[0] comes back as a plain list of int. Both reads of the seed-42 knapsack sample (weights = [2, 1, 5, 4, 4], values = [5, 4, 18, 3, 19], capacity = 18) produce the same list: [1, 1, 1, 0, 1]. Host decoding is less code for a one-off script already holding the sample in memory. The decoder program is the version worth keeping once decoding needs to happen the same way regardless of which language or environment is driving the run, or needs to be inspected and verified as an artifact in its own right the way the encoder and verifier already are.

Where This Page Stops

Decoding turns a sample into an answer; it says nothing about whether that answer is any good. valid and energy come from the verifier, not the decoder – see Compiling for what a verifier’s validity check does and does not cover. Choosing a solver and judging solution quality belong to Solving Overview and Running Programs, not here.

Compiling

problem.compile() walks the action list problem.define_model(), your objective terms, your constraints and problem.output() recorded, and returns CompiledPrograms(encoder, verifier, decoder) – three .xqasm strings. Three Programs covers why the split exists and what each program is for; this page covers what compile() actually emits and how to read it when something is wrong.

Three Independent Passes

compile() runs three separate compiler functions over the same action list, each keeping a different subset. Nothing computed for one program carries into another; there is no shared intermediate representation.

Compiling examples/knapsack/runner.py’s build_problem() – the exact code from Constraints, unaltered – produces this encoder:

; === Inputs ===
PUSH 0
INPUT r0
PUSH 1
INPUT r1
PUSH 2
INPUT r2
PUSH 3
INPUT r3

; === Allocations ===
LOAD r0
BQMX r4

; === Objective ===
PUSH 0
LOAD r0
RANGE
  LVAL r5
  LOAD r5
  VECGET r2
  STOW r6
  LOAD r5
  LOAD r6
  NEG
  ADDLINE r4
NEXT
VEC r7
VEC r8
PUSH 0
LOAD r0
RANGE
  LVAL r9
  LOAD r9
  VECPUSH r7
  LOAD r9
  VECGET r1
  VECPUSH r8
NEXT
LOAD r0
LOAD r3
SLACK r7 r8

; === Constraints ===
LOAD r3
PUSH 0x64
EQUALITY r4 r7 r8

; === Output ===
PUSH 0
OUTPUT r4
HALT

PUSH 0x64 is 100, the penalty passed to apply_equality – the compiler renders a handful of common penalty values in hex for readability; every other constant here prints in decimal.

This is the encoder for the exact problem shape examples/knapsack/runner.py builds, independent of the item count: register numbers and instruction count come from how many input(), stow(), vec() and define_model() calls the Python code makes, not from --n at the command line.

What compile_encoder Does

Per spec/xqcp/COMPILER.md, the encoder partitions the action list into inputs, the model allocation, and a body that splits again into objective and constraint blocks: a top-level block is a constraint block if it contains any of onehot_row/onehot_col/exclude/implies/equality/atleast/atleastw/inequality, otherwise it is objective. ; === Objective === and ; === Constraints === are these two blocks, in that order, regardless of the order you wrote them in Python. Knapsack’s objective loop and the SLACK call both come from code written before apply_equality, and neither one is itself a constraint action, so the encoder above places both in the objective section (see Control Flow for the one case, a constraint call inside a branch arm, where this partition surprises). The output section is fixed: PUSH 0 / OUTPUT r{model} / HALT, since an encoder’s only output, always on slot 0, is the model it built.

Register allocation is one incrementing counter shared across the whole program. spec/xqcp/COMPILER.md fixes the order. Inputs go first. A 2D model’s cols register comes ahead of the model register; a 1D model has no cols register, so its model register follows the inputs directly. Loop variables, stowed values, vec() registers and output registers fill in afterward, each claiming a register in the order it is called. Knapsack’s model is 1D: its four inputs claim r0-r3, the model claims r4, and everything after follows call order – r5/r6 for the objective loop’s LoopVar and stow, r7/r8 for the two vec() calls. Inputs and Model Shape shows the 2D case, where the cols register lands before the model register: one input at r0, cols at r1, model at r2. Going past r255 raises RuntimeError at compile time, before any assembly is generated.

What compile_verifier and compile_decoder Do

The verifier replays the encoder. A constraint’s operands – the index and coefficient vectors an apply_equality was handed – are register handles, not data: those vectors are built by VECPUSH instructions that run inside the encoder at VM runtime, often nested in loops. A separate program with its own register file cannot inherit them. So the verifier re-executes the encoder’s inputs, loops, stows, branches and vector construction, drops every model mutation, and emits a check of the sample in place of each constraint, at the same point in the stream. Its register and vector state at each constraint site is then identical to the encoder’s.

That is why the verifier keeps the encoder’s register numbers instead of starting a fixed layout of its own, and why it takes the encoder’s calldata. It claims eight registers above the encoder’s high-water mark: the sample, the valid flag, energy, the model’s declared size, a weighted-sum accumulator, an ITER position, an element, and a counter tracking the index each REDUCE would have allocated. Going past r255 raises RuntimeError at compile time.

Knapsack’s encoder stops at r11, so its verifier’s sample lands on r12 and its valid flag on r13:

; === Inputs ===
PUSH 0
INPUT r0
PUSH 1
INPUT r1
PUSH 2
INPUT r2
PUSH 3
INPUT r3
PUSH 4
INPUT r4
PUSH 5
INPUT r12

; === Model shape ===
LOAD r0
STOW r15

; === Validity checks ===
PUSH 1
STOW r13

; Check every declared variable is in the model's domain
PUSH 0
LOAD r15
RANGE
  LVAL r17
  LOAD r17
  GETLINE r12
  COPY
  PUSH 0
  EQ
  SWAP
  PUSH 1
  EQ
  OR
  LOAD r13
  AND
  STOW r13
NEXT

; === Objective (replayed for state, not for energy) ===
PUSH 0
LOAD r0
RANGE
  LVAL r5
  LOAD r5
  VECGET r2
  STOW r6
NEXT
VEC r7
VEC r8
PUSH 0
LOAD r0
RANGE
  LVAL r9
  LOAD r9
  VECPUSH r7
  LOAD r9
  VECGET r1
  VECPUSH r8
NEXT

; === Constraints ===
PUSH 0
STOW r16
PUSH 0
VECLEN r7
ITER r7
  LIDX r17
  LVAL r18
  LOAD r17
  VECGET r8
  LOAD r18
  GETLINE r12
  MUL
  LOAD r16
  ADD
  STOW r16
NEXT
LOAD r16
LOAD r3
LTE
LOAD r13
AND
STOW r13

; === Energy ===
ENERGY r4 r12
STOW r14

; === Output ===
PUSH 0
OUTPUT r14
PUSH 1
OUTPUT r13
HALT

Three things in that listing are worth reading closely. The model is INPUT into r4, the register the encoder allocated it to, so replayed references resolve without a renumbering pass; BQMX is skipped, but the size expression is replayed and stowed because the domain check needs the bound. The objective loop stowing r6 has no effect on the outcome and runs anyway – a later constraint could read that register, and the replay does not try to work out which stows matter. And the capacity check reads LTE, not EQ: knapsack builds its constraint with slack() followed by apply_equality, and a slack-extended equality is an inequality over the real variables. The SLACK instruction is not replayed, so r7 and r8 hold the four item entries and no slack entries.

The domain check runs to the model’s declared size, r15, replayed from define_model. Slack and REDUCE auxiliary variables live past that size and are not domain-checked. A SPIN model is checked against -1 and +1 instead of 0 and 1; the binary-only constraint kinds – onehot_row, onehot_col, exclude, implies – raise at compile time on a spin model rather than emit a check that does not mean anything there.

The decoder puts the sample on r0 and N on r1, allocates every output vector with VECI under one ; === Decode === header, walks everything recorded after the first problem.output() call, then ships every output with PUSH {slot} / OUTPUT r{out} under one ; === Output === header. Inside the decoder, every scalar reference – an input the encoder read, or a value problem.stow() put in a register – resolves to LOAD r1, because one scalar is all the decoder is handed:

; === Inputs ===
PUSH 0
INPUT r0
PUSH 1
INPUT r1  ; num_items

; === Decode ===
VECI r2  ; selected
PUSH 0
LOAD r1
RANGE
  LVAL r10
  LOAD r10
  GETLINE r0
  VECPUSH r2
NEXT

; === Output ===
PUSH 0
OUTPUT r2
HALT

num_items in with problem.range(0, num_items) as i is an InputRef in the encoder, so in the decoder it becomes LOAD r1, the same register slot 1 was read into two lines above – the decoder has no independent notion of num_items, only of the one scalar it is passed. Which scalar that is, is the program’s choice rather than a fixed N, and the header comment names it. Referencing a second, distinct scalar is rejected at compile(); see Outputs and Decoding for the full list of what a decoder block may and may not name.

The Calldata and Output Contract

Each program’s calldata order and output slots are fixed by what it was compiled from, and a host has to match them exactly:

ProgramCalldata (in order)Outputs
EncoderOne entry per problem.input() call, in call orderSlot 0: the model
VerifierOne entry per problem.input() call, in call order, then the model, then the sampleSlot 0: energy, slot 1: valid
DecoderSample, NOne slot per problem.output() call, in declaration order

examples/knapsack/runner.py’s run() function drives exactly this contract: vm.set_calldata([n, weights, values, capacity]) and vm.set_output_slots(1) before the encoder, matching the four problem.input() calls in build_problem() in order and the encoder’s one output slot; vm.set_calldata([n, weights, values, capacity, model, sample]) and vm.set_output_slots(2) before the verifier; vm.set_calldata([sample, n]) and vm.set_output_slots(1) before the decoder. The output slot count defaults to 0; running a program that executes OUTPUT against a slot that was never allocated raises OutputIndex (see Limits and Errors).

Problem.verifier_calldata() returns that order as a list of names – ["num_items", "weights", "values", "capacity", "model", "sample"] for knapsack – so a host can zip its own values against it rather than rebuilding the order from the problem definition.

Inspecting the Emitted Assembly

programs.encoder, programs.verifier and programs.decoder are plain Python strings – printing one is the fastest way to see what a problem definition actually compiled to, which is how every listing on this page was produced. From there, the xquad CLI static-inspects a single program without needing a Python host at all: xquad vm asm and xquad vm dis round-trip a .xqasm file to bytecode and back to a readable listing, and xquad vm check runs the same structural, jump-target, loop-nesting, register type-state and stack-depth checks problem.compile() already runs automatically through xqffi when that package is installed. See CLI for the full command reference; nothing about compiling changes it.

xquad vm check and xquad vm dis work on any of the three programs as they stand, but xquad vm run does not, because none of the three programs’ inputs are all plain integers – the encoder above needs two vectors on calldata positions 1 and 2, and the CLI’s --calldata flag only accepts a comma-separated list of i64s. Handing it integers where a program expects a vector does not fail to parse; it fails at run time, once the program tries to use the value:

$ xquad vm run --text knapsack.encoder.xqasm --calldata 2,10,20,5
Error: run failed: RegisterType { reg: 2, expected: "vec<int>", got: "int" }

Running any of these three programs against real calldata needs a host that can construct vectors, models and samples – the Python VM/Program surfaces Ways to Use XQuad describes, covered in full in Running Programs. The CLI’s role here is static: assemble, disassemble, and verify a program’s structure before handing it to a host that can supply calldata rich enough to run it.

Running Programs

This page covers driving an already-compiled .xqasm or .xqb program from Python: loading it, supplying calldata, running it, and reading outputs back. Ways to Use XQuad names the surfaces; this page is the full coverage for three of them: xquad.program, and the two VM wrappers underneath and alongside it. For writing a problem with the xqcp DSL, see Modelling. For what xquad vm check checks and how to fix a rejected program, see Verification.

All three surfaces run the same Rust xqvm interpreter, and every example on this page runs as shown.

Program and Session

xquad.program.Program loads a program once; Program.session() gives you a Session you call .run() on repeatedly, each time with fresh calldata. Each run returns a RunResult with dict-keyed outputs, the residual stack, and the step count:

from xquad.program import Program

src = """
PUSH 0
INPUT r0
PUSH 1
INPUT r1
LOAD r0
LOAD r1
ADD
STOW r2
PUSH 0
OUTPUT r2
HALT
"""

program = Program.from_source(src)
assert program.instruction_count == 11
assert program.source is not None   # retained for debugging

session = program.session(output_slots=4)
session.set_calldata([40, 2])
result = session.run()

assert dict(result.outputs) == {0: 42, 1: None, 2: None, 3: None}
assert result.stack == []
assert result.steps == 11

# Re-run with different calldata -- sessions carry no hidden state.
session.set_calldata([100, 200])
assert dict(session.run().outputs) == {0: 300, 1: None, 2: None, 3: None}

RunResult.outputs is a dict keyed by slot index; a slot the program never wrote reads as None rather than raising. Program is immutable and reusable; Session carries the mutable calldata and output-slot count. Deriving a second Session from the same Program gives you two runners that share no state.

Step limits

Session.set_step_limit(n) caps how many instructions a run may execute before run() raises. The limit is exact: 0 permits no instructions at all. A fresh Session starts at xquad.vm.DEFAULT_STEP_LIMIT, which is 10,000,000. None is the only unbounded spelling, and a caller has to write it – unbounded execution is asked for, never inherited.

A run that faults raises the xqffi.vm.XqvmError subclass named for the fault – here StepLimitExceeded – with the fault’s description as its message. XqvmError is a RuntimeError.

from xqffi.vm import StepLimitExceeded

session = program.session(output_slots=1)
session.set_calldata([1, 2])
assert "step_limit=10000000" in repr(session)

session.set_step_limit(3)
try:
    session.run()
except StepLimitExceeded as e:
    print(e)
# step charge of 1 exceeds the step limit of 3 (3 steps already charged)

# 0 is not a sentinel for "unlimited" -- it executes nothing, so this
# 11-step program fails at its first instruction.
session.set_step_limit(0)
try:
    session.run()
except StepLimitExceeded:
    pass

# None removes the bound. A program that never halts will not return,
# and it does not take SIGALRM: the GIL is held inside the Rust run.
session.set_step_limit(None)
assert "step_limit=unlimited" in repr(session)
assert dict(session.run().outputs) == {0: 3}

A budget that exactly covers the program succeeds. This one needs 11 steps, so set_step_limit(11) runs it and set_step_limit(10) does not.

Loading bytecode directly

Program.load(bytes) parses raw wire-format bytes – a .xqb blob produced by xquad vm asm or by program.bytecode(). No source is retained:

blob = program.bytecode()
reloaded = Program.load(blob)
assert reloaded.source is None
assert reloaded.instruction_count == program.instruction_count

Program.load never validates its input; a malformed blob decodes successfully as a Program object and only fails once a Session tries to run it. A one-byte blob is too short even for the wire-format header:

p = Program.load(b"\x43")
s = p.session()
try:
    s.run()
except RuntimeError as e:
    print(e)
# decode error: XQBC header is truncated

Program.from_source validates at load time instead: an unassemblable string raises ValueError immediately, before a Session exists.

Calldata types

A calldata list may mix any of int, list[int], xqffi.vm.XqmxModel, xqffi.vm.XqmxSample, and None (unset). Session.set_calldata checks every element’s type as soon as you call it, not at run time:

from xqffi.vm import (
    Domain,
    IndexOutOfBounds,
    InvalidGridDimensions,
    XqmxModel,
    XqmxSample,
)

model = XqmxModel.binary(size=4)   # or XqmxModel(Domain.BINARY, size=4)
model.set_linear(0, -1)
model.set_quad(0, 1, 2)

sample = XqmxSample.spin(values=[-1, 1, -1, 1])

session.set_calldata([model, sample, [1, 2, 3], 42])
# session.run() now sees four typed input slots.

try:
    XqmxModel.binary(size=4, rows=2, cols=3)
except InvalidGridDimensions as e:
    print(e)
# a 2 x 3 grid does not fit 4 variables; rows and cols must both be 0,
# or both positive with rows * cols <= size

try:
    session.set_calldata([object()])
except TypeError as e:
    print(e)
# unsupported calldata element type: object; expected int, list[int],
# XqmxModel, XqmxSample, or None

The constructors also refuse a size over xqffi.vm.MAX_ALLOCATION_SIZE, the largest allocation bytecode can make, with InvalidAllocation, and XqmxSample refuses a value outside its domain with ValueError. The coefficient accessors (set_linear, get_linear, set_quad, get_quad) raise IndexOutOfBounds for a variable index outside 0..size, a negative index included. InvalidAllocation, InvalidGridDimensions and IndexOutOfBounds are XqvmError faults, so RuntimeError subclasses:

try:
    model.set_linear(4, 1)
except IndexOutOfBounds as e:
    print(e)
# index 4 out of bounds (len 4)

Inspecting a model or sample

XqmxModel and XqmxSample are pyo3 objects; there is no dict-conversion helper in this package today. Read a model’s coefficients through its own accessors:

model = XqmxModel.binary(size=4)
model.set_linear(0, -1)
model.set_linear(2, 3)
model.add_quad(0, 1, 2)

model.domain          # Domain.BINARY
model.domain.name     # "binary"
model.size            # 4
list(model.linear_items())     # [(0, -1), (2, 3)]
list(model.quadratic_items())  # [((0, 1), 2)]
model.get_linear(0)   # -1
model.get_quad(0, 1)  # 2
repr(model)            # 'XqmxModel(domain=binary, size=4)'

model.energy(XqmxSample.binary([1, 1, 0, 0]))  # 1, as ENERGY computes it

add_linear and add_quad accumulate into a coefficient and raise xqffi.vm.ArithmeticOverflow, leaving the model unchanged, if the result leaves the signed 64-bit range. A sample’s values are checked against its domain when it is constructed; XqmxSample.binary([2]) raises ValueError.

__repr__ is intentionally minimal. To inspect every coefficient or serialise a model, iterate linear_items() and quadratic_items().

The other two VM surfaces

Two lower-level wrappers sit underneath and alongside xquad.program, both worth knowing about directly.

xqffi.vm.Vm is the thinnest possible wrapper over the Rust interpreter: construct it, call set_calldata / set_output_slots, call .run(bytecode), read .outputs() as a positional list. .reset() clears the stack, registers, loop stack, and step counter, but calldata and output slots are untouched by it – both persist across runs until you call set_calldata() or set_output_slots() again. It is what Session.run() builds internally:

from xqffi.vm import Vm

vm_src = "PUSH 7\nPUSH 5\nADD\nSTOW r0\nPUSH 0\nOUTPUT r0\nHALT\n"
bytecode = Program.from_source(vm_src).bytecode()

vm = Vm()
vm.set_output_slots(1)
vm.run(bytecode)
assert vm.outputs() == [12]

Ways to Use XQuad covers when to reach for it directly instead of Session.

xquad.vm.VM is a separate wrapper that every example runner under examples/ actually uses (examples/maxcut/runner.py, examples/knapsack/runner.py). It wraps xqffi.vm.Vm and runs .xqasm source text directly, assembling it on each run(); run_bytecode() takes pre-assembled bytes instead:

from xquad.vm import VM

src = "PUSH 7\nPUSH 5\nADD\nSTOW r0\nPUSH 0\nOUTPUT r0\nHALT\n"
v = VM()
v.set_output_slots(1)
v.run(src)
assert v.outputs() == [12]
assert v.stack() == []

VM keeps calldata, output slots, and the step and memory limits across runs until reset(), but resets the machine before every run, so no stack or register state leaks from one run into the next. VM.outputs() returns the same XqmxModel / XqmxSample objects as xqffi.vm.Vm.outputs() and Session.run().outputs. Pick VM when you run .xqasm source from a script and want the settings to carry across runs; pick Session for the dict-keyed, slot-sparse ergonomics shown above; pick xqffi.vm.Vm only if you are building your own layer on top of the raw Rust FFI.

VM.set_calldata() stores a copy of the list it is given, holding the same objects, and checks nothing itself, because the values check themselves. An XqmxSample raises ValueError when it is built with a value outside its domain, and again when such a value is written to it, so no out-of-domain sample can reach run(). The element types are checked at run(): anything other than an int, a list[int], an XqmxModel, an XqmxSample or None raises TypeError there. A program that writes an out-of-domain value with SETLINE or ADDLINE faults with SampleOutOfDomain, an XqvmError.

A complete run across three programs

A compiled problem is three independent programs – see Three Programs for why. Driving all three from xquad.program looks like this, using the exact build_problem function examples/knapsack/runner.py defines (see Compiling for what compile() emits):

from examples.knapsack.runner import build_problem
from xquad.program import Program
from xqffi.vm import XqmxSample

n = 4
weights = [2, 3, 4, 5]
values = [3, 4, 5, 8]
capacity = 8

problem = build_problem(n, weights, values, capacity)
programs = problem.compile()

# Encoder: inputs in, model out.
encoder = Program.from_source(programs.encoder)
enc_session = encoder.session(output_slots=1)
enc_session.set_calldata([n, weights, values, capacity])
model = enc_session.run().outputs[0]

# A hand-picked sample -- items 1, 2, 3 selected, no solver involved.
sample = XqmxSample.binary(values=[0, 1, 1, 1, 0, 0, 0, 0])

# Verifier: the encoder's own inputs, then model and sample; (energy, valid) out.
verifier = Program.from_source(programs.verifier)
ver_session = verifier.session(output_slots=2)
ver_session.set_calldata([n, weights, values, capacity, model, sample])
ver_result = ver_session.run()
energy, valid = ver_result.outputs[0], ver_result.outputs[1]

# Decoder: sample + N in, decoded selection out.
decoder = Program.from_source(programs.decoder)
dec_session = decoder.session(output_slots=1)
dec_session.set_calldata([sample, n])
selected = dec_session.run().outputs[0]

assert (energy, valid, selected) == (-4817, 0, [0, 1, 1, 1])

Items 1, 2 and 3 weigh 3 + 4 + 5 = 12 against a capacity of 8, so the sample violates the capacity constraint and valid comes back 0. energy is still computed: ENERGY recomputes the objective whatever the sample’s feasibility, so the two outputs answer different questions. Swap in [1, 1, 0, 0, 0, 0, 0, 0] – items 0 and 1, weighing 5 – and the same run gives (-5507, 1). Verification says what valid covers.

The verifier takes the encoder’s calldata because it replays the encoder to rebuild the constraint data, then appends the model and the sample. Each program’s own .set_calldata order and .set_output_slots count are fixed by what it was compiled from – Compiling has the full table. A real pipeline replaces the hand-picked sample above with xqsa.build_solver(...).solve(model).sample; see Solving Overview.

What this page does not cover

  • Tracing. Step-by-step execution inspection is not part of Session today.
  • Keyword calldata (session.set_calldata(n=4, ...)). Session takes a positional list only; there is no input-slot labelling to key against.
  • A _repr_html_ for notebooks. __repr__ is the only representation Program, Session, RunResult, XqmxModel and XqmxSample provide.
  • A to_numpy() helper. Convert via linear_items() / quadratic_items() and numpy.asarray(...) on your own side; this package does not carry a numpy dependency.

Verification

xquad vm check rejects a program before it runs, printing the first structural or semantic problem the bytecode verifier finds. This page takes you from that message to a working program. Verifier is the reference half, covering what each phase checks and what every error means. This page does not repeat that; it shows each error actually firing and what to change.

xquad vm check --text file.xqasm assembles the source in memory and verifies the result; xquad vm check file.xqb verifies pre-assembled bytecode. Both forms and their exit codes are covered in CLI: check; this page assumes you already know how to invoke it.

Reading an error

Every example below is the smallest program that reproduces its error, followed by the exact xquad vm check output it produces.

Structural errors

BadOpcode – a byte in the instruction stream does not map to any of the 93 known instructions. This cannot come from a .xqasm file the assembler accepted; it means a .xqb file was hand-edited or corrupted:

Error: check failed: BadOpcode: unknown opcode 0x0d at byte 0x0000

Fix: reassemble from source rather than editing bytecode by hand, and check the .xqb file was not truncated or modified in transit.

TruncatedInstruction – the byte stream ends partway through an instruction’s operand bytes. A PUSH1 opcode byte with no value byte after it triggers it:

Error: check failed: TruncatedInstruction: truncated instruction at byte 0x0000

Fix: same as BadOpcode – the .xqb file is not a complete, valid encoding. Reassemble it.

Jump-target and loop-nesting errors

UndefinedJumpTarget – a jump instruction’s label id is at or past the program’s target count. Text assembly cannot produce this: the assembler resolves every .N label reference against the labels actually defined in the source and refuses to emit a jump to one that does not exist – xqasm::undefined_label, at assemble time, before the verifier ever runs. UndefinedJumpTarget is a defense for bytecode assembled some other way, or corrupted after assembly: a jump instruction’s label-id byte, changed to a value the program’s target count does not cover, produces:

Error: check failed: UndefinedJumpTarget: jump at byte 0x0003 references undefined target label 5 (program has 1 targets)

Fix: this is not something valid .xqasm can trigger, so seeing it means the .xqb file in hand is not what the assembler produced. Reassemble from source.

NoActiveLoop – NEXT, LVAL, or LIDX appears with no RANGE or ITER open at that point:

NEXT
HALT
Error: check failed: NoActiveLoop: loop instruction at byte 0x0000 executed outside any active loop

Fix: every NEXT, LVAL, and LIDX needs an enclosing RANGE/ITER on every path that reaches it – check you have not placed one outside the loop body, or past a jump that skips the loop opener.

UnmatchedLoop – a RANGE or ITER opens with no matching NEXT before the program ends:

PUSH 0
PUSH 3
RANGE
HALT
Error: check failed: UnmatchedLoop: unmatched loop: RANGE/ITER at byte 0x0004 has no corresponding NEXT (1 loop(s) still open at end of program)

Fix: add the missing NEXT. If this program came from xqcp, it usually means a with problem.range(...) block (see Control Flow) whose body raised or returned before the DSL closed it correctly – check the generated .xqasm for the loop in question.

Register errors

ReadUnsetRegister – a register is read before anything writes it, or before every incoming control-flow path writes it (must-init analysis, phase 3, catches the second case even when phase 2’s type check alone would not):

LOAD r0
HALT
Error: check failed: ReadUnsetRegister: register r0 read at byte 0x0000 before being written

Fix: write the register before reading it. If the read follows a branch, confirm both arms write it – a register set on only one side of a conditional is ReadUnsetRegister at the first read after the join, even though nothing about the type looked wrong on either arm individually. DROP also resets a register to unset; a LOAD right after DROP on the same register fails the same way.

RegisterTypeMismatch – a register holds a RegVal variant other than the one the instruction needs. Here BQMX writes a Model, and LOAD only handles Int:

PUSH 4
BQMX r0
LOAD r0
HALT
Error: check failed: RegisterTypeMismatch: register r0 at byte 0x0004: expected int, got model

Fix: LOAD is for scalar integers. To move a Model out of a register for inspection or as another instruction’s operand, pass the register directly to an instruction that accepts Model (GETLINE, ENERGY, and others); do not route it through LOAD.

Stack-depth errors

StackUnderflow – a reachable instruction would pop more items than the analysis can prove are on the stack:

ADD
HALT
Error: check failed: StackUnderflow: stack underflow at byte 0x0000

Fix: push the operands an instruction needs before it runs. This same check also catches loop-related shortfalls; see the “does not guarantee” section below for its blind spot.

StackOverflowRisk – the analysis converges on a depth past the 8,192-item limit for some reachable block. A straight-line run of 8,200 PUSH instructions with no loop involved:

Error: check failed: StackOverflowRisk: potential stack overflow at byte 0x0000 (depth 8200)

Fix: this fires on a single basic block’s static depth, so the usual cause is building up a large flat structure with individual PUSH/VECPUSH calls rather than a loop. Reducing the item count, or restructuring the build as a loop the analysis can bound per iteration, both work; see the next section for why a loop is not automatically safe either.

LoopStackImbalance – a loop body’s net stack effect is non-zero: the depth the analysis computes at NEXT differs from the depth at the matching RANGE/ITER. An unmatched PUSH inside the loop body:

PUSH 0
PUSH 3
RANGE
PUSH 99
NEXT
HALT
Error: check failed: LoopStackImbalance: loop starting at byte 0x0004 has non-zero stack effect (entry depth 0, exit depth 1)

Fix: every value a loop body pushes needs a matching pop (directly, or via STOW into a register) before NEXT, so each iteration leaves the stack exactly as it found it. SCLR inside a loop body is a special case: it resets the tracked depth to zero unconditionally, so this error fires when the loop’s entry depth was already non-zero, not when the body’s depth before the reset was non-zero. If the entry depth was zero, the same reset can instead mask a real imbalance earlier in the body, and the mismatch surfaces later, at the next downstream join point, rather than here.

StackDepthMismatch – two control-flow paths reach the same join point with different stack depths, so the depth at that point is not well-defined. A conditional that pushes on only one arm:

PUSH 1
JUMPI .0
PUSH 2
.0: HALT
Error: check failed: StackDepthMismatch: stack depth mismatch at join point byte 0x0006: one path has depth 0, another has depth 1

Fix: make every arm of a branch leave the same stack depth before it rejoins. If only one arm pushes a value, move that push above the branch so both arms inherit it.

What passing verification does not guarantee

A pass means every phase’s static checks succeeded. It does not mean the program runs to completion. Verifier: what passing verification guarantees states the precise scope. Treat a pass as “structurally sound,” not as “will run to completion.”

A whole class of faults is outside the verifier’s reach: they depend on values, and the verifier tracks types and depths rather than values. An allocator size, a grid extent, a loop bound, a calldata index and every arithmetic operand are ordinary popped stack values, so InvalidAllocation, InvalidGridDimensions, InvalidIntegerK, ArithmeticOverflow, IndexOutOfBounds, SampleOutOfDomain, LoopStackOverflow, StepLimitExceeded and MemoryLimitExceeded are all runtime faults with no static counterpart. SampleOutOfDomain is the clearest case of why: the value written into a sample can arrive from calldata, so no amount of static analysis can know it. A verified program can still raise any of them, and for an embedder that is the point: the budgets and the range checks are what bound a program the verifier has already passed. See Limits and Errors.

The generated verifier program is not the bytecode verifier

xquad vm check and the verifier program are two different things that share a name by coincidence of vocabulary.

xquad vm check runs the bytecode verifier described above and in Verifier: a static analysis over any .xqasm or .xqb file, checking that the program is well-formed before anything executes. It has no idea what problem, if any, the program encodes.

A compiled problem’s verifier program is one of the three .xqasm outputs problem.compile() returns – see Three Programs and Compiling. It is an ordinary program like any other: xquad vm check can check it is well-formed, the same as it checks the encoder or the decoder. What the verifier program itself does at runtime – taking a model and a candidate sample as calldata and producing (energy, valid) – is domain logic the DSL emitted, unrelated to the bytecode verifier’s job. Running the verifier program does not verify a program in the bytecode-verifier sense, and running xquad vm check against the verifier program’s .xqasm text does not check whether a sample is a good answer.

What the generated verifier’s valid flag covers

valid is the conjunction of one check per constraint the problem declared, plus a domain check over every declared variable. A sample that violates an onehot_row, onehot_col, equality, inequality, atleast, atleastw, exclude, implies, or the Rosenberg auxiliary a reduce() allocated, comes back valid = 0. Compiling shows how each check is emitted.

The checks are over the variables the problem declared, not over the encoding. slack() extends an equality’s index vector with the encoder’s own variables; the verifier checks that constraint as sum <= bound over the real variables and leaves the slack bits unconstrained. Checking the expanded form instead would report valid = 0 for a feasible sample whose slack bits a solver happened to leave inconsistent, which on a settlement path means refusing to pay for a correct answer.

Two things valid = 1 does not claim. The domain check runs to the model’s declared size, so slack and REDUCE auxiliaries allocated past that size are not domain-checked. And valid says nothing about optimality: ENERGY recomputes the objective independently of where the sample came from, and a feasible sample can still be a poor one.

Solving Overview

Backends names the five solvers and what distinguishes them. This chapter covers each one in enough depth to run it: installation, parameters, and what its result actually contains.

xqsa is the package that does this. It defines one base class, Solver, and four classes built on it – SolverCPU, SolverGPU, SolverDWave, and SolverQuip – which cover the five backends: SolverGPU runs on either a CUDA or a Metal GPU, picked by its engine. A model built from xqcp or by hand in XQVM bytecode does not know or care which one samples it.

The Solver Interface

Every backend is called through the same single method:

class Solver:
    def solve(self, model: XqmxModel, **kwargs: Any) -> SolverResult: ...

model is an XqmxModel over the binary or spin domain. **kwargs override that solver’s constructor defaults for this call only – solver = SolverCPU(num_reads=100) then solver.solve(model, num_reads=500) runs 500 reads without building a new solver. On every backend except quip, a keyword the solver does not take raises TypeError instead of being ignored, so a typo such as num_raeds=500 fails loudly. solve() raises on failure (hardware unreachable, no embedding found, connection lost); it never returns a partial or empty result.

solve() is the same code for every backend except quip: it validates the model, runs the backend, recomputes the energy and assembles the result. A backend only supplies the part that turns a model into a sample, so the result has the same shape whichever one ran.

The return value is a frozen dataclass:

@dataclass(frozen=True)
class SolverResult:
    sample: XqmxSample
    energy: int
    timing: float
    metadata: dict[str, Any] = field(default_factory=dict)

sample is the solution, an XqmxSample with one value per variable. energy is the authoritative Hamiltonian energy. timing is wall-clock seconds spent solving. metadata holds what the run used, covered below.

energy is never the backend’s own reported value taken on faith. Every solve() recomputes it with the same integer formula the XQVM ENERGY opcode uses, over the model and the returned sample. See Energy and Precision for what that buys you and what it costs on hardware with limited precision.

metadata has exactly three keys on cpu, cuda, metal, and dwave:

KeyHolds
seedThe seed the run used; None when unseeded, and always None on dwave (physical hardware has no seed)
readsHow many samples the backend took
paramsEverything else: the selectors that chose the backend (algorithm, engine, mode), the parameters the run used, and diagnostics such as the raw pre-recompute energy
result = SolverCPU(seed=42).solve(model)
result.metadata["params"]["algorithm"]
# 'sa'

Backend-specific detail lives under params, so code that reads seed and reads works on any of the four; see D-Wave QPU for what dwave puts there. quip is the exception: it returns a different set of keys entirely, with no seed, reads, or params; see Quip Network.

Picking a Backend by Name

build_solver constructs any of the five from a short string, so a caller – an example runner, a CLI flag, a script – can stay backend-agnostic:

from xqsa import SOLVERS, DEFAULT_SOLVER, build_solver

print(sorted(SOLVERS))
# ['cpu', 'cuda', 'dwave', 'metal', 'quip']
print(DEFAULT_SOLVER)
# cpu

solver = build_solver("cpu", seed=42)
result = solver.solve(model)

seed reaches the three classical simulated-annealing backends (cpu, cuda, metal) and is ignored for dwave (physical hardware has no seed) and quip (configured from the environment; the miner’s own randomness is out of the caller’s control).

The registry otherwise uses each backend’s own constructor defaults, with one exception: its cuda and metal entries raise num_reads to 200 and num_sweeps to 2000, rather than the 100 and 1000 the bare constructor defaults to. They build SolverGPU(engine="cuda", num_reads=200, num_sweeps=2000) and the same with engine="metal". Local Solvers has the constructor defaults for every backend.

Swapping Backends Without Changing the Model

This is the claim the whole toolchain rests on, so here it is checked rather than asserted. examples/maxcut/runner.py builds one Max-Cut model with xqcp, runs the encoder on the Rust XQVM, and samples the result with whichever solver --solver names, built through build_solver. Run it against two different backends – cpu (CPU simulated annealing, 100 reads and 1000 sweeps) and metal (an Apple Silicon GPU kernel, a different process on different hardware, and – per the registry’s override above – 200 reads and 2000 sweeps) – and nothing about the model or the encoder changes:

uv run python examples/maxcut/runner.py --n 6 --seed 42 --solver cpu
uv run python examples/maxcut/runner.py --n 6 --seed 42 --solver metal

Both print the same result: "energy": -571, "cut_weight": 571, "valid": 1, the identical partition. The encoder produced one QUBO; two unrelated solvers minimised it and agreed. That agreement is not guaranteed in general – a harder model can leave different backends in different local optima – but the model they were handed, and the verifier that checked what came back, never changed.

The Domain Every Backend Rejects

XQVM has three domains: binary, spin, and integer. Every current xqsa solver’s _validate_model() accepts binary and spin and raises ValueError on integer:

from xqsa import SolverCPU
from xquad.types import XqmxModel

model = XqmxModel.integer(size=3, k=4)
SolverCPU().solve(model)
# ValueError: Unsupported domain for solving: integer

An integer XqmxModel is a real thing you can build in XQVM bytecode today – see Three Domains – but nothing in this chapter can solve one. spec/xqsa/DOMAINS.md calls this “reserved”: a future solver may relax the check, but none of the current five does. Re-encoding the problem in binary or spin is the only route around it today.

Choosing Among the Rest

You wantRead
A baseline that runs anywhere, no hardware or credentials requiredLocal Solvers, cpu
A local GPU for a bigger or faster runLocal Solvers, cuda / metal
Real quantum annealing hardwareD-Wave QPU
A decentralised, miner-solved compute marketQuip Network
What the numbers in SolverResult actually meanEnergy and Precision

That baseline requirement – runs anywhere, needs nothing – is exactly DEFAULT_SOLVER: every other backend is worth comparing against cpu before you trust its answer.

Local Solvers

Three of the five backends run entirely on the machine calling them, using only local hardware and no account. cpu runs on the CPU; cuda and metal run custom simulated-annealing kernels on an NVIDIA or Apple Silicon GPU. The metal examples below ran for real, on an Apple Silicon Mac; no NVIDIA GPU was available while writing this chapter, so the cuda material comes from reading xqsa/inprocess/cuda.py rather than from execution.

SolverCPU: The Baseline

SolverCPU wraps dwave-samplers’ SimulatedAnnealingSampler. It needs only a CPU and no credentials, and that is exactly what makes cpu the xqsa.DEFAULT_SOLVER: every other backend in this chapter is worth comparing against it before you trust a faster or more exotic answer. It ships in the base package – pip install xqsa is enough.

from xqsa import SolverCPU
from xquad.types import XqmxModel

model = XqmxModel.binary(size=4)
model.set_linear(0, -1)
model.set_quad(0, 1, 2)

solver = SolverCPU(seed=42)
result = solver.solve(model)
print(result.energy, result.metadata)

Constructor parameters, all overridable per call via solve(**kwargs):

ParameterDefaultMeaning
num_reads100Independent annealing runs; the best is kept
num_sweeps1000Sweeps per run
num_sweeps_per_beta1Sweeps held at each temperature level before cooling; num_sweeps must divide evenly
beta_rangeNoneInverse-temperature (start, end); None lets dwave-samplers pick
seedNoneRNG seed; None means non-deterministic

SolverCPU also takes algorithm, which is "sa" and has no other value yet. A misspelled parameter raises TypeError, whether you pass it to the constructor or to solve().

GPU Backends: SolverGPU

SolverGPU bypasses the D-Wave SDK entirely. It converts the model to a dense array and runs parallel-replica simulated annealing directly on the GPU, in custom CUDA or Metal kernels. engine picks which, and it is required: SolverGPU(engine="cuda") or SolverGPU(engine="metal"), which are what the cuda and metal names build. Picking between the CPU backend and either GPU engine is a performance question, not a correctness one – see Backends; all three run the identical annealing algorithm family, just on different processors.

Install the extra that matches your hardware:

pip install xqsa[cuda]     # NVIDIA GPU, CUDA 12.x driver
pip install xqsa[metal]    # Apple Silicon Mac with a Metal device
from xqsa import SolverGPU
from xquad.types import XqmxModel

model = XqmxModel.binary(size=4)
model.set_linear(0, -1)
model.set_quad(0, 1, 2)

solver = SolverGPU(engine="metal", algorithm="sa", num_reads=200, num_sweeps=2000, seed=42)
result = solver.solve(model)
print(result.energy)
# -1

Wall-clock result.timing is not shown here: it varies by roughly 2x between runs on identical hardware, so a single pasted number would read as a performance claim it cannot support. The Metal engine also supports algorithm="gibbs" (block Gibbs sampling over a greedy graph coloring of the coupling graph). Both algorithms produce correct output on real Metal hardware: xqsa/tests/test_gpu_validation.py checks each against models with a known exact ground state, plus a spin-glass model matched within 2% of the CPU reference energy, all six of which pass.

The CUDA engine supports only algorithm="sa" – CUDA has no Gibbs kernel. Any other algorithm raises ValueError saying it is not available in-process; further algorithms arrive with the solver binaries. See xqsa/inprocess/cuda.py for the implementation the rest of this section describes.

Parameterengine="cuda" defaultengine="metal" defaultMeaning
algorithm"sa" (only option)"sa"; also "gibbs"Kernel to dispatch; constructor-only
num_reads100100Parallel replicas, one per GPU threadgroup/block
num_sweeps10001000Sweeps per replica
num_sweeps_per_beta11Sweeps held per temperature level before cooling
beta_schedule_type"linear""geometric"Shape of the cooling schedule – note the two engines default to different shapes
beta_rangeNoneNoneInverse-temperature (start, end); None auto-computes it from the model’s coefficients
seedNoneNoneBase seed for the on-device RNG

Every parameter except algorithm is overridable per call via solve(**kwargs). These are the constructor’s own defaults. xqsa.build_solver, the backend-agnostic constructor covered in Solving Overview, raises num_reads to 200 and num_sweeps to 2000 for both GPU engines instead of using these defaults.

The Metal engine computes coefficients in float32; the CUDA engine computes them in float64, end to end in its kernels. result.energy is always recomputed afterward in exact integer arithmetic on both, so either GPU run and a CPU run report the same authoritative energy regardless of which precision the search itself used. See Energy and Precision for what the float32 downcast costs the Metal engine and when it matters.

Driver Prerequisites

ExtraHardwarePrerequisiteVerify
[cuda]NVIDIA GPUCUDA 12.x drivernvidia-smi
[metal]Apple SiliconA Metal devicebuilt in on every Apple Silicon Mac

After installing, confirm the solver class constructs before building a model around it:

python -c "from xqsa import SolverGPU; SolverGPU(engine='cuda'); print('ok')"
python -c "from xqsa import SolverGPU; SolverGPU(engine='metal'); print('ok')"

Both raise ImportError with a pip install xqsa[...] hint if the extra is missing, and RuntimeError if the extra is installed but no matching GPU is detected – import xqsa itself never fails on a missing extra, only constructing the solver that needs it does.

D-Wave QPU

SolverDWave submits the model to a physical D-Wave Advantage quantum annealer over the D-Wave Leap cloud API. This page is derived from xqsa/inprocess/dwave.py and spec/xqsa/*; no D-Wave Leap account was available while writing it, so nothing on this page was run.

Credentials

Requires the [dwave] extra:

pip install xqsa[dwave]

SolverDWave() resolves an API token from the token= constructor argument first, then the DWAVE_API_TOKEN environment variable; with neither set, construction raises ValueError. An optional endpoint= or DWAVE_API_ENDPOINT overrides the default Leap API URL.

import os
os.environ["DWAVE_API_TOKEN"] = "your-leap-token"

from xqsa import SolverDWave
from xquad.types import XqmxModel

model = XqmxModel.binary(4)
model.set_linear(0, -1)
model.set_quad(0, 1, 2)

solver = SolverDWave()                           # auto-selects a Pegasus-topology Advantage system
result = solver.solve(model)
print(result.metadata["params"]["solver"])       # e.g. "Advantage_system5.4"
print(result.metadata["params"]["qpu_timing"])   # QPU timing breakdown from Leap

Pass solver="Advantage_system5.4" to target a specific system instead of the default Pegasus-topology filter. SolverDWave also takes mode, which is "qpu" and has no other value yet; hybrid modes are not available.

What Embedding Means

A D-Wave Advantage chip is not fully connected: each physical qubit couples only to a fixed, small set of neighbours defined by its Pegasus topology. Your model’s coupling graph is almost never a subgraph of that hardware graph directly. SolverDWave wraps the sampler in dwave.system.EmbeddingComposite, which finds a minor embedding: each logical variable maps to a chain of one or more physical qubits, wired together so the chain acts as a single variable by strongly coupling its members (chain_strength).

Problem size on real hardware is not simply “how many variables.” A densely coupled model needs longer chains to embed, chains compete for the chip’s limited qubits, and a model that does not embed at all raises rather than silently degrading. EmbeddingComposite handles the search automatically; there is no separate embedding step to call in xqsa. Contrast this with Quip Network, where SolverQuip requires an exact subgraph match onto a fixed topology and refuses to do this chain-based embedding at all.

Parameters

ParameterConstructor defaultMeaning
tokenNone (env fallback)Leap API token
endpointNone (env fallback)Leap API endpoint override
solverNone (Pegasus filter)Specific Advantage system name
num_reads100Annealing runs read back from the chip
annealing_time20Microseconds per anneal
chain_strengthNone (not passed)Coupling that holds each chain together
auto_scaleNone (not passed)Whether Ocean rescales coefficients to the chip’s range

Everything except token, endpoint, and solver is overridable per call: solver.solve(model, annealing_time=100, chain_strength=2.0). chain_strength and auto_scale are the only Ocean sampling parameters xqsa forwards to the sampler, and only when set. Any other keyword raises TypeError instead of reaching the sampler unchecked.

How a QPU Result Differs

The result shape is the same SolverResult every backend returns, but two things are specific to this backend. result.energy is still recomputed by xqsa in exact integer arithmetic from the returned sample – the chip’s own reported energy is never trusted directly – but the sample search itself ran on physical hardware subject to analog noise and the embedding above, not an exact digital simulation. And result.metadata["params"] carries QPU-specific detail that the local backends do not report at all.

metadata has the same three keys as every backend except quip:

KeyOn this backend
seedAlways None; there is no seed on physical hardware
readsnum_reads for the call
paramsmode, solver, qpu_timing, annealing_time, raw_energy, plus chain_strength and auto_scale when forwarded

params["solver"] names the physical Advantage system that ran the job. params["qpu_timing"] is the timing breakdown Leap returns – a dict when present, with keys that come from Leap and are not fixed by xqsa, or None when the sampler response carries no timing info at all. params["raw_energy"] is the chip’s own reported energy, informational only.

result.timing covers the whole call, including converting the model to a BQM and the sample back, not only the round trip to Leap. See Solving Overview for the shape every backend shares.

Quip Network

SolverQuip is the one backend in this chapter that does not solve anything itself. It proposes the model as a job to the Quip Network’s QuantumComputeMempool pallet, waits for a miner – hardware it does not control, running an algorithm it does not choose – to solve it, and decodes whatever comes back as an XQMX sample.

This page is derived from xqsa/quip/, spec/xqsa/SOLVERS.md, and an internal contributor testing guide. The mechanism it describes has been exercised end to end against aglais, the public Quip test network. Your First Quip Job walks one job through it; Gaps lists what the figures there cannot promise.

Installation and Configuration

Requires the [quip] extra:

pip install "xquad[quip]"

The umbrella xquad package forwards [quip] to the matching xqsa extra, as it does [cuda], [dwave], and [metal]; with xqsa alone, pip install "xqsa[quip]" is the same install. The extra brings in substrate-interface (the chain RPC client), certifi (the CA bundle wss:// falls back to on a macOS Python with none), and quip-signer, a native extension providing the chain’s hybrid signature scheme (sr25519 plus FN-DSA-512), which substrate-interface alone cannot produce. quip-signer resolves a prebuilt wheel on Linux and builds from source with a local Rust toolchain on macOS and Windows. Both guards are lazy: import xqsa and every other solver in this chapter work without the extra installed; only constructing SolverQuip needs it.

Both the chain RPC connection over wss:// and the faucet request verify the certificate chain, host name and validity dates, and by default neither checks revocation (no CRL, no OCSP). To check revocation lists, pass an ssl.SSLContext as ssl_context to SolverQuip, SolverQuip.for_network or fund_from_faucet. Both clients then use it in place of the default CA-bundle context, reconnects and autofund included; it is ignored for ws:// and http:// URLs, and SolverQuip refuses it with an https:// RPC URL, whose transport cannot use it. The context replaces the default entirely, so it must carry its own trusted roots: on a macOS Python whose trust store is empty, load certifi’s bundle as below. A standard-library context checks only revocation lists you fetch and load yourself. With VERIFY_CRL_CHECK_LEAF, as below, it needs the list from each server certificate’s issuer; with VERIFY_CRL_CHECK_CHAIN, one for every certificate in each chain. One context serves both servers, so with autofund on, load the lists for the faucet’s issuer as well as the RPC node’s. A handshake missing a list it needs fails.

import ssl
import certifi
from xqsa.quip import SolverQuip

ctx = ssl.create_default_context(cafile=certifi.where())
ctx.load_verify_locations(cafile="issuer-crl.pem")  # a PEM CRL you fetched
ctx.verify_flags |= ssl.VERIFY_CRL_CHECK_LEAF
solver = SolverQuip.for_network("aglais", ssl_context=ctx)

A standard-library context cannot check OCSP, which needs a different TLS stack that fails closed when the check does not finish. Code that opens its own chain connection with connect in xqsa.quip.metadata passes a context through ws_options["sslopt"]["context"] instead.

Configuration resolves from constructor arguments first, then environment variables:

ArgumentEnvironment variableMeaning
urlQUIP_RPC_URLWebsocket RPC endpoint
seedQUIP_SIGNER_SEED32-byte hex master seed
keystoreQUIP_KEYSTOREKeystore file path (loaded, or created on first use). Unset, the account preference or ~/.quip/keystore.json applies; see Accounts
rewardQUIP_REWARDReward in planck; falls back to the chain’s MinReward
faucetQUIP_FAUCET_URLFaucet base URL used to top up a short account when autofund allows it. The variable is read only when the RPC URL also comes from QUIP_RPC_URL, so an explicit url never picks up a faucet for another chain
autoconfirmQUIP_AUTOCONFIRMWhether to submit without asking. True (the default) submits; False asks on the terminal (stdin, and stderr or stdout) and raises QuipCancelledError without one, a notebook included; a callable judges the job’s JobQuote. The variable takes 1/true/yes/on or 0/false/no/off
accessnoneWho may answer: "Open" or AccessOpen() (the default), or AccessAllow(accounts=[...], types=[...])
resolutionnoneThe payout rule: "SingleBest" (the default), ResolutionSingleBest(), ResolutionTopNEqual(n) or ResolutionTopNWeighted(n)
deliverynoneHow results are delivered; only "OnChainOnly" (the default) is accepted
deadline_blocksnoneBlocks an order stays open; defaults to 100
block_waitnoneBlocks an order keeps accepting answers after the first one arrives; defaults to 10
autofundQUIP_AUTOFUNDWhether to draw one drip from faucet when the account cannot cover the quoted job; a shortfall larger than one drip, or an account already holding more than one drip, raises instead. Same forms and default as autoconfirm
inclusion_window_blocksnoneBlocks a signed proposal stays valid for, from the finalized block it was signed at: 64 to 2048, rounded up to a power of two. Defaults to 64, DEFAULT_INCLUSION_WINDOW_BLOCKS. A client setting, not an order option; see Settling a send
ssl_contextnoneAn ssl.SSLContext used for both the wss:// RPC connection and the faucet request in place of the default CA-bundle context; ignored for ws:// and http:// URLs, refused with an https:// RPC URL. See the revocation note above

The signer comes from the first of these that is set: seed, keystore, QUIP_SIGNER_SEED, QUIP_KEYSTORE, the project account preference, the user account preference, then ~/.quip/keystore.json, created on first use. An explicit argument wins over the environment, so a keystore argument wins over an exported QUIP_SIGNER_SEED. spec_id and topology both default to chain state (DefaultIsingSpecId and DefaultTopology). spec_id is a constructor-only override with no environment variable. topology also reads QUIP_TOPOLOGY, accepts "native", and can be overridden per order; see The Topology Constraint.

The order options among these (reward, access, resolution, deadline_blocks, block_wait and topology) are client defaults. Every order create_order() builds inherits them, and create_order() or JobOrder.set() override them for that order. The quality floors (max_energy, min_solutions and min_diversity) exist per order only, never as client defaults. Job Orders covers each option, including the payout rules and access rules. delivery accepts only "OnChainOnly": the client has no subscriber for the ResultReady event the pallet’s Callback delivery emits when the order expires.

Before connecting, the constructor raises ValueError for a delivery other than "OnChainOnly", and for an access or resolution that is not one of the rules above. After connecting, it checks the default access against the chain’s allowlist limits. The remaining options are checked against the chain’s limits when an order is created.

Accounts

A keystore is an account: the file holds its 32-byte master seed in plain text, so treat it as the account itself and keep it out of version control. Each account level holds keystore files and an account preference:

LevelKeystore filesPreference
Project.quip/keystores/<name>.json.quip/config.toml
User~/.quip/keystores/<name>.json, plus ~/.quip/keystore.json as the account default~/.quip/config.toml

The project level is the nearest .quip/ directory found walking up from the working directory, stopping below the home directory, so ~/.quip/ is never a project. A .quip/ owned by another user or writable by everyone is skipped, so a shared directory such as /tmp cannot choose the signer. A preference is the single line account = "<name>". A project preference may name a project or a user keystore, and a name present at both levels resolves to the project one; a user preference applies in every directory, so it names a user keystore. Account names are lowercase letters, digits, _ and -, at most 64 characters, starting with a letter or digit.

xqsa.quip.accounts manages them. Each call takes the level explicitly:

from xqsa.quip import create_account, resolve_accounts, use_account

create_account("work", level="user")
use_account("work", level="project")
print(resolve_accounts().in_effect)

create_account writes a keystore from a fresh seed, or from seed=, and refuses to replace one unless overwrite=True; with no name it writes default. At project level with no .quip/ found, it makes one in the working directory, and anything created under a project .quip/ also writes a .quip/.gitignore of * when there is none. use_account and clear_account set and remove a preference. resolve_accounts lists every keystore with its account and walks the signer order above, reporting the step in effect. Each step is a SignerSource, whose account_id is the 32-byte account of the keystore it points at, read from the account the keystore caches, so no seed is loaded; it is None for the seed steps and for a missing or unusable keystore. resolve_accounts never writes a file or reads a seed, so an unusable keystore or a broken preference is listed with the problem rather than raised. SolverQuip raises QuipSigningError only when such a step is the one it would sign with.

For reads that need no signer, pass read_only=True:

solver = SolverQuip.for_network("aglais", read_only=True)
print(solver.balance(account=address))  # SS58 text, 0x hex or bytes

A read-only client resolves no signer, so it never creates a keystore, and refuses seed and keystore. get_receipt, and list_orders, balance, reserved and order_slots given account=, work on it. Everything that signs, account, and those four without an account raise QuipSigningError.

A signing client reads its own account the same way:

solver = SolverQuip.for_network("aglais")
print(solver.account)
print(solver.balance(), solver.reserved(), solver.order_slots())
5FXRydTJYE4cPGBH9CSf3dyGvaJNnYATEZJMyFqMiLPHv9oc
0 0 (0, 32)

account is the signer’s account in SS58. balance() is the free balance in planck, which an order’s reward and fee are paid from. reserved() is the reserved balance: the chain reserves an order’s reward when it is proposed and releases it when a claim or a reclaim closes the order, and it includes anything else the runtime reserves. order_slots() returns (used, limit): the orders not yet closed, each holding a slot until a claim or a reclaim closes it, and the runtime’s MaxOrdersPerProposer, None when the runtime sets no cap. Each takes account= to read another account. xquad quip account show prints all four; see Accounts. (Captured on 2026-10-26 with a fresh, unfunded account.)

Named networks

SolverQuip.for_network(name, **kwargs) builds a solver from a named preset in xqsa.quip.networks instead of an explicit url:

SolverQuip.for_network("aglais")

Two presets are registered: aglais, the public test network, and devnet, the local Docker stack contributors run for offline testing. The RPC and faucet coordinates behind each preset live in xqsa.quip.networks, move with the deployment, and are corrected in patch releases. The test network’s own page, aglais.quip.network, publishes the current endpoints and the faucet; check the preset against it if a connection fails. The preset also names its network’s faucet, if it has one, which the solver uses unless faucet= names another; QUIP_FAUCET_URL does not override a preset.

A preset may also pin its network’s genesis hash and may have no faucet. With a pinned genesis, for_network() refuses a node that serves another chain with a QuipConnectionError naming both hashes. With no faucet, an account that cannot cover a quote is refused with a QuipSubmissionError that says <network> has no faucet; fund the account another way. Neither current preset does either: both name a faucet, and both leave the genesis open, because a test network relaunches with a new genesis. The Quip MainNet preset will pin its genesis and have no faucet.

Runtime compatibility

SolverQuip reads the runtime’s specVersion once, when it connects; a client that stays connected across an upgrade keeps the version it read until it reconnects. Runtime 119 renamed the order’s energy-ceiling field from min_energy_milli to max_energy_milli. xquad 0.4.5 and later send the ceiling as max_energy_milli only, so they need runtime 119 or later: on an older runtime the proposal fails to encode. xquad 0.4.2 and earlier cannot propose on 119.

ResolutionTopNWeighted needs runtime 119 or later. On an older runtime every order that uses it is refused before signing, because older runtimes weight the split by absolute energy, so a worse answer can take the larger share.

Moving a network to runtime 119 starts it afresh instead of upgrading it in place. Orders placed before the move do not carry over, so an order id recorded before it does not name the same order after it. aglais has run runtime 119 since its relaunch on 2026-10-15.

The signed-extension order comes from the chain’s metadata, not from a list in the client. At its first connect, SolverQuip checks that it can encode the extrinsic envelope and every signed extension the runtime declares, and every signature is checked against the runtime the same way. A runtime it cannot read or sign for raises QuipMetadataError: at connect, or at signing when the runtime changed under a live client, before anything is sent. The remedy is upgrading xquad; retrying the same node gives the same answer.

Every reconnect must reach the chain the first connection reached: a node that serves another genesis is refused with a QuipConnectionError naming both hashes. A reconnect checks the genesis only, and fails with QuipMetadataError only for a node whose metadata this client cannot read. A submit() that is settling a send stops at once on either fault and leaves the order sending (see Settling a send).

Each connection logs the network, runtime and genesis at INFO on the xqsa.quip logger, with reconnected to in place of connected to on a reconnect (captured on 2026-10-26):

connected to Quip aglais at wss://bootnode-1.aglais.quip.network:20049/rpc: specVersion 119, transactionVersion 7, genesis 0x542a9915acf840b8940d35ca6de242f669a0c6e3b40f9a8d8c14b9bf7b8ebecb

Job Lifecycle

Submitting an order calls propose_job, which reserves the full reward and emits a JobProposed event. Miners discover work only from that live event stream – there is no storage backfill, so a job proposed before a miner subscribed stays invisible to it. A miner that accepts solves it on its own hardware with its own algorithm and submits a solution.

The chain marks an order Expired at its effective expiry, or a few blocks later when many orders expire at once, and only a claim or a reclaim closes it. SolverQuip computes finality itself from block height instead of waiting for an OrderClosed event:

effective_expiry = min(created_at + deadline_blocks, first_solution_at + block_wait)

JobOrder.wait() and solve() poll until the chain head reaches that height. solve(model, **options) is create_order(model, **options).submit().wait() followed by decoding the answer into a SolverResult (see Metadata); unlike create_order(), it warns about an unknown option and drops it instead of raising. The order surface – drafts, quotes, receipts, reclaiming, and reading answers – is on Job Orders.

Only solve(), and the deprecated query(), reclaim on their own. When an order finalizes with zero solutions, they reclaim the reserved reward through reclaim_order and raise QuipJobFailedError, whose message says whether the reward was reclaimed or why it was not. JobOrder.wait(), JobOrder.best() and a receipt’s wait() never reclaim; call reclaim() on the order or its receipt.

The Topology Constraint

Real annealing hardware – and the miner fleet behind this network – has a fixed set of physical couplings. SolverQuip skips the chains and minor embedding that D-Wave QPU’s EmbeddingComposite builds, and instead requires subgraph placement: an injective variable-to-node mapping where every one of the model’s couplings maps onto a real hardware edge. A model whose coupling graph is not a subgraph of the target topology raises PlacementError, carrying the couplings that could not be placed. Placement is deterministic, computed from the model alone, so nothing about it needs to be persisted or looked up later.

The target topology is resolved once, at construction, from three sources in order: the topology= argument, the QUIP_TOPOLOGY environment variable, then the chain’s QuantumPow.DefaultTopology. Because the same topology shape hashes differently per deployment – each network’s allowed-value specifications fold into the hash – a hash from one Quip deployment is not portable to another. That is why nothing is pinned in the codebase: every source is deployment-local, and a topology that resolves from none of them raises rather than selecting a hash no chain would accept. QUIP_TOPOLOGY is the operator’s override for whichever chain they are pointed at, and targets a registered non-default topology without threading a constructor argument through. solve() does not check the resolved hash against QuantumPow.MineableTopologies. That set is the chain’s active mining set: it gates submit_proof, and so block production, not the compute mempool. An order carries its nodes, edges and coefficients inline and no topology hash at all, so the chain cannot perceive which topology an order was built against, and the solver fleet has no topology field to filter on. A topology that is registered but not mineable is proposed, matched and solved like any other.

Native topology mode

Passing topology="native" builds the topology from the model’s own variables and couplings instead of reading one from the chain. It is accepted on the constructor, as an order option on create_order() and JobOrder.set(), per call on solve() and the deprecated quote() and query(), and through QUIP_TOPOLOGY=native, matched without regard to case or surrounding whitespace:

result = solver.solve(model, topology="native")

The variables, after any BINARY-to-SPIN fold, are relabelled densely to 0..n-1, so placement is the identity and cannot raise PlacementError. No chain topology is read, and the native topology carries no allowed-value sets, so the allowed-value warning stays silent; the mempool never enforces those sets. mapping= has no hardware graph to target in this mode and raises ValueError. A per-call value overrides the constructor’s in either direction. A native order is as large as the model, so one over the mempool’s MaxNodes (5,000) or MaxEdges (50,000) raises EncodingError before it is quoted. The solver reads both bounds from the chain for each native order; a failed read raises QuipConnectionError.

Native mode narrows who can answer. A miner that cannot embed an arbitrary graph, such as a QPU-backed one, cannot solve these orders; the simulated-annealing miner fleet can. Native mode is never chosen automatically on PlacementError: a call that raises today would otherwise start proposing jobs and spending funds.

Coefficient Encoding

The normative rules for this section are Coefficient encoding and allowed values – the same URL EncodingError, the rounding warning, and the allowed-value warning below all carry.

XQMX coefficients are natural-scale integers. The pallet stores couplings and fields as milli-scale i32 values, read back on-chain as value / 1000. Confirmed directly from xqsa/quip/codec.py:

from xqsa.quip.codec import MILLI_SCALE, MAX_NATURAL_COEFFICIENT
print(MILLI_SCALE, MAX_NATURAL_COEFFICIENT)
# 1000 2147483

SolverQuip computes spin coefficients from the model, multiplies each by MILLI_SCALE, and rounds to the nearest integer milli value, ties to even. A coefficient finer than 1/1000 is therefore rounded, not refused. Each rounding moves a coefficient by at most 0.0005. SolverQuip reports the largest such error once per solver instance as a warnings.warn when the model is placed (by create_order(), a set() that places it again, or solve()), and on every result as metadata["quantization_error"]. A coefficient whose rounded milli value would overflow i32 still raises EncodingError. An energy ceiling (max_energy) on a rounded model warns as well, because the chain applies the ceiling to the rounded model; Energy and Precision explains the ceiling’s units.

Rounding changes only the model the miners search. result.energy is recomputed on your original model, so a returned sample is never scored against the rounded coefficients. The winning submission is still chosen by the chain’s energy on the rounded model; only the vectors inside it are re-scored on yours. quantization_error bounds each coefficient, not the energy: a sample’s energy can move by up to quantization_error times the number of nonzero terms. A model whose coefficients all round to zero raises EncodingError rather than proposing an empty order. A BINARY model with ordinary three-decimal weights is the common case: the basis change below divides each quadratic weight by 4, so 1.5 on x_0 and 0.001 on x_0 x_1 give a spin field of 0.75025, which encodes as 750.

MAX_NATURAL_COEFFICIENT is the exact ceiling only for a SPIN model, whose h/j coefficients pass through to the pallet unchanged. A BINARY model goes through a basis change first (s = 2x - 1, via dimod), which folds every quadratic coefficient into the linear field of both variables it touches: j_ij = b_ij / 4, and h_i = a_i / 2 + 1/4 * (sum of every quadratic coefficient incident on i). _to_milli() only ever sees these post-conversion values, so for a BINARY model the natural-scale ceiling moves in both directions from what the constant alone suggests:

  • An isolated coefficient survives past MAX_NATURAL_COEFFICIENT: an isolated quadratic term up to about 8_589_934 (roughly 4x), an isolated linear term up to about 4_294_967 (roughly 2x).
  • A high-degree variable can overflow well below it, because h_i aggregates a quarter of every incident quadratic term. Ten quadratic terms of 1_000_000 all touching variable 0 give h_0 = 2_500_000 – nowhere near MAX_NATURAL_COEFFICIENT – but its milli value, 2_500_000_000, exceeds i32 and raises EncodingError.

The error in that case reads h[0] = 2500000.0 overflows the encodable range, naming a linear field the caller never set directly; the actual cause is the aggregate of ten quadratic terms landing on variable 0 during the basis change. This is a range shift, not a precision loss – quarters and halves of integers are exact in floating point, and exact at 1/1000 milli scale too (0.25 becomes 250) – the stored value is correct, only the range it has to fit in moved.

Rescaling a model does not change its optimum (see Choosing a Penalty Weight), so an oversized coefficient is fixable by scaling the whole model down, not a hard ceiling on what you can express.

Each topology also declares an allowed-value set for its coefficients (the default plain-Ising spec allows {-1, 0, +1} for fields and {-1, +1} for couplings), but the pallet does not enforce it, so SolverQuip submits coefficients as-is. A miner matches jobs by an exact hash over the topology’s structure, not over the coefficient values, and real hardware rescales monotonically in any case. An out-of-spec coefficient produces a one-time warnings.warn, not a rejection.

Cost Model

A job costs two amounts, both in planck, the chain’s smallest denomination:

  • The reward: the order’s reward, else the client’s reward, else QUIP_REWARD, else the chain’s MinReward. A reward below MinReward raises QuipOrderOptionError before anything is signed. propose_job reserves the reward from the caller’s account. A solved order’s reward is allocated to its winning solvers on chain and paid when one of them claims it; a receipt’s settlement() shows the split. An order that finalizes with no solutions can have its reward reclaimed, and solve() reclaims it itself, so a failed job costs the fee, not the reward.
  • The transaction fee for propose_job, burned either way.

create_order(model).quote() prices an order without proposing it. It signs the propose_job extrinsic the order would submit, asks the chain for its fee through payment_queryInfo, and reads the account balance. Nothing is submitted and no nonce is consumed. submit() later sends the same signed extrinsic, re-signing only if the account’s nonce moved in between or half its inclusion window has passed (see Settling a send). quote() returns a JobQuote:

FieldMeaning
networkThe preset name, or None for an explicit url
accountThe paying account, in SS58 form
quoted_at_blockThe block the quote was read at
inclusion_window_blocksBlocks the signed proposal stays valid for; see Settling a send
num_variablesThe model’s variables
num_spins, num_couplingsThe spins and couplings the model was placed onto
placement"native", or the topology hash the model was placed on
reward_planckThe reward the job reserves
fee_planckThe transaction fee
fee_exactTrue when the fee is the chain’s answer; False when that query failed and a fixed headroom stands in for it
balance_planckThe account’s free balance
token_symbol, token_decimalsThe chain’s token, used to render amounts
total_planckreward_planck + fee_planck: what the account must hold
shortfall_planckHow far the balance falls short of the total, or 0
affordableWhether the balance covers the total

str(quote) renders a 72-column box: the network, account and block; the problem and where it was placed; the reward, fee and total in token units; then the balance, the shortfall and whether the account can afford the job. submit() and solve() build the same quote before proposing, log it at INFO on the xqsa.quip logger (also printing it to stderr when stderr is a terminal), and pass it to the autoconfirm and autofund gates. A field the solver could not read is left out of the box.

What each part of the price measures:

  • The fee is metered. It comes from benchmarked weights plus the encoded length, so it grows with the model’s nodes and edges. It is small next to the reward. Fees are burned, not paid to the block author, and do not respond to congestion: the runtime’s fee multiplier is constant.
  • The reward is arbitrary. MinReward is a round runtime constant that gates submissions. Nothing derives it from the cost of solving, yet it is nearly all of what a job costs.
  • Solver compute is not metered. The runtime declares type VM = NoOpVm, so nothing on-chain executes or measures the miner’s work, and that work never enters the price.
  • The token is subsidised. On aglais, AGLS comes free from the faucet and has no external price. autofund draws from the faucet for a short account, and fund_from_faucet(dest, url=...) requests one drip directly.

This page does not print a current figure, because every one of these amounts is live chain state; JobOrder.quote() reads it. For a dated figure from an observed job, see Your First Quip Job.

Metadata

SolverResult.metadata for quip carries nine keys, eight read from the order and one from the encoding:

KeyTypeMeaning
order_idintThe on-chain order id; get_receipt(order_id) rebuilds the order’s receipt in any session.
solverstrThe solver account whose vector is returned; always present.
best_energy_milliintThe leader’s ranked energy on chain, in milli units. It stays the leader’s even when a stored-energy mismatch makes solve() return another solver’s vector; only when the ranking cannot be read is it the chosen solver’s stored best.
energy_matches_chainboolWhether any stored vector of the chosen solver reaches the chain’s energy. False means a fallback returned a different vector, not necessarily an encoding fault.
leader_readboolWhether the chain’s ranking of the order was read.
leaderboolWhether the sample is the leader’s: the solver the chain pays.
num_submissionsintHow many solvers have a stored answer; a solver’s resubmission overwrites its earlier one.
num_solutionsintHow many vectors the chain stores for the returned sample’s solver.
quantization_errorfloatThe largest absolute error, in natural units, from rounding a spin coefficient to the milli grid. 0.0 when every coefficient was already on it; at most 0.0005.

best_energy_milli is milli-scale (see Coefficient Encoding) and is not directly comparable to result.energy, the authoritative natural-scale energy recomputed on your model; diffing the two without converting scale first will make a correct result look wrong.

metadata["solver"] here names the solver account that answered, not a piece of hardware – D-Wave QPU records the physical Advantage system that ran the job under metadata["params"]["solver"], so the same name refers to a different kind of thing depending on the backend.

solve() returns the leader’s best vector, the answer the chain pays for, rather than the best vector any solver stored. leader, leader_read and energy_matches_chain say whether that rule held or a fallback chose another vector; Reading the answer describes the rule and both fallbacks.

Failure Taxonomy

Sixteen exception classes cover this backend – one base class and fifteen concrete failures, spanning local encoding checks, order checks, the consent and funding gates, and network-dependent lifecycle failures. Fifteen are exported from xqsa.quip and xquad.sa.quip; the deprecated QuipUnconfirmedError still imports from both, with a warning, but import * does not bring it in:

ExceptionRaised when
QuipErrorBase class for every error below
EncodingErrorThe model is not an XqmxModel, its domain is unsupported, or it has no nonzero linear or quadratic term. Also covers a coefficient whose milli value overflows i32 (see Coefficient Encoding), and a native order over the mempool’s MaxNodes or MaxEdges
PlacementErrorThe model’s coupling graph is not a subgraph of the target topology. Raised in default mode only; see Native topology mode
QuipSigningErrorExtrinsic assembly, keystore handling, or submission fails
QuipConnectionErrorThe node is unreachable; a configured Ising spec or topology is not registered on-chain; a mempool constant cannot be read (MinReward, MaxDeadlineBlocks, MaxBlockWait, MaxSolutions and MaxBidMiners at construction, MaxNodes and MaxEdges per native order); the chain defines no MinReward and no reward was given; an order id names no order; a balance, nonce or block read fails; or the node serves another chain than the preset pins, named by both genesis hashes. A reconnect to another chain than the first connection reached raises it too, but that happens only while a send is settling, so it reaches the caller as the __cause__ of QuipSubmitIncompleteError. Most come from the constructor or for_network(), which read the limits once
QuipMetadataErrorThis client cannot read or sign for the node’s runtime: its metadata, an extrinsic envelope or signed extension it cannot encode, or blocks or events it cannot decode while settling a send. Raised at connect, on a reconnect whose metadata it cannot read, and when quote() or submit() signs; while a send is settling it is the __cause__ of QuipSubmitIncompleteError instead. The remedy is upgrading xquad, not retrying
QuipSubmissionErrorAn extrinsic cannot be submitted or the chain rejects it. Raised when propose_job fails on chain (the fee was paid, no order was placed, and the order moves to failed), and when the account’s nonce moves twice while submitting because another client is sending from the account. Also raised before proposing when the account cannot cover the quote and autofund cannot help, with a hint for each case: a named network with no faucet says to fund the account another way, a raw url= with none says how to name one, a shortfall larger than one drip suggests checking reward=, and a balance already above one drip is one the faucet will not top up. Also raised when a sent proposal certainly never reached the chain: the order stays a draft
QuipOrderOptionErrorA QuipSubmissionError: an order option fails a check before signing, such as a reward below MinReward, a deadline or block wait out of range, a bad payout or access rule, or a quality floor out of range. Carries the option’s name as option. Nothing is spent
QuipReclaimRefusedErrorA QuipSubmissionError: reclaim() refuses before signing because another account proposed the order, it is already closed, it is still open, or it was answered; or the chain refuses the reclaim and charges the fee
QuipSubmitIncompleteErrorA QuipSubmissionError: a sent propose_job could not be settled by the end of its window; or settling stopped at once, because its block or events could not be decoded, a reconnect reached another chain or a node whose metadata this client cannot read, or the node discarded the state it needs. The order is sending; calling submit() again resumes it and never places a second order. Carries order, extrinsic_hash, nonce and window_end, with the cause as __cause__; see Settling a send
QuipUnconfirmedErrorDeprecated, and no longer raised since v0.4.5; importing it from xqsa.quip warns. A QuipSubmissionError that carried extrinsic_hash and block_hash for a send whose outcome was unknown; QuipSubmitIncompleteError replaces it
QuipCancelledErrorThe autoconfirm or autofund gate declines the job, at the prompt, by callable, or for want of a terminal; carries the declined JobQuote as quote. Nothing has been proposed or funded
QuipFaucetErrorThe faucet refuses a drip or cannot be reached; carries the HTTP status (None for a transport failure) and the parsed JSON body
QuipTopologyErrorRetained for compatibility; nothing raises it. It reported a topology absent from MineableTopologies, which does not gate the mempool
QuipTimeoutErrorAn order does not reach finality before the configured timeout. Carries order_id and, when a wait raised it, the order’s receipt, so the caller can call exc.receipt.wait() again; another session rebuilds the receipt with get_receipt(order_id)
QuipJobFailedErrorA final order has no usable solution; carries order_id. From solve(), the reward of an unanswered order was reclaimed first, and the message says whether the reclaim succeeded; it also names any quality floors or allowlist that may explain why nobody answered. JobOrder.best() raises it without reclaiming

The EncodingError emptiness check tests whether linear_items() and quadratic_items() are both empty. Both list nonzero terms only – an XqmxModel drops a term the moment its coefficient returns to 0 – so “has no terms” and “carries only zero terms” are the same condition, and an all-zero model never reaches the chain.

EncodingError and PlacementError can be raised entirely locally, before anything touches the network – they come from xqsa.quip.codec, the same module the coefficient encoding above uses. The one exception is the native size check, which reads MaxNodes and MaxEdges from the chain first; a failed read raises QuipConnectionError. QuipOrderOptionError and QuipCancelledError are also raised before anything is signed or proposed. The rest depend on chain state or the round trip completing.

Deprecated APIs

Each of these warns with a DeprecationWarning and is removed in v0.6.0:

DeprecatedSinceUse insteadNotes
SolverQuip.quote(model, ...)v0.4.3solver.create_order(model, ...).quote()
SolverQuip.status(order_id)v0.4.3solver.get_receipt(order_id).status()The keys differ: state and chain_status replace status and is_final
SolverQuip.query(order_id, model, ...)v0.4.3solver.get_receipt(order_id) to follow an order; JobOrder.wait() and best() in the session that submitted itStill reclaims an unanswered order; see below
mode= on the constructor, for_network(), quote() and solve()v0.4.3access=
A raw chain dict for access or resolution on solve() or quote()v0.4.3AccessOpen(), AccessAllow(...), or a Resolution rule
An unknown keyword on solve() or quote(), including delivery= and spec_id=v0.4.3None; the option is dropped
xqsa.quip_codec, quip_signing, quip_metadata, quip_faucet, quip_networksv0.4.3xqsa.quip.codec, .signing, .metadata, .faucet, .networksWarns on the first import only
The 27 Quip names on xqsa and xquad.sa, such as xqsa.SolverQuipv0.4.3The same names on xqsa.quip or xquad.sa.quipThey left both __all__ lists, so from xqsa import * no longer brings them in
QuipUnconfirmedErrorv0.4.5QuipSubmitIncompleteErrorNo longer raised; warns on import from xqsa.quip
Loading a pickled unconfirmed orderv0.4.5NoneOrders pickled by 0.4.3 or 0.4.4 in that state still load, warn, and are never sent again; look up the extrinsic the warning names
xqsa.quip.signing.SIGNED_EXTENSIONSv0.4.5NoneThe signed-extension order now comes from the runtime metadata
The top-level commands xquad asm, dism, run and verifyv0.4.5xquad vm asm, dis, run and checkPrint a warning line to stderr rather than a DeprecationWarning; see xquad vm

solve() is not deprecated, and neither is a typed resolution= on solve(). The strict paths refuse what the deprecated ones accept: create_order() and JobOrder.set() raise TypeError for mode=, a raw chain dict, or an unknown option.

query() has no full replacement yet. A receipt follows an order on chain but cannot decode its answers onto your model, so from a session other than the one that submitted the order, query(order_id, model, ...) is still the only way to get a decoded SolverResult.

Python shows a DeprecationWarning by default only when the deprecated call is made from __main__: a script sees it, but a library calling the same API does not. Run with -W default::DeprecationWarning to see every one; each points at the calling line.

Gaps

Your First Quip Job now walks a funded job end to end with the numbers one run produced. What stays open is that those numbers move:

  • MinReward is a runtime constant, not storage governance can set. It changes only with a runtime upgrade, with no extrinsic and no event. SolverQuip reads it live, so the toolchain follows a change; prose does not.
  • The fee weights are benchmark output. Re-running the runtime’s benchmarks moves the fee for the same model.
  • Congestion is not a cause. The fee multiplier is constant, so for a given model and the default reward, only an upgrade moves either figure. JobOrder.quote() is the only reading to trust at submission time.
  • Timing and fleet behaviour are unmeasured. How long a job takes and how many miners answer it depend on the fleet at that moment; the tutorial reports one run, not a distribution.

Your First Quip Job

This page submits one job to aglais, the public Quip test network, from an empty account: define a four-city Travelling Salesman Problem in XQCP, run its encoder on the XQVM locally, and hand the resulting model to SolverQuip as a job order. Every output block below was captured from the code on this page, run against aglais on 2026-10-15 at runtime specVersion 119. Quip Network is the reference for the mechanism and Job Orders for the order surface; this page is the walkthrough.

Install

pip install "xquad[quip]"

The extra pulls in the chain client and quip-signer, the native extension that signs extrinsics. quip-signer ships a prebuilt wheel on Linux and builds from source with a local Rust toolchain on macOS and Windows.

Set Up an Account

xquad quip account show is the setup check: it prints the account in effect, where it comes from, and its balance. On a first run there is none. At a terminal it prints the guidance, then offers to create an account; the answer defaults to skip, and nothing is written until the last answer:

xquad quip --network aglais account show
No Quip account is set up.

No keystores found. Create one with `xquad quip account new`, or bring a seed with `xquad quip account import`.

Resolution order (the first match signs):

  1  QUIP_SIGNER_SEED     unset
  2  QUIP_KEYSTORE        unset
  3  project preference   unset
  4  user preference      unset
* 5  default              ~/.quip/keystore.json

Setting the account:

  xquad quip account use <name>             prefer <name> in this project, else for your user
  xquad quip account use <name> --global    prefer <name> for your user
  xquad quip account use --unset [--global] clear the preference at that level
  --keystore PATH, QUIP_KEYSTORE, QUIP_SIGNER_SEED override the preferences
Create one now? (new, import, skip) [skip]: skip

Answering new creates the account there and then. Off a terminal, or with --json, the command prints the same guidance as an error and exits 1 without asking or creating anything.

Create the account, then check it again:

xquad quip account new
xquad quip --network aglais account show
created default at the user level: ~/.quip/keystore.json
  5GGt9yK5EbpdQNv3fCnpZLo93DgL663VsT7VeS5ueJqNoaBD
it is the user default, which signs when nothing else is set
Account   5GGt9yK5EbpdQNv3fCnpZLo93DgL663VsT7VeS5ueJqNoaBD
Source    default keystore ~/.quip/keystore.json
Balance   0.000000000000 AGLS
Reserved  0.000000000000 AGLS
Orders    0 of 32 slots used

The keystore holds the account’s signing key. Keep the file: it is the account. It starts empty; Submit funds it from the faucet. These two blocks were captured on 2026-10-24, with a fresh account. The receipts further down came from a different, funded account, so their Account line is not the one shown here.

Build the Model

The model is the TSP example at n = 4, with a fixed distance matrix instead of a random one so the numbers below are reproducible. The encoder runs on your machine; nothing touches the network yet.

from xquad import cp
from xquad.vm import VM

n = 4
distances = [12, 30, 25, 18, 40, 22]  # xq_triu order: 0-1, 0-2, 1-2, 0-3, 1-3, 2-3

problem = cp.Problem("TSP")
num_cities = problem.input("num_cities", type=cp.Types.Int)
distance_matrix = problem.input("distance_matrix", type=cp.Types.Vec)
problem.define_model(size=num_cities * num_cities, domain=cp.Domain.BINARY, rows=num_cities, cols=num_cities)

with problem.range(0, num_cities - 1) as city_i:
    with problem.range(city_i + 1, num_cities) as city_j:
        dist = problem.stow("dist", distance_matrix.get(cp.xq_triu(city_i, city_j)))
        with problem.range(0, num_cities) as position:
            next_position = (position + 1) % num_cities
            problem.model.quadratic[(city_i, position), (city_j, next_position)].add(dist)
            problem.model.quadratic[(city_j, position), (city_i, next_position)].add(dist)

with problem.range(0, num_cities) as city:
    problem.model.apply_onehot_row(city, penalty=100)
with problem.range(0, num_cities) as position:
    problem.model.apply_onehot_col(position, penalty=100)

tour = problem.output("tour", type=cp.Types.Vec)
with problem.range(0, num_cities) as position:
    tour.append(problem.sample.colfind(col=position, value=1))

programs = problem.compile()

vm = VM()
vm.set_calldata([n, distances])
vm.set_output_slots(1)
vm.run(programs.encoder)
model = vm.outputs()[0]

model is a 16-variable BINARY XQMX: one variable per city and tour position.

Connect and Quote

from xquad import sa

solver = sa.quip.SolverQuip.for_network("aglais", topology="native")
order = solver.create_order(model)
print(order)
print(order.quote())
╭─ Job order (draft) ──────────────────────────────────────────────────╮
│ Network     aglais                                                   │
├──────────────────────────────────────────────────────────────────────┤
│ Model       16 variables, 96 couplings, binary                       │
│ Placed      native, 16 spins                                         │
├──────────────────────────────────────────────────────────────────────┤
│ Payout      single best                                              │
│ Access      open                                                     │
│ Floors      none                                                     │
│ Deadline    100 blocks                                               │
│ Block wait  10 blocks                                                │
│ Reward      1.000000000000 AGLS                                      │
├──────────────────────────────────────────────────────────────────────┤
│ Quote       not quoted                                               │
│ Receipt     not submitted                                            │
╰──────────────────────────────────────────────────────────────────────╯
╭─ Job quote ──────────────────────────────────────────────────────────╮
│ Network     aglais                                                   │
│ Account     5DL9B7rhMw1cg7kXzqmdj5QZ1V67vBvfYPq2bTEgZfSXiBSJ         │
│ Quoted at   block 1896                                               │
├──────────────────────────────────────────────────────────────────────┤
│ Problem     16 variables -> 16 spins, 96 couplings                   │
│ Placed on   native                                                   │
├──────────────────────────────────────────────────────────────────────┤
│ Reward      1.000000000000 AGLS  refunded if unanswered              │
│ Fee         0.002011933394 AGLS  exact                               │
│ Total       1.002011933394 AGLS                                      │
╞══════════════════════════════════════════════════════════════════════╡
│ Balance     0.000000000000 AGLS                                      │
│ Shortfall   1.002011933394 AGLS                                      │
│ Affordable  no                                                       │
╰──────────────────────────────────────────────────────────────────────╯

Two arguments do the work:

  • "aglais" selects the preset carrying the network’s RPC endpoint and faucet; see Named networks.
  • topology="native" submits the order over the model’s own coupling graph. In the default mode, the placement search finds no embedding of this model in the network’s topology within its step budget and raises PlacementError; native mode skips placement. See Native topology mode.

create_order() places the model and returns a draft JobOrder with the client’s defaults: winner takes all, any solver may answer, no quality floors, a 100-block deadline and a 10-block wait after the first answer. Nothing has been signed or sent. Job Orders covers every option a draft takes.

No signer is passed, so SolverQuip signs with the account xquad quip account show reported. To use a different keystore, pass it:

solver = sa.quip.SolverQuip.for_network("aglais", keystore="~/.quip/work.json", topology="native")

quote() signs the order’s propose_job extrinsic and asks the chain what it would cost, without submitting anything. The returned JobQuote carries each amount in planck, the chain’s smallest unit, as reward_planck, fee_planck, total_planck, balance_planck and shortfall_planck; fee_exact says the fee came from the chain rather than a fallback estimate. The signature is kept, and submit() sends those same bytes.

What the figure means

As of 2026-10-15, at specVersion 119, a job costs 1 AGLS in reward plus about 0.0020 AGLS in fee: about 1.0020 AGLS in total. The reward is the chain’s MinReward, a runtime constant. It is charged to gate submissions and maps to no measured compute: the runtime declares type VM = NoOpVm, so the chain meters none of the solver’s work. The fee is metered, burned, and does not rise with congestion. AGLS on aglais comes free from the faucet. Both figures move when the runtime is upgraded, so read them from quote() rather than from this page.

Submit

order.submit()
╭─ Job quote ──────────────────────────────────────────────────────────╮
│ Network     aglais                                                   │
│ Account     5DL9B7rhMw1cg7kXzqmdj5QZ1V67vBvfYPq2bTEgZfSXiBSJ         │
│ Quoted at   block 1896                                               │
├──────────────────────────────────────────────────────────────────────┤
│ Problem     16 variables -> 16 spins, 96 couplings                   │
│ Placed on   native                                                   │
├──────────────────────────────────────────────────────────────────────┤
│ Reward      1.000000000000 AGLS  refunded if unanswered              │
│ Fee         0.002011933394 AGLS  exact                               │
│ Total       1.002011933394 AGLS                                      │
╞══════════════════════════════════════════════════════════════════════╡
│ Balance     0.000000000000 AGLS                                      │
│ Shortfall   1.002011933394 AGLS                                      │
│ Affordable  no                                                       │
╰──────────────────────────────────────────────────────────────────────╯

submit() quotes once more and shows that quote: it always goes to the xqsa.quip logger at INFO, and it is also printed when stderr is a terminal, as here. It then runs two gates with their defaults. autoconfirm=True accepts the price. The account is short, so autofund=True draws one drip of 10 AGLS from the faucet. Finally it sends the signed propose_job, which reserves the reward, and returns the order, now submitted, once the block holding it is finalized, a few blocks after it is included. The order is read-only from here: set(), quote() and submit() refuse it.

Read the Result

order.wait()
solution = order.best()
print(solution.energy)
print(solution.solver, solution.leader, solution.energy_milli)

vm = VM()
vm.set_calldata([solution.sample, n])
vm.set_output_slots(1)
vm.run(programs.decoder)
print(list(vm.outputs()[0]))
-696
5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn True -1790000
[0, 1, 3, 2]

wait() blocks until the order is final by block height. Here the first answer arrived six blocks after the order was included, and the order was final ten blocks later, at the end of its 10-block wait; the script returned about 122 seconds after it started.

  • The decoded tour visits cities 0, 1, 3, 2 and returns to 0. Its length is 12 + 40 + 22 + 30 = 104. It is a valid tour but not the shortest: of the three distinct tours on four cities, 0, 1, 2, 3 is shortest, at 77, with energy -723. The network pays for the best answer it receives, not for the optimum, and here the two answers that arrived missed it. The one-call form below came back with the shortest tour.
  • solution.energy is that length plus -800, the constant the eight one-hot penalties contribute at any valid assignment. It is recomputed on your model.
  • best() returns the leader’s best vector: the answer from the solver the chain ranks first, and pays. solution.leader is True when that rule held; Reading the answer covers the cases where it falls back to another solver’s vector.
  • energy_milli is the vector’s energy on the chain’s milli scale, in the SPIN basis, and is not comparable to solution.energy directly.

The order’s receipt shows what the chain recorded, including the fee and the payout:

print(order.receipt())
╭─ Job order 20 receipt ───────────────────────────────────────────────╮
│ Network     aglais                                                   │
│ Proposer    5DL9B7rhMw1cg7kXzqmdj5QZ1V67vBvfYPq2bTEgZfSXiBSJ         │
│ Submitted   block 1898, 2026-10-15 04:22:24 UTC                      │
├──────────────────────────────────────────────────────────────────────┤
│ Problem     16 spins, 96 couplings                                   │
│ Spec        0x8f46f3a31321d1d093314fc769c42cbe7a83d71a0b69e6571a0f68 │
│             e2a04067f0                                               │
├──────────────────────────────────────────────────────────────────────┤
│ Payout      single best                                              │
│ Access      open                                                     │
│ Floors      none                                                     │
│ Deadline    100 blocks                                               │
│ Block wait  10 blocks                                                │
│ Reward      1.000000000000 AGLS                                      │
├──────────────────────────────────────────────────────────────────────┤
│ Status      final                                                    │
│ Answers     2                                                        │
│ Leader      5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn         │
│ Energy      -1790                                                    │
╞══════════════════════════════════════════════════════════════════════╡
│ Fee         0.002011933394 AGLS                                      │
│ Winner      1.000000000000 AGLS, awaiting claim                      │
╰──────────────────────────────────────────────────────────────────────╯

Two solvers answered; the leader wins the whole reward, which it receives once a claim is made. The account now holds the drip less one job’s total.

The one-call form

solve() does all of the above in one call: it creates the order, submits it, waits, and returns the leader’s answer as a SolverResult, whose metadata names the order:

result = solver.solve(model)
print(result.energy)
print(result.metadata)
-723
{'order_id': 23, 'solver': '5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn', 'best_energy_milli': -1817000, 'energy_matches_chain': True, 'leader_read': True, 'leader': True, 'num_submissions': 1, 'num_solutions': 1, 'quantization_error': 0.0}

solve() prints the quote box the same way submit() does. Metadata explains each key.

Confirm Before Paying

autoconfirm=False asks on the terminal before anything is proposed. Answering anything but y raises QuipCancelledError, which carries the quote:

from xquad import sa

solver = sa.quip.SolverQuip.for_network("aglais", topology="native", autoconfirm=False)
try:
    result = solver.solve(model)
except sa.quip.QuipCancelledError as exc:
    print("cancelled:", exc)
    print(exc.quote.total_planck)
╭─ Job quote ──────────────────────────────────────────────────────────╮
│ Network     aglais                                                   │
│ Account     5DL9B7rhMw1cg7kXzqmdj5QZ1V67vBvfYPq2bTEgZfSXiBSJ         │
│ Quoted at   block 1952                                               │
├──────────────────────────────────────────────────────────────────────┤
│ Problem     16 variables -> 16 spins, 96 couplings                   │
│ Placed on   native                                                   │
├──────────────────────────────────────────────────────────────────────┤
│ Reward      1.000000000000 AGLS  refunded if unanswered              │
│ Fee         0.002011933394 AGLS  exact                               │
│ Total       1.002011933394 AGLS                                      │
╞══════════════════════════════════════════════════════════════════════╡
│ Balance     4.989940332991 AGLS                                      │
│ Shortfall   0.000000000000 AGLS                                      │
│ Affordable  yes                                                      │
╰──────────────────────────────────────────────────────────────────────╯
[quip] Submit this job for 1.002011933394 AGLS? [y/N] n
cancelled: autoconfirm declined: answered 'n' at the prompt
1002011933394

The same gate runs in order.submit(). With no terminal to ask on, a notebook included, autoconfirm=False raises QuipCancelledError without prompting. A declined job costs nothing: the gate runs before funding, so it draws no drip, and nothing reaches the chain.

To decide in code instead, pass a callable that receives the JobQuote and returns whether to submit. This one accepts any job up to 2 AGLS (a token here has 12 decimals):

solver = sa.quip.SolverQuip.for_network(
    "aglais",
    topology="native",
    autoconfirm=lambda quote: quote.total_planck <= 2 * 10**12,
)
result = solver.solve(model)
print(result.metadata["order_id"], result.energy)
╭─ Job quote ──────────────────────────────────────────────────────────╮
│ Network     aglais                                                   │
│ Account     5DL9B7rhMw1cg7kXzqmdj5QZ1V67vBvfYPq2bTEgZfSXiBSJ         │
│ Quoted at   block 1936                                               │
├──────────────────────────────────────────────────────────────────────┤
│ Problem     16 variables -> 16 spins, 96 couplings                   │
│ Placed on   native                                                   │
├──────────────────────────────────────────────────────────────────────┤
│ Reward      1.000000000000 AGLS  refunded if unanswered              │
│ Fee         0.002011933394 AGLS  exact                               │
│ Total       1.002011933394 AGLS                                      │
╞══════════════════════════════════════════════════════════════════════╡
│ Balance     5.991952266385 AGLS                                      │
│ Shortfall   0.000000000000 AGLS                                      │
│ Affordable  yes                                                      │
╰──────────────────────────────────────────────────────────────────────╯
24 -723

What a Failed Job Costs

An order that becomes final with no answers costs its transaction fee, which is burned either way. Its reward stays reserved until the order is reclaimed. solve() reclaims it for you and raises QuipJobFailedError, carrying the order_id, with a message that says whether the reclaim succeeded:

QuipJobFailedError: order 25 finalized with no solutions; only allowlisted solvers could answer (accounts 5DL9..iBSJ) and none did; the reserved reward was reclaimed

That order was restricted to an account that is not a registered solver, so nobody could answer it. Its total cost was the proposal fee plus the reclaim fee, about 0.004 AGLS. A faucet drip drawn for a job stays in the account either way.

On an order you submitted yourself, wait() and best() never reclaim. best() raises QuipJobFailedError and tells you to reclaim; do it once the order is final:

refunded = order.reclaim()

A job that outlasts the solver’s timeout raises QuipTimeoutError instead, carrying order_id and the order’s receipt. The job is still live on chain, so wait on the receipt again:

from xquad import sa

try:
    order.wait()
except sa.quip.QuipTimeoutError as exc:
    exc.receipt.wait()

Coming Back Later

The order lives on chain, not in your session. A new process lists the account’s orders and rebuilds any one’s receipt from its id:

solver = sa.quip.SolverQuip.for_network("aglais")
print(solver.list_orders())
receipt = solver.get_receipt(20)
print(receipt.status())
[24, 23, 21, 20]
{'state': 'finalized', 'order_id': 20, 'chain_status': 'Expired', 'created_at': 1898, 'first_solution_at': 1904, 'effective_expiry': 1914, 'current_block': 2219, 'solution_count': 2}

chain_status is Expired: the chain marked the order expired at its effective expiry. It turns Closed once someone claims it, and print(receipt) then shows the Winner row as claimed. A closed order drops out of list_orders(), which lists only orders not yet closed, but its receipt still rebuilds from the id. Orders 21 and 22 were placed for the examples on Job Orders. Orders 22 and 25 are already missing: they went unanswered and were reclaimed. A receipt also lists the solvers that answered and their raw spin vectors, shows the settlement, and reclaims an unanswered order with receipt.reclaim().

From a shell, xquad quip --network aglais orders lists the same orders, one line each, and xquad quip --network aglais status 20 prints the receipt box; see Orders.

A receipt does not hold your model, so it cannot decode an answer into a tour. Decoding needs the JobOrder from the session that submitted it. From a new session, the deprecated solver.query(order_id, model, topology="native") still returns a decoded SolverResult, with the same topology the order was proposed with; it warns and is removed in v0.6.0 (see Deprecated APIs).

What Native Mode Gives Up

A miner that cannot embed an arbitrary graph, such as a QPU-backed one, cannot answer a native order. The simulated-annealing miners on aglais answered every open job on this page.

Job Orders

SolverQuip.solve() proposes a job, waits for it and decodes the answer in one call. create_order() splits that call into steps: it returns a draft JobOrder that you can inspect, change, price and submit, and a submitted order leaves a receipt that follows it on chain from any session. Every option is checked against the chain’s limits before anything is signed, so a refused option costs nothing.

This page covers the order surface. Quip Network covers installation, configuration, encoding, cost and the error classes, and Your First Quip Job walks one order through end to end. The examples import from xquad.sa.quip; xqsa.quip exports the same names. Every output on this page was captured on aglais at specVersion 119 on 2026-10-15, from the 16-variable TSP model the tutorial builds, unless it says otherwise.

Orders

from xquad import sa

solver = sa.quip.SolverQuip.for_network("aglais", topology="native")
order = solver.create_order(model, deadline_blocks=200)
print(order)
print(order.quote())
order.submit().wait()
print(order.receipt())

create_order(model, **options) places the model and returns a draft. It takes ten options. The first six default to the client’s values (see Installation and Configuration); mapping and the three quality floors exist per order only:

OptionDefaultMeaning
rewardThe client’s reward, else QUIP_REWARD, else MinRewardPlanck reserved at proposal and paid to the winners
accessAccessOpen()Who may answer; see Access
resolution"SingleBest"How the reward is split; see Payout rules
deadline_blocks100Blocks the order stays open, from 10 to MaxDeadlineBlocks
block_wait10Blocks the order keeps accepting answers after the first, from 0 to MaxBlockWait
topologyThe client’s topologyA registered topology hash, or "native"
mappingNoneAn explicit variable-to-node placement; not allowed with "native"
max_energy, min_solutions, min_diversityNoneSee Quality floors

Any other keyword raises TypeError, including mode= (deprecated) and delivery= (client-only):

TypeError: unknown order option 'mode'; expected one of access, block_wait, deadline_blocks, mapping, max_energy, min_diversity, min_solutions, resolution, reward, topology

reward, deadline_blocks and block_wait must be integers (TypeError: reward must be an int, not str). A raw chain dict for access or resolution raises TypeError: access: raw chain values are not accepted.

States

An order is in one of five states, or the deprecated unconfirmed. status()["state"] reports it, and the order box shows it in its title.

StateMeaning
draftBuilt by create_order(); nothing is on chain. Only a draft accepts set(), quote() and a fresh submit(); a sending order is settled first, and submit() resumes it
sendingsubmit() sent the signed propose_job but has not settled it: the proposal may still land. The order keeps its signed bytes and nonce, pickling included, and is never signed again while they can land; see Settling a send
submittedProposed, and the block holding it is finalized; the order has an id and a receipt
finalizedThe chain head reached the order’s effective expiry (see Job Lifecycle); wait() and status() move a submitted order here
failedpropose_job was included in a finalized block but its dispatch failed: the fee was paid and no order was placed
unconfirmedDeprecated, and no longer produced since v0.4.5. An order pickled in this state by xquad 0.4.3 or 0.4.4 loads with a DeprecationWarning and is never sent again; see Deprecated APIs

Calling set(), quote() or submit() on any order other than a draft or a sending one raises QuipSubmissionError:

QuipSubmissionError: order 20 is finalized; set() works only on a draft order

Changing a draft

set(**options) takes the same options as create_order(). It re-runs every check over the whole configuration and leaves the order unchanged if any check refuses, so it also repeats the warnings for options you did not touch. It places the model again only when topology or mapping changes. Any change drops the cached signature, so the next quote() or submit() signs afresh. options() returns all ten options as a dict, and each is also a read-only property (order.reward, order.access, order.max_energy and so on).

Quote and submit

quote() signs the order’s propose_job extrinsic once and caches it, then prices it: the fee from the chain’s payment_queryInfo, the account balance and the head block, all re-read on every call. Nothing is sent and no nonce is consumed. It returns a JobQuote; Cost Model lists its fields.

submit() sends the cached extrinsic, signing first if the order was never quoted. Before sending, it quotes again and runs the autoconfirm and autofund gates, as solve() does. Once the proposal is sent, it ends in one of three states:

  • submitted: the order is in a finalized block, and submit() returns the order.
  • failed: the dispatch failed, and submit() raises QuipSubmissionError("propose_job 0x... failed on chain in block 0x...: <error>. The fee was paid and no order was placed.").
  • sending: the send could not be settled, and submit() raises QuipSubmitIncompleteError; see Settling a send.

A proposal the node refuses, or one proven never to have reached the chain, leaves the order a draft, and submit() raises QuipSubmissionError; submitting it again signs afresh.

A signature carries the account’s nonce. When another order from the same account is sent between quote() and submit(), the nonce has moved, so submit() signs again. Two drafts quoted together and then submitted show it:

a = solver.create_order(model, resolution=sa.quip.ResolutionTopNEqual(2))
b = solver.create_order(model, access=sa.quip.AccessAllow(accounts=[me]), deadline_blocks=10, block_wait=0)
a.quote(), b.quote()
a.submit()
b.submit()
proposed Quip job: order_id=21 spec_id=0x8f46...67f0 reward=1000000000000 planck (block 0x2114...c0fa)
account nonce moved from 1 to 2 since signing; signing again
proposed Quip job: order_id=22 spec_id=0x8f46...67f0 reward=1000000000000 planck (block 0x2a8b...1752)

(Log lines from the xqsa.quip logger at INFO, hashes shortened.) If the nonce moves again while submit() signs, it refuses rather than race the other client: QuipSubmissionError: account nonce moved twice while submitting; another client is sending from this account. Nothing was sent.

submit() logs the quote box at INFO on the xqsa.quip logger, and also prints it to stderr when stderr is a terminal.

Settling a send

Every proposal is signed mortal: the chain accepts it only within inclusion_window_blocks of the finalized block it was signed at, and never after. The window is a client setting, not an order option. SolverQuip and for_network() take 64 to 2048 blocks, rounded up to the next power of two, so inclusion_window_blocks=100 gives 128; a value outside that range raises QuipOrderOptionError (inclusion_window_blocks=4096 must be from 64 to 2048), and one that is not an integer raises TypeError, both before connecting. The default, DEFAULT_INCLUSION_WINDOW_BLOCKS, is 64 blocks, about 6.4 minutes. A longer window rides out more congestion or a longer outage, but a send that never lands takes longer to be proven dead. The quote box shows the window in its Window row (included within 64 blocks), and a draft’s status()["inclusion_window_blocks"] reports it.

quote() signs once, but submit() signs again when the account nonce has moved or half of the cached bytes’ window has passed, so a quote left sitting never sends bytes that could expire before they land. It refuses to sign at all while finality lags the best block by half the window or more: QuipSubmissionError: finality lags 40 blocks behind the best block, at least half the 64-block inclusion window; nothing was sent, try again once it catches up.

submit() reads the outcome from finalized state alone. The order becomes submitted, with its order id, or failed once the block holding the proposal is finalized, so submit() returns a few blocks after the proposal is included, not at inclusion. A failed dispatch is read from that block’s events, as System.ExtrinsicFailed: <error>. While the proposal may still land, submit() sends the same bytes again, so a node that lost them learns them again; the bytes carry one hash and one nonce, so they land once at most. A lost connection is reopened with backoff. Settling reads past blocks’ state, so the node must keep full state history (--state-pruning=archive); the named network presets do.

When submit() cannot settle the send by the end of its window, because no node could be reached or finality stalled, it raises QuipSubmitIncompleteError and leaves the order sending. It stops at once, with the same error, when the block that spent the nonce or its events cannot be decoded, when a reconnect reaches another chain or a node whose runtime metadata this client cannot read, or when the node has discarded the state it needs. The error is a QuipSubmissionError, so existing handlers still catch it. It carries order (None after pickling, since the order holds a live connection), extrinsic_hash, nonce and window_end, the last block that can include the proposal; the specific cause is its __cause__. The message ends with what to do. This example is illustrative, built from the message in xqsa/quip/client.py, not captured:

QuipSubmitIncompleteError: propose_job 0x5e0c...81d4 (nonce 3) was sent but is not settled (<the last error>). Its window ends at block 4160. The order is sending: calling submit() again resumes it; it never places a second order.

Calling submit() on a sending order resumes it. It settles the bytes already sent, up to the end of their window, and signs nothing. If they landed, the order is submitted (or failed, raised as above). If they are proven never on chain, the order is a draft again and is quoted, gated, signed and sent as a new one. Either way it never places a second order:

from xquad import sa

try:
    order.submit()
except sa.quip.QuipSubmitIncompleteError as exc:
    print(exc.extrinsic_hash, exc.nonce, exc.window_end)
    exc.order.submit()  # resumes; never places a second order

A KeyboardInterrupt, or any other exception, after the bytes may have reached the node also leaves the order sending. submit() prints the hash, nonce and window end to stderr and re-raises the exception unchanged:

[quip] submit() stopped after propose_job 0x5e0c...81d4 (nonce 3) may have reached the node; its window ends at block 4160. The order is sending: calling submit() again resumes it; it never places a second order.

Before that point the order stays a draft. While an order is sending, every other method settles it first: status() makes one read without waiting and reports the state it settles to, or, while it cannot, state, extrinsic_hash, nonce and window_end. The order box shows the hash, nonce and window end in its Receipt row. A sending order pickles with its signed bytes, but copy() and deepcopy() refuse it, since two live orders must not share one nonce.

Each send spends one account nonce, and the chain includes an account’s transactions in nonce order. A send signed while an earlier one from the same account is still waiting in the node’s pool takes the next nonce, so it cannot land before the earlier one. If the earlier send’s window ends with it never on chain, its nonce stays unspent: the order settles back to a draft, and its submit() raises QuipSubmissionError: propose_job 0x... never reached the chain. The later send cannot land while that nonce is unspent. Submitting the earlier draft again signs with that nonce, and once it lands, the later send can still land within its own window. If nothing spends the nonce before the later window ends, the later order settles back to a draft the same way.

Checks before signing

create_order() and set() check each option against the chain’s limits, which the client reads once when it connects. A refusal raises QuipOrderOptionError, a QuipSubmissionError whose option attribute names the option. A warning is a UserWarning; the order is still built. The limits on aglais are MinReward 1 AGLS, MaxDeadlineBlocks 1000 and MaxBlockWait 100.

OptionRefusedWarns
rewardreward=999999999999 planck is below the chain's MinReward of 1000000000000
deadline_blocksdeadline_blocks=1001 must be at least 10 and at most MaxDeadlineBlocks (1000)deadline_blocks=50: orders shorter than 100 blocks often go unanswered
block_waitblock_wait=101 must be at least 0 and at most MaxBlockWait (100)block_wait=100 is not below deadline_blocks=100; the deadline ends the order before the wait for better answers can

The chain itself accepts a deadline of 0, which expires on arrival. The short-deadline warning reflects how often aglais answered: an order with a 1-block deadline was answered 0 times out of 11, 30 blocks 28 times out of 38, and 100 blocks 108 times out of 108. The other options’ checks are in their sections below.

The order box

print(order) renders a 72-column box. Its sections are the network; the model and where it was placed; the terms (payout, access, floors, deadline, block wait and reward); and a summary of the latest quote and the receipt:

╭─ Job order (draft) ──────────────────────────────────────────────────╮
│ Network     aglais                                                   │
├──────────────────────────────────────────────────────────────────────┤
│ Model       16 variables, 96 couplings, binary                       │
│ Placed      native, 16 spins                                         │
├──────────────────────────────────────────────────────────────────────┤
│ Payout      single best                                              │
│ Access      open                                                     │
│ Floors      none                                                     │
│ Deadline    100 blocks                                               │
│ Block wait  10 blocks                                                │
│ Reward      1.000000000000 AGLS                                      │
├──────────────────────────────────────────────────────────────────────┤
│ Quote       not quoted                                               │
│ Receipt     not submitted                                            │
╰──────────────────────────────────────────────────────────────────────╯

On a draft, the Quote row shows the last quote taken and whether the account can afford it. Once the order has an id, a Paid row replaces it: the fee the chain charged, read from the order’s TransactionFeePaid event, the reward, and the quote’s total beside them for comparison. The quoted fee is an estimate at the head block, so the two can differ. When the fee cannot be read, the row says fee unknown. A failed order paid the fee but placed nothing, so its Paid row shows the fee and the quote with no reward. A sending order’s outcome is unknown, so a Quoted row shows the quote alone.

Payout rules

The resolution option decides how the reward is split:

  • "SingleBest" or ResolutionSingleBest(), the default: the leader, the solver the chain ranks first, takes the whole reward.
  • ResolutionTopNEqual(n): the best n ranked solvers split it evenly.
  • ResolutionTopNWeighted(n): the best n split it by rank. With k ranked solvers, rank r (0 is best) gets weight k - r; energies decide only the order.
from xquad import sa

order = solver.create_order(model, resolution=sa.quip.ResolutionTopNEqual(2))

n runs from 1 to 32. The rule’s own constructor checks it, so a bad n raises ValueError (ResolutionTopNEqual: n must be from 1 to 32, got 33) or TypeError before any order exists. n=1 warns ResolutionTopNEqual(1) pays exactly like ResolutionSingleBest(). Any other value for resolution is refused: with ValueError on the constructor, and with QuipOrderOptionError on an order:

resolution='TopNEqual' is not a payout rule; pass 'SingleBest' or one of ResolutionSingleBest(), ResolutionTopNEqual(n), ResolutionTopNWeighted(n)

ResolutionTopNWeighted needs runtime 119 or later (see Runtime compatibility). On a network below runtime 119, create_order(), set(), solve() and quote() refuse it before signing (captured on runtime 118):

QuipOrderOptionError: ResolutionTopNWeighted needs runtime 119 or later, and the chain runs runtime 118; older runtimes weight by absolute energy, so a worse answer can take the larger share

A top-N rule with n above 1 and block_wait=0 warns, because the order closes on its first answer: resolution=top 3, equal split: with block_wait=0 only the first answer is accepted, so there can be only one winner.

Settlement

The chain ranks solvers by energy, lowest first, with the earlier submitter first on a tie. Winners are the first n ranked solvers, so an order with fewer answers than n has fewer winners. A receipt’s settlement() computes each winner’s share:

  • TopNEqual: each winner gets reward // k, and rank 0 also gets the remainder. A reward of 10 over 3 winners splits [4, 3, 3].
  • TopNWeighted: with total = k * (k + 1) / 2, rank r gets (reward // total) * w + (reward % total) * w // total for its weight w, and rank 0 also gets the remainder. A reward of 10 over 3 winners splits [6, 3, 1].

A top-2 order answered by three solvers, two of them tied at the best energy:

print(order.settlement())
{'claimed': False, 'winners': [{'solver': '5Gn1BACWJGtxCnkHpbonhLZJw3ZYF3LzwXEoWjub74E9RPv9', 'energy_milli': -1790000, 'allocation_planck': 500000000000}, {'solver': '5FNGHjGLzoo8CXv4iiX7z71xJycYpTGZUaA37281kZhWoWun', 'energy_milli': -1790000, 'allocation_planck': 500000000000}]}

An allocation is not a payment. One claim_reward extrinsic pays every winner; until then the receipt shows each as awaiting claim, and claimed turns True once the chain closes the order. Below runtime 119, settlement() of a TopNWeighted order raises NotImplementedError, and the receipt prints that message as its Reward row. The order box and the receipt show the rule as single best, top 2, equal split or top 2, weighted by rank.

Access

The access option decides who may answer. "Open" or AccessOpen(), the default, admits every registered solver. AccessAllow(accounts=[...], types=[...]) admits a solver whose account is listed or whose declared type is listed:

from xquad import sa

order = solver.create_order(model, access=sa.quip.AccessAllow(types=["QpuDwave"]))

Solver types are self-declared when a solver registers, and the protocol does not verify them, so a type entry admits any solver that claims it. On aglais, an order allowlisted to Cpu was answered by our own solver and three others.

The allowlist is bounded:

  • It must name at least one account or type: AccessAllow: name at least one account or solver type; the chain refuses an empty list (ValueError, from the constructor). An empty list would fail on chain after the fee was paid.
  • At most 8 types (ValueError).
  • At most MaxBidMiners accounts, 16 on aglais: AccessAllow lists 17 accounts; the chain's MaxBidMiners is 16 (QuipOrderOptionError).
  • Type names come from the runtime’s MinerType, on aglais Cpu, Gpu, QpuDwave, QpuIbm, QpuIonq, QpuPasqal and Asic: AccessAllow: unknown solver type 'Fpga'; the chain has Cpu, Gpu, QpuDwave, QpuIbm, QpuIonq, QpuPasqal, Asic (QuipOrderOptionError).

Over either cap the runtime fails with no readable error, which is why the client refuses first. Duplicate entries are dropped, and accounts, given as SS58, 0x hex or bytes, are normalised to 0x hex. A bad value for access raises ValueError on the constructor and QuipOrderOptionError on an order: access='Bid' is not an access rule; pass 'Open' or one of AccessOpen(), AccessAllow(accounts=[...], types=[...]).

An allowlisted account that is not a registered solver is never refused, but it warns, naming the account in SS58:

UserWarning: access: 5DL9B7rhMw1cg7kXzqmdj5QZ1V67vBvfYPq2bTEgZfSXiBSJ is not a registered solver, so it cannot answer until it registers

The constructor warns for its default access, and create_order() and set() warn when they change it. When nobody on the allowlist answers, the no-answer error says so: order 22 has no answers; only allowlisted solvers could answer (accounts 5DL9..iBSJ) and none did; once it is final, reclaim its reward with order.reclaim().

A solver that is not on the list and answers anyway is refused on chain with System.ExtrinsicFailed: QuantumComputeMempool.NotEligibleSolver, followed by the error’s description when the metadata carries one. Network coordinators submit to allowlisted orders regardless and pay for each refused submission, so a block explorer can show many refused submissions against one allowlisted order: 4 to 142 per order on aglais.

Quality floors

Three per-order options set a minimum quality an answer must reach. None of them is a client default.

  • max_energy: an energy ceiling. The chain drops each submitted vector above it.
  • min_solutions: how many vectors each answer must carry. The chain stores exactly that many from each submission.
  • min_diversity: how different those vectors must be, as a fraction of the placed spins. 0 means no floor.

max_energy is in model units, the same units as result.energy. For a BINARY model the chain’s energy differs from the model’s by a constant offset, which the client subtracts before it sends the ceiling; An Energy Ceiling on the Quip Network works through the conversion. A ceiling that moves shows both values in the order box’s Floors row, here for max_energy=-2 on the model -2*x0 + x0*x1:

│ Floors      energy <= -2 (chain -1.25)                               │

Refusals (QuipOrderOptionError unless marked) and warnings:

OptionRefusedWarns
max_energyA bool or a non-number (TypeError); not finite; too large for a float; outside i64 once converted to milli; below the lowest energy the placed model can reach: max_energy=-1000000.0 is below -5388, the lowest energy the placed model can reach, so no answer could meet itAt or above the highest energy: max_energy=1000000.0 is at or above 7576, the highest energy the placed model can reach, so it filters nothing. On a model whose coefficients were rounded: max_energy=-1: the model's coefficients were rounded to the chain's milli grid, and the chain applies the ceiling to the rounded model, so answers near it can land on either side
min_solutionsNot an int (TypeError); outside 1 to MaxSolutions (20 on aglais): min_solutions=21 must be at least 1 and at most MaxSolutions (20)Exactly 1: min_solutions=1: the chain stores only the first vector of each answer, but ranks the answer by its best vector
min_diversityNot a number (TypeError); outside 0 to 0.5; not a whole number of thousandths; above what the placed spins allow: min_diversity=0.4 is above 1/3, the most diversity 3 placed spins allow, since a vector and its full flip count as one; positive with min_solutions=1: min_diversity=0.3 needs min_solutions of at least 2 or unset; one solution has no diversity (the chain's InvalidDiversityConfig)

What the chain does with each floor:

  • The ceiling drops each submitted vector above it. A submission is refused (InsufficientEnergy) only when none of its vectors is left.
  • With min_solutions=k, the chain stores exactly the k most diverse vectors of each submission. That subset can leave out a solver’s best vector, which is one reason best() can fall back (see Reading the answer).
  • Diversity is the average pairwise Hamming distance over the placed spins, in thousandths, with a vector and its full flip counted as the same vector. That is why n spins cap it at floor(n/2)/n: a 3-spin model caps at 0.333.

Floors narrow the answers a solver can give, and a small model may have too few distinct vectors to meet min_solutions or min_diversity at all, so an order with floors can go unanswered. The no-answer error then names them: best() in model units, solve() in chain units. The order box shows floors in model units; the receipt shows them in chain units; both show none when no floor is set.

Receipts

A submitted order has a receipt, a JobOrderReceipt. It holds only the order id and the network’s genesis hash, needs no model, and reads the chain on every call. order.receipt() returns it; on a draft it raises QuipSubmissionError: this order is a draft; submit() it first. Any session rebuilds a receipt from the order id:

solver = sa.quip.SolverQuip.for_network("aglais")
print(solver.list_orders())
receipt = solver.get_receipt(20)
print(receipt.status())
[24, 23, 21, 20]
{'state': 'finalized', 'order_id': 20, 'chain_status': 'Expired', 'created_at': 1898, 'first_solution_at': 1904, 'effective_expiry': 1914, 'current_block': 2219, 'solution_count': 2}

get_receipt() raises QuipConnectionError for an id that names no order (order 1000000000 not found on-chain (QuantumComputeMempool.JobOrders)).

list_orders(state=None, account=None) returns the ids of an account’s orders that are not yet closed, newest first: the chain drops an order from the account’s list once a claim or a reclaim closes it. state is "submitted", "finalized", "unanswered" (final with no answers, so reclaim() refunds it) or None, and any other value raises ValueError; account is SS58, 0x hex or bytes, and defaults to the signer. It returns at most the newest 32 ids, after the state filter, and warns when there are more: showing 32 of N orders on this account. This 32-order page limit was imposed for the old protocol cap of 32 orders per account; please flag it to the xquad team so it can be lifted.

receipt.summary() returns the order’s outcome as one flat dict, the fields the receipt box shows under its status rows; xquad quip orders and status --short print its order id, state and leader as one line (see Orders):

print(solver.get_receipt(20).summary())
{'order_id': 20, 'state': 'final', 'open_until': None, 'answers': 2, 'leader': '5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn'}

Its state is open, final (answered, the reward awaiting a claim), reclaimable (final with no answers, so reclaim() refunds it) or closed (claimed or reclaimed). open_until is the block an open order closes at, None otherwise; leader is None with no answers. Captured on 2026-10-26.

The receipt and the in-session JobOrder share these methods:

MethodReturns
status()state (submitted or finalized, which sticks once reached), order_id, chain_status (the chain’s OrderStatus), created_at, first_solution_at, effective_expiry, current_block and solution_count
wait()The receipt (or order) once it is final, polling every poll_interval; raises QuipTimeoutError after the client’s timeout
solvers()One entry per solver that answered: solver, ranked_energy_milli, energy_milli, submitted_at, num_solutions. Ranked solvers come first, then the rest by stored energy and then by submission time. energy_milli is the solver’s stored best, which a later resubmission can overwrite
raw_solutions(solver=None){solver: {"energy_milli": ..., "spins": ...}} over the placed chain nodes, not decoded onto the model; KeyError for a solver that did not answer
settlement(){"claimed": bool, "winners": [...]}; see Settlement
reclaim()The planck refunded; see below

chain_status and state answer different questions. state is finalized once the chain head passes the order’s effective expiry, while chain_status is the chain’s own record: Opened until the effective expiry, Expired from then, and Closed once a claim or a reclaim closes the order. The chain expires a bounded number of orders per block, so when many expire at once an order can read Opened for a few blocks past its expiry. The deprecated SolverQuip.status() returns the old keys, status and is_final, in place of state and chain_status.

wait() raises QuipTimeoutError with the receipt attached, so the caller can wait again instead of rebuilding it:

from xquad import sa

try:
    order.wait()
except sa.quip.QuipTimeoutError as exc:
    receipt = exc.receipt
    receipt.wait()

QuipTimeoutError drops the receipt when it is pickled, since the receipt holds a live connection; another process rebuilds it with get_receipt(exc.order_id).

The receipt box

print(receipt) renders a box with five sections: the network, proposer and submission block and time; the problem; the terms, with floors in chain units; the outcome (status, answer count, leader and its energy); and, under a double rule, the money. The fee is what the chain charged the proposing extrinsic, tip included, read from the block that placed the order; when it cannot be read the row says unknown. The Reward row is reserved, leader so far, reserved, no answers yet, reclaimed, or unanswered: reclaim with get_receipt(N).reclaim(); an answered, final order shows one Winner row per winner, awaiting claim or claimed:

╭─ Job order 20 receipt ───────────────────────────────────────────────╮
│ Network     aglais                                                   │
│ Proposer    5DL9B7rhMw1cg7kXzqmdj5QZ1V67vBvfYPq2bTEgZfSXiBSJ         │
│ Submitted   block 1898, 2026-10-15 04:22:24 UTC                      │
├──────────────────────────────────────────────────────────────────────┤
│ Problem     16 spins, 96 couplings                                   │
│ Spec        0x8f46f3a31321d1d093314fc769c42cbe7a83d71a0b69e6571a0f68 │
│             e2a04067f0                                               │
├──────────────────────────────────────────────────────────────────────┤
│ Payout      single best                                              │
│ Access      open                                                     │
│ Floors      none                                                     │
│ Deadline    100 blocks                                               │
│ Block wait  10 blocks                                                │
│ Reward      1.000000000000 AGLS                                      │
├──────────────────────────────────────────────────────────────────────┤
│ Status      final                                                    │
│ Answers     2                                                        │
│ Leader      5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn         │
│ Energy      -1790                                                    │
╞══════════════════════════════════════════════════════════════════════╡
│ Fee         0.002011933394 AGLS                                      │
│ Winner      1.000000000000 AGLS, awaiting claim                      │
╰──────────────────────────────────────────────────────────────────────╯

Reclaiming

reclaim() returns an unanswered order’s reserved reward and returns the planck refunded. It reads the head block, then refuses before signing, with QuipReclaimRefusedError, in this order:

  1. order N was proposed by <account>, not by this account
  2. order N is already closed (reclaimed or settled)
  3. order N is open until block B (about M min); reclaim after it closes
  4. order N was answered by K solvers; its reward awaits a claim and cannot be reclaimed

If the chain refuses the reclaim, it raises QuipReclaimRefusedError naming the block and the chain’s error, and the fee is charged. If the outcome is unknown, it raises QuipSubmissionError("the reclaim of order N (<extrinsic>) was sent but its outcome is unknown"); a retry is safe, since a reclaim that landed makes the next one refuse as already closed. A reclaim costs its own transaction fee, about 0.0019 AGLS on aglais.

Only solve(), and the deprecated query(), reclaim on their own: when an order finalizes with no answers, they reclaim it and raise QuipJobFailedError, saying whether the reclaim succeeded:

QuipJobFailedError: order 25 finalized with no solutions; only allowlisted solvers could answer (accounts 5DL9..iBSJ) and none did; the reserved reward was reclaimed

JobOrder.wait(), JobOrder.best() and a receipt’s wait() never reclaim.

Reading the answer

An in-session JobOrder holds the model it was built from, so it decodes answers onto it:

solution = order.best()
print(solution.energy)
print(solution.solver, solution.leader, solution.energy_milli)
-696
5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn True -1790000

best() returns a Solution (importable from xquad.sa.quip, or xqsa.quip, only) with five fields:

FieldMeaning
sampleThe vector decoded onto the model, as an XQMX sample
energyIts energy recomputed on your model; authoritative
energy_milliIts energy on the chain’s milli scale, over the coefficients the order carried
solverThe account that answered with it
leaderWhether the chain ranks that account first

solutions(solver=None) returns every stored answer as a Solution, best first by recomputed energy with the leader’s vectors first on a tie. With solver, it returns that solver’s vectors alone, and raises KeyError for a solver that did not answer. An unanswered order returns an empty list.

best() returns the leader’s best vector, the answer the chain pays for, not necessarily the best vector any solver stored. It falls back to the best stored vector overall, the leader first on a tie, in two cases:

  • The ranking cannot be read, or names a solver with no stored answer. metadata["leader_read"] is then False on a solve() result.
  • None of the leader’s stored vectors recomputes to the energy the chain ranked it at. energy_matches_chain is then False, and the client logs both energies at WARNING. A worse resubmission can overwrite a solver’s stored answer, and min_solutions can store a subset without the best vector, so this is not necessarily an encoding fault.

Under a fallback, leader stays True only when the leader’s own vector is also the best overall. best() reads the chain each time, so on an open order a later call can return a different answer. It never reclaims: with no answers, it raises QuipJobFailedError and tells you to call order.reclaim() once the order is final.

Decoding needs the in-session JobOrder, because a receipt does not hold the model. From another session, get_receipt(order_id) gives you the order’s status, solvers, raw vectors, settlement and reclaim, but not a decoded sample; the deprecated query(order_id, model, ...) is the only way to decode one there (see Deprecated APIs).

Energy and Precision

Every backend in this chapter returns the same kind of number, computed the same way, no matter what found the sample: an integer. This page covers how that number is computed and compared, and where precision is lost or preserved as a model moves from an exact integer formula onto real hardware.

The Formula

A sample’s energy with respect to a model is:

$$E = \sum_i \text{linear}[i] \cdot x_i ;+; \sum_{i \le j} \text{quadratic}[i,j] \cdot x_i \cdot x_j$$

identical to the model’s Hamiltonian from Quadratic Models, evaluated at one concrete assignment x, and identical to the XQVM ENERGY opcode (see Energy Evaluation). A verifier program run through XQVM and xqsa’s own recomputation compute the same formula two independent ways.

The Precision Contract

SolverResult.energy must equal model.energy(sample) exactly, an integer-to-integer comparison with zero tolerance for drift. Every XQMX coefficient and every variable assignment is an integer, so the energy formula is a sum of integer products, not a float that merely rounds to the right answer. It is an exact integer whenever it is representable at all, and representable is a real condition: XqmxModel.energy checks every term product and every partial sum against the i64 range and raises ArithmeticOverflow rather than returning a wrapped number, so a model whose energy leaves that range has no energy this contract can report. Solver._recompute_energy() returns model.energy(sample); every backend in this chapter uses it instead of trusting whatever float its underlying library reports. That raw float, where one exists, survives only as metadata["params"]["raw_energy"], for diagnostics – never as the authoritative value.

A verifier program never has to take a solver’s word for its own answer. It recomputes the same integer independently with ENERGY. There are three outcomes, not two: the comparison holds, the comparison fails, or ENERGY raises ArithmeticOverflow and the verifier produces no verdict at all. The third is not a solver disagreement and must not be read as one – it says the model’s energy left the i64 range on the way to being computed. XqmxModel.energy is the function ENERGY itself calls, so the two accumulate in the same sorted key order, check the same intermediates, and agree on which models fall into it.

A Large Penalty Does Not Corrupt the Model

Choosing a Penalty Weight argues that a large \(P\) cannot corrupt a model, because energy differences between feasible assignments survive exactly however large \(P\) gets. Take that page’s three-node Max-Cut model with a one-hot penalty \(P \cdot (x_0 + x_2 - 1)^2\) added on top, and score two feasible assignments and one infeasible one through XqmxModel.energy() at three wildly different weights:

P=           2 | E(feasible_a)=           -8 E(feasible_b)=           -6 E(infeasible)=  -8 diff= 2
P=        1000 | E(feasible_a)=        -1006 E(feasible_b)=        -1004 E(infeasible)=  -8 diff= 2
P=  2000000000 | E(feasible_a)=  -2000000006 E(feasible_b)=  -2000000004 E(infeasible)=  -8 diff= 2

The difference between the two feasible assignments stays exactly 2 at every weight. The penalty term is 0 on both, so what separates them is the underlying objective alone, and integer arithmetic never lets it drift.

The infeasible assignment’s own energy does not move either – it sits at -8 at every \(P\) above. What moves is the two feasible energies, and they fall without bound as \(P\) grows. That is the missing +P constant, not the penalty term itself: the true, unstorable Hamiltonian adds +P to every assignment that violates the constraint and +0 to every assignment that satisfies it, so dropping that constant subtracts a uniform P from every stored energy. On the infeasible sample the missing +P exactly cancels the penalty’s own +P, leaving it unchanged; on a feasible sample there is nothing to cancel, so the stored energy falls by P.

What a Large Penalty Costs Instead

Nothing in the model degrades, but two things outside it do, and both matter to backends in this chapter.

Fixed-precision hardware loses resolution. The Metal engine (SolverGPU(engine="metal")) computes coefficients in float32 on the GPU, not the integer arithmetic ENERGY uses – result.energy is still recomputed in exact integers afterward, but the search that finds the sample runs at float32 precision. The CUDA engine does not share this: its kernels take const double* throughout, its acceptance draws stay float64, and result.energy is recomputed the same way as every other backend. See Local Solvers for the per-backend split. The rest of this section applies to the Metal engine and to fixed-precision annealing hardware, not to every GPU backend.

A float32 mantissa carries about seven decimal digits. Once a term in the combined Hamiltonian is large enough that a difference of 2 no longer changes the float, the Metal engine’s search cannot distinguish the two assignments that difference separates, even though ENERGY still resolves them exactly afterward:

import numpy as np
np.float32(-2000000006) == np.float32(-2000000004)   # True -- the 2 vanished, at the Metal engine's width
np.float64(-2000000006) == np.float64(-2000000004)   # False -- still resolved, at the CUDA engine's width
np.float32(-1006) == np.float32(-1004)                 # False -- still resolved

At P = 2000000000 the two feasible assignments above are computationally indistinguishable to the Metal engine’s float32 search even though XqmxModel.energy() still tells them apart exactly; at P = 1000 they are not. The CUDA engine’s float64 search keeps this pair distinguishable at this scale, though a large enough \(P\) would eventually exhaust its wider but still finite mantissa too. Neither GPU backend’s correctness ever suffers – the returned energy is always the exact integer – but the Metal engine’s search can lose the signal, at penalty weights where the CUDA engine’s still keeps it visible.

Real annealing hardware rescales, and the Quip network encodes at fixed precision. A physical D-Wave annealer maps coefficients onto its analog range with a monotonic, optimum-preserving rescaling, so an oversized \(P\) is not rejected there – it costs usable range on the device instead. SolverQuip goes further: it scales every coefficient by MILLI_SCALE = 1000 into the chain’s i32 fields, so an oversized coefficient can overflow i32 and raise EncodingError, and one finer than 1/1000 is rounded to the nearest milli, with the largest rounding error reported. MAX_NATURAL_COEFFICIENT = 2_147_483 is the exact bound only for a SPIN model; a BINARY model – the domain every worked example on this page uses – goes through a basis change first that shifts the bound in both directions, and can overflow well before any single coefficient looks anywhere near that large. See Quip Network for the full derivation.

A large penalty also raises the barrier between feasible regions, independent of any hardware precision question: a heuristic search such as simulated annealing has to cross that barrier to leave the first feasible region it reaches, and a larger \(P\) makes crossing less likely within a fixed number of sweeps. Sizing \(P\) in practice, and reading a solver’s output to tell which of these failure modes you hit, belongs to Constraints.

An Energy Ceiling on the Quip Network

SolverQuip’s max_energy order option (see Quality floors) is an energy ceiling, and it is in model units: the same units as XqmxModel.energy and result.energy. The chain never sees your model, only its spin fields and couplings at milli scale, so the client converts the ceiling before sending it.

For a SPIN model the conversion is a change of scale: the ceiling times 1000, rounded down, so the chain admits exactly the energies at or below it. A BINARY model also carries a constant. The basis change s = 2x - 1 (see Quip Network) turns the model into spin fields and couplings plus an offset that no spin multiplies, and the chain, which stores only the fields and couplings, computes energies without it. The client subtracts the offset first:

chain ceiling (milli) = floor((max_energy - offset) * 1000)

A value within a millionth of a milli of the grid snaps to it before the floor, so float noise such as -1.2500000000001 does not lower the ceiling to -1251.

Take E = -2*x0 + x0*x1. Its spin form has fields h0 = -0.75 and h1 = 0.25, a coupling of 0.25, and an offset of -0.75. The assignment x0 = 1, x1 = 0 has model energy -2; on the chain, with spins s0 = +1, s1 = -1, its energy is -0.75 - 0.25 - 0.25 = -1.25. So max_energy=-2 goes on chain as -1250 milli, and the order box shows both values:

│ Floors      energy <= -2 (chain -1.25)                               │

The chain applies the ceiling to the model it holds. When the model’s coefficients were rounded to the milli grid (see What a Large Penalty Costs Instead), the chain’s energies differ slightly from yours, so an answer near the ceiling can land on either side of it, and create_order() warns. A ceiling below the lowest energy the placed model can reach is refused, and one at or above the highest warns that it filters nothing; Quality floors lists every check.

Examples

Graph problems

Examples that encode graph cuts, colouring, covers, independent sets, and tours.

  • Max-Cut – Find a 2-colour partition of a weighted graph that maximises the cut weight.
  • Graph Coloring – Assign colours to graph nodes so adjacent nodes do not share a colour.
  • Maximum Independent Set – Select the largest subset of graph nodes with no selected edge between them.
  • Vertex Cover – Select the smallest vertex subset that covers every graph edge.
  • Travelling Salesman Problem – Find the shortest Hamiltonian tour through a random symmetric distance matrix.

Selection and packing

Examples that select subsets, cover demands, pack bins, and balance integer weights.

  • Knapsack – Select items that maximise value while respecting a capacity constraint.
  • Bin Packing – Pack items into the minimum number of fixed-capacity bins.
  • Set Cover – Select the minimum set collection whose union covers the universe.
  • Weighted Set Cover – Select sets with capacities to cover element demands at minimum cost.
  • Number Partition – Split positive integers into two subsets with nearly equal sums.
  • Portfolio Optimization – Select a fixed-size portfolio while penalising higher-order risk interactions.
  • Portfolio Rebalance – Choose signed integer asset weights against a risk matrix and a budget.

Satisfiability and higher-order

Examples that reduce clauses and higher-order pseudo-Boolean objectives to quadratic models.

  • Max-3-SAT – Find the assignment that satisfies the maximum number of 3-literal clauses.
  • Cubic Optimization – Minimise a cubic pseudo-Boolean objective through HOBO degree reduction.
  • Quartic Optimization – Minimise a degree-4 pseudo-Boolean objective through two-stage REDUCE chaining.

Using the Examples

examples/ holds fourteen self-contained problems, one directory each, listed on the gallery page. This page covers what every directory has in common, how to run one, and how to turn one into a problem of your own – the task the rest of this chapter does not cover, because it is not specific to any single example.

What Every Example Shares

Each examples/<name>/ holds a README.md, which this chapter’s other pages are generated from, and a runner.py. Nothing else – no .xqasm files. problem.compile() builds the three programs in memory at runtime as plain strings; see Three Programs for what those three programs are and Compiling for what compile() emits. Every runner.py follows the same shape:

  • build_problem(...) – an xqcp Problem definition: inputs, a model, an objective, constraints, and outputs. This is the part that changes from problem to problem, and the part Modelling covers in full.
  • run(programs, ...) – three VM.run() calls against the compiled encoder, verifier, and decoder, with one xqsa solver call between the first two. Every example wires calldata and output slots the same way: encoder in, model out; model plus sample plus N in, energy and valid out; sample plus N in, decoded result out.
  • main(...) – an argparse CLI, and a JSON result printed to stdout or written with -o.

examples/manifest.yaml lists every directory once, under a group (dir, title, blurb), and drives both the gallery grouping and scripts/gen-example-docs.py’s validation that every listed directory has a runner.py and a README.md. This page itself is exempt: it is named in the manifest’s preserved_pages, so make regen-docs leaves it alone instead of overwriting it from a source README.md that does not exist.

Running One

uv run python examples/<name>/runner.py --seed 42

Every runner accepts --seed, --solver, and -o/--output. Most also take --n for problem size; a few split size across two flags instead (--n plus --bins, --colors, --budget, or --m), or use --num-elements and --num-sets in place of --n. Check the individual page’s Usage table for the exact flags.

The compiled programs run on the Rust XQVM through xqffi. Backends covers what differs by solver backend. Where a model has several optima of equal energy, the solver returns one of them, and a different optimum decodes to a different answer even though both are equally correct: a different solver or seed can change the decoded result without changing valid or energy.

Adapting an Example

Copying and editing an existing runner.py is the fastest way to model a new problem, and it is what this section walks through concretely: turning examples/maxcut/runner.py’s Max-Cut into a minimum s-t cut with two fixed terminals.

Max-Cut splits a graph’s nodes into two groups to maximise the crossing edge weight, with no constraint on which node ends up where. Quadratic Models derives its formulation; build_problem’s inner loop is the part that matters here:

with problem.range(0, edge_count) as e:
    offset = e * 3
    i = problem.stow("i", edges_in.get(offset))
    j = problem.stow("j", edges_in.get(offset + 1))
    w = problem.stow("w", edges_in.get(offset + 2))
    problem.model.linear[i].add(-w)
    problem.model.linear[j].add(-w)
    problem.model.quadratic[i, j].add(w * 2)

Suppose two specific nodes must end up on opposite sides – node 0 and node n - 1, say, standing in for two machines a network partition must separate. That is a different problem, minimum s-t cut with fixed terminals, and it is two small edits away:

with problem.range(0, edge_count) as e:
    offset = e * 3
    i = problem.stow("i", edges_in.get(offset))
    j = problem.stow("j", edges_in.get(offset + 1))
    w = problem.stow("w", edges_in.get(offset + 2))
    problem.model.linear[i].add(w)          # sign flipped: minimise, not maximise
    problem.model.linear[j].add(w)
    problem.model.quadratic[i, j].add(w * -2)

# Fix node 0 to side 0 and node n-1 to side 1. 10_000 comfortably
# exceeds the total weight of every edge (at most C(n,2) * 100 for
# this graph), so neither fixed node is ever worth flipping to save
# weight elsewhere.
problem.model.linear[0].add(10_000)
problem.model.linear[num_nodes - 1].add(-10_000)

Everything else stays: the random weighted graph, problem.define_model, the partition output loop, and run(). The DSL calls used – model.linear[i].add() and model.quadratic[i, j].add() – are the same two maxcut/README.md already lists. main() needs one further edit, covered in the next section: drop the canonicalize_partition(partition) call, because this variant’s two sides stop being interchangeable.

Running the original and the variant on the same seed and graph (--n 5 --seed 42 on both):

Unmodified examples/maxcut/runner.py:

{"cut_weight": 354, "energy": -354, "partition": [0, 1, 0, 0, 1], "valid": 1}

The variant:

{"cut_weight": 196, "energy": -9804, "partition": [0, 1, 1, 1, 1], "valid": 1}

The cut drops from 354 to 196, confirming the variant found a smaller crossing weight rather than reusing the maximiser’s answer, and partition[0] == 0 with partition[-1] == 1 confirms the two terminals landed where they were pinned.

Telling a Variant Wrong from a Variant Different

valid alone does not catch a modelling mistake here: Max-Cut and this variant both declare no constraints, so valid reports only that every sample value is 0 or 1 – see Constraints for what a verifier’s built-in check does and does not cover. energy is not a safe single number either. Get the terminal bias backwards – problem.model.linear[0].add(-10_000) and problem.model.linear[num_nodes - 1].add(10_000), the two signs swapped – and the run reports the same cut_weight (196) and the same energy (-9804) as the correct version. A cut and its complement (every node’s side flipped) always cost the same, so reversing which two nodes get pinned just picks the other member of that pair; nothing about the aggregate numbers changes.

partition is where the mistake shows, but only once canonicalize_partition(partition) is gone from main(). Max-Cut calls it because a cut and its complement are the same cut, so normalising node 0 to side 0 throws nothing away. This variant cannot afford that: the two terminals make the two sides distinguishable, and canonicalising erases the one fact the pinning added. Building both versions with that call removed and running each on the same seed and graph (--n 5 --seed 42):

Correct:

{"cut_weight": 196, "energy": -9804, "partition": [0, 1, 1, 1, 1], "valid": 1}

Swapped:

{"cut_weight": 196, "energy": -9804, "partition": [1, 0, 0, 0, 0], "valid": 1}

cut_weight and energy still agree, for the reason above. partition[0] and partition[-1] no longer do: 0 and 1 on the correct run, 1 and 0 on the swapped one. Catching this mistake means checking the property the edit was supposed to guarantee – that the two terminals sit on the sides they were pinned to – against a sample nothing downstream has normalised. The same habit generalises past this one example: name the specific property an edit is supposed to produce, and check that property against output no later step has adjusted, not just valid and a plausible-looking energy.

Max-Cut

Source: examples/maxcut/README.md

Find a 2-colour partition of a weighted graph that maximises the total weight of edges crossing the partition.

QUBO formulation

  • Input: num_nodes (int), edges (list of flat (i, j, w) triples, 3*|E| entries)
  • Model: n binary variables, one per node. x[v] in {0, 1} selects the side of the cut.
  • Objective: for each edge (i, j, w), add -w*(x_i + x_j) and +2w*x_i*x_j. Minimising this minimises -sum w*[x_i != x_j], i.e. maximises the cut.

DSL methods used

  • problem.input() – declare typed calldata inputs
  • problem.define_model() – allocate binary XQMX model
  • problem.stow() – bind intermediate computations to named registers
  • problem.range() – emit RANGE loops
  • model.linear[i].add() – accumulate linear bias on variable i
  • model.quadratic[i, j].add() – accumulate quadratic coupling between variables i and j
  • problem.output() – declare typed output slots
  • problem.sample.getline() – read a row from the sample bitstring

Pipeline overview

  1. CP (xqcp) – build a random weighted complete graph, declare binary variables (one per node), and add linear/quadratic QUBO terms per edge.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary, then computes energy; this problem declares no constraints for it to check
  6. Decode – decoder extracts the 2-colour partition

Usage

uv run python examples/maxcut/runner.py --seed 42
uv run python examples/maxcut/runner.py --n 6 --seed 7 -o /tmp/mc.json
FlagDefaultDescription
--n5Number of nodes in the complete graph
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Graph Coloring

Source: examples/graph_coloring/README.md

Assign one of C colors to each node of an undirected graph such that no two adjacent nodes share the same color (proper C-coloring).

QUBO formulation

  • Input: number of nodes N, number of colors C, edge list
  • Model: N categorical variables over C cases. Domain.CATEGORICAL records that as an N x C binary grid, x[v,c] = 1 if node v gets color c, with a one-hot row per node.
  • Objective: none. The problem is pure constraint satisfaction: a colouring is scored only by its constraint violations. A valid C-colouring scores -penalty * N, not 0, because XQMX has no constant-term field and each satisfied ONEHOTR stores -penalty rather than 0. That is -1000 at the --n 5, penalty 200 defaults. The colour count defaults to 4 because a G(5, 0.5) graph routinely contains a 4-clique – the default seed’s does – and a 4-clique has no 3-colouring, so --colors 3 would make the canonical instance unsatisfiable. See Constraints.
  • Constraints:
    • One-hot per node: sum_c x[v,c] = 1 (ONEHOTR, penalty 200) – implied by the domain, not written
    • Exclusion per (edge, color): x[u,c] + x[v,c] <= 1 (EXCLUDE, penalty 200)

Encoding strategy

The one-hot row per node is what makes a categorical variable categorical, so define_model(domain=Domain.CATEGORICAL, k=C, penalty=200) emits it: the grid and one ONEHOTR per row, through the same record paths a hand-written loop would use. The compiled program is what this example emitted before the domain existed.

EXCLUDE is applied per (edge (u,v), color c) pair via a nested range loop. The 2D coordinates (u, c) and (v, c) are resolved to flat indices using IDXGRID: u * num_colors + c and v * num_colors + c.

Reading the answer back is sample.case(v), a ROWFIND for the 1 in node v’s row. ROWFIND returns -1 where a row holds no 1, which is the uncoloured sentinel this example used to produce in host code.

DSL methods used

  • problem.define_model(size=N, domain=Domain.CATEGORICAL, k=C, penalty=200) – the grid and its one-hot rows
  • model.apply_exclude((u, c), (v, c), penalty) – EXCLUDE per edge per color
  • sample.case(node) – the case a node took, or -1

Pipeline overview

  1. CP (xqcp) – generate a random graph, declare N categorical variables over C cases, and add EXCLUDE constraints per (edge, color) pair.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary, the one-hot row sums, and the per-edge exclusions, then computes energy
  6. Decode – decoder reads each node’s case with ROWFIND, giving one color index per node

Usage

uv run python examples/graph_coloring/runner.py --seed 42
uv run python examples/graph_coloring/runner.py --n 6 --colors 4
FlagDefaultDescription
--n5Number of nodes
--colors4Number of colors
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Maximum Independent Set

Source: examples/max_independent_set/README.md

Find the largest subset of nodes in an undirected graph such that no two selected nodes share an edge.

QUBO formulation

  • Input: number of nodes N, edge list
  • Model: N binary variables. x_i = 1 if node i is in the independent set.
  • Objective: minimise -sum(x_i) (maximise set size)
  • Constraints: per edge (i,j): x_i + x_j <= 1 (SLACK + EQUALITY)

Each edge inequality is encoded via SLACK + EQUALITY. A single binary slack variable s (capacity = 1) converts x_i + x_j <= 1 into the equality x_i + x_j + s = 1, and EQUALITY adds the penalty P*(x_i + x_j + s - 1)^2.

Slack variable indices start at num_nodes and are allocated one per edge.

DSL methods used

  • problem.vec() – allocate untyped vector registers for indices and coefficients
  • problem.slack(indices, coeffs, start_index, capacity) – append one slack entry per edge
  • model.apply_equality(indices, coeffs, target, penalty) – EQUALITY constraint

Pipeline overview

  1. CP (xqcp) – generate a random graph, declare binary variables (one per node), and encode each edge independence constraint via SLACK + EQUALITY.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that the independence constraints hold, then computes energy
  6. Decode – decoder extracts the selected nodes

Usage

uv run python examples/max_independent_set/runner.py --seed 42
uv run python examples/max_independent_set/runner.py --n 7
FlagDefaultDescription
--n5Number of nodes
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Vertex Cover

Source: examples/vertex_cover/README.md

Find the minimum subset of vertices such that every edge in an undirected graph has at least one endpoint in the subset.

QUBO formulation

  • Input: number of nodes N, edge list
  • Model: N binary variables. x_v = 1 if vertex v is in the cover.
  • Objective: minimise sum(x_v)
  • Constraints: per edge (i,j): x_i + x_j >= 1 (ATLEAST with k=1)

The at-least-1 constraint is encoded directly with ATLEAST. For each edge, ATLEAST allocates one slack variable at model.size and adds the penalty P*(x_i + x_j - 1 - s)^2, where s in {0,1} accounts for the case when both endpoints are selected (sum = 2).

DSL methods used

  • problem.vec() – allocate a vector register for the two endpoint indices per edge
  • model.apply_atleast(indices, k, penalty) – ATLEAST constraint with k=1

Pipeline overview

  1. CP (xqcp) – generate a random graph, declare binary variables (one per vertex), and encode per-edge coverage constraints via ATLEAST.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that every edge is covered, then computes energy
  6. Decode – decoder extracts the selected vertices

Usage

uv run python examples/vertex_cover/runner.py --seed 42
uv run python examples/vertex_cover/runner.py --n 7
FlagDefaultDescription
--n5Number of nodes
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Travelling Salesman Problem

Source: examples/tsp/README.md

Find the shortest Hamiltonian tour through N cities given a random symmetric distance matrix.

QUBO formulation

  • Input: num_cities (int), distance_matrix (list, flat upper triangle, n*(n-1)/2 entries)
  • Model: an n x n binary grid. x[i, p] = 1 means city i is at tour position p.
  • Objective: sum of distances between consecutive positions in the tour.
  • Constraints: one-hot row (each city at exactly one position) and one-hot column (each position holds exactly one city), both with penalty 100.

Hamiltonian

The Hamiltonian is the sum of three terms:

$$H = H_{\text{dist}} + H_{\text{row}} + H_{\text{col}}$$

\(H_{\text{dist}}\) is the tour length. For every pair of cities \(i < j\) and every position \(p\), a term fires whichever direction the tour visits them in – city i at p and city j at the next position, or the reverse:

$$H_{\text{dist}} = \sum_{i<j} \sum_{p=0}^{n-1} d_{ij} \left( x_{i,p}, x_{j,(p+1) \bmod n} + x_{j,p}, x_{i,(p+1) \bmod n} \right)$$

\(H_{\text{row}}\) and \(H_{\text{col}}\) are the one-hot constraints apply_onehot_row and apply_onehot_col add, at penalty \(P = 100\):

$$H_{\text{row}} = P \sum_{i=0}^{n-1} \left( \sum_{p=0}^{n-1} x_{i,p} - 1 \right)^2, \qquad H_{\text{col}} = P \sum_{p=0}^{n-1} \left( \sum_{i=0}^{n-1} x_{i,p} - 1 \right)^2$$

XQMX has no field for a constant term. Each satisfied one-hot constraint drops its +P and leaves -P in the stored model instead of 0 – see Constraints.

\(H_{\text{row}}\) forces each city onto exactly one position; \(H_{\text{col}}\) forces each position to hold exactly one city. Both terms are zero as written on a valid tour, so they never change which valid tour is shortest – but because of the dropped constant above, what a satisfied constraint actually contributes is -P, not 0. That is the -2 * P * n = -800 term in the worked example below. A violation (a city visiting two positions, or a position holding two cities) still costs more than a satisfied constraint.

Whether that makes every invalid grid’s energy exceed every valid tour’s depends on \(P\) relative to the tour-length difference a violation can buy back. Constraints gives the general criterion. At \(P = 100\) it holds for this seed-42, 4-city instance. The best invalid grid scores -581, 6 above the worst valid tour’s -587. It does not hold at every instance where \(P = 100\) is used. At --n 5 --seed 7, the second command under Usage below, an invalid grid scores -770, below 40 of the 120 valid tours.

DSL methods used

  • problem.input() – declare typed calldata inputs
  • problem.define_model() – allocate binary 2D grid XQMX model
  • problem.stow() – bind intermediate computations to named registers
  • problem.range() – emit RANGE loops
  • model.quadratic[(city_i, pos), (city_j, pos)].add() – accumulate quadratic coupling using 2D grid coordinates
  • model.apply_onehot_row() – ONEHOTR constraint per city
  • model.apply_onehot_col() – ONEHOTC constraint per position
  • problem.output() – declare typed output slots
  • problem.sample.colfind() – find the row index with value 1 in a given column

Pipeline overview

  1. CP (xqcp) – build a random symmetric distance matrix, declare an n x n binary grid, and add quadratic distance terms plus one-hot row/column constraints.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and the one-hot row and column constraints, then computes energy
  6. Decode – decoder extracts the tour as a sequence of city indices

Steps 3-6 are three independent XQVM programs plus a solver call in between, sharing data only through calldata and outputs:

flowchart LR
    ENC[Encoder] -->|model| SLV[xqsa solver]
    ENC -->|model| VER[Verifier]
    SLV -->|sample| VER
    SLV -->|sample| DEC[Decoder]
    VER -->|energy, valid| OUT[Host program]
    DEC -->|tour| OUT

The encoder’s calldata is num_cities, distance_matrix; the verifier’s is model, sample, num_cities; the decoder’s is sample, num_cities. See Three Programs for why the split exists.

Worked example

uv run python examples/tsp/runner.py --n 4 --seed 42 builds this 4-city instance (edge labels are distance_matrix, indexed by the upper-triangle position of each city pair):

graph LR
    0 ---|82| 1
    0 ---|15| 2
    0 ---|95| 3
    1 ---|4| 2
    1 ---|36| 3
    2 ---|32| 3

The runner returns this tour:

{
  "_note": "canonical CI golden",
  "_seed": 42,
  "energy": -650,
  "n": 4,
  "tour": [0, 2, 1, 3],
  "tour_distance": 150,
  "valid": 1
}

tour_distance is d(0,2) + d(2,1) + d(1,3) + d(3,0) = 15 + 4 + 36 + 95 = 150, matching \(H_{\text{dist}}\) for a valid tour. energy is H_{\text{dist}} + H_{\text{row}} + H_{\text{col}}: on a valid tour, each of the 4 rows and 4 columns contributes its one active variable’s -P linear term and nothing from the quadratic term (no two variables in the same row or column are both 1), for -P * n per constraint family, so energy = tour_distance - 2 * P * n = 150 - 2 * 100 * 4 = -650, matching the run.

Usage

uv run python examples/tsp/runner.py --seed 42
uv run python examples/tsp/runner.py --n 5 --seed 7 -o /tmp/tsp.json
FlagDefaultDescription
--n4Number of cities
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Knapsack

Source: examples/knapsack/README.md

The 0/1 Knapsack problem: given N items with integer weights and values, select a subset maximising total value subject to a weight capacity constraint.

QUBO formulation

  • Input: N item weights and values, capacity W
  • Model: N binary variables. x_i = 1 means item i is selected.
  • Objective: minimise -sum(v_i * x_i)
  • Constraints: capacity sum(w_i * x_i) <= W (SLACK + EQUALITY)

The inequality is encoded via SLACK + EQUALITY. SLACK appends binary slack variable entries (s_j with coefficients 2^j) to the index and coefficient vectors, converting the inequality to the equality sum(w_i*x_i) + sum(s_j*2^j) = W. EQUALITY then adds the penalty term P*(sum(a_k*x_k) - W)^2 to the QUBO.

DSL methods used

  • problem.vec() – allocate untyped vector registers for indices and coefficients
  • problem.slack(indices, coeffs, start_index, capacity) – append slack entries
  • model.apply_equality(indices, coeffs, target, penalty) – EQUALITY constraint

Pipeline overview

  1. CP (xqcp) – generate random item weights and values, declare binary variables, and encode the capacity inequality via SLACK + EQUALITY.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that the capacity constraint holds, then computes energy
  6. Decode – decoder extracts the item selection

Usage

uv run python examples/knapsack/runner.py --seed 42
uv run python examples/knapsack/runner.py --n 6
FlagDefaultDescription
--n5Number of items
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Bin Packing

Source: examples/bin_packing/README.md

Pack N items with given integer sizes into the minimum number of bins, each with a fixed capacity C.

QUBO formulation

  • Input: N item sizes (list), number of bins B, bin capacity C
  • Model: (N + 1) * B binary variables in an (N + 1) x B grid. Rows 0..N-1 are the assignment cells: x[i,b] = 1 if item i is placed in bin b. Row N holds one indicator per bin: y[b] = 1 if bin b is open.
  • Objective: +BIN_COST on each indicator y[b], so the energy counts the bins the packing opens. A bias spread over the assignment cells instead would be identically N on every feasible packing, because each item lands in exactly one bin, and could not tell a one-bin packing from a three-bin one.
  • Constraints:
    • Assignment per item i: sum_b x[i,b] = 1 (ONEHOTR, penalty 200)
    • Linking per (i, b): x[i,b] -> y[b] (IMPLIES, penalty 200)
    • Capacity per bin b: sum_i s_i * x[i,b] <= C (SLACK + EQUALITY, penalty 100)

The capacity inequality is encoded by appending binary slack variable entries to the column index/coefficient vectors, converting it to a weighted equality. Each bin gets its own slack block: bin b’s entries start at (N + 1) * B + b * bitlen(C), past every model variable and past every earlier bin’s block. Sharing one start index across bins would let one bin’s slack absorb another’s overflow, leaving the capacity constraint under-constrained.

DSL methods used

  • model.apply_onehot_row(row, penalty) – ONEHOTR assignment constraint
  • model.apply_implies(coord_a, coord_b, penalty) – IMPLIES linking constraint
  • problem.vec() – allocate untyped vector registers for indices and coefficients
  • problem.slack(indices, coeffs, start_index, capacity) – append slack entries
  • model.apply_equality(indices, coeffs, target, penalty) – EQUALITY constraint
  • xq_bitlen(value) – BITLEN, used to size each bin’s slack block

Pipeline overview

  1. CP (xqcp) – generate random item sizes, declare an (N + 1) x B binary grid, and add ONEHOTR assignment constraints per item, IMPLIES links per cell, and SLACK + EQUALITY capacity constraints per bin.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary, the per-item assignment, the bin-usage links and the per-bin capacity, then computes energy
  6. Decode – decoder extracts the bin assignments

Usage

uv run python examples/bin_packing/runner.py --seed 42
uv run python examples/bin_packing/runner.py --n 5 --bins 4
FlagDefaultDescription
--n4Number of items
--bins3Number of bins
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Set Cover

Source: examples/set_cover/README.md

Given a universe of E elements and a collection of S sets, find the minimum sub-collection whose union equals the universe.

QUBO formulation

  • Input: number of elements E, number of sets S, coverage membership matrix (flat list of E*S entries, covers[e][s] = 1 if set s covers element e)
  • Model: S binary variables. x_s = 1 if set s is selected.
  • Objective: minimise sum(x_s)
  • Constraints: per element e: sum_{s: covers[e][s]=1} x_s >= 1 (ATLEAST with k=1)

For each element, the encoder iterates over all sets and uses a branch to conditionally push only covering set indices into the element’s index vector. ATLEAST then enforces that at least one covering set is selected.

DSL methods used

  • problem.vec() – allocate a vector register for each element’s covering set indices
  • problem.branch(cond, arm, default) – conditional VECPUSH based on coverage membership
  • model.apply_atleast(indices, k, penalty) – ATLEAST constraint with k=1

Pipeline overview

  1. CP (xqcp) – generate a random coverage matrix, declare binary variables (one per set), and encode per-element coverage constraints via conditional branching and ATLEAST.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that every element is covered, then computes energy
  6. Decode – decoder extracts the selected sets

Usage

uv run python examples/set_cover/runner.py --seed 42
uv run python examples/set_cover/runner.py --num-sets 6
FlagDefaultDescription
--num-elements4Number of elements in the universe
--num-sets5Number of sets
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Weighted Set Cover

Source: examples/weighted_set_cover/README.md

A generalisation of Set Cover where each set s has a coverage capacity cap[s] and each element e has a demand demand[e]. The goal is to select sets of minimum total cost such that the total capacity of covering selected sets meets each element’s demand.

QUBO formulation

  • Input: number of elements E, number of sets S, set costs, set capacities, element demands, coverage membership matrix
  • Model: S binary variables. x_s = 1 if set s is selected.
  • Objective: minimise sum(cost[s] * x_s)
  • Constraints: per element e: sum_{s: covers[e][s]=1} cap[s] * x_s >= demand[e] (ATLEASTW)

For each element, a branch conditionally pushes (set index, capacity) pairs into per-element index/coefficient vectors, then ATLEASTW enforces the weighted threshold.

DSL methods used

  • problem.vec() – allocate vector registers for covering set indices and capacities
  • problem.branch(cond, arm, default) – conditional VECPUSH based on coverage membership
  • model.apply_atleastw(indices, coeffs, k, penalty) – ATLEASTW constraint

Pipeline overview

  1. CP (xqcp) – generate a random weighted coverage instance, declare binary variables (one per set), and encode per-element weighted demand constraints via conditional branching and ATLEASTW.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that every demand is met, then computes energy
  6. Decode – decoder extracts the selected sets

Usage

uv run python examples/weighted_set_cover/runner.py --seed 42
uv run python examples/weighted_set_cover/runner.py --num-sets 6
FlagDefaultDescription
--num-elements4Number of elements in the universe
--num-sets5Number of sets
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Number Partition

Source: examples/number_partition/README.md

Given N positive integers, find a way to split them into two subsets of equal sum (or as close as possible if an exact split does not exist).

QUBO formulation

  • Input: N positive integers a_i
  • Model: N binary variables. x_i = 1 puts number a_i in subset A.
  • Objective: minimise P * (sum(a_i * x_i) - S/2)^2 where S = sum(a_i)

An exact partition exists when S is even and the penalty evaluates to zero. The QUBO minimiser finds the balanced partition when one exists, or the most balanced split when the total is odd.

DSL methods used

  • problem.vec() – allocate untyped vector registers for indices and coefficients
  • model.apply_equality(indices, coeffs, target, penalty) – EQUALITY constraint

Pipeline overview

  1. CP (xqcp) – generate random positive integers, declare binary variables (one per number), and encode the half-sum equality constraint via EQUALITY.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that the partition constraint holds, then computes energy
  6. Decode – decoder extracts the subset assignment

Usage

uv run python examples/number_partition/runner.py --seed 42
uv run python examples/number_partition/runner.py --n 8
FlagDefaultDescription
--n6Number of integers
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Portfolio Optimization

Source: examples/portfolio_opt/README.md

Select a portfolio of exactly B assets from N candidates to maximise expected return while penalising higher-order risk cross-interactions.

QUBO formulation

  • Input: N asset returns, cubic risk interactions (i, j, k, sigma), budget B
  • Model: N binary variables. x_i = 1 if asset i is selected.
  • Objective: -sum(r_i * x_i) + sum(sigma_ijk * x_i * x_j * x_k) – first term maximises return (minimising its negation), second penalises correlated three-asset risk interactions.
  • Constraints: budget sum(x_i) = B (EQUALITY with unit coefficients, penalty 200)

Encoding strategy

Return terms are linear: ADDLINE(i, -r_i) per asset.

Cubic risk terms (i, j, k, sigma) are degree-reduced:

  1. REDUCE(i, j, P_AUX) -> w (Rosenberg enforcement for w = x_i * x_j)
  2. ADDQUAD(w, k, sigma) (sigma * w * x_k = sigma * x_i * x_j * x_k)

Budget constraint builds uniform-coefficient index/coeff vecs then calls EQUALITY with target = B and penalty = 200. EQUALITY is emitted after all objective (body) actions because it lands in the constraint section.

DSL methods used

  • model.reduce(var_a, var_b, p_aux) – HOBO degree reduction for cubic risk terms
  • problem.vec() – allocate index/coefficient vecs for the budget constraint
  • model.apply_equality(indices, coeffs, target, penalty) – budget EQUALITY

Pipeline overview

  1. CP (xqcp) – generate random returns and cubic risk interactions, declare binary variables, degree-reduce risk terms via REDUCE, and add a budget EQUALITY constraint.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary, that the budget constraint holds, and that each REDUCE auxiliary equals the product it stands for, then computes energy
  6. Decode – decoder extracts the selected assets

Usage

uv run python examples/portfolio_opt/runner.py --seed 42
uv run python examples/portfolio_opt/runner.py --n 6 --budget 3
FlagDefaultDescription
--n5Number of assets
--budget2Number of assets to select
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Portfolio Rebalance

Source: examples/portfolio_rebalance/README.md

Choose a signed integer weight for each asset, trading expected return off against a risk matrix, subject to the weights summing to a budget. Negative weights are short positions, which is the whole reason this example is not a binary select-or-not model.

QUBO formulation

  • Input: number of assets N, a return per asset, a symmetric N x N risk matrix flattened row-major
  • Model: N integer variables x_i in [-5, 5], declared with lo=/hi=. The model holds y_i = x_i - lo in {0, ..., 10}; coefficients are written over x and sample.value() shifts back on the way out.
  • Objective: -sum(r_i * x_i) + sum_{i <= j} C_ij * x_i * x_j
  • Constraint: sum(x_i) = B, as the penalty P * (sum_i x_i - B)^2
  • Energy: shifted twice over, and not comparable with any other example’s. The ranged domain drops the constant that substituting x = y + lo produces, because XQMX carries no offset field, and the budget square drops its own P*B^2 the way EQUALITY does. Both shifts are uniform across assignments, so argmin is exact even though the number is not the objective’s true value.

Encoding strategy

The budget constraint is written out by hand rather than handed to apply_equality. Every high-level constraint in the VM expands under x^2 = x, which holds for binary variables only, so xqcp refuses all of them off a binary model. Expanded, P * (sum_i x_i - B)^2 is P on each diagonal, 2P on each off-diagonal pair, and -2PB on each linear coefficient. Writing that square is the same work the HLF would have done, and it is what the open question about per-domain expansions is about.

The ranged domain then does its own rewriting underneath. Each quadratic write records w*lo against the linear coefficient of both named indices, because w * x_i * x_j expands to w*y_i*y_j + w*lo*y_i + w*lo*y_j + w*lo^2 once x = y + lo is substituted. A write to the diagonal lands both corrections on the one index, giving the 2*w*lo that squaring asks for. None of that is visible in the model-building code above.

lo and hi are literals. A runtime lo would want the decoder’s single calldata scalar, which the output loop bound already spends, and xqcp raises naming both rather than picking one.

DSL methods used

  • problem.define_model(size=N, domain=Domain.INTEGER, lo=-5, hi=5) – ranged integer weights
  • model.linear[i].add(w) and model.quadratic[i, j].add(w) – the only operations a non-binary model supports
  • sample.value(i) – the weight in the domain it was declared over, rather than the stored y

What this example proves

valid == 1 proves that the domain check, the record-layer shift and the decode compose on a chosen assignment. It proves nothing about optimisation: no solver samples an integer model yet, so --solver is accepted, ignored, and the pipeline runs against a hand-picked weight vector that sits inside the domain and sums to the budget.

Pipeline overview

  1. CP (xqcp) – generate returns and a symmetric risk matrix, declare N ranged integer weights, and write the objective and the budget square as coefficients
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – skipped; the runner supplies the assignment itself
  5. Verify – verifier checks every weight is inside {0, ..., 10} against the k replayed from define_model, then computes energy
  6. Decode – decoder reads each weight and adds lo, giving the signed weights back

Usage

uv run python examples/portfolio_rebalance/runner.py --seed 42
uv run python examples/portfolio_rebalance/runner.py --n 6
FlagDefaultDescription
--n5Number of assets
--solvercpuAccepted and ignored
--seed42Random seed
-ostdoutWrite JSON result to file

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Max-3-SAT

Source: examples/max3sat/README.md

Given M clauses of 3 positive literals over N binary variables, find the assignment that satisfies the maximum number of clauses.

QUBO formulation

  • Input: N binary variables, M clauses of 3 positive literals
  • Model: N binary variables. x_v in {0, 1}.
  • Objective: minimise sum over clauses of P*(1-x_i)(1-x_j)(1-x_k)

A clause (i,j,k) is violated when all three variables are 0. Expanding the product (dropping the constant term):

P*(-x_i - x_j - x_k + x_i*x_j + x_i*x_k + x_j*x_k - x_i*x_j*x_k)

The cubic term -P*x_i*x_j*x_k is degree-reduced via REDUCE(i, j) -> w, introducing one auxiliary variable w per clause with Rosenberg enforcement P_AUX*(x_i*x_j - 2*x_i*w - 2*x_j*w + 3*w). The cubic term becomes the quadratic term -P*w*x_k.

DSL methods used

  • model.reduce(var_a, var_b, p_aux) – HOBO degree reduction; returns a RegLoad holding the auxiliary variable index for chaining into quadratic terms

Pipeline overview

  1. CP (xqcp) – generate random 3-literal clauses, declare binary variables, and degree-reduce the cubic violation terms via REDUCE.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that each Rosenberg REDUCE auxiliary equals the product it stands for, then computes energy
  6. Decode – decoder extracts the variable assignment

Usage

uv run python examples/max3sat/runner.py --seed 42
uv run python examples/max3sat/runner.py --n 8 --m 10
FlagDefaultDescription
--n6Number of Boolean variables
--m8Number of clauses
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Cubic Optimization

Source: examples/cubic_opt/README.md

Minimise a cubic pseudo-Boolean objective over binary variables via single-stage HOBO degree reduction.

QUBO formulation

  • Input: N binary variables, M cubic interaction terms (i, j, k, c)
  • Model: N binary variables. Linear bias -1 per variable rewards selection, creating tension with the positive cubic terms.
  • Objective: sum(c_t * x_i * x_j * x_k) - sum(x_v)

Each cubic term (i, j, k, c) is degree-reduced to quadratic via:

  1. REDUCE(i, j, P_AUX) -> w – allocates auxiliary variable w with Rosenberg enforcement P_AUX*(x_i*x_j - 2*x_i*w - 2*x_j*w + 3*w)
  2. ADDQUAD(w, k, c) – adds c*w*x_k = c*x_i*x_j*x_k to the QUBO

DSL methods used

  • model.reduce(var_a, var_b, p_aux) – single-stage HOBO degree reduction

Pipeline overview

  1. CP (xqcp) – generate random cubic interaction terms, declare binary variables with linear bias, and degree-reduce each cubic term via REDUCE.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that each Rosenberg REDUCE auxiliary equals the product it stands for, then computes energy
  6. Decode – decoder extracts the variable assignment

Usage

uv run python examples/cubic_opt/runner.py --seed 42
uv run python examples/cubic_opt/runner.py --n 5 --m 4
FlagDefaultDescription
--n4Number of variables
--m3Number of cubic terms
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Quartic Optimization

Source: examples/quartic_opt/README.md

Minimise a degree-4 pseudo-Boolean objective via two-stage REDUCE chaining.

QUBO formulation

  • Input: N binary variables, M quartic interaction terms (i, j, k, l, c)
  • Model: N binary variables. Linear bias -1 per variable rewards selection, creating tension with the positive quartic terms.
  • Objective: sum(c_t * x_i * x_j * x_k * x_l) - sum(x_v)

Each quartic term (i, j, k, l, c) is encoded via two-stage REDUCE:

  1. w = REDUCE(i, j, P_AUX) – introduces auxiliary w; w approximates x_i*x_j.
  2. v = REDUCE(w, k, P_AUX) – introduces auxiliary v; v approximates w*x_k = x_i*x_j*x_k. Here w is the variable index returned from the first REDUCE.
  3. ADDQUAD(v, l, c) – adds c*v*x_l = c*x_i*x_j*x_k*x_l to the QUBO.

Each quartic term allocates 2 auxiliary variables. With M terms, the model grows by 2*M variables beyond the original N.

DSL methods used

  • model.reduce(var_a, var_b, p_aux) – two chained HOBO degree reductions; the RegLoad returned by the first REDUCE is passed as var_a to the second

Pipeline overview

  1. CP (xqcp) – generate random quartic interaction terms, declare binary variables with linear bias, and two-stage degree-reduce each quartic term via chained REDUCE.
  2. Assemble – .xqasm text to bytecode via xquad.asm
  3. Encode – run encoder on chosen XQVM to produce the XQMX model
  4. Sample – solver runs SA/QPU/GPU over the model
  5. Verify – verifier checks the sample is binary and that each Rosenberg REDUCE auxiliary equals the product it stands for, then computes energy
  6. Decode – decoder extracts the variable assignment

Usage

uv run python examples/quartic_opt/runner.py --seed 42
uv run python examples/quartic_opt/runner.py --n 6 --m 3
FlagDefaultDescription
--n5Number of variables
--m2Number of quartic terms
--solvercpuSolver backend (see Choosing a solver)
--seed42Random seed
-ostdoutWrite JSON result to file

Choosing a solver

Solver selection and install extras are the same for every example: see Using the Examples and Solving Overview. The default is cpu, and a non-default solver will not reproduce the canonical result.

Canonical output

example-smoke validates that the runner produces valid == 1 with --seed 42 --solver cpu. The smoke test is invariant-based – it checks validity, not exact output.

Cookbook

The fourteen examples under examples/ each solve one problem – Knapsack, TSP, Graph Coloring – and each already contains a working encoding. Nothing in the repository names those encodings as patterns you can reuse on a problem that is not itself Knapsack or TSP. This chapter names five recurring shapes instead, each grounded in the example or examples that embody it and shown as code that actually ran. One further page covers getting the arithmetic right once a shape is chosen.

This chapter assumes Modelling – it builds problems with the same xqcp calls that chapter introduces and does not re-explain them. It also assumes Quadratic Models for what a penalty term is and why one exists at all.

The Five Shapes

PageReach for it whenCanonical example
PermutationsN things need a full ordering: each thing gets exactly one position and each position gets exactly one thingexamples/tsp/
AssignmentN things each pick one of B slots, with no requirement that every slot is used or that N equals Bexamples/bin_packing/
Selection Under BudgetChoose a subset of weighted things without a total exceeding a fixed capacityexamples/knapsack/
Mutual ExclusionTwo choices conflict, or one requires anotherexamples/graph_coloring/, examples/max_independent_set/
Soft vs. Hard ConstraintsA rule that must hold sits next to a preference that should holdexamples/portfolio_opt/, examples/max3sat/

Permutations and Assignment are the same grid with one constraint family removed – see Assignment for the direction that mistake most often runs. Selection Under Budget and Mutual Exclusion overlap at one boundary: a capacity of exactly 1 on a pair is the same instruction family EXCLUDE gives you directly, covered in Mutual Exclusion. examples/bin_packing/ and examples/graph_coloring/ each compose two of these shapes in one problem (assignment plus a capacity, and assignment plus exclusion, respectively) – reading a problem as “which of these shapes does each part look like” scales to a combined problem the same way it does to a single-pattern one.

The Arithmetic Page

Integer Scaling is not a shape – every pattern above needs it, sooner or later, while turning real-valued problem data into the integer coefficients every pattern above is written in terms of. Sizing the penalty weight itself, and reading a solver’s output to tell whether a cheap constraint violation bought a good-looking energy, is Constraints’s territory, not a cookbook page of its own.

Where This Chapter Stops

None of these pages re-derives what a one-hot, EXCLUDE, or EQUALITY constraint expands to in linear and quadratic coefficients – that table is High-Level Constraints, already written, and this chapter links to it rather than repeating it. This chapter is about recognising which instruction family a new problem needs and what it costs once chosen, not about the instructions themselves.

Permutations

Assign N things to N positions so that each thing gets exactly one position and each position gets exactly one thing. Recognise it whenever “order” or “visit each once” appears in a problem statement: a tour, or a seating arrangement of N guests into N chairs.

The encoding is an \(N \times N\) binary grid, x[thing, position] = 1 meaning that thing sits at that position, with a one-hot constraint on every row and every column: ONEHOTR for “this thing takes exactly one position,” ONEHOTC for “this position holds exactly one thing.” A feasible assignment is a permutation matrix – exactly one 1 per row and per column – and the two constraint families together are what force that shape.

The Shape, Isolated

examples/tsp/ is the one example in this repository that embodies this pattern: x[city, position] = 1 means a city sits at a tour position, and the objective adds the distance between consecutive positions’ cities on top of the same row/column one-hot pair (TSP, examples/tsp/runner.py). Stripping the distance objective out leaves the pattern on its own – allocate the grid, apply both one-hot families, decode with colfind:

from xquad import cp

def build_problem(n: int) -> cp.Problem:
    problem = cp.Problem("PurePermutation")
    num_items = problem.input("num_items", type=cp.Types.Int)
    problem.define_model(size=num_items * num_items, domain=cp.Domain.BINARY,
                          rows=num_items, cols=num_items)

    with problem.range(0, num_items) as row:
        problem.model.apply_onehot_row(row, penalty=10)
    with problem.range(0, num_items) as col:
        problem.model.apply_onehot_col(col, penalty=10)

    perm = problem.output("perm", type=cp.Types.Vec)
    with problem.range(0, num_items) as position:
        perm.append(problem.sample.colfind(col=position, value=1))
    return problem

Compiling for n = 3 and running the encoder on the Rust VM, then solving the result with SolverCPU at seed=42. All six permutations of three things are tied at -60, so a different seed decodes to a different one and both lines below still hold:

decoded permutation (thing at each position): [0, 1, 2]
is a permutation of range(N): True
energy: -60

xquad vm check --text accepts the compiled encoder:

ok: permutation_encoder.xqasm (29 instructions)

and xquad vm run --text ... --calldata 3 reproduces the same model deterministically from the assembled bytecode, independent of the Python solve above:

outputs:
  [0] = Model(XqmxModel { domain: Binary, size: 9,
    linear: {0: -20, ..., 8: -20},
    quadratic: {(0, 1): 20, (0, 2): 20, ..., (7, 8): 20}, rows: 3, cols: 3 })

Nine linear terms at -20 (one per cell: each cell belongs to one row and one column, so it picks up -penalty twice) and eighteen quadratic terms at +20 (every same-row and every same-column pair) – exactly ONEHOTR’s and ONEHOTC’s expansions from High-Level Constraints superimposed on the same grid. examples/tsp/ builds the identical row/column structure and adds distance coefficients on top; nothing about the permutation half changes when an objective joins it.

Cost in Variables

The grid is \(N^2\) variables – no slack, no auxiliary variables, since both ONEHOTR and ONEHOTC only rewrite coefficients on cells that already exist. That quadratic growth is the real cost of this pattern: doubling the number of things to order quadruples the variable count, before an objective term is added.

Failure Mode

Applying only one axis – ONEHOTR without ONEHOTC, or the reverse – does not raise an error; it produces a model where each thing still picks exactly one position, but nothing stops two things from picking the same one. That looser rule is Assignment. The other failure is structural rather than semantic: ONEHOTR/ONEHOTC read grid dimensions from RESIZE, so a model with no grid set has no row or column for them to constrain. Both raise InvalidGridDimensions at the instruction that needed the grid – see High-Level Constraints. xquad vm check does not catch it, since grid dimensions are runtime values, so the failure surfaces when the encoder runs rather than when it compiles.

Assignment

Match each of N things to exactly one of B slots, with no requirement that every slot receives a thing and no requirement that B equals N. Recognise it whenever one set is matched into another with no requirement that the match run both ways: items into bins, nodes onto colours.

This is Permutations with the column half removed. Encode it as an \(N \times B\) grid, x[thing, slot] = 1 meaning that thing occupies that slot, with a one-hot family on every row only: each thing picks exactly one slot, but a slot may hold zero, one, or many things. Dropping the column constraint gives a different rule, not a weaker form of the same one, and Permutations names the mistake in the other direction: applying only one axis where both are wanted.

Recognising the Row-Only Shape

examples/bin_packing/ assigns each item to exactly one bin – one ONEHOTR per item row of its grid (Bin Packing, examples/bin_packing/runner.py):

# Assignment constraint: each item i must go in exactly one bin
with problem.range(0, num_items) as i:
    problem.model.apply_onehot_row(i, 200)

ONEHOTR is the special case of EQUALITY with \(a_k = 1\) and \(b = 1\), which High-Level Constraints states directly. Writing it by hand – a vec() pair per row, an index and a 1 pushed per column, then apply_equality(indices, coeffs, 1, 200) – expresses the same constraint and costs a loop and two vectors per row. The model here is defined with both rows and cols set, so apply_onehot_row applies and the hand-rolled form buys nothing.

Bin packing’s grid has one row more than it has items. Rows 0..N-1 are the assignment cells; the extra row N holds one indicator variable per bin, and apply_implies((i, b), (num_items, b), 200) opens bin b as soon as any item lands in it. That indicator row is what lets the objective count bins: a bias spread over the assignment cells would sum to N on every feasible packing, because each item lands in exactly one bin, so it could not tell a one-bin packing from a three-bin one.

Running examples/bin_packing/runner.py --seed 42 (4 items, 3 bins, capacity 5, sizes [1, 2, 1, 1]) decodes to assignment: [0, 0, 0, 0] – all four items in bin 0, total size 5 against a capacity of 5. Which single bin gets used is a tie: packing everything into bin 1 or bin 2 has the same energy, and the solver’s seed decides which one comes back. Every item appears in exactly one bin, which is what the row constraint guarantees; nothing requires a bin to be used, and the bin-count objective pushes the other way.

Bin packing composes this row-assignment pattern with a second, unrelated one – each bin’s contents must not exceed its capacity, encoded with SLACK plus EQUALITY exactly like the capacity constraint in Selection Under Budget. That capacity half is not this page’s concern; see Selection Under Budget for the pattern on its own.

Cost in Variables

The grid costs \(N \times B\) variables against Permutations’ \(N^2\), and the saving is entirely the grid: it is cheaper exactly when there are fewer slots than things (\(B < N\)). Dropping the column constraint costs nothing and saves nothing in variables, since neither one-hot family allocates any – EQUALITY with in-range indices does not grow model.size on its own (High-Level Constraints).

Failure Mode

The one this page warns about runs the other direction from Permutations: adding a column one-hot to an assignment problem that does not want one silently turns “N things into B slots” into “a bijection between things and slots,” which only has a feasible solution at all when \(N = B\). If a Permutations-shaped model with RESIZEd grid dimensions comes back consistently infeasible, check whether the problem actually needs both axes constrained or only one.

Selection Under Budget

Choose a subset of N things, each with a cost, so the total cost does not exceed a fixed capacity, while maximising (or minimising) something else about the chosen subset. Recognise it whenever a problem says “at most,” “no more than,” or “fits within” about a sum of per-item weights: a knapsack, a budget of assets, a shipment under a weight limit.

An inequality (<=) combined with a per-item weight that is not always 1 is what distinguishes this shape from Mutual Exclusion’s <= on a pair, or a plain one-hot’s = 1 – here the bound is a general capacity and the items carry arbitrary integer weights. None of ONEHOTR, ONEHOTC, EXCLUDE, or IMPLIES express an inequality; ATLEAST and ATLEASTW cover the mirrored >= direction directly (Vertex Cover, Weighted Set Cover), but XQVM has no <= counterpart over an arbitrary weighted sum. SLACK bridges the gap, turning the inequality into an equality that EQUALITY can enforce – see Constraints for why that substitution is exact, and SLACK for the instruction itself. model.apply_inequality(indices, coeffs, target, capacity, penalty) composes both calls into one – see Constraints, including the caveat there that the target parameter is not a target value: it is where slack variables begin, normally the count of real variables, and capacity is the actual bound. This page assumes both and covers only the recognition and the cost.

The Canonical Instance

examples/knapsack/ is this pattern with nothing else mixed in – Knapsack, examples/knapsack/runner.py. Calling its own build_problem with a fresh instance (six items, capacity 12) rather than the seed-42 instance the Modelling chapter already worked through:

from examples.knapsack.runner import build_problem
from xquad.vm import VM

n = 6
weights = [2, 3, 4, 5, 6, 7]
values = [3, 4, 5, 8, 9, 10]
capacity = 12

problem = build_problem(n, weights, values, capacity)
programs = problem.compile()

vm = VM()
vm.set_calldata([n, weights, values, capacity])
vm.set_output_slots(1)
vm.run(programs.encoder)
model = vm.outputs()[0]

model.size comes back 10: six item variables plus four slack bits, S = floor(log2(12)) + 1 = 4. Solving with SolverCPU and decoding:

selection [0, 0, 0, 1, 0, 1] weight 12 capacity 12 feasible True value 18 energy -14418
verifier energy -14418 valid 1

Items 3 and 5 (weights 5 and 7, values 8 and 10), weight exactly at capacity, value 18. Brute-forcing all 64 subsets confirms 18 is the true optimum, not just a feasible value: 28 of the 64 fit within capacity, and no other reaches value 18. xquad vm check on the compiled encoder (43 instructions) passes.

Cost in Variables

\(N\) item variables plus \(S = \lfloor \log_2(\text{capacity}) \rfloor + 1\) slack bits – logarithmic in the capacity, not the capacity itself, which is what makes representing “at most a million units” cost twenty extra variables rather than a million. Integer Scaling covers a consequence of this formula worth knowing before choosing how finely to scale a fractional capacity: the slack count grows with the scaled capacity, so a needlessly large scale factor costs real variables, not just larger coefficients.

examples/bin_packing/ repeats this exact SLACK + EQUALITY shape once per bin, for a per-bin capacity rather than a single global one – see Assignment, which covers the rest of that problem’s structure. Its start_index argument to SLACK advances per bin – model_vars + b * xq_bitlen(capacity) – so each bin’s capacity constraint reaches for its own slack block. Passing the same fixed start index on every iteration is the trap when copying this block as a per-bin template: the bins then share slack variables, and one bin’s overflow can be absorbed by another’s slack. The capacity-constraint shape itself, in isolation, is the same one this page covers.

Failure Mode

A SLACK/EQUALITY pair that looks right can still be infeasible for a reason that has nothing to do with the penalty weight: if every item’s weight already exceeds capacity, or the cheapest single item does, no slack combination rescues it, since slack only adds toward the target, never subtracts from the real items’ contribution (SLACK). Check the raw numbers – smallest item weight against capacity – before reaching for penalty-weight guidance to explain a solver that keeps returning something that looks wrong.

Mutual Exclusion

Two choices conflict, and a solution may take at most one of them. Recognise it in “not both,” “at most one,” or an adjacency rule that forbids a shared property between connected things: two colours on adjacent nodes, two overlapping bookings.

EXCLUDE is the direct opcode: pop penalty, then two variable indices, and add quad[i, j] += penalty – a pure coupling term, no linear part, that costs penalty exactly when both variables are 1 and nothing otherwise (High-Level Constraints). This page also covers IMPLIES, the directional relative: “picking i requires j” rather than “picking both is forbidden.”

EXCLUDE, Direct

examples/graph_coloring/ applies EXCLUDE once per edge per colour: two adjacent nodes may not both hold the same colour (Graph Coloring, examples/graph_coloring/runner.py):

with problem.range(0, num_edges) as e:
    offset = problem.stow("offset", e * 2)
    u = problem.stow("u", edges_in.get(offset))
    v = problem.stow("v", edges_in.get(offset + 1))
    with problem.range(0, num_colors_in) as c:
        problem.model.apply_exclude((u, c), (v, c), 200)

Running examples/graph_coloring/runner.py --seed 1 (5 nodes, 4 colours, 6 edges) returns colors: [0, 1, 0, 0, 2], is_valid: true, energy: -1000 – every one of the six edges checked by hand connects two nodes with different colours.

Forcing the default --seed 42 instance down to --colors 3 shows what EXCLUDE can and cannot do. That seed’s random edges form a 4-clique among nodes {0, 2, 3, 4}, every pair among them an edge, and a 4-clique has no 3-colouring at all. The run reports is_valid: false, energy: -800, and valid: 0; no adjustment to EXCLUDE’s penalty weight changes that. EXCLUDE enforces a rule, and it can only enforce a colouring that exists. The colour count defaults to 4 for exactly this reason. Note that the generated verifier’s valid agrees with the runner’s own is_valid here: it checks each EXCLUDE against the sample directly.

The Same Rule, Encoded Without EXCLUDE

examples/max_independent_set/ needs the identical x_i + x_j <= 1 rule per edge – two adjacent nodes cannot both be in the set – but builds it with SLACK plus EQUALITY instead of EXCLUDE (Max Independent Set):

edge_indices = problem.vec()
edge_coeffs = problem.vec()
edge_indices.push(ni)
edge_indices.push(nj)
edge_coeffs.push(1)
edge_coeffs.push(1)
problem.slack(edge_indices, edge_coeffs, num_nodes + e, 1)
problem.model.apply_equality(edge_indices, edge_coeffs, 1, 200)

Both encode the same inequality; EXCLUDE is a one-instruction convenience for exactly the pairwise case, while SLACK + EQUALITY costs one extra slack variable per edge to reach the identical rule (Selection Under Budget’s pattern, applied here to a capacity of 1). Reach for EXCLUDE whenever the conflict is pairwise, which it almost always is; reach for the manual form only when the exclusion needs to compose with other terms already riding the same index/coefficient vectors.

Running examples/max_independent_set/runner.py --seed 42 (same 5-node, 6-edge graph as the graph-coloring default above, node set {0, 2, 3, 4} forming the clique) returns in_set: [0, 1, 0, 0, 1] – nodes {1, 4}, is_independent: true. Running examples/vertex_cover/runner.py --seed 42 against the identical graph returns cover: [1, 0, 1, 1, 0] – nodes {0, 2, 3}, the exact complement of {1, 4} in {0, 1, 2, 3, 4}. That is not a coincidence of this one instance: a set of nodes covers every edge exactly when the nodes left out share no edge, so a minimum vertex cover and a maximum independent set on the same graph are always complements of each other. But vertex_cover’s own constraint, x_i + x_j >= 1 via ATLEAST, is not this page’s pattern – it requires at least one endpoint, the opposite direction from forbidding both. Do not reach for EXCLUDE when a problem says “at least one of,” even on the same graph shape that motivates exclusion elsewhere.

IMPLIES

No example in this repository calls apply_implies. A minimal demonstration, run rather than assumed, checks the sign directly:

from xquad import cp
from xquad.vm import VM

problem = cp.Problem("ImpliesDemo")
n = problem.input("n", type=cp.Types.Int)
problem.define_model(size=n, domain=cp.Domain.BINARY)
problem.model.apply_implies(0, 1, 50)   # picking 0 requires picking 1
problem.model.linear[0].add(-30)        # a reason to pick 0 at all

programs = problem.compile()
vm = VM()
vm.set_calldata([2])
vm.set_output_slots(1)
vm.run(programs.encoder)
model = vm.outputs()[0]

Reading the resulting coefficients back and evaluating all four assignments by hand:

linear: {0: 20}, quadratic[0,1]: -50
x0=0 x1=0 -> H=0
x0=0 x1=1 -> H=0
x0=1 x1=0 -> H=20
x0=1 x1=1 -> H=-30

x0=1, x1=0 – picking 0 without 1, the forbidden combination – costs 20 more than leaving both off, and exactly 50 more than the legal x0=1, x1=1, matching the penalty=50 passed in. The -30 reward on x0 alone is what gives the solver a reason to pick it in the first place; without it, x0=0 would dominate trivially and the implication would never be tested. IMPLIES’ own expansion, linear[i] += penalty, quad[i, j] += -penalty (High-Level Constraints), is exactly what produced 20 and -50 here: linear[0] = -30 + 50 = 20, no change to linear[1], quadratic[0,1] = 0 - 50 = -50.

Cost in Variables

EXCLUDE and IMPLIES add no variables at all – both rewrite an existing coefficient pair. The SLACK-based encoding of the same rule costs one extra slack bit per pair, since a capacity of 1 needs \(S = \lfloor \log_2(1) \rfloor + 1 = 1\) bit (SLACK).

Failure Mode

A graph that needs more colours than it is given, as the graph-coloring seed-42 default above shows, is not a penalty-tuning problem: no penalty value turns an infeasible instance feasible, since the penalty only controls how costly a violation is, not whether one is avoidable. If EXCLUDE or IMPLIES constraints keep the best returned sample stuck at a nonzero violation at more than one penalty weight, check whether the underlying combinatorial structure allows a solution to exist before reaching for penalty-weight guidance.

Soft vs. Hard Constraints

Every rule in a quadratic model is a penalty term, but not every penalty term means the same thing to the problem it belongs to. A hard constraint is a rule the solution must obey. It is satisfied or it is violated, with nothing in between, and the only question a penalty weight answers is how expensive violating it becomes. A soft constraint – a preference is the more precise name – is a cost a solution pays continuously: more of the disfavoured thing costs more, but nothing marks any amount as forbidden. apply_equality, apply_onehot_row, apply_exclude, and the rest of the constraint family from Constraints are how a hard rule is written. A preference is written directly into the objective, the way Objectives covers, with no constraint call at all.

Both in One Problem

examples/portfolio_opt/ carries one of each. The budget is hard – sum(x_i) = B via apply_equality, unconditional, penalty 200 – and the risk term is soft – a cubic penalty added straight into the objective for every asset triple whose combination the problem considers risky, with no accompanying constraint call (Portfolio Optimization, examples/portfolio_opt/runner.py):

# Cubic risk cross-terms via REDUCE
with problem.range(0, num_risk) as t:
    ...
    w = problem.model.reduce(ti, tj, _P_AUX)
    problem.model.quadratic[w, tk].add(sigma)

# Budget constraint: sum(x_i) = B  (EQUALITY with unit coefficients)
...
problem.model.apply_equality(indices, coeffs, budget_in, 200)

reduce and ADDQUAD build the risk cost the same way any other objective term is built – see Objectives and High-Level Constraints for REDUCE’s own Rosenberg terms, which enforce the auxiliary variable’s algebraic identity, not the risk rule itself. Nothing about sigma is a threshold; it is a coefficient like any other, weighing a return against a risk directly in the same sum.

The Discriminating Difference: A Threshold vs. a Continuous Trade-off

Fix four assets with returns [10, 9, 8, 7], a budget of 3, and one risky triple (0, 1, 2, sigma), then sweep sigma. run_case below covers the encoder half – compiling the problem, running the encoder to build the model, and solving it – the same way the earlier snippets on this page do. Decoding the returned sample back into a portfolio runs programs.decoder over [sample, n], exactly as examples/portfolio_opt/runner.py does it. That step is elided here because it is the same on both sides of the comparison:

from examples.portfolio_opt.runner import build_problem
from xquad.vm import VM
from xqsa import build_solver

def run_case(sigma):
    risk_terms = [(0, 1, 2, sigma)]
    problem = build_problem(4, [10, 9, 8, 7], 3, risk_terms)
    programs = problem.compile()
    flat_risk = [v for term in risk_terms for v in term]
    vm = VM()
    vm.set_calldata([4, [10, 9, 8, 7], 3, 1, flat_risk])
    vm.set_output_slots(1)
    vm.run(programs.encoder)
    model = vm.outputs()[0]
    result = build_solver("cpu", seed=42).solve(model, num_reads=200)
    ...  # decode via programs.decoder and print, as below

Decoded and printed, sigma=1 and sigma=2 give:

sigma=1: portfolio=[1, 1, 1, 0] total_return=27 energy=-1826
sigma=2: portfolio=[1, 1, 0, 1] total_return=26 energy=-1826

Across twenty solver seeds at each sigma (seed in range(1, 21)), sigma=2 always switches to {0, 1, 3} (return 26), stable on every seed. sigma=1 does not behave the same way: it sits exactly at the tie, so the sampler returns either portfolio, split close to evenly across seeds – {0, 1, 2} on 12 of 20 and {0, 1, 3} on 8 of 20, both at the identical energy -1826. The tie is exact, not approximate: asset 2’s risk cost only applies when all three of 0, 1, 2 are selected together (REDUCE’s product term), so choosing the risky trio costs -27 + sigma against the safer trio’s fixed -26; the two costs are equal at sigma = 1 and the risky choice stops paying for itself once sigma > 1, which is exactly where the stable switch to {0, 1, 3} sets in. A weight that lands exactly on a tie is not evidence either portfolio is preferred – it is evidence the weight is at the boundary, and a single sample there does not tell you which side you are on. Every selection in this sweep still has exactly three assets – the hard budget constraint held at every sigma, unaffected – while which three assets the soft risk cost favoured changed at its break-even point, with a tie rather than a clean switch exactly at that point. That contrast is what the two kinds of constraint buy: a hard rule’s own penalty weight only decides whether the rule can be broken at all (Quadratic Models, Constraints), while a soft term’s weight decides how much of the preference the solution actually buys.

Soft, With No Hard Constraint at All

examples/max3sat/ sits at the other end: every clause is a preference, none is a rule the solution must satisfy, and the problem carries no apply_* call anywhere (Max-3-SAT). A clause (i, j, k) costs P_CLAUSE exactly when all three of its literals are false, added directly to the objective; a solution that satisfies zero clauses is legal, just expensive. Running examples/max3sat/runner.py --seed 42 (6 variables, 8 clauses) satisfies all eight (satisfied: 8, energy: -80) for this random instance, but nothing in the model would reject a sample that satisfied fewer – there is no valid check tied to clause satisfaction, because there is no constraint to check.

The valid flag draws exactly this line. A hard constraint is an apply_* call, and the generated verifier emits one check per call: portfolio_opt’s budget is an apply_equality, so a sample selecting all four assets against a budget of 3 comes back valid: 0. Max-3-SAT’s clauses are objective terms, so nothing in its verifier looks at them and a sample satisfying two clauses is as valid as one satisfying eight. If a rule must hold, declare it as a constraint; Verification says what valid then covers.

Cost in Variables

Neither pattern taxes the model directly – a soft term is ordinary linear/quadratic coefficient writes, and a hard constraint’s cost is whatever its own instruction adds (ATLEAST/ATLEASTW/REDUCE grow model.size; EXCLUDE/IMPLIES/EQUALITY-with-in-range-indices do not). portfolio_opt’s risk terms cost one REDUCE auxiliary variable each, so the model grows from n to n + num_risk, independent of the hard budget constraint sharing the same model.

Failure Mode

Writing a preference as a hard constraint at a large penalty forces an all-or-nothing rule where a graded cost was wanted – the model will never choose to pay a little of the disfavoured thing even when the rest of the objective would gain more from it, because the constraint’s cost jumps from 0 to penalty * d^2 the instant it is touched at all (Constraints). The reverse mistake, writing a rule that must always hold as a soft objective term, is worse: nothing stops the solver from breaking it whenever the rest of the objective offers enough of a reward, and there is no way to check afterward that it did not, the way max3sat’s missing valid check above demonstrates directly.

Integer Scaling

Every XQMX coefficient is an i64, and every arithmetic operation on one is exact – Quadratic Models and Energy and Precision both rest on that fact. Real problem data rarely arrives as integers: prices, weights in kg, fractional returns. This page covers turning that data into i64 coefficients without losing precision the problem needs, and what a scale factor costs once it is chosen. No example in this repository has fractional inputs to scale, so everything below is a fresh, executed worked instance rather than a citation.

Choosing the Scale Factor

Multiply every fractional value by a factor large enough that the result is exactly an integer, and use that integer as the coefficient. The factor has to be large enough for every value in the problem, not just the ones that look round. A four-item knapsack with quarter-kilogram weights and cent-precision values:

weights_f = [2.50, 3.75, 1.25, 4.00]
values_f = [10.20, 15.75, 8.40, 20.00]
capacity_f = 7.50

def scale(values, factor):
    scaled = [round(v * factor) for v in values]
    for v, s in zip(values, scaled):
        assert abs(v * factor - s) < 1e-9, (v, factor, s)   # catch silent rounding
    return scaled

Scaling by 4 – enough for the weights, which are all quarters – fails the assertion the moment it reaches the values: 10.2 * 4 = 40.8 is not integral (rounds to 41), so scale() raises before the demo builds a model from this factor:

AssertionError: (10.2, 4, 41)

The values are cent-precision but not all quarters (10.20, 8.40 are not multiples of 0.25), so 4 is not enough. The minimal factor that is exact for every value here is the least common multiple of every value’s decimal denominator – 20 (every one of these numbers is a multiple of 0.05), not the naive 100 a “two decimal places” guess would reach for:

from fractions import Fraction
from math import lcm
denoms = [Fraction(v).limit_denominator(10000).denominator
          for v in weights_f + values_f + [capacity_f]]
lcm(*denoms)   # 20

At factor = 20: weights = [50, 75, 25, 80], values = [204, 315, 168, 400], capacity = 150. Building this through xqcp (the same SLACK + EQUALITY shape Selection Under Budget covers, at the penalty weight of 4 derived in Coefficient Magnitude below), compiling, running the encoder on the Rust VM, solving with SolverCPU, and dividing the decoded totals back by the scale factor:

selection=[1, 1, 1, 0] total_weight=7.5 total_value=34.35 capacity_ok=True energy=-90687

7.5 and 34.35 are exact – not rounded back, computed by an integer sum divided by 20 with no remainder, since 20 was chosen to be exact for every input. xquad vm check accepts the compiled encoder (43 instructions), and the same selection, {0, 1, 2}, comes back identically across five different solver seeds.

What Scaling Costs: Slack Bits

SLACK’s bit count is \(S = \lfloor \log_2(\text{capacity}) \rfloor + 1\) (SLACK) – logarithmic in the capacity, but the capacity that matters is the scaled one. The naive factor = 100 scaling of the same instance needs 10 slack bits (capacity = 750); the minimal exact factor = 20 needs 8 (capacity = 150). Scaling five times more finely than the data requires costs two extra variables here, and the gap widens as the capacity grows – an unnecessarily large scale factor is not free, even before its effect on coefficient magnitude below.

What Scaling Costs: Coefficient Magnitude, and Two Backends That Disagree

A penalty weight multiplies every constraint coefficient (High-Level Constraints), so a scale factor and a penalty compound. Using the loose safe penalty Constraints gives as a fallback – one more than the sum of absolute linear coefficients, 1088 for the values above, since the rule is penalty greater than that sum – against the factor = 20 model:

loose_penalty=1088: max |coefficient| = 23,953,408
fits MAX_NATURAL_COEFFICIENT (2,147,483): False

Quip Network confirms the exact bound directly from xqsa.quip.codec:

from xqsa.quip.codec import MILLI_SCALE, MAX_NATURAL_COEFFICIENT
print(MILLI_SCALE, MAX_NATURAL_COEFFICIENT)
# 1000 2147483

The loose bound, safe on its own terms, overflows SolverQuip’s milli-scale i32 encoding more than tenfold at this scale factor. Enumerating this specific instance the way Constraints enumerates its own – sixteen subsets, tightest violator {0, 2, 3} at weight 155, value gap 85 over an excess of 5 – gives a tight threshold of 85 / 5^2 = 3.4, so the smallest safe integer penalty is 4, not 1088:

tight penalty=4: max |coefficient| = 88,064
fits MAX_NATURAL_COEFFICIENT: True

4 clears the tight threshold and the solver still returns the correct optimum, {0, 1, 2}, identically across five seeds – the two hundred seventy-two-fold gap between the loose and tight penalty is exactly what made the difference between overflowing SolverQuip’s encoding and fitting it comfortably. This is Constraints’s own warning about the loose bound being loose, made concrete by a scale factor large enough to expose it: a bound that is merely safe at natural scale can become the deciding factor once a scale factor multiplies every coefficient it touches.

The same coefficient growth affects the Metal engine’s float32 search differently from the CUDA engine’s float64 one, the way Energy and Precision covers for an unscaled model. At the loose penalty above, the resulting solved energy (-24,480,687, order \(10^7\)) is past float32’s roughly seven decimal digits of resolution for a difference of 1, but not for a difference of 100:

import numpy as np
e = -24480687   # this model's actual solved energy at the loose penalty
np.float32(e) != np.float32(e - 1)     # False -- the 1-unit difference is lost
np.float32(e) != np.float32(e - 100)   # True -- a 100-unit difference still resolves

A scaling choice that looks safe by natural-scale reasoning can cost the Metal engine resolution that the CUDA engine keeps, on the identical model – check the actual coefficient magnitudes your factor produces against the target backend, not just against the input data’s precision.

Cost in Variables

\(S = \lfloor \log_2(\text{scaled capacity}) \rfloor + 1\) slack bits on top of the item count, same formula as Selection Under Budget, with the scaled capacity – not the original – as the input.

Failure Mode

Picking a scale factor from “how many decimal places does this look like” rather than the actual least common multiple of every value’s denominator either drops precision silently (a factor too small, caught here only because the demo asserts the rounding was exact) or costs variables and coefficient headroom for no reason (a factor larger than any value needs, the 100 vs. 20 case above). Compute the minimal exact factor once, from every value the problem uses, rather than guessing a round number.

CLI Reference

xquad is the command-line driver for the XQuad toolchain. It ships with the xquad Python package, so the install that gives you the Python API also puts the command on your PATH.

Installation

Install XQuad into a project environment first: a virtual environment, or a uv project.

python -m venv .venv && source .venv/bin/activate
pip install xquad
# or, in a uv project:
uv add xquad

For the xquad command alone, outside any project, install it as a uv tool, with the quip extra for the xquad quip commands:

uv tool install xquad            # the xquad command
uv tool install 'xquad[quip]'    # with the xquad quip commands

On macOS, a user install (pip install --user, or pip falling back to one) puts the xquad command in ~/Library/Python/3.x/bin, which is not on PATH; python -m xquad runs the same command without it.

More generally, if your shell reports xquad: command not found, the script directory of that Python environment is not on PATH, and python -m xquad --help works in its place.

From a checkout of this repository, make xquad syncs the workspace virtual environment, and uv run xquad runs the command from it.

The command used to be a Rust binary from the xqcli crate, installed with cargo install. That crate is retired. If you installed it, run cargo uninstall xqcli, or the old binary in ~/.cargo/bin may shadow the new command on PATH.

Commands

xquad groups its commands by what they work on:

GroupDescription
xquad vmAssemble, disassemble, run and verify XQVM programs.
xquad quipAccounts and job orders on a Quip network. Needs the quip extra.
xquad --help
xquad <GROUP> --help
xquad --version

xquad vm

xquad vm is the command group for the XQVM tools: assemble, disassemble, run and verify programs. It is part of the xquad command-line driver; CLI Reference covers installing the command.

Commands

The XQVM tools sit under xquad vm, one per stage of the toolchain:

CommandDescription
xquad vm asmAssemble .xqasm source into binary bytecode.
xquad vm disDisassemble bytecode into a human-readable listing.
xquad vm runExecute bytecode or assembly, with optional tracing.
xquad vm checkRun the bytecode verifier over a program without running it.

The old top-level names xquad asm, xquad dism, xquad run and xquad verify still work in this release. Each prints one deprecation warning to stderr when it runs and otherwise behaves exactly like its xquad vm counterpart, so stdout and the exit code are unchanged. They are removed in v0.6.0.

General usage

xquad vm <COMMAND> [OPTIONS] [ARGS]
xquad vm <COMMAND> --help

xquad vm asm reads .xqasm source and writes bytecode; it has no --text flag. xquad vm dis reads bytecode from a file or stdin, also with no --text flag. xquad vm run and xquad vm check read bytecode by default and read .xqasm assembly instead when --text is passed.

Exit codes and errors

CodeMeaning
0Success.
1The command ran and failed: an unreadable file, an assembly or decode error, a verifier violation, or a VM fault.
2A usage error: an unknown option, a missing argument, or two options that cannot be combined.

Errors are one line on stderr, prefixed Error: and naming the stage that failed:

Error: assembly failed: unknown mnemonic `BADOP`

A usage error prints the command’s usage line first. Errors carry no source snippet: a VM fault names the error variant and its byte offset (run failed: StackUnderflow { pos: 2 }), and xquad vm dis on the same bytecode shows the instruction at that offset.

A minimal round trip

cat > add.xqasm <<'EOF'
; add.xqasm -- push two integers and add them
PUSH 10
PUSH 32
ADD
HALT
EOF

xquad vm asm add.xqasm -o add.xqb
xquad vm check add.xqb
xquad vm run add.xqb

xquad vm run add.xqb prints the residual stack:

stack (bottom to top):
  42

xquad vm asm

Assemble an .xqasm source file into binary XQVM bytecode.

Usage

xquad vm asm <INPUT> [-o <OUTPUT>] [--stdout]

Arguments

ArgumentDescription
INPUTPath to the assembly source file.

Options

OptionDefaultDescription
-o, --output <OUTPUT><INPUT> with its extension replaced by .xqbOutput file path.
--stdoutoffWrite bytecode to stdout instead of a file. Conflicts with -o/--output.

-o and --stdout are mutually exclusive; passing both is a usage error (exit 2), not a runtime one:

xquad vm asm add.xqasm -o build/program.xqb --stdout
Usage: xquad vm asm [OPTIONS] INPUT
Try 'xquad vm asm --help' for help.

Error: --output cannot be used with --stdout

Examples

Assemble to the default output

xquad vm asm add.xqasm

Writes add.xqb next to the source and prints a summary to stderr:

assembled 4 instructions (21 bytes) -> add.xqb

Assemble to a specific output file

mkdir -p build
xquad vm asm add.xqasm -o build/program.xqb

Assemble does not run the verifier

xquad vm asm only assembles; it does not check the result with xquad vm check. A source file with no mnemonic errors can still fail verification once assembled.

Pipe bytecode to another tool

xquad vm asm add.xqasm --stdout | xquad vm dis
  0x0000:  PUSH1   10
  0x0002:  PUSH1   32
  0x0004:  ADD     
  0x0005:  HALT    

Output

On success, prints a summary to stderr (suppressed with --stdout, since the bytecode itself occupies stdout). The instruction count includes every TARGET the assembler emits, so a program with jumps counts higher than its visible mnemonics. branch.xqasm below has eight mnemonic lines and two labels (.0, .1), each of which emits a TARGET:

PUSH 5
PUSH 10
GT
JUMPI .0
PUSH 99
JUMP .1
.0: PUSH 0
.1: HALT
xquad vm asm branch.xqasm -o branch.xqb
assembled 10 instructions (31 bytes) -> branch.xqb

Error reporting

An assembly error is one line on stderr, and the command exits 1:

Error: assembly failed: unknown mnemonic `BADOP`

xquad vm dis

Disassemble XQVM bytecode into a human-readable listing.

Usage

xquad vm dis [FILE]

Arguments

ArgumentDescription
FILEBytecode file to disassemble. Reads from stdin when omitted.

dis decodes FILE (or stdin) as a full XQBC container, the same format xquad vm asm writes: a 15-byte header (magic, version, code length, CRC32) followed by the raw instruction stream. It has no path for a bare instruction stream missing that header – a buffer that starts straight with opcode bytes fails to decode on the magic check before disassembly gets a chance to run, and the command exits 1:

Error: failed to decode program: decode error: XQBC header is truncated
``` This is why the stdin example below pipes from
`xquad vm asm --stdout`, which emits a full container, rather than from a
raw instruction buffer.

## Examples

### Disassemble a file

```sh
xquad vm dis add.xqb
  0x0000:  PUSH1   10
  0x0002:  PUSH1   32
  0x0004:  ADD     
  0x0005:  HALT    

Disassemble from stdin

xquad vm asm add.xqasm --stdout | xquad vm dis

Produces the same listing as above; dis reads the bytes either way.

A program with jumps

branch.xqb, assembled from a program with two labels (.0, .1) and two jumps:

  0x0000:       PUSH1   5
  0x0002:       PUSH1   10
  0x0004:       GT      
  0x0005:       JUMPI1  .0
  0x0007:       PUSH1   99
  0x0009:       JUMP1   .1
  0x000B:  .0:  TARGET  
  0x000C:       PUSH1   0
  0x000E:  .1:  TARGET  
  0x000F:       HALT    

Output format

Each line shows a byte offset, an optional label, and a decoded instruction:

  • Byte offset (0x0000:) – position in the instruction stream.
  • Label column – present only when the program contains at least one TARGET. When present, it holds .0:, .1:, and so on at the offset where each TARGET opcode sits, and is blank on every other line so the instruction column still lines up. When the program has no TARGET at all, as in add.xqb above, the column is omitted entirely rather than printed blank on every line.
  • Instruction – mnemonic and decoded operands. JUMP/JUMPI operands are printed as the same .N label that marks their destination, not as a raw byte offset or an internal id.
  • PUSH values – shown as sign-extended decimal integers (PUSH1 10, not a hex byte).
  • Undecodable bytes – an invalid opcode or a truncated operand does not abort the listing. It is rendered as a .byte 0xNN pseudo-instruction and the walk continues from the next byte, so no byte is silently dropped from the output.

The label numbering comes from a single left-to-right scan over the decoded program: the first TARGET found is .0, the second is .1, and so on. This is the same numbering the assembler resolves .0/.1-style source labels against, so a disassembly’s labels read back as valid .xqasm jump targets – provided the listing contains no .byte lines. .byte is not a recognised mnemonic, so a listing that hit an undecodable byte is not itself valid .xqasm input, even though its labels are correctly numbered. See Bytecode Format for how that scan works and why a JUMP operand cannot be validated in isolation from it.

xquad vm run

Execute XQVM bytecode or assembly source, printing the residual outputs and stack, with optional step-by-step tracing.

Usage

xquad vm run [OPTIONS] <FILE>

Arguments

ArgumentDescription
FILEBytecode (.xqb) file to run, or assembly (.xqasm) source when --text is set.

Options

OptionDefaultDescription
--textoffTreat FILE as assembly source and assemble it before running.
--calldata <CALLDATA>noneComma-separated i64 integers passed to INPUT instructions.
--outputs <OUTPUTS>16Number of output slots available for OUTPUT instructions.
--step-limit <STEP_LIMIT>10000000, the VM’s DEFAULT_STEP_LIMITMaximum number of steps to execute. The limit is exact: 0 executes nothing. Conflicts with --unlimited-steps. See Step limits.
--unlimited-stepsoffRun without a step limit. Conflicts with --step-limit. A program that never halts will not return.
--memory-limit <MEMORY_LIMIT>1073741824 (1 GiB), the VM’s DEFAULT_MEMORY_LIMITAllocation budget in bytes for models, samples and vectors. See The allocation budget.
--traceoffEnable step-by-step execution tracing.
--trace-format <TRACE_FORMAT>textTrace output format: text or json. Requires --trace.
--trace-file <TRACE_FILE>stderrWrite trace output to a file instead of stderr. Requires --trace.

xquad vm run does not call the verifier. A program that fails verification can still be handed to run; some defects (an unset register read, for example) are also caught by the VM’s own runtime checks and abort with a runtime error, but others are not. See xquad vm check and Verification for what static verification does and does not guarantee.

Examples

Run bytecode

xquad vm run add.xqb
stack (bottom to top):
  42

Run assembly directly

xquad vm run --text add.xqasm

Skips the separate xquad vm asm step; the source is assembled in memory and run immediately.

Pass calldata and read outputs

INPUT and OUTPUT each take one register operand in assembly, but both pop the slot index off the value stack at runtime – the index is not implicit in the instruction. A program that reads calldata[0] into r0, adds one, and writes the result to output slot 0 needs the index pushed explicitly before each call: PUSH 0; INPUT r0; ...; PUSH 0; OUTPUT r1; HALT.

xquad vm run io.xqb --calldata 41
outputs:
  [0] = Int(42)

--calldata accepts a comma-separated list; each value is consumed in order by successive INPUT instructions. OUTPUT faults UnsetRegister if the register it reads was never written, the same as LOAD. Both INPUT and OUTPUT fault with CallDataIndex/OutputIndex respectively if the popped index is out of range for the calldata or output-slot array – the index itself is not guaranteed to be valid just because it came off the stack. See Register I/O for the full set of INPUT/OUTPUT error modes.

Enable tracing

xquad vm run regs.xqb --trace

Trace lines go to stderr:

step    offset    instruction            stack                      read-regs        written-regs   
     1  0x0000    PUSH1 7                [7]                                                        
     2  0x0002    STOW r0                []                                          r0=7           
     3  0x0004    LOAD r0                [7]                        r0=7                            
     4  0x0006    HALT                   [7]                                                        

and the residual stack goes to stdout, separately from the trace:

stack (bottom to top):
  7

Trace lines go to stderr by default so the stdout result is not interleaved with them in a terminal. Redirect stderr separately to capture the trace on its own:

xquad vm run regs.xqb --trace 2>trace.txt

JSON trace

xquad vm run regs.xqb --trace --trace-format json --trace-file trace.jsonl

trace.jsonl then holds one JSON object per step:

{"step":1,"pos":0,"instruction":"PUSH1 7","stack":[7],"read_regs":{},"written_regs":{}}
{"step":2,"pos":2,"instruction":"STOW r0","stack":[],"read_regs":{},"written_regs":{"0":{"type":"int","value":7}}}
{"step":3,"pos":4,"instruction":"LOAD r0","stack":[7],"read_regs":{"0":{"type":"int","value":7}},"written_regs":{}}
{"step":4,"pos":6,"instruction":"HALT","stack":[7],"read_regs":{},"written_regs":{}}

Custom step limits

countloop.xqb assembles a five-iteration RANGE loop around a NOP: six encoded instructions, 14 executed steps, because the body and its NEXT run once per iteration. A limit that is reached before HALT aborts the run:

xquad vm run countloop.xqb --step-limit 3
Error: run failed: StepLimitExceeded { pos: None, requested: 1, used: 3, limit: 3 }

The limit is exact, and 0 is not a sentinel for “unlimited”. A zero budget permits no instructions at all, so the first fetch fails:

xquad vm run countloop.xqb --step-limit 0
Error: run failed: StepLimitExceeded { pos: None, requested: 1, used: 0, limit: 0 }

Passing --unlimited-steps alongside --step-limit is a usage error (exit 2) rather than resolved by precedence, so opting out of the bound cannot be said two ways at once:

xquad vm run countloop.xqb --step-limit 0 --unlimited-steps
Usage: xquad vm run [OPTIONS] FILE
Try 'xquad vm run --help' for help.

Error: --step-limit cannot be used with --unlimited-steps

The conflict fires on an explicit --step-limit, not on the default, so --unlimited-steps on its own is accepted. It removes the bound entirely, which is only safe where you control the program or can abandon the thread – a program that never halts will not return:

xquad vm run countloop.xqb --unlimited-steps

prints nothing, since the program leaves no outputs and no residual stack. To raise the limit rather than remove it, pass a larger number, up to u64::MAX:

xquad vm run countloop.xqb --step-limit 18446744073709551615

also runs to completion, but for a different reason: the limit is higher, not absent. A budget that exactly covers the program succeeds – --step-limit 14 runs countloop.xqb to completion, and --step-limit 13 does not.

Output

After execution, xquad vm run prints, in order:

  1. Outputs – every output slot that was written, with its index and value.
  2. Stack – any values remaining on the value stack, bottom to top.

A run that both writes outputs and leaves values on the stack prints both sections:

outputs:
  [0] = Int(42)
  [1] = VecInt([1, 2, 3])
stack (bottom to top):
  7

Error reporting

A runtime fault is one line on stderr naming the VM error and the byte offset it happened at, and the command exits 1. underflow.xqb is PUSH 1 / ADD / HALT: ADD needs two stack values and only one was pushed.

xquad vm run underflow.xqb
Error: run failed: StackUnderflow { pos: 2 }

xquad vm dis underflow.xqb shows the ADD at 0x0002. The trace options above write the steps that led there.

xquad vm check

Run the bytecode verifier over a program without executing it, and report the first violation found.

Usage

xquad vm check [OPTIONS] <FILE>

Arguments

ArgumentDescription
FILEProgram to check: bytecode (.xqb) by default, assembly (.xqasm) when --text is set.

Options

OptionDefaultDescription
--textoffTreat FILE as assembly source and assemble it before checking.

xquad vm check runs the same default pipeline documented in Verifier: structural checks, jump-target checks, loop-nesting balance, register type-state, must-init analysis, and stack depth, in that order, stopping at the first phase that fails. This page covers only the command’s own behaviour; for what each phase checks and what each error means, see that page. For a task-oriented walkthrough of fixing a rejected program, see Verification.

A passing program

xquad vm check add.xqb
ok: add.xqb (4 instructions)

The instruction count includes the whole decoded stream: labelled positions as well as visible mnemonics. check writes this line to stdout and exits 0. With --text, the same message names the .xqasm source path rather than a .xqb file, since the source is assembled in memory first and never written to disk.

A failing program

uninit.xqasm reads register r0 before anything ever writes it:

LOAD r0
HALT
xquad vm check --text uninit.xqasm
Error: check failed: ReadUnsetRegister: register r0 read at byte 0x0000 before being written

check exits 1 on the first violation any phase finds. A program with two independent defects reports only this one; fixing it and re-running may surface the next.

Passing verification is not a runtime guarantee

Verification is a set of static checks over types, control flow and stack depth. It does not look at values. An allocator size, a grid extent, a loop bound and every arithmetic operand are runtime quantities, so a program that allocates a negative model, resizes past its own variable count, overflows an i64 or exhausts its step or allocation budget verifies cleanly and faults when run. See Verifier for the precise scope of what a pass proves, and Limits and Errors for the faults that have no static counterpart.

xquad quip

xquad quip manages Quip accounts and reads the job orders they placed on a Quip network. It is a front end over the xqsa.quip Python API that Quip Network and Job Orders describe; it places no orders itself. Submit orders from Python, then follow them from the shell.

CommandDescription
account listThe keystores found, the order that picks the signer, and how to change it.
account showThe account in effect: its source, balance, reserved amount and order slots.
account newCreate a keystore with a fresh seed, and use it.
account importCreate a keystore from a seed you already have, and use it.
account usePrefer an account at a level, or clear the preference.
ordersThe account’s orders not yet closed, one line each.
statusOne order’s receipt.
reclaimRecover the reward of a final order nobody answered.

Installation

The group needs the quip extra, which brings in the chain client and the signing extension:

pip install 'xquad[quip]'

Without it, every xquad quip command, and --help below the group level, prints this on stderr and exits 1, while xquad quip --help, xquad --help and xquad vm keep working:

`xquad quip` needs the Quip extra: `pip install 'xquad[quip]'`

Global options

These go between quip and the command, as in xquad quip --network aglais account show:

FlagMeaning
--network NAMEA named network preset, such as aglais (see Named networks).
--url URLA node’s RPC URL. Defaults to QUIP_RPC_URL.
--keystore PATHSign with this keystore, over every preference.
--jsonPrint one JSON object on stdout instead of text.

--network and --url are exclusive. A command that reads the chain needs one of them, or QUIP_RPC_URL:

Error: name a network with --network or a node with --url, or set QUIP_RPC_URL

account list, account new, account import and account use work on local files only and need no network.

What signs

Only reclaim sends a transaction, and only it spends anything: each reclaim pays its own fee. account show, orders and status read through a read-only client that resolves no signer, so they never create a keystore. Under QUIP_SIGNER_SEED, account show and orders still build a signer, to learn which account the seed is; they sign nothing with it. account new, account import and account use write files under .quip/ or ~/.quip/ and touch no chain.

Output and exit codes

With --json, a command prints exactly one JSON object on stdout. Accounts are SS58 text and amounts are integers in planck; the text output formats amounts in the network’s token instead. --json is an option of the group, so it goes before the command: xquad quip --network aglais --json account show. Errors go to stderr prefixed Error:, in both modes. The no-account error carries the account list guidance under it, and a usage error prints a Usage: line above it; the per-order lines of reclaim --all and the missing-extra hint have no prefix.

Exit codeMeaning
0Success.
1The command failed: a chain or account error, the missing extra, no account set up, or a reclaim --all in which some order was not reclaimed.
2A usage error, such as an unknown flag or --network with --url.

Accounts

A Quip account is a keystore file holding a 32-byte master seed in plain text. Keystores live at two levels, as git keeps config: a project level in the nearest .quip/ directory above the working directory, and a user level in ~/.quip/. Each level also holds an account preference naming the keystore to sign with. Accounts on the Quip Network page covers the layout and the full signer order; this page covers the commands.

The outputs on this page were captured on 2026-10-26 in a fresh project directory with a throwaway account, against aglais at specVersion 119.

Levels

account new, account import and account use act on one level:

  • --project acts on the project level, creating .quip/ in the working directory when none is found.
  • --global acts on the user level.
  • With neither, the level is the project when the working directory is inside one, else the user.

Anything created under a project .quip/ also writes a .quip/.gitignore of * when there is none, so keystores stay out of version control.

account list

xquad quip account list

Lists every keystore found, the order that decides which one signs, and how to change it. It reads local files only:

Keystores found:

* default  project  .quip/keystores/default.json
           5FXRydTJYE4cPGBH9CSf3dyGvaJNnYATEZJMyFqMiLPHv9oc

Resolution order (the first match signs):

  1  QUIP_SIGNER_SEED     unset
  2  QUIP_KEYSTORE        unset
* 3  project preference   default (.quip/config.toml)
  4  user preference      unset
  5  default              ~/.quip/keystore.json

Setting the account:

  xquad quip account use <name>             prefer <name> in this project, else for your user
  xquad quip account use <name> --global    prefer <name> for your user
  xquad quip account use --unset [--global] clear the preference at that level
  --keystore PATH, QUIP_KEYSTORE, QUIP_SIGNER_SEED override the preferences

* marks the keystore that signs and the step that chose it. A keystore that cannot be read, or a preference naming one that does not exist, is listed with a ! line saying why, rather than failing the listing. With --keystore PATH, the keystore list marks that file * with an (override) suffix, as a - row when it is not one of the listed keystores, and no step of the resolution order is marked.

account show

xquad quip --network aglais account show

The setup check: the account in effect, where it came from, and what it holds on the network.

Account   5FXRydTJYE4cPGBH9CSf3dyGvaJNnYATEZJMyFqMiLPHv9oc
Source    project preference default
Balance   0.000000000000 AGLS
Reserved  0.000000000000 AGLS
Orders    0 of 32 slots used

Balance is the free balance an order’s reward and fee are paid from. Reserved is the reward still held by orders not yet closed, plus anything else the runtime reserves. Orders counts the slots the account’s unclosed orders hold against the runtime’s MaxOrdersPerProposer; each order holds a slot until a claim or a reclaim closes it, and the line reads unlimited when the runtime sets no cap.

With --json:

{
  "account": "5FXRydTJYE4cPGBH9CSf3dyGvaJNnYATEZJMyFqMiLPHv9oc",
  "source": "project",
  "balance": 0,
  "reserved": 0,
  "order_slots": {
    "used": 0,
    "limit": 32
  }
}

With no usable account, it prints the account list guidance on stderr, creates nothing, and exits 1:

Error: No Quip account is set up.

No keystores found. Create one with `xquad quip account new`, or bring a seed with `xquad quip account import`.
...

When nothing at all is set up (no seed, no override, no preference and no default keystore) and the command runs at a terminal without --json, it first offers to create one: new, import or skip (the default), then the level and the name. After creating it, it shows the new account when a network is named; without --network, --url or QUIP_RPC_URL, the keystore is still created and the command then exits 1 with the missing-network error. Skipping exits 1. A broken step, such as a preference naming a missing keystore, gets an error naming the fix instead of the offer.

account new and account import

xquad quip account new [--name NAME] [--project | --global] [--force] [--no-use]
xquad quip account import [--name NAME] [--project | --global] [--no-use]

account new writes a keystore from a fresh seed; account import writes one from a seed you already have, as 32 bytes of hex. The seed is read from a hidden prompt at a terminal, else from the first line of stdin, and has no flag, so it never lands in shell history:

xquad quip account import --name work < seed.txt

--name defaults to default. Names are lowercase letters, digits, _ and -, at most 64 characters, starting with a letter or digit.

$ xquad quip account new --project
created default at the project level: .quip/keystores/default.json
  5FXRydTJYE4cPGBH9CSf3dyGvaJNnYATEZJMyFqMiLPHv9oc
now in use at the project level; change it with `xquad quip account use`

The new account becomes the preference at its level, unless --no-use is given or it is the user-level default, which signs when nothing else is set and needs no preference. Neither command replaces an existing keystore of the same name: account new --force replaces it and names the account it replaced, and account import asks for another --name.

account use

xquad quip account use NAME [--project | --global]
xquad quip account use --unset [--project | --global]

Sets the preference at a level to the keystore NAME, or clears it with --unset. A project preference may name a project or a user keystore; a user preference applies in every directory, so it names a user keystore. Pass a name or --unset, not both. --keystore, QUIP_KEYSTORE and QUIP_SIGNER_SEED still override any preference.

Here a user keystore work, made with account new --name work --global --no-use, is chosen for the project, then the preference is cleared again:

$ xquad quip account use work
work is now in use at the project level: 5FRkgm6s4rddCTdMWGU4aPm3Uu642r96oCXLZmcwp184ByNp
$ xquad quip account use --unset
cleared the project account preference

account new, account import and account use take --json too, before the command (xquad quip --json account use work --global), printing the account, its name, the level and the keystore path (here with the home directory shortened):

{
  "account": "5FRkgm6s4rddCTdMWGU4aPm3Uu642r96oCXLZmcwp184ByNp",
  "name": "work",
  "level": "user",
  "path": "/home/me/.quip/keystores/work.json"
}

account use --unset prints null for the account and name, and the cleared config.toml as the path. account list --json prints the keystores, the resolution steps and the step in effect.

Orders

These commands follow job orders already on chain. They need no model and no session: an order id is enough. Placing an order is done from Python, with create_order() or solve() (see Job Orders).

The orders and status outputs on this page were captured on 2026-10-26 against aglais at specVersion 119, reading the orders Your First Quip Job placed.

orders

xquad quip --network aglais orders [--state STATE] [--account SS58]

Lists the account’s orders not yet closed, newest first, one line each: the order id, its state and the leader, tab-separated. The account is the one in effect (see account show), or --account names another; reading another account needs no keystore.

$ xquad quip --network aglais orders --account 5DL9B7rhMw1cg7kXzqmdj5QZ1V67vBvfYPq2bTEgZfSXiBSJ
24	final	5Ho6x2cpdd34W89rgFrUTms4JsEZfiWKyUpwW9vnpbGDvsN4
23	final	5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn
21	final	5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn
20	final	5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn

The state column is one of:

StateMeaning
open until block NStill accepting answers.
finalFinal and answered; the reward awaits a claim.
reclaimableFinal with no answers; reclaim refunds it.
closedClaimed or reclaimed; the chain records both alike.

The leader is none until someone answers. The chain drops an order from the account’s list once a claim or a reclaim closes it, so a closed order is listed only by status. --state keeps the orders that are submitted (still open), finalized, or unanswered (final with no answers, the ones reclaim --all would take); an empty result says so:

$ xquad quip --network aglais orders --account 5DL9B7rhMw1cg7kXzqmdj5QZ1V67vBvfYPq2bTEgZfSXiBSJ --state unanswered
no orders in state unanswered

At most the newest 32 orders are listed, with a warning when there are more. With --json, the same fields come as one object per order:

{
  "orders": [
    {
      "order_id": 24,
      "state": "final",
      "open_until": null,
      "answers": 2,
      "leader": "5Ho6x2cpdd34W89rgFrUTms4JsEZfiWKyUpwW9vnpbGDvsN4"
    },
    ...
  ]
}

open_until is the block an open order closes at, null otherwise; answers counts the solvers that answered.

status

xquad quip --network aglais status ORDER_ID [--short]

Prints the order’s receipt, the same box print(receipt) renders in Python; The receipt box explains each row. Any order on the network can be read, not only the account’s own:

$ xquad quip --network aglais status 20
╭─ Job order 20 receipt ───────────────────────────────────────────────╮
│ Network     aglais                                                   │
│ Proposer    5DL9B7rhMw1cg7kXzqmdj5QZ1V67vBvfYPq2bTEgZfSXiBSJ         │
│ Submitted   block 1898, 2026-10-15 04:22:24 UTC                      │
├──────────────────────────────────────────────────────────────────────┤
│ Problem     16 spins, 96 couplings                                   │
│ Spec        0x8f46f3a31321d1d093314fc769c42cbe7a83d71a0b69e6571a0f68 │
│             e2a04067f0                                               │
├──────────────────────────────────────────────────────────────────────┤
│ Payout      single best                                              │
│ Access      open                                                     │
│ Floors      none                                                     │
│ Deadline    100 blocks                                               │
│ Block wait  10 blocks                                                │
│ Reward      1.000000000000 AGLS                                      │
├──────────────────────────────────────────────────────────────────────┤
│ Status      final                                                    │
│ Answers     2                                                        │
│ Leader      5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn         │
│ Energy      -1790                                                    │
╞══════════════════════════════════════════════════════════════════════╡
│ Fee         0.002011933394 AGLS                                      │
│ Winner      1.000000000000 AGLS, awaiting claim                      │
╰──────────────────────────────────────────────────────────────────────╯

--short prints the one line orders prints, for scripts:

$ xquad quip --network aglais status 20 --short
20	final	5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn

xquad quip --network aglais --json status 20 --short prints that line’s fields as one object; --json belongs to the group, so it goes before the command. --json alone prints summary (those fields), status (the receipt’s status() dict), solvers and settlement, as Receipts describes them. An id that names no order exits 1:

Error: order 999999 not found on-chain (QuantumComputeMempool.JobOrders)

reclaim

xquad quip --network aglais reclaim ORDER_ID
xquad quip --network aglais reclaim --all [--yes]

Returns the reserved reward of a final order nobody answered to the account that proposed it. It is the one xquad quip command that signs: each reclaim is a transaction from the account in effect and pays its own fee. Pass an order id or --all, not both.

reclaim ORDER_ID refuses before signing when the order was proposed by another account, is already closed, is still open, or was answered; the reasons are listed under Reclaiming. A refused reclaim costs nothing.

reclaim --all takes every order orders --state unanswered lists. It shows them and their total, then asks before signing anything. The output below is illustrative, assembled from the messages in xquad/cli/quip.py rather than captured, with made-up order ids:

$ xquad quip --network aglais reclaim --all
order 31  1.000000000000 AGLS
order 28  1.000000000000 AGLS
2 orders, 2.000000000000 AGLS to recover; each reclaim pays its own fee
Reclaim them? [y/N]: y
reclaimed order 31: 1.000000000000 AGLS refunded
reclaimed order 28: 1.000000000000 AGLS refunded
2.000000000000 AGLS recovered from 2 of 2 orders

The prompt defaults to no; declining reclaims nothing and exits 1. Without a terminal, or with --json, there is no prompt, so --yes is required:

Error: pass --yes to reclaim without a prompt

With nothing to reclaim it prints nothing to reclaim on <account>, or with --json {"reclaimed": [], "refused": [], "total": 0}, and exits 0. An order that is refused or fails part way is reported on stderr as order N not reclaimed: <reason>, and the rest still go ahead; the command then exits 1. With --json, reclaim ORDER_ID prints order_id and refunded, and reclaim --all --yes prints the reclaimed and refused lists and the total refunded, in planck.

XQVM Reference

XQVM is the virtual machine at the core of XQuad: a stack-based interpreter with a 256-slot register file that executes compiled quadratic-optimisation programs. This section is the reference for that machine – its execution model, assembly language, instruction set, binary format, and verifier. For what a QUBO/Ising model is and why you would want one, start at Concepts instead; this section assumes you already have a program, or want to write one directly in .xqasm, and want to know exactly what the machine does with it.

A minimal program

Save this as add.xqasm, then assemble and run it:

; push two integers and add them
PUSH 10
PUSH 32
ADD
HALT
$ xquad vm asm add.xqasm -o add.xqb
assembled 4 instructions (21 bytes) -> add.xqb
$ xquad vm run add.xqb
stack (bottom to top):
  42

The program pushes 10 and 32 onto the value stack, adds them, and halts. The result remains on the stack and is printed by xquad vm run. xquad vm run --text add.xqasm skips the separate assembly step and runs the source directly.

What this section covers

VM Architecture

XQVM is a stack-based bytecode interpreter. A running VM holds four pieces of mutable state:

StateHoldsNotes
Stacki64 valuesLIFO operand stack, max 8,192 items
Register FileRegVal values256 slots, indexed r0–r255
Loop Stackloop framesone per active RANGE/ITER; see Loops
Calldata / OutputsRegVal valuesread-only inputs (INPUT) and writable output slots (OUTPUT); see Calldata and Outputs

Design Principles

  • Stack-based computation – arithmetic and comparisons operate on an integer stack. This keeps the instruction set simple and compact.
  • Typed register file – registers hold polymorphic RegVal values (integers, vectors, models, samples). Type checking happens at runtime.
  • No heap / no pointers – there is no explicit memory allocation. Vectors and models grow dynamically within registers. Programs cannot address raw memory.
  • Deterministic execution – given the same program, calldata, and configuration, the VM always produces the same output. There are no random instructions or non-deterministic operations.
  • Embeddable – the VM crate supports no_std + alloc, enabling deployment in WASM runtimes and bare-metal environments.

Operand Stack

The operand stack is the primary workspace for computation. It holds i64 signed 64-bit integers and is used by arithmetic, comparison, logical, and bitwise instructions.

Properties

PropertyValue
Element typei64 (signed 64-bit integer)
Maximum depth8,192 items
OrderingLIFO (last in, first out)
Initial stateEmpty

Operations

  • Push – PUSH1–PUSH8 push constants. LOAD pushes a register’s integer value. COPY duplicates the top element.
  • Pop – most instructions implicitly pop their operands. POP explicitly discards the top element.
  • Swap – SWAP exchanges the top two elements.
  • Clear – SCLR removes all elements.

Stack Diagrams

Throughout this documentation, stack effects are written as:

$$[\ldots, a, b] \to [\ldots, r]$$

  • \(\ldots\) represents elements below the operands.
  • Rightmost = top of stack.
  • \(b\) is popped first (it was pushed last).
  • \(r\) is the result pushed after the operation.

Errors

  • StackUnderflow – popping from an empty stack or when there are fewer elements than the instruction requires.
  • StackOverflow – pushing when the stack already contains 8,192 items.

Interaction with Registers

The stack holds only i64 integers. Richer types (models, vectors, samples) live exclusively in registers. The bridge between them:

  • LOAD reg – pushes a register’s Int value onto the stack.
  • STOW reg – pops a stack value into a register as Int.

To move non-integer values, use INPUT/OUTPUT with calldata and output slots.

Register File

The register file is a fixed array of 256 slots, indexed r0 through r255. Each slot holds a typed RegVal value.

Properties

PropertyValue
Count256 (r0–r255)
Index typeu8
Value typeRegVal (polymorphic enum)
Default valueUnset for all slots

RegVal Variants

VariantRust TypeDescription
Unset–Default. No value; a register never written, or reset by DROP.
Int(i64)i64Exchanged with the stack via LOAD/STOW.
VecInt(Vec<i64>)Vec<i64>Integer vector. Created by VEC/VECI.
VecXqmx(Vec<XqmxModel>)Vec<XqmxModel>Vector of models. Created by VECX.
Model(XqmxModel)structQUBO/Ising/integer Hamiltonian. Created by BQMX/SQMX/XQMX.
Sample(XqmxSample)structVariable-assignment vector. Created by BSMX/SSMX/XSMX.

Type Checking

Register access is type-checked at runtime. Instructions that expect a specific variant (e.g. LOAD expects Int, VECPUSH expects VecInt, SETQUAD expects Model, and SETLINE accepts either Model or Sample) will produce a RegisterType error if the register holds a variant the instruction does not accept. The error message includes the expected and actual type names. Reading an Unset register (via LOAD or OUTPUT) is a separate case: it produces an UnsetRegister error rather than RegisterType, since there is no variant to compare against.

XqmxModel Structure

A model represents a QUBO/Ising/integer Hamiltonian:

XqmxModel {
    domain: Domain,                      // Binary | Spin | Integer(k)
    size: usize,                         // number of variables
    linear: BTreeMap<usize, i64>,        // bias terms h_i
    quadratic: BTreeMap<(usize,usize), i64>,  // coupling terms J_{ij}
    rows: usize,                         // grid rows (set by RESIZE)
    cols: usize,                         // grid cols (set by RESIZE)
}

Coefficients are stored sparsely. Missing entries read as 0; setting a coefficient to 0 removes it from the map.

XqmxSample Structure

A sample holds a vector of variable assignments:

XqmxSample {
    domain: Domain,        // must match the model's domain
    values: Vec<i64>,      // one value per variable
    rows: usize,           // grid rows (set by RESIZE; 0 if ungridded)
    cols: usize,           // grid cols (set by RESIZE; 0 if ungridded)
}

Memory Management

There is no garbage collector. Registers hold their values until explicitly overwritten. Use DROP reg to reset a register to Unset, releasing any heap allocation (models, vectors, samples) it held. A register reset this way faults with UnsetRegister on the next LOAD or OUTPUT, until something is written back into it.

Loop Stack

The loop stack manages RANGE and ITER loop state. Each active loop pushes a frame; NEXT either advances the loop or pops the frame when iteration completes.

Loop Frames

Each frame records:

  • Kind – Range or Iter.
  • body_start – byte offset of the first instruction after RANGE/ITER. This is where NEXT seeks back to on each iteration.

Range Loops

A range frame tracks two values:

  • current – the current iteration value.
  • end – the exclusive upper bound (start + count).

RANGE pops count and start from the stack. The loop iterates current from start to end - 1 (where end = start + count). On each NEXT, current is incremented. If current < end, execution seeks back to body_start; otherwise the frame is popped and execution falls through. If count is zero or negative, no frame is pushed at all: execution scans forward past the matching NEXT and the body never runs.

PUSH 5       ; start = 5
PUSH 3       ; count = 3
RANGE        ; iterates current = 5, 6, 7
  LVAL r0    ; r0 ← Int(current)
  ; ... body ...
NEXT

Iterator Loops

An iterator frame tracks three values:

  • elements – a copy of the slice vec[start_idx..end_idx], holding either integers or models depending on the source register’s variant.
  • start_offset – the original start_idx, used by LIDX to report absolute positions.
  • index – the current position within elements.

ITER reg pops end_idx, then start_idx, validates that reg holds VecInt or VecXqmx, and copies vec[start_idx..end_idx] into a new frame with index = 0. The slice is duplicated so that mutations to the source vec inside the loop body do not affect what LVAL sees.

On each NEXT, index is incremented. If index is still within the copied elements, execution seeks back to body_start; otherwise the frame is popped.

; Assume r1 holds VecInt([10, 20, 30, 40, 50])
PUSH 1
PUSH 4
ITER r1            ; iterate r1[1..4] -> values 20, 30, 40
  LVAL r2          ; r2 -> Int(20), Int(30), Int(40)
  LIDX r3          ; r3 -> Int(1), Int(2), Int(3) (absolute positions)
  ; ... body ...
NEXT

ITER errors with IndexOutOfBounds if either index is negative or exceeds vec.len().

An empty slice skips the body, exactly like RANGE with a count of zero: no frame is pushed and execution resumes after the matching NEXT. The condition is start_idx >= end_idx, so an inverted range is empty rather than an error, and a slice that is never taken is not bounds-checked.

LVAL – Reading the Loop Value

LVAL reg copies the current loop value into a register:

  • Range: reg ← Int(current)
  • Iter over VecInt: reg ← Int(elements[index]) (the slice copy, not the source vec)
  • Iter over VecXqmx: reg ← Model(elements[index]) (cloned)

The element type is preserved: iterating over a VecXqmx yields Model values, not integers. Because elements is a slice copy taken at ITER time, mutating the source vec inside the loop body never changes what subsequent LVAL calls return.

LIDX – Reading the Loop Index

LIDX reg copies the current loop index into a register:

  • Range: reg ← Int(current) (identical to LVAL because Range values are themselves indices).
  • Iter: reg ← Int(start_offset + index) – the absolute position in the source vec, not the 0-based slice position. This lets loop bodies reach back into the source vec by absolute index even after slicing.

Nesting

Each RANGE or ITER pushes a new frame, and LVAL and NEXT always operate on the innermost (most recently pushed) frame. The loop stack is capped at 8,192 frames, the same cap the value stack has carried since the first release; a program past it fails with LoopStackOverflow. Real nesting never approaches that. The cap exists because only NEXT pops a frame, so a back-edge that re-enters a loop header without running its NEXT grows the stack once per execution and would otherwise be bounded only by the step budget.

PUSH 0
PUSH 3
RANGE              ; outer loop: 0, 1, 2
  LVAL r0
  PUSH 0
  PUSH 4
  RANGE            ; inner loop: 0, 1, 2, 3
    LVAL r1
    ; r0 = outer value, r1 = inner value
  NEXT
NEXT

Errors

  • NoActiveLoop – NEXT, LVAL, or LIDX with an empty loop stack.
  • RegisterType – ITER on a register that is not VecInt or VecXqmx. Raised before the empty-slice check, so an ITER on the wrong register type faults whether the slice is empty or not.
  • LoopStackOverflow – RANGE or ITER pushing a frame past the 8,192-frame cap.
  • IndexOutOfBounds – ITER with a slice index outside the vector.
  • ArithmeticOverflow – RANGE whose start + count leaves the i64 range. The bound is computed before the frame is pushed, so the loop does not run at all.

Calldata and Outputs

Calldata and output slots provide the interface between the VM and the host environment. They allow programs to receive input and return results without direct access to external systems.

Calldata (Input)

Calldata is a read-only array of RegVal values, set before execution begins. Programs access calldata via the INPUT instruction:

PUSH 0       ; slot index
INPUT r0     ; r0 ← calldata[0]

Any RegVal variant can be passed as calldata: integers, vectors, models, and samples. This enables multi-program pipelines where one program’s output model becomes another program’s input.

Setting Calldata (Rust API)

#![allow(unused)]
fn main() {
let mut vm = Vm::new();
vm.set_calldata(vec![
    RegVal::Int(42),
    RegVal::VecInt(vec![1, 2, 3]),
    RegVal::Model(my_model),
]);
}

Setting Calldata (CLI)

xquad vm run program.xqb --calldata 10,20,30

The CLI --calldata flag only supports integer values. For richer types, use the Rust API.

Output Slots

Output slots are a writable array of RegVal values, initialised to Unset (the same default a fresh register holds). Programs write to output slots via the OUTPUT instruction:

PUSH 0       ; slot index
OUTPUT r0    ; outputs[0] ← r0

Reading Outputs (Rust API)

#![allow(unused)]
fn main() {
let mut vm = Vm::new();
vm.set_output_slots(4);
vm.run(&program)?;

for (i, val) in vm.outputs().iter().enumerate() {
    println!("[{i}] = {val:?}");
}
}

Reading Outputs (CLI)

xquad vm run prints all non-default (not Unset) output slots after execution. --outputs defaults to 16 when omitted:

xquad vm run program.xqb --outputs 4
outputs:
  [0] = Int(42)

Pipeline Pattern

Calldata and outputs enable multi-program pipelines. A common pattern in the TSP example:

  1. Encoder receives N and distances as calldata, outputs a QUBO model.
  2. Verifier receives the model and a sample as calldata, outputs energy and validity.
  3. Decoder receives the sample as calldata, outputs the tour.

Each program runs in its own Vm instance. The host (Rust code or pallet) marshals outputs from one VM into calldata for the next.

Errors

  • CallDataIndex – INPUT with an index ≥ calldata length.
  • OutputIndex – OUTPUT with an index ≥ output slot count.
  • UnsetRegister – OUTPUT from a register that was never written, or was reset with DROP.

Execution Model

This chapter describes how the VM fetches, decodes, and executes instructions.

Fetch-Decode-Execute Cycle

The VM processes instructions in a loop:

1. Fetch and decode the next instruction from the instruction stream
   - end of stream → stop execution, without consulting the step budget
2. Check the step budget → StepLimitExceeded if the counter already
   reached the limit; the fetched instruction does not execute
3. Increment the step counter
4. Dispatch to the handler for that instruction
5. Handle the control flow result:
   - Continue   → advance to next instruction
   - Halt       → stop execution
   - Jump(id)   → seek to the byte offset recorded for that TARGET id
   - Seek(off)  → seek to byte offset (used by NEXT)
   - StartLoop  → a loop frame was pushed; continue to the next instruction
   - SkipLoop   → loop count was zero or negative; scan past the matching NEXT
6. Repeat from step 1

The fetch comes before the budget check, not after, and that ordering is normative rather than an implementation detail – see Step budget in the specification. The fetch that finds no instruction ends the run before the budget is consulted, so it is not a step.

Instruction Stream

The instruction stream is a cursor over the program’s raw bytecode. It decodes one instruction at a time, advancing the cursor past the opcode byte and its operands. The stream supports seeking to arbitrary byte offsets for jumps and loop backs.

Before execution begins, the raw bytecode is scanned once for TARGET opcodes: each is assigned the next sequential id (0, 1, 2, …) in program order, and its byte offset is recorded against that id. A Jump result carries one of these ids; the run loop resolves it to the recorded offset and seeks the stream there.

Each decoded instruction yields:

  • Byte offset – position in the bytecode buffer.
  • Optional label – the sequential TARGET id recorded at this offset, if any.
  • Instruction – the fully decoded instruction with typed operands.

Step Counting

The VM maintains two counters, not one. instructions counts dispatches: it increments once per instruction, regardless of what that instruction does. steps counts metered cost units: every instruction charges a base cost before dispatch, and opcodes whose work scales with data the program controls – evaluating a model, expanding a constraint, copying a register that holds a model – charge more before they do that work. A step is not an instruction; see spec/xqvm/METERING.md for the full cost model. A configurable step limit (default: 10,000,000) prevents runaway programs by bounding steps, not instructions. When a charge does not fit the remaining budget, execution stops with a StepLimitExceeded error.

#![allow(unused)]
fn main() {
let mut vm = Vm::new();
vm.set_step_limit(1_000_000);  // custom limit
vm.set_step_limit(0);          // exact: permits no instructions at all
vm.set_unlimited_steps();      // the only unbounded spelling
}

The limit is exact. Until 0.4.0 set_step_limit(0) meant u64::MAX, which made a zero budget the most dangerous value a caller could pass rather than the safest – the wrong way round for anything taking a limit from untrusted input. There is no sentinel now: set_unlimited_steps() is how a caller opts out, and it has to be written.

Both counters are accessible after execution: vm.steps() returns the metered cost total, vm.instructions() returns the dispatch count. instructions() reports the exact number of instructions dispatched, whether the program ends at a HALT or by running off the end of the instruction stream: the loop probes for the next instruction before it counts anything, so the fetch that finds nothing and breaks the loop is never counted.

One exception is worth knowing: when a loop opener skips an empty body, the scan forward to the matching NEXT charges steps for each instruction it consumes but does not dispatch them, so those instructions are metered without being counted in instructions().

Allocation Accounting

Steps bound how long a program runs, not how much memory it asks for: a three-instruction program can name a sample of any size the value stack can hold. A second counter tracks bytes. Every allocating instruction is charged against a configurable budget (default: 1 GiB) before it allocates, and one that cannot pay stops execution with a MemoryLimitExceeded error without allocating anything.

#![allow(unused)]
fn main() {
let mut vm = Vm::new();
vm.set_memory_limit(16 * 1024 * 1024);  // 16 MiB
}

vm.memory_used() reports what the run spent. See Runtime Limits for the per-opcode charges.

Control Flow Results

Each instruction handler returns a StepResult that tells the execution loop what to do next:

ResultMeaning
ContinueAdvance to the next instruction in sequence.
HaltStop execution. Returned by HALT.
Jump(label)Seek the instruction stream to the byte offset recorded for that TARGET id.
Seek(offset)Seek to a raw byte offset. Used by NEXT to loop back.
StartLoopA loop frame was pushed; continue to the next instruction (which becomes the loop body start).
SkipLoopThe loop count was zero or negative; scan forward past the matching NEXT without pushing a frame.

Tracing

The VM supports optional step-by-step tracing via the Tracer trait. When tracing is enabled, the VM captures state before and after each instruction:

#![allow(unused)]
fn main() {
pub struct StepState<'a> {
    pub pos: usize,                     // byte offset
    pub step: u64,                      // step count
    pub instruction: &'a Instruction,   // decoded instruction
    pub stack: &'a [i64],               // current stack
    pub read_regs: &'a [(u8, RegVal)],  // registers read
    pub written_regs: &'a [(u8, RegVal)], // registers written
    pub loop_depth: usize,              // nesting level
}
}

Two built-in tracer implementations are provided:

  • TextTracer – human-readable aligned columns, written to any Write target.
  • JsonTracer – one JSON object per step (JSONL format).

When tracing is disabled (NoopTracer), the tracer code is eliminated by dead code optimisation, adding zero overhead to execution.

Error Handling

Runtime errors carry the byte offset (pos) of the faulting instruction, enabling precise error reporting. When the std feature is enabled, errors can be converted to miette::Diagnostic with a disassembled listing highlighting the faulting instruction.

Assembly Language

XQVM programs are written in a simple assembly language and stored in .xqasm files. The assembler (the xqasm crate, invoked via xquad vm asm) parses the source, resolves labels, and emits compact bytecode.

Overview

  • Line-oriented format: one instruction per line.
  • Comments start with ; and run to end of line.
  • Mnemonics are case-insensitive (PUSH, push, Push all work).
  • Labels use numeric .N syntax (.0, .1, .42).
  • Registers use r<digits> syntax (r0, r255).
  • Integer literals may be signed decimal or 0x-prefixed hexadecimal.

Quick Example

; Compute 10 + 32 = 42
PUSH 10
PUSH 32
ADD
HALT

Assembly Syntax

This page defines the complete syntax of the XQVM assembly language, derived from the canonical PEG grammar in xqasm/src/grammar.pest.

Line Structure

Each source line has the form:

[label_def:] [INSTRUCTION [operands...]] [; comment]

All three parts are optional. Blank lines and comment-only lines are valid.

Examples

                        ; blank line (valid)
; this is a comment     ; comment-only line
PUSH 42                 ; instruction only
.0: NOP                 ; label + instruction
.1:                     ; label only (anchors a jump target)
LOAD r0                 ; register operand
JUMP .0                 ; label reference operand

Comments

Comments begin with ; and extend to the end of the line. They can appear on their own or after an instruction:

; full-line comment
PUSH 10  ; inline comment

Mnemonics

Instruction mnemonics are case-insensitive ASCII identifiers. All of these are equivalent:

PUSH 42
push 42
Push 42

The assembler recognises 85 of the 93 XQVM mnemonics directly. The eight PUSH1–PUSH8 opcodes are not typeable: PUSH is a special mnemonic that accepts an integer operand and selects the smallest of those encodings for you, and writing one of them by hand is rejected as an unknown mnemonic. PUSHC is an alias for PUSH. JUMP and JUMPI are the same kind of sugar, but layered on top of opcodes that are themselves among the 85: JUMP1, JUMPI1, JUMP2, JUMPI2 are part of the 93 and can be written directly, though doing so interacts badly with the unused-label check – see Control Flow. JUMP and JUMPI are two further mnemonics, not among the 93, that pick the narrowest width for you the same way PUSH picks a PUSHn width. See Stack Manipulation for the width rules.

Operands

Three operand types exist:

Registers

r0, r1, r2, ..., r255

A lowercase r followed by 1–3 decimal digits. Valid range: r0–r255.

Integer Literals

42          ; positive decimal
-99         ; negative decimal
+7          ; explicit positive
0xFF        ; hexadecimal (0x prefix)
0x0         ; hex zero

Integers are signed i64 values. Decimal and hexadecimal (0x prefix) formats are supported. An optional + or - sign may precede the digits.

Label References

.0, .1, .42, .255

A dot followed by one or more decimal digits. Label references are used as operands for the JUMP/JUMPI mnemonics (and their explicit-width forms JUMP1/JUMPI1/JUMP2/JUMPI2).

Labels

Labels are defined either with the .N: shorthand or the explicit TARGET .N directive:

.0: NOP            ; shorthand: define label .0 at this position
.1:                ; label on its own line (useful for readability)

TARGET .2          ; explicit form: identical to ".2:"
HALT

Both forms compile to the same bytecode: placing a label emits an inline TARGET opcode at the current position and records that position under the label’s assigned id. .0: and TARGET .0 are interchangeable spellings for the same operation; pick whichever reads better in context. Defining the same label with both forms is a DuplicateLabel error, the same as defining .N: twice.

A bare TARGET (no operand) emits a raw Target opcode without binding any label. That’s useful only for hand-built bytecode where you do not need a corresponding jump destination; user-facing programs should use the labelled forms.

Labels must be defined before or after they are referenced – both forward and backward references are resolved by the assembler. Every label used as a JUMP/JUMPI target must be defined somewhere in the program.

The .N digits are assembler-only syntax: they let the assembler pair a JUMP/JUMPI reference with the .N: (or TARGET .N) that defines it, and they are never emitted into the bytecode. What reaches the instruction stream is only a sequence of bare TARGET opcodes. See Bytecode Format for how a decoder assigns those opcodes their sequential ids and resolves a jump operand against them.

Whitespace

Spaces and tabs between tokens are ignored. Lines are separated by \n or \r\n. Indentation is purely cosmetic and has no semantic meaning. A common convention is to indent loop bodies:

PUSH 0
PUSH 10
RANGE
  LVAL r0
  LOAD r0
  PUSH 2
  MUL
  POP
NEXT

Error Reporting

The assembler uses miette for rich terminal diagnostics. Errors include the source file name, line/column numbers, and a snippet highlighting the problematic token:

Error: xqasm::unknown_mnemonic

  × unknown mnemonic `BADOP`
   ╭─[bad.xqasm:2:1]
 1 │ PUSH 1
 2 │ BADOP r0
   · ──┬──
   ·   ╰── unknown mnemonic
 3 │ HALT
   ╰────

These are the Rust API’s diagnostics. xquad vm asm reports the same error from the command line as one line, without the snippet.

Assembly Examples

This page presents annotated XQVM assembly programs, from simple to complex.

Hello, Stack

Push two numbers, add them, and halt. The result remains on the stack.

PUSH 10        ; stack: [10]
PUSH 32        ; stack: [10, 32]
ADD            ; stack: [42]
HALT

Conditional Branch

Skip an instruction if a condition is true:

PUSH 5
PUSH 10
GT             ; 5 > 10 ? -> 0 (false)
JUMPI .0       ; condition is 0, so we fall through
PUSH 99        ; this executes (condition was false)
JUMP .1
.0: PUSH 0     ; taken path leaves the stack at the same depth
.1: HALT

Summing a Range

Accumulate the values of a range loop into a register:

PUSH 0         ; accumulator in r0
STOW r0

PUSH 0
PUSH 3
RANGE          ; iterate 0, 1, 2
  LVAL r1      ; r1 = loop value
  LOAD r0
  LOAD r1
  ADD
  STOW r0      ; r0 += r1
NEXT

LOAD r0        ; push accumulated value (0+1+2 = 3)
HALT

Fibonacci Sequence

Compute the first N Fibonacci numbers and store them in a vector:

; N is passed as calldata[0]
PUSH 0
INPUT r0       ; r0 = N

VEC r1         ; r1 = empty vec

; Push first two values
PUSH 0
VECPUSH r1     ; vec = [0]
PUSH 1
VECPUSH r1     ; vec = [0, 1]

; Compute remaining values
LOAD r0
PUSH 2
SUB            ; count = N - 2
STOW r2

PUSH 0
LOAD r2
RANGE
  LVAL r3      ; r3 = loop index (unused, just for iteration)
  VECLEN r1
  DEC
  STOW r4      ; r4 = last index

  LOAD r4
  DEC
  VECGET r1    ; stack: fib[n-2]
  LOAD r4
  VECGET r1    ; stack: fib[n-2], fib[n-1]
  ADD           ; stack: fib[n]
  VECPUSH r1   ; append to vec
NEXT

; Output the vector
PUSH 0
OUTPUT r1
HALT

Building a QUBO Model

Create a simple 3-variable QUBO and set coefficients:

; Allocate a 3-variable binary model
PUSH 3
BQMX r0

; Set linear coefficients: h = [-1, -2, -3]
PUSH 0
PUSH -1
SETLINE r0     ; linear[0] = -1

PUSH 1
PUSH -2
SETLINE r0     ; linear[1] = -2

PUSH 2
PUSH -3
SETLINE r0     ; linear[2] = -3

; Set quadratic coefficient: J[0,1] = 4
PUSH 0
PUSH 1
PUSH 4
SETQUAD r0     ; quad[0,1] = 4

; Output the model
PUSH 0
OUTPUT r0
HALT

Grid with One-Hot Constraints

Set up a 2x3 grid model with one-hot constraints on each row:

; 6 variables in a 2x3 grid
PUSH 6
BQMX r0
PUSH 2         ; rows
PUSH 3         ; cols
RESIZE r0

; One-hot constraint on each row with penalty = 100
PUSH 0
PUSH 2
RANGE
  LVAL r1
  LOAD r1
  PUSH 100
  ONEHOTR r0
NEXT

; Output
PUSH 0
OUTPUT r0
HALT

Instruction Set Reference

This section documents all 93 XQVM instructions, organised into the 14 categories below. Each page covers the semantics, error conditions, and worked examples for one category. For the opcode byte, mnemonic, operand layout, and stack effect of every instruction, see the generated Opcode Reference – that table is the single source of truth for wire-level facts, so the pages in this section carry prose and teaching content instead of repeating it.

Notation

  • Stack effect – described in prose on this page (“Pop b, then a”) and summarised on the generated Opcode Reference as a Stack column of the form 2 → 1, meaning two values popped and one pushed. The value stack is last-in-first-out: whichever value was pushed most recently is popped first, so PUSH a; PUSH b; SUB computes a - b, not b - a. Every “pop x, then y” operand order on this section’s pages, including the ones on constraints.md and index-math.md, depends on this rule.
  • reg – the u8 operand encoded in the instruction byte stream, identifying a register slot (r0–r255).
  • label – a sequential id assigned to each TARGET instruction, in program order, during the load-time pre-scan described in Bytecode Format. It is not a byte offset, and not the .N token used in .xqasm source – that token is assembler-only and is resolved to the sequential id before encoding. JUMP1/JUMPI1 encode the id as a single u8 operand; JUMP2/JUMPI2 encode it as a u16 operand (big-endian). See Control Flow for how the assembler picks between the two.
  • Assignments use \(\leftarrow\) (register write) and \(\to\) (stack push).
  • Iverson brackets – \([P]\) equals \(1\) if \(P\) is true, \(0\) otherwise.
  • Checked arithmetic – every integer operation is checked against the i64 range. A result that would leave it raises ArithmeticOverflow rather than wrapping or panicking; see Arithmetic.

Register Effect Vocabulary

A handful of pages, constraints.md and energy.md, annotate individual opcodes with a **Register effect:** line using three terms:

  • read – register contents are inspected but not changed.
  • write – register is replaced wholesale with a new value.
  • mutate – register’s existing value is modified in-place (e.g. appending to a vec, incrementing a coefficient).

Most category pages group opcodes by topic or mechanism rather than walking through them one at a time, so they carry no such line.

RegVal – The Register Value Type

Each of the 256 registers holds one variant of RegVal – Unset, Int, VecInt, VecXqmx, Model, or Sample. See VM Architecture for the full table.

Type mismatches at runtime produce a RegisterType error with the expected and actual variant names; reading an Unset register via LOAD or OUTPUT produces UnsetRegister instead, since there is no variant to compare against.

Categories

CategoryPageOpcodes
Control Flowcontrol-flow.mdTARGET, JUMP1, JUMP2, JUMPI1, JUMPI2, NEXT, LVAL, LIDX, RANGE, ITER, NOP, HALT
Register I/Oregister-io.mdLOAD, STOW, DROP, INPUT, OUTPUT
Stack Manipulationstack-manipulation.mdPOP, PUSH1-PUSH8, SCLR, SWAP, COPY
Arithmeticarithmetic.mdADD, SUB, MUL, DIV, MOD, SQR, ABS, NEG, MIN, MAX, INC, DEC, BITLEN
Comparisoncomparison.mdEQ, LT, GT, LTE, GTE
Logical Booleanlogical.mdNOT, AND, OR, XOR
Bitwisebitwise.mdBAND, BOR, BXOR, BNOT, SHL, SHR
Allocatorsallocators.mdBQMX, SQMX, XQMX, BSMX, SSMX, XSMX, VEC, VECI, VECX
Vector Operationsvector-ops.mdVECPUSH, VECGET, VECSET, VECLEN, SLACK
Index Mathindex-math.mdIDXGRID, IDXTRIU
Coefficient Accesscoefficient-access.mdGETLINE, SETLINE, ADDLINE, GETQUAD, SETQUAD, ADDQUAD
Grid Operationsgrid.mdRESIZE, ROWFIND, COLFIND, ROWSUM, COLSUM
High-Level Constraintsconstraints.mdONEHOTR, ONEHOTC, EXCLUDE, IMPLIES, EQUALITY, ATLEAST, ATLEASTW, REDUCE
Energy Evaluationenergy.mdENERGY

Reserved Opcodes

Every byte value not listed in Opcode Reference is illegal; the decoder rejects it. The 93 assigned bytes are not contiguous – there are gaps both between and within the ranges above, plus one large unassigned block above the normal instruction space. The full gap set, derived by diffing the 93 assigned byte values against the complete 0x00-0xFF space:

RangeUnassigned bytes
Register I/O0x0D
Stack Manipulation0x19, 0x1D-0x1F
Arithmetic0x2D-0x2F
Comparison0x35
Allocators0x46-0x49, 0x4D-0x4F
Vector Operations0x55-0x59
Index Math / Coefficient Access0x5C-0x5F
Grid Operations0x6B-0x6F
High-Level Constraints0x78-0x7E
Outside the normal instruction space0x80-0xEF, 0xF1-0xFE

NOP (0xF0) and HALT (0xFF) are the only two assigned bytes outside 0x00-0x7F; every other byte in 0x80-0xFF is illegal. See spec/xqvm/ISA.md for the normative list, organised the same way.

Control Flow

Instructions for branching, looping, and program termination: TARGET, JUMP1/JUMP2, JUMPI1/JUMPI2, NEXT, LVAL, LIDX, RANGE, ITER, NOP, and HALT. For each instruction’s opcode byte, operand layout, and stack effect, see the Opcode Reference.

The TARGET pre-scan

TARGET has no operand and does nothing at runtime – it exists purely to mark a valid jump destination at a fixed byte position. Before execution begins, the VM scans the raw instruction stream once for TARGET opcodes: the first one encountered is assigned sequential id 0, the second id 1, and so on in program order, with each id recorded against the byte offset where its TARGET starts. This is the same scan the verifier uses to check that every jump references a valid id, and the Bytecode Format chapter covers its wire-level details.

JUMP1, JUMP2, JUMPI1, and JUMPI2 operands carry one of these sequential ids – never a raw byte offset, and never the .N token that appears in .xqasm source. .N is resolved to the sequential id by the assembler before encoding and is never itself emitted into the bytecode. When the VM executes a jump, it looks up the id’s recorded byte offset and seeks the instruction stream there; see Execution Model for how this fits into the fetch-decode-execute loop.

TARGET must appear at every label destination. The assembler inserts one automatically wherever a label is placed in source, so authors do not normally type it by hand:

; Shorthand: label form
.0: HALT
; Explicit form: TARGET directive bound to a label
TARGET .0
HALT

Both compile to [TARGET, HALT]. Use whichever is clearer in context. A bare TARGET (with no operand) emits a raw Target opcode without binding any label; that is only useful for direct bytecode construction, and most user programs should prefer one of the label-bearing forms above.

Branching

JUMP unconditionally seeks to the byte offset recorded for a label. JUMPI pops the top of the stack and seeks only if that value is non-zero; otherwise it falls through to the next instruction.

Each has a narrow and a wide encoding, distinguished by the width of the label operand:

  • JUMP1 / JUMPI1 encode the label as a single u8 byte.
  • JUMP2 / JUMPI2 encode the label as a u16 big-endian pair.

At the assembly level, both are written as JUMP .N / JUMPI .N (.N being the dot-prefixed label token, e.g. .0, .1); the assembler resolves .N to its sequential id and picks the narrowest encoding automatically – the *1 form whenever the id fits in a u8 (the common case, since most programs have fewer than 256 labels), falling back to the *2 form otherwise. The desugared forms (JUMP1 .N, JUMP2 .N, JUMPI1 .N, JUMPI2 .N) are accepted by the grammar, but the assembler’s unused-label check does not count them as a use: a program whose only reference to .0 is JUMP1 .0 fails with xqasm::unused_label, whichever side of the label the jump sits on. The one exception is a label whose TARGET lands at byte offset 0, the entry block, which the check exempts. Write JUMP/JUMPI and let the assembler pick the width. Disassembled output always shows the explicit form the program was actually encoded with, so a round-tripped disassembly preserves the exact wire encoding rather than the assembler’s shorthand.

Looping

RANGE and ITER each push a loop frame and hand control to the loop body; NEXT advances the innermost frame, seeking back to the body’s start until the loop is exhausted, then pops the frame. LVAL copies the current loop value into a register, and LIDX copies the current loop index. Calling LVAL, LIDX, or NEXT with no active loop frame raises NoActiveLoop.

PUSH 0       ; start
PUSH 10      ; count
RANGE
  LVAL r0    ; r0 = current iteration value (0, 1, ..., 9)
NEXT
HALT
VECI r1
PUSH 10
VECPUSH r1
PUSH 20
VECPUSH r1
PUSH 30
VECPUSH r1
PUSH 40
VECPUSH r1

PUSH 0       ; start_idx
PUSH 4       ; end_idx
ITER r1      ; r1 must hold a VecInt or VecXqmx
  LVAL r2    ; r2 = current element (10, 20, 30, 40)
  LIDX r3    ; r3 = absolute position in r1 (0, 1, 2, 3)
NEXT
HALT

Loops nest to arbitrary depth; LVAL, LIDX, and NEXT always act on the innermost frame. Loops covers frame contents, the RANGE versus ITER distinction, nesting, and the full error conditions in depth – this page only orients; that one is the reference.

NOP and HALT

NOP does nothing and advances to the next instruction; it exists mainly for hand-assembled bytecode and testing. HALT stops execution immediately, leaving the stack and registers as they were at the point of the halt.

Register I/O

Instructions for moving data between the value stack, the 256-slot register file, calldata, and output slots: LOAD, STOW, DROP, INPUT, OUTPUT. Byte values, operand layouts and stack effects are in the Register I/O section of the opcode reference.

Registers Default to Unset, Not Int(0)

Every register starts as RegVal::Unset, not Int(0). Only LOAD and OUTPUT fault UnsetRegister when reading an unset register; every other instruction that reads a register instead reports RegisterType, with unset as the actual variant held, for example register r7 holds unset, expected vec<int> from VECPUSH r7, register r7 holds unset, expected model|sample from GETLINE r7, or register r7 holds unset, expected sample from ENERGY r0 r7. LOAD r3 as the first instruction of a program, before anything has written to r3, fails at runtime with register r3 is unset. The verifier’s uninitialised-register check catches the same defect statically for straight-line code, one reason to run xquad vm check before xquad vm run: the sequence PUSH 1 / STOW r0 / DROP r0 / LOAD r0 is rejected by check with register r0 read at byte 0x0006 before being written, before the runtime fault is ever reached.

LOAD distinguishes two failure modes for a register that is not usable as an integer. An unset register faults UnsetRegister; a register holding something other than Int, for example a Model from BQMX, faults RegisterType instead, naming the actual variant held. LOAD on a register that holds a freshly allocated Model fails with register r0 holds model, expected int, a different message from the unset case.

Stack-Register Bridge

The value stack holds only i64 integers. LOAD pushes a register’s Int value onto the stack; STOW pops the stack top and writes it into a register as Int. Neither coerces: LOAD on a non-Int register errors rather than reinterpreting the value as an integer.

Richer types, models, vectors and samples, never touch the stack directly. Moving them into or out of the VM goes through INPUT and OUTPUT instead, against the calldata and output-slot arrays rather than the stack. INPUT reg pops a calldata slot index and clones calldata[slot] into reg; OUTPUT reg pops an output slot index and clones reg’s current value into outputs[slot]. Either direction transfers any RegVal variant, not only Int, since calldata and outputs are typed as Vec<RegVal> on the Rust side; the xquad CLI’s --calldata flag only accepts a comma-separated integer list, so injecting a Model or Sample through calldata is something host code building on the xqvm/xqasm crates can do, not something the CLI exposes directly. PUSH 0; INPUT r0; PUSH 0; OUTPUT r0, given --calldata 77, round-trips a value through a register with no arithmetic in between: output slot 0 ends up holding Int(77). Both INPUT and OUTPUT fault with CallDataIndex/OutputIndex respectively when the popped slot index is out of range, and OUTPUT faults UnsetRegister if reg was never written, the same as LOAD.

Memory Management

DROP reg is the only instruction that explicitly frees a register’s allocation, resetting it to Unset and releasing whatever Model, Sample, VecInt or VecXqmx the slot held. It does not leave an integer zero behind: the register is unreadable until the next STOW, INPUT or allocator call.

Stack Manipulation

Instructions for pushing constants and rearranging the top of the value stack: POP, PUSH1..PUSH8, SCLR, SWAP and COPY. Byte values, operand layouts and stack effects are in the Stack Manipulation section of the opcode reference. This page is mostly about the constant-pushing family, since the four rearrangement instructions (POP, SCLR, SWAP, COPY) are exactly what their names say: discard the top, clear everything, exchange the top two, or duplicate the top without consuming it. SWAP and COPY both error StackUnderflow rather than reading past an empty stack: a bare PUSH 1; SWAP needs a second element that is not there.

PUSH Size Selection

PUSH1 through PUSH8 are not mnemonics you write. In assembly, and through InstructionBuilder::push(), there is only PUSH <value>:

PUSH 42       ; assembler selects PUSH1 (fits in i8)
PUSH 1000     ; assembler selects PUSH2 (fits in i16)
PUSH -1       ; assembler selects PUSH1 (0xFF sign-extends to -1)

The assembler picks the narrowest PUSH1..PUSH8 variant that faithfully round-trips the literal, so small constants cost 2 bytes total (1 opcode + 1 operand byte) and the full 8-byte PUSH8 is only ever emitted for values that need all 64 bits. In the program above, PUSH 42 assembles to PUSH1 42 (0x2A), PUSH 1000 assembles to PUSH2 1000 (0x03E8), and PUSH -1 assembles to PUSH1 -1, whose single operand byte is 0xFF.

Writing PUSH1 directly does not work: it is not a recognised mnemonic, and the assembler rejects it with unknown mnemonic 'PUSH1'. PUSHC is accepted as an alias for PUSH and goes through the same size-selection path; there is no reason to prefer one over the other beyond house style.

The encoding itself is big-endian and sign-extended regardless of which PUSHn variant is chosen, which is what makes PUSH -1 a 1-byte operand rather than an 8-byte one: PUSH1 0xFF decodes as \(-1_{i64}\), not \(255_{i64}\), because the single byte is read as signed and then sign extended, not as an unsigned magnitude.

Arithmetic

The usual signed-integer operations, all on i64, plus two helpers that show up often enough in penalty-weight and slack-variable arithmetic to earn their own opcodes: SQR, a one-instruction shorthand for COPY plus MUL, and BITLEN, which the VM has no other way to compute at all, since there is no logarithm instruction to compose it from. Byte values, operand layouts and stack effects are in the Arithmetic section of the opcode reference. None of these instructions touch a register; every operand comes off the stack and every result goes back onto it.

Overflow

Every arithmetic instruction on this page is checked. ADD, SUB, MUL, NEG, ABS, SQR, INC and DEC raise ArithmeticOverflow when the result would leave the i64 range, rather than wrapping to a value the program never asked for:

PUSH 9223372036854775807   ; i64::MAX
PUSH 1
ADD                        ; raises ArithmeticOverflow

The rule is on the result, not on the machine operation underneath it. ABS and NEG of i64::MIN raise, because no positive i64 has that magnitude. i64::MIN / -1 raises for the same reason. i64::MIN % -1 does not: the remainder is 0, which is perfectly representable, even though a fixed-width machine reaches it through an overflowing division.

Values built from several operations – a constraint expansion’s coefficients, an ENERGY accumulation – are checked at every step, so a computation that leaves the range on its way to an in-range answer raises rather than quietly recovering.

Division and Remainder

DIV rounds toward negative infinity: it is floor division, matching Python’s //, not Rust’s default /, which truncates toward zero. MOD returns a remainder with the sign of the divisor, matching Python’s %, so the identity \(a = \lfloor a / b \rfloor \cdot b + (a \bmod b)\) holds under floored division. PUSH -7; PUSH 2; DIV pushes -4, not the -3 that truncating division would give, and PUSH -7; PUSH 2; MOD pushes 1, not -1. Both error with DivisionByZero when the divisor is zero rather than wrapping or producing a sentinel value: PUSH 5; PUSH 0; DIV halts execution with division by zero instead of continuing.

MIN, MAX and BITLEN

MIN and MAX are signed comparisons with no domain restriction. BITLEN pops a value and pushes the number of bits needed to represent it in binary, \(\lfloor\log_2(a)\rfloor + 1\), returning \(0\) for non-positive input rather than erroring (there is no bit length for a number that is not positive, and 0 is a more useful sentinel here than a fault, since BITLEN output typically feeds straight into SLACK’s capacity calculation). BITLEN(1) = 1, BITLEN(7) = 3, BITLEN(8) = 4, BITLEN(255) = 8.

Comparison

EQ, LT, GT, LTE and GTE compare two signed i64 values and push exactly 1 (true) or 0 (false). Byte values, operand layouts and stack effects are in the Comparison section of the opcode reference. None of these instructions touch a register, and none of them can fail beyond the ordinary StackUnderflow every popping instruction shares; every i64 value is comparable to every other.

The Iverson bracket notation \([P]\), used throughout this book, equals \(1\) if \(P\) is true and \(0\) otherwise; EQ a b is exactly \([a = b]\), and the other four follow the same pattern.

Boolean Convention

XQVM uses the integer convention for booleans: 0 is false, any non-zero value is true. Comparison instructions always produce exactly 1 or 0, never some other non-zero encoding of “true”, which is what makes their output directly usable as a JUMPI condition or as an operand to Logical AND/OR/XOR/NOT without an extra normalisation step. PUSH 3; PUSH 5; LT pushes 1, and PUSH 3; PUSH 5; GT pushes 0, both read back through STOW/OUTPUT.

Logical Boolean

NOT, AND, OR and XOR treat their operands as booleans under the integer convention: 0 is false, any non-zero value, including negative values, is true. Results are always exactly 1 or 0. Byte values, operand layouts and stack effects are in the Logical Boolean section of the opcode reference. None of these instructions touch a register.

AND, OR and XOR each pop both operands unconditionally before producing a result; there is no short-circuit evaluation the way there would be in a language with lazy boolean operators, since a stack VM has already evaluated both operand expressions and pushed both values by the time the logical instruction runs. This only matters if evaluating one of the operand expressions has a side effect, register writes, or a Coefficient Access mutation, that a short-circuiting language would skip; AND/OR/XOR never skip it.

Logical vs. Bitwise

These instructions perform boolean operations on the truthiness of a value, not on its bit pattern. For bit-pattern operations, see Bitwise (BAND, BOR, BXOR, BNOT). The two families diverge sharply outside {0, 1} inputs: NOT 5 is 0 (5 is truthy, so its logical negation is false), while BNOT 5 is ~5, the bitwise complement, a large negative number (-6). NOT on 5 returns 0, and BNOT on 5 returns -6, from the same program run against both instructions.

Bitwise

BAND, BOR, BXOR and BNOT operate on the raw two’s-complement bit pattern of an i64; SHL and SHR shift it. Byte values, operand layouts and stack effects are in the Bitwise section of the opcode reference. None of these instructions touch a register, and none but the shifts can fail: BAND/BOR/BXOR/BNOT are defined for every i64 bit pattern, with no invalid input. SHL and SHR fault on a shift amount outside \([0, 64)\), and SHL also faults when the shift would lose a significant bit.

For the boolean-algebra equivalents that treat 0/non-zero as false/true instead of operating bit by bit, see Logical.

Shift Behaviour

SHL performs a left shift, filling the vacated low bits with zero. It does not discard bits that leave the high end: a shift that loses a significant bit takes the value outside the i64 range, so it raises ArithmeticOverflow like any other overflowing operation. PUSH 4611686018427387904; PUSH 1; SHL fails rather than yielding i64::MIN. Shifting the result back recovers the operand exactly whenever nothing was lost, which is the test the VM applies.

SHR performs an arithmetic (sign-preserving) right shift, not a logical (zero-filling) one: the sign bit is replicated into the vacated high bits, so a negative operand stays negative. -8 >> 1 yields -4, and i64::MIN >> 1 yields -4611686018427387904, half the magnitude of i64::MIN and still negative, rather than a small positive number a logical shift would produce. This matches Rust’s i64 >> b operator and Python’s >> on integers.

Both instructions require the shift amount to satisfy \(0 \le b < 64\) and error InvalidShift otherwise; a shift by 64 or more is not defined as “shift everything out to zero” the way it might be on some hardware, it is rejected outright: PUSH 8; PUSH 64; SHR fails with invalid shift amount 64.

Where a logical right shift is actually needed, mask the sign-extended bits off after SHR with BAND. For a shift of exactly 1, BAND with 0x7FFFFFFFFFFFFFFF (i64::MAX) clears only the replicated sign bit, which is everything an arithmetic shift by 1 could have filled from the sign: SHR -8 1 followed by masking with i64::MAX gives 9223372036854775804, the same result a zero-filling right shift by 1 would give. A shift by more than 1 replicates the sign into more than one bit, so the mask has to widen to match: BAND with a fixed i64::MAX mask only emulates a logical shift for b = 1.

Allocators

Every problem starts here: allocate a Model to accumulate coefficients into, or a Sample to hold a candidate assignment, or a Vec to stage indices and weights before a high-level constraint call. Byte values, operand layouts and stack effects for this family are in the Allocators and Vector Operations sections of the opcode reference; this page is about the three domains and when each applies.

Model Allocators

BQMX and SQMX each pop a variable count, size, and write a fresh XqmxModel into a register, with empty linear and quadratic coefficient maps. XQMX pops two values, k (top of stack) then size, since an integer model also needs the per-variable domain width. The model holds the Hamiltonian being built up:

$$H(x) = \sum_i \text{linear}[i] \cdot x_i + \sum_{i \le j} \text{quadratic}[i, j] \cdot x_i \cdot x_j$$

which Coefficient Access instructions populate one term at a time. What differs between the three allocators is the domain the variables \(x_i\) are drawn from, and that choice is a problem-modelling decision, not an implementation detail:

  • BQMX allocates a QUBO model: variables are binary, \(x_i \in \{0, 1\}\). This is the domain most combinatorial-optimisation formulations target directly (selection, assignment, one-hot encodings), and the one most solver backends consume without translation.
  • SQMX allocates an Ising model: variables are spins, \(x_i \in \{-1, 1\}\). Physically motivated (each variable is a magnetic moment pointing up or down), and the native representation for quantum annealers, which minimise an Ising Hamiltonian directly. Allocate the domain the target backend expects rather than assuming a QUBO model can be handed to an Ising-only backend unchanged.
  • XQMX allocates an integer model: variables take one of \(k\) values, \(x_i \in \{0, 1, \ldots, k{-}1\}\). \(k\) is a count and not a half-width. This generalises past binary and spin to give a variable more than two states directly, suited to a quantity with a natural ordering or magnitude – a position in a small enumerated set – without one-hot-encoding it into several binary variables first. It does not encode an unordered categorical choice such as a colour: a quadratic form over integer variables cannot express that two values merely differ without also expressing by how much. XQMX errors with InvalidIntegerK when \(k < 2\), since a domain needs at least two values to carry a decision: \(k = 1\) leaves the single value \(\{0\}\), which is a constant rather than a variable.

Sample Allocators

BSMX, SSMX and XSMX mirror the three model allocators, including the same pop count per opcode – BSMX and SSMX pop only size; XSMX pops k then size, the same order as XQMX – but write an XqmxSample: a vector of per-variable assignments in the same domain, rather than a coefficient map. A sample is what a solver returns, or what ENERGY evaluates against a model to score a candidate solution.

The default assignment differs by domain and is chosen so it is always in-domain without a special case: binary and integer samples default every variable to \(0\), and spin samples default every variable to \(-1\) (spin-down), since \(0\) is not a member of \(\{-1, 1\}\). The integer default of \(0\) is always valid because the domain \(\{0, \ldots, k{-}1\}\) starts at zero for every \(k \ge 2\).

Those defaults are not merely conventional. SETLINE and ADDLINE check every write into a sample against its domain and raise SampleOutOfDomain otherwise, so a freshly allocated sample has to be in-domain from the start or the first read of an untouched variable would return a value the same register could not have been written.

Vec Allocators

VECI creates an empty VecInt and VECX creates an empty VecXqmx (a vector of models, used to batch several sub-models together). VEC is an untyped convenience form: at the bytecode level it produces exactly the same VecInt as VECI, so VEC r0 and VECI r0 are interchangeable. Prefer VECI in generated or reviewed bytecode, where being explicit about the element type documents intent; VEC reads naturally in hand-written assembly where the type is obvious from what gets pushed next.

Domain Types

DomainVariable valuesModelSample
Binary\(\{0, 1\}\)BQMXBSMX
Spin\(\{-1, 1\}\)SQMXSSMX
Integer(\(k\))\(\{0, 1, \ldots, k{-}1\}\), \(k \ge 2\)XQMXXSMX

Example

PUSH 4
PUSH 1
XQMX r0     ; errors: InvalidIntegerK, k = 1 is not >= 2

XQMX and XSMX both check k only after popping both operands, so the stack is consumed either way; on k < 2 they fault InvalidIntegerK with the message XQMX/XSMX requires k >= 2 for the {0, ..., k-1} domain, got k = 1.

Vector Operations

Instructions for reading, writing and querying the VecInt and VecXqmx containers that Allocators create. Byte values, operand layouts and stack effects are in the Vector Operations section of the opcode reference. This page covers the type rules that apply across the family and the details of SLACK, the one instruction here that does more than plain container access.

Reading, Writing and Sizing

VECPUSH appends to the end of a VecInt, growing it by one element. VECGET and VECSET read and write by index, both bounds-checked against the current length: an out-of-range index errors IndexOutOfBounds at runtime rather than reading past the end of the vector. VECLEN reads the current length as an i64, and is the only instruction in this family that accepts either VecInt or VecXqmx; the other three require VecInt specifically, since VecXqmx elements are whole models rather than integers and there is no VECXGET/VECXSET.

VEC r0          ; r0 = empty VecInt
PUSH 10
VECPUSH r0      ; r0 = [10]
PUSH 20
VECPUSH r0      ; r0 = [10, 20]
PUSH 0
VECGET r0       ; stack top = 10

An OUTPUT of the loaded value writes 10 to the output slot, and a VECLEN r0 issued after the two VECPUSHes reads 2.

SLACK

SLACK indices coeffs is the one instruction in this family that is not a plain accessor: it exists to turn an inequality constraint into an equality one, by appending binary-weighted slack variables to two parallel vecs. Pop capacity (top of stack), then start_index. Compute

$$S = \lfloor \log_2(\text{capacity}) \rfloor + 1$$

and append \(S\) entries to each register:

  • to indices: \([\text{start}, \text{start}{+}1, \ldots, \text{start}{+}S{-}1]\), consecutive variable indices for the new slack variables;
  • to coeffs: \([1, 2, 4, \ldots, 2^{S-1}]\), their binary weights.

SLACK appends rather than overwrites, so item variables and slack variables coexist in the same indices/coeffs pair, ready to hand to EQUALITY. This is what makes a knapsack-style “total weight at most capacity” constraint expressible as a single weighted equality: the slack variables absorb any unused capacity, so the equality holds exactly whenever the inequality would have held. If capacity <= 0, no slack variables are needed and SLACK appends nothing.

VEC r5            ; indices
VEC r6            ; coeffs
; ... populate with item indices and weights ...
PUSH 3            ; start_index (first slack variable index)
PUSH 10           ; capacity
SLACK r5 r6       ; appends 4 slack entries: floor(log2(10)) + 1 = 4

With start_index = 3, capacity = 10: VECLEN r5 after SLACK reads 4, indices starts at 3 and ends at 6 ([3, 4, 5, 6]), and coeffs is [1, 2, 4, 8], matching \(S = 4\) and the powers of two up to \(2^{S-1}\).

Index Math

IDXGRID and IDXTRIU compute the two flat-indexing schemes the rest of the VM relies on, so that bytecode which builds indices by hand does not have to reimplement either formula. Byte values, operand layouts and stack effects are in the Index Math section of the opcode reference. Neither instruction touches a register, and both use the same wrapping i64 arithmetic as Arithmetic; a sufficiently large IDXGRID product wraps rather than trapping, the same as MUL would.

IDXGRID: Row-Major Flat Index

IDXGRID pops C (columns), then c (column), then r (row), and pushes

$$\text{index} = r \cdot C + c$$

the same row-major convention RESIZE attaches to a model. Computing this by hand and computing it with IDXGRID produce identical results, since RESIZE does not change how flat indices are interpreted internally; IDXGRID exists so bytecode that needs the flat index as an ordinary stack value, for example to pass to SETLINE/SETQUAD without going through a grid instruction, does not have to spell out r * C + c as separate MUL and ADD instructions.

A worked example: a TSP over 4 cities and 4 positions models \(x[\text{city}][\text{position}]\) as a \(4 \times 4\) grid, so city 2 at position 1 is variable

PUSH 2        ; row = city = 2
PUSH 1        ; col = position = 1
PUSH 4        ; cols = 4
IDXGRID       ; index = 2 * 4 + 1 = 9

This program’s result, read back with STOW/OUTPUT, is 9.

IDXTRIU: Upper-Triangular Packed Index

IDXTRIU pops j, then i, and pushes

$$\text{index} = \frac{j \cdot (j - 1)}{2} + i \qquad (i < j)$$

the packed index for the pair \((i, j)\) in the strictly upper triangle of a symmetric matrix, useful for iterating over quadratic coefficient pairs without visiting \((i, j)\) and \((j, i)\) as two different positions. If i > j, IDXTRIU swaps them before computing the index, so the result is order-independent: \((i, j)\) and \((j, i)\) pack to the same value, the same guarantee SETQUAD/GETQUAD give by normalising the pair internally.

The enumeration is strictly upper-triangular, so it has no slot for a diagonal cell even though the quadratic table has one. SETQUAD and GETQUAD accept \(i = j\) and store a self-coupling; IDXTRIU called with \(i = j\) yields the index of some off-diagonal pair rather than of the diagonal one. Do not use it to address a diagonal coefficient.

IDXTRIU and IDXGRID do not range-check their operands, but every intermediate of the computation is checked, so a product or sum that leaves the i64 range raises ArithmeticOverflow even when the final index would land back inside it.

PUSH 1        ; i = 1
PUSH 3        ; j = 3
IDXTRIU       ; index = 3 * 2 / 2 + 1 = 4

This program’s result is 4.

Coefficient Access

Read and write the linear (bias) and quadratic (coupling) terms of the Hamiltonian

$$H(x) = \sum_i \text{linear}[i] \cdot x_i + \sum_{i \le j} \text{quadratic}[i, j] \cdot x_i \cdot x_j$$

that a Model accumulates. Byte values, operand layouts and stack effects are in the XQMX Coefficient Access section of the opcode reference. All coefficient values are i64.

Linear Access Reads and Writes Samples Too

GETLINE, SETLINE and ADDLINE accept reg holding either a Model or a Sample: on a Model register they read or write the sparse bias map, and on a Sample register the identical instruction reads or writes the sample’s per-variable assignment at the same index instead. GETLINE r0 on a freshly allocated BSMX sample (no coefficients, only assignments) returns the assigned value at that index.

xquad vm check accepts the same programs. It did not until QUI-1168: xqvm/src/verifier/reg_type.rs grouped the linear trio with the quadratic and constraint opcodes under a single check requiring Model, so GETLINE r0 on the program above failed with register r0 at byte 0x0006: expected model, got sample even though the VM ran it. spec/xqvm/ISA.md had said all along that the linear opcodes accept either mode, so that was a defect in the verifier rather than a documented restriction, and it is fixed.

The quadratic trio, GETQUAD, SETQUAD and ADDQUAD, has no sample equivalent at either level and rejects a Sample register with RegisterType (“expected model, got sample”), since a sample has no coupling terms to read or write.

Sample Writes Are Domain-Checked

A sample’s entry is an assignment, so it has to be a value the variable can actually take. SETLINE and ADDLINE raise SampleOutOfDomain when the value they would store is not a member of the register’s domain – {0, 1} for BSMX, {-1, +1} for SSMX, {0, ..., k-1} for XSMX. Note that 0 is out of domain for spin: the spin domain has two members and a gap between them, so a check written as a range would wrongly admit it.

ADDLINE checks the result of the addition, not the delta. Adding 1 to a binary variable already holding 1 raises; adding -1 to it succeeds and leaves 0, even though -1 is not itself a binary value.

A Model register is never checked. Its linear[i] is a bias coefficient, whose magnitude is the weight the objective gives that variable and has nothing to do with the values the variable may take – a binary model routinely carries large negative biases.

The guarantee is about the two opcodes and not about the register. A host that installs a whole sample through calldata bypasses them, and GETLINE will read back whatever it installed; the xqffi bindings validate at that boundary instead.

Every Index Is Bounds-Checked

All six instructions bounds-check their popped indices against the register’s declared size (model.size or sample.values.len()) and error IndexOutOfBounds if an index is negative or at least size: GETLINE r0 for i = 99 on a 4-variable model fails with index 99 out of bounds (len 4), even though the coefficient map holds no fixed-size backing array. The size bound comes from the model’s declared variable count, not from the map itself, which is why an absent in-range coefficient reads as 0 while a read past the end raises.

Reads are bounded exactly as writes are. GETQUAD r0 for (i, j) = (99, 100) on a 4-variable model raises rather than answering 0, because a GETQUAD that reads back 0 from a pair SETQUAD refuses would leave the family disagreeing with itself about which variables exist.

SETQUAD and ADDQUAD check i before j, and both before the i > j normalisation below, so the reported index is the operand the program supplied rather than whichever one sorted lower.

The same bound covers EXCLUDE and IMPLIES, which write coefficients through the same surfaces. Nothing in either family can put a coefficient outside the variable count a BQMX/SQMX/XQMX call declared: an unbounded write would grow the sparse map and leave the model carrying a constraint over variables that do not exist, which still solves cleanly and answers wrongly.

Sparse Storage

Coefficients live in sparse BTreeMap structures, not a fixed-size array:

  • Linear: BTreeMap<usize, i64> keyed by variable index.
  • Quadratic: BTreeMap<(usize, usize), i64> keyed by variable pair.

An absent key reads as 0. Setting a coefficient to exactly 0 removes its entry rather than storing a zero, for both the linear and the quadratic map, so memory usage tracks the number of non-zero terms rather than the number of SETLINE/SETQUAD calls ever made; writing 0 and never reading that index again leaves no trace in the map.

Key Normalisation

Quadratic keys are normalised so that \(i \le j\): SETQUAD, ADDQUAD and GETQUAD all swap i and j internally when i > j, so \(\text{quad}[3, 5]\) and \(\text{quad}[5, 3]\) name the same entry. SETQUAD r0 with (i, j) = (5, 3) followed by GETQUAD r0 with (i, j) = (3, 5) returns the value just written, not 0.

Grid Operations

A model’s linear surface is stored flat, indexed by a single usize. Grid operations let it additionally be addressed as a 2-D matrix, \((\text{row}, \text{col})\), by attaching row and column counts as metadata and reinterpreting the flat index as \(\text{row} \cdot \text{cols} + \text{col}\), the same convention IDXGRID computes by hand. Byte values, operand layouts and stack effects are in the XQMX Grid section of the opcode reference.

Every instruction here accepts reg holding either a Model or a Sample, not only a Model: ROWFIND, COLFIND, ROWSUM and COLSUM read a “linear surface” that is the model’s sparse coefficient map for a Model register, and the sample’s dense assignment vector for a Sample register, so the same instruction reads either a model’s biases or a solved sample’s variable values depending on what is in the register. ROWSUM on a BSMX sample (no coefficients to speak of, only assignments) raises no RegisterType error and returns the row’s summed values. A sample has to be gridded first, though: a freshly allocated one carries rows = cols = 0, and all four opcodes raise InvalidGridDimensions on a register with no grid.

RESIZE Attaches, It Does Not Reshape

RESIZE pops cols then rows and stores them on the model or sample; it does not touch the coefficient map or assignment vector at all. A model with 16 variables and no grid set, and the same model after RESIZE r0 with rows = 4, cols = 4, have byte-for-byte identical linear and quadratic maps; what changes is how ROWFIND, COLFIND, ROWSUM, COLSUM and, outside this page, ONEHOTR/ONEHOTC interpret a flat index. A grid has to satisfy two conditions. Both rows and cols must be strictly positive, so RESIZE r0 with either argument \(\le 0\) errors at runtime with invalid grid dimensions, for example with rows = 0, cols = 4. And \(\text{rows} \cdot \text{cols}\) must not exceed the register’s declared size: a grid is a reinterpretation of variables the program already declared, so it cannot describe cells that do not exist. RESIZE r0 to \(3 \times 3\) on a 4-variable model raises InvalidGridDimensions for the same reason a negative row count does.

The extent bound is \(\le\), not \(=\). EQUALITY, ATLEAST, ATLEASTW and REDUCE append slack and auxiliary variables past the grid and nothing ever shrinks a register’s size, so a model whose size exceeds its extent is the normal state after any of them; a later RESIZE over a strict subset of the variables is still accepted.

Both checks are runtime checks, not ones the verifier catches statically, since grid dimensions are ordinary popped stack values rather than something the verifier’s dataflow passes track.

The canonical use is encoding a two-index variable directly instead of computing flat indices by hand at every access site. A 4-city TSP, for example, models \(x[\text{city}][\text{position}]\) as a \(4 \times 4\) grid:

PUSH 16        ; size = 4 * 4
BQMX r0        ; allocate binary model
PUSH 4         ; rows = 4
PUSH 4         ; cols = 4
RESIZE r0      ; set grid dimensions

after which row 2 is every variable for city 2 across all four positions, and ONEHOTR r0 over row 2 is exactly the constraint “city 2 occupies exactly one position”.

Row and Column Bounds Are Enforced

ROWFIND, COLFIND, ROWSUM and COLSUM each check the index they pop against the grid axis they address. A negative index and an index at or past the declared extent both raise IndexOutOfBounds, naming the index and the extent it exceeded. ROWSUM r0 for row = 99 on a model RESIZEd to \(2 \times 2\) raises rather than returning 0:

PUSH 4
BQMX r0
PUSH 2         ; rows
PUSH 2         ; cols
RESIZE r0
PUSH 99        ; row -- past the two rows the grid declares
ROWSUM r0
HALT
Error: run failed: IndexOutOfBounds { pos: 12, index: 99, len: 2 }

A register with no grid at all is a separate fault: rows = cols = 0 addresses no line, so all four opcodes raise InvalidGridDimensions rather than reducing over nothing. That is the same identity ONEHOTR and ONEHOTC raise without a grid.

RESIZE’s dimensions are a bound the VM enforces, not a convention correct bytecode is trusted to honour. xqvm/tests/vectors/xqmx-grid/ pins all three cases.

ROWFIND and COLFIND

ROWFIND pops v then r, scans row r left to right, and pushes the column of the first entry whose value equals v, or \(-1\) if none matches. COLFIND pops v then c and is the column-major mirror. Since a model’s storage is sparse, an unset coefficient reads as 0 (see Coefficient Access), so searching for v = 0 against a model can match either an explicit zero or nothing at all, depending on write history. An unmatched search returns \(-1\) rather than erroring, but only once the search has run: the register still has to carry a grid, and r still has to name a row that grid declares. \(-1\) means “scanned, no match”, never “no such row”. On a \(2 \times 2\) model with linear[0] = 9 and nothing else set, ROWFIND for v = 9 in row 0 returns 0 (the match), and the identical search in row 1 returns \(-1\); the same search in row 2 raises IndexOutOfBounds.

ROWSUM and COLSUM

ROWSUM pops r and pushes \(s = \sum_{c=0}^{C-1} \text{linear}[r \cdot C + c]\); COLSUM pops c and pushes the column-major equivalent. Both sum over the full declared row or column width, treating absent model entries as 0 rather than skipping them. On a Sample register the same sum runs over the sample’s assignment values instead of coefficients, which is how bytecode checks a solved one-hot row without decoding each variable index by hand: after a solver’s result is loaded back into a Sample register, ROWSUM over a row that carried a ONEHOTR constraint should read exactly 1 if the constraint holds in that sample.

High-Level Constraints

These instructions inject QUBO penalty terms for common combinatorial constraints, expanding into linear and quadratic coefficient deltas automatically. The model register must hold a Model in model mode. Grid-based opcodes (ONEHOTR, ONEHOTC) read the grid dimensions set by RESIZE and require them, in both halves of the grid precondition spec/xqvm/ISA.md states normatively. On a model with no grid set (rows and cols both 0) both raise InvalidGridDimensions rather than expanding over nothing, so forgetting RESIZE fails at the instruction that needed it rather than producing a model missing a constraint. A row outside [0, rows), or a col outside [0, cols), raises IndexOutOfBounds rather than applying the constraint to variables the grid does not address – the same fault the four grid opcodes raise for an out-of-range row or column. xquad vm check cannot catch either, because the grid dimensions and the index alike are popped stack values rather than something the verifier’s dataflow passes track. Vec-based opcodes (EQUALITY, ATLEAST, ATLEASTW, REDUCE) operate on arbitrary variable sets. All coefficients are i64. For each opcode’s byte value and operand layout, see the Opcode Reference.

ONEHOTR reg

Register effect: mutate

Pop penalty, then row. Apply the one-hot constraint over all variables in grid row row:

$$H \mathrel{+}= \text{penalty} \cdot \left(\sum_c x_{\text{row},c} - 1\right)^2$$

Expanding (binary variables: \(x^2 = x\)):

$$\text{linear}[\text{row} \cdot \text{cols} + c] \mathrel{+}= -\text{penalty} \qquad \forall; c \in [0, \text{cols})$$

$$\text{quad}[\text{row} \cdot \text{cols} + c_i,; \text{row} \cdot \text{cols} + c_j] \mathrel{+}= 2 \cdot \text{penalty} \qquad \forall; c_i < c_j$$

ONEHOTC reg

Register effect: mutate

Pop penalty, then col. One-hot over all variables in grid column col:

$$\text{linear}[r_i \cdot \text{cols} + \text{col}] \mathrel{+}= -\text{penalty} \qquad \forall; r_i \in [0, \text{rows})$$

$$\text{quad}[r_i \cdot \text{cols} + \text{col},; r_j \cdot \text{cols} + \text{col}] \mathrel{+}= 2 \cdot \text{penalty} \qquad \forall; r_i < r_j$$

EXCLUDE reg

Register effect: mutate

Pop penalty, then \(j\), then \(i\). Add mutual-exclusion: penalise \(x_i = 1\) and \(x_j = 1\) simultaneously.

$$\text{quad}[i, j] \mathrel{+}= \text{penalty}$$

IMPLIES reg

Register effect: mutate

Pop penalty, then \(j\), then \(i\). Add implication \(i \Rightarrow j\): penalise \(x_i = 1\) with \(x_j = 0\).

$$H \mathrel{+}= \text{penalty} \cdot x_i \cdot (1 - x_j) = \text{penalty} \cdot x_i - \text{penalty} \cdot x_i \cdot x_j$$

$$\text{linear}[i] \mathrel{+}= \text{penalty}$$

$$\text{quad}[i, j] \mathrel{+}= -\text{penalty}$$

EQUALITY model indices coeffs

Register effect: read indices, coeffs; mutate model

Pop penalty, then target. Read variable indices from indices (VecInt) and coefficients from coeffs (VecInt). Expand the weighted equality constraint into QUBO terms on model:

$$H \mathrel{+}= P \cdot \left(\sum_k a_k \cdot x_{\text{idx}_k} - b\right)^2$$

Expanding:

$$\text{linear}[\text{idx}_k] \mathrel{+}= P \cdot a_k \cdot (a_k - 2b) \qquad \forall; k$$

$$\text{quad}[\text{idx}_k, \text{idx}_m] \mathrel{+}= 2P \cdot a_k \cdot a_m \qquad \forall; k < m$$

The constant term \(P \cdot b^2\) is dropped. EQUALITY is the general form of ONEHOTR/ONEHOTC – setting all \(a_k = 1\) and \(b = 1\) produces the same expansion.

If an index in indices is at or past the model’s current size, EQUALITY grows model.size to fit it rather than erroring – unlike ATLEAST and ATLEASTW below, which validate incoming indices against the model’s existing size and raise IndexOutOfBounds on an out-of-range one. Otherwise, indices and coeffs must have equal length or the instruction raises VecLengthMismatch.

ATLEAST model indices

Register effect: read indices; mutate model (grows size)

Pop penalty, then \(k\). Read variable indices from indices. Enforce \(\sum x_i \ge k\) by allocating \(S = \lfloor\log_2(N - k)\rfloor + 1\) slack variables at model.size and applying an EQUALITY expansion with target \(k\), where \(N\) is the number of indices:

$$\sum_i x_{\text{idx}i} - \sum{j=0}^{S-1} 2^j \cdot s_j = k$$

This formula covers \(N - k > 0\). When \(N - k \le 0\) – only possible at \(k = N\), since IndexOutOfBounds below already rejects \(k > N\) – the constraint is already an equality with nothing left to slacken, so ATLEAST allocates zero slack variables and applies the EQUALITY expansion directly, with no \(S\) term at all.

Raises IndexOutOfBounds if \(k \le 0\) or \(k > N\), and if any index in indices is at or past the model’s existing size – ATLEAST does not grow the model to fit an out-of-range input index, only to hold the slack variables it allocates itself.

ATLEASTW model indices coeffs

Register effect: read indices, coeffs; mutate model (grows size)

Pop penalty, then \(k\). Same as ATLEAST but with arbitrary weights from coeffs. Enforces \(\sum w_i \cdot x_i \ge k\). The slack count is computed from \(\text{max_excess} = \sum w_i - k\).

Raises VecLengthMismatch if indices and coeffs have different lengths, or IndexOutOfBounds if \(k \le 0\).

REDUCE model

Register effect: mutate model (grows size)

Pop \(P_{\text{aux}}\), then \(\text{var_b}\), then \(\text{var_a}\). Allocate auxiliary variable \(w\) at model.size. Add Rosenberg enforcement terms constraining \(w = x_a \cdot x_b\):

$$\text{quad}[\text{var_a}, \text{var_b}] \mathrel{+}= P_{\text{aux}}$$

$$\text{quad}[\text{var_a}, w] \mathrel{+}= -2 P_{\text{aux}}$$

$$\text{quad}[\text{var_b}, w] \mathrel{+}= -2 P_{\text{aux}}$$

$$\text{linear}[w] \mathrel{+}= 3 P_{\text{aux}}$$

Push \(w\) (the auxiliary index). Enables chaining for higher-order terms: reduce a quartic \(x_i x_j x_k x_l\) by calling REDUCE twice to get \(w_1 = x_i x_j\) then \(w_2 = w_1 x_k\), and finish with ADDQUAD on \((w_2, x_l)\).

Usage Pattern

Constraint instructions are designed to work with grid models. A typical pattern for a TSP-style assignment grid:

; Allocate model and set grid
PUSH 16
BQMX r0
PUSH 4
PUSH 4
RESIZE r0

; Apply one-hot constraints on each row and column
PUSH 0
PUSH 4
RANGE
  LVAL r1
  LOAD r1
  PUSH 100       ; penalty weight
  ONEHOTR r0     ; each city visits exactly one position
NEXT

PUSH 0
PUSH 4
RANGE
  LVAL r1
  LOAD r1
  PUSH 100
  ONEHOTC r0     ; each position has exactly one city
NEXT

HALT

Energy Evaluation

ENERGY is the sole instruction in this category: it evaluates a model’s Hamiltonian against a candidate sample and pushes the result. Byte value, operand layout and stack effect are in the XQMX High-Level Constraints section of the opcode reference. See Allocators for how a Model and a Sample are built in the first place.

ENERGY model sample

Register effect: read – both model and sample are read-only

ENERGY takes two register operands, model and sample. Both checks are strict: the model register must hold a Model, the sample register must hold a Sample, and a RegisterType error is raised if either register holds any other variant – a Model cannot be passed in the sample slot or vice versa.

To populate a sample with concrete variable assignments, construct an xqvm::XqmxSample in the host and pass it to the program through a calldata slot, then INPUT it into a register before calling ENERGY. SETLINE and ADDLINE can also write a sample’s per-variable assignment values in place, the same way they write a model’s linear bias map, and xquad vm check accepts them – see Coefficient Access. Each write is checked against the sample’s domain, so a program can only build assignments the variables can actually take. Constructing an XqmxSample in the host and passing it through calldata remains the way to hand a verified program a whole candidate solution at once.

Hamiltonian

Evaluates the quadratic Hamiltonian:

$$E = \sum_{i} \text{linear}[i] \cdot x_i ;+; \sum_{i \le j} \text{quad}[i,j] \cdot x_i \cdot x_j$$

where \(x_i\) is sample.values[i], the variable assignment at index \(i\). The sum runs over \(i \le j\): the diagonal is legal, and a self-coupling quad[i,i] is evaluated as \(\text{quad}[i,i] \cdot x_i \cdot x_i\) like any other entry. The result is pushed as i64.

Overflow Is a Fault, Not a Wrap

Every term product and every partial sum is range-checked, and one that leaves the i64 range raises ArithmeticOverflow. The check is per step rather than on the final total, so a model whose mathematical energy is perfectly representable still faults when an intermediate is not. Accumulation runs in sorted key order, linear terms before quadratic ones, and that order is normative precisely because it decides which partial sums a checked implementation sees.

Take a 3-variable binary model with linear = {0: i64::MAX, 1: 1, 2: -1} and a sample assigning all three variables 1. The energy is \(2^{63} - 1 + 1 - 1 = 2^{63} - 1\), which is i64::MAX and representable. Wrapping arithmetic would have computed it correctly, overflowing to i64::MIN at the second term and back at the third. Per-step checking raises at the second term instead:

Error: run failed: ArithmeticOverflow { pos: None }

Reordering the model’s coefficients so no partial sum leaves the range makes the same program succeed. A program that relies on cancellation between large terms has to be written to keep every running total representable.

Errors

  • RegisterType – if model is not a Model or sample is not a Sample.
  • SizeMismatch – if \(\lvert\text{sample}\rvert \neq \text{model.size}\).
  • ArithmeticOverflow – if any term product or partial sum leaves the i64 range.

Example

; Build a 2-variable binary model in r0:
;   linear[0] = 3, linear[1] = -2, quad[0,1] = 5.
PUSH 2
BQMX r0

PUSH 0
PUSH 3
SETLINE r0
PUSH 1
PUSH -2
SETLINE r0

PUSH 0
PUSH 1
PUSH 5
SETQUAD r0

; A freshly-allocated binary sample is initialised to all zeros, so
; H(0, 0) = 0.
PUSH 2
BSMX r1

ENERGY r0 r1
HALT

In this example, the sample is [0, 0] and the Hamiltonian evaluates to \(E = 0\). To exercise a non-zero assignment, construct an XqmxSample in host code with XqmxSample::new(Domain::Binary, vec![1, 1]) and INPUT it into r1 before calling ENERGY.

Opcode Reference

Concise reference table for every opcode in the XQVM bytecode format. Derived directly from xqvm/opcodes.yaml, which is kept in sync with the Rust opcodes! x-macro.

For the normative bytecode specification, see spec/xqvm/SPEC.md.

Columns:

  • Code – wire-encoding byte.
  • Mnemonic – uppercase assembly name.
  • Operands – post-opcode operand layout; empty for no-operand instructions.
  • Stack – stack effect as pop → push; 0 → 1 means one value produced. any → 0 marks an instruction that empties the stack outright rather than applying a fixed net effect.
  • Description – single-sentence semantic summary.

Reserved wire bytes (rejected by the decoder as illegal): 0x0D, 0x19, 0x1D-0x1F, 0x2D-0x2F, 0x35, 0x46-0x49, 0x4D-0x4F, 0x55-0x59, 0x5C-0x5F, 0x6B-0x6F, 0x78-0x7E, 0x80-0xEF, 0xF1-0xFE.

Total: 93 opcodes.


Control Flow

CodeMnemonicOperandsStackDescription
0x00TARGET–0 → 0Mark a valid jump destination.
0x01JUMP1label: u80 → 0Unconditionally jump to a basic block by u8 label index (narrow form).
0x02JUMPI1label: u81 → 0Jump to a basic block by u8 label index if the top of the stack is non-zero (narrow form).
0x03JUMP2label: u160 → 0Unconditionally jump to a basic block by u16 label index (wide form).
0x04JUMPI2label: u161 → 0Jump to a basic block by u16 label index if the top of the stack is non-zero (wide form).
0x05LIDXreg: Register0 → 0Copy the current loop index (offset-adjusted) into a register.
0x06LVALreg: Register0 → 0Copy the current loop value into a register.
0x07NEXT–0 → 0Advance the loop index; jump back or exit the current loop.
0x08RANGE–2 → 0Start a range loop over [start, start + count).
0x09ITERreg: Register2 → 0Start a vec iteration over a slice of a register’s vec.

Register I/O

CodeMnemonicOperandsStackDescription
0x0ALOADreg: Register0 → 1Push the value of an int register onto the stack.
0x0BSTOWreg: Register1 → 0Pop the top of the stack into an int register.
0x0CDROPreg: Register0 → 0Reset a register to Unset, releasing any value it held.
0x0EINPUTreg: Register1 → 0Pop a calldata slot index and load that slot into a register.
0x0FOUTPUTreg: Register1 → 0Pop an output slot index and write the register to it.

Stack Manipulation

CodeMnemonicOperandsStackDescription
0x10POP–1 → 0Discard the top of the stack.
0x11PUSH1val: [u8; 1]0 → 1Push a 1-byte big-endian signed constant, sign-extended to i64.
0x12PUSH2val: [u8; 2]0 → 1Push a 2-byte big-endian signed constant, sign-extended to i64.
0x13PUSH3val: [u8; 3]0 → 1Push a 3-byte big-endian signed constant, sign-extended to i64.
0x14PUSH4val: [u8; 4]0 → 1Push a 4-byte big-endian signed constant, sign-extended to i64.
0x15PUSH5val: [u8; 5]0 → 1Push a 5-byte big-endian signed constant, sign-extended to i64.
0x16PUSH6val: [u8; 6]0 → 1Push a 6-byte big-endian signed constant, sign-extended to i64.
0x17PUSH7val: [u8; 7]0 → 1Push a 7-byte big-endian signed constant, sign-extended to i64.
0x18PUSH8val: [u8; 8]0 → 1Push a full 8-byte big-endian signed constant (i64).
0x1ASCLR–any → 0Clear the entire value stack.
0x1BSWAP–2 → 2Swap the top two stack elements.
0x1CCOPY–1 → 2Duplicate the top of the stack.

Arithmetic

CodeMnemonicOperandsStackDescription
0x20ADD–2 → 1Pop b and a; push a + b.
0x21SUB–2 → 1Pop b and a; push a - b.
0x22MUL–2 → 1Pop b and a; push a * b.
0x23DIV–2 → 1Pop b and a; push a / b (floor division, rounds toward negative infinity).
0x24MOD–2 → 1Pop b and a; push a % b.
0x25SQR–1 → 1Pop a; push a * a.
0x26ABS–1 → 1Pop a; push |a|.
0x27NEG–1 → 1Pop a; push -a.
0x28MIN–2 → 1Pop b and a; push min(a, b).
0x29MAX–2 → 1Pop b and a; push max(a, b).
0x2AINC–1 → 1Pop a; push a + 1.
0x2BDEC–1 → 1Pop a; push a - 1.
0x2CBITLEN–1 → 1Pop a; push floor(log2(a))+1. If a <= 0, push 0.

Comparison

CodeMnemonicOperandsStackDescription
0x30EQ–2 → 1Pop b and a; push 1 if a == b, else 0.
0x31LT–2 → 1Pop b and a; push 1 if a < b, else 0.
0x32GT–2 → 1Pop b and a; push 1 if a > b, else 0.
0x33LTE–2 → 1Pop b and a; push 1 if a <= b, else 0.
0x34GTE–2 → 1Pop b and a; push 1 if a >= b, else 0.

Logical Boolean

CodeMnemonicOperandsStackDescription
0x36NOT–1 → 1Pop a; push 1 if a == 0, else 0.
0x37AND–2 → 1Pop b and a; push 1 if both are non-zero, else 0.
0x38OR–2 → 1Pop b and a; push 1 if either is non-zero, else 0.
0x39XOR–2 → 1Pop b and a; push 1 if exactly one is non-zero, else 0.

Bitwise

CodeMnemonicOperandsStackDescription
0x3ABAND–2 → 1Pop b and a; push a & b.
0x3BBOR–2 → 1Pop b and a; push a | b.
0x3CBXOR–2 → 1Pop b and a; push a ^ b.
0x3DBNOT–1 → 1Pop a; push ~a.
0x3ESHL–2 → 1Pop b and a; push a << b.
0x3FSHR–2 → 1Pop b and a; push a >> b (arithmetic right shift, sign-preserving).

Allocators

CodeMnemonicOperandsStackDescription
0x40BQMXreg: Register1 → 0Pop size; allocate a binary QUBO model ({0, 1} domain) into a register.
0x41SQMXreg: Register1 → 0Pop size; allocate a spin Ising model ({-1, +1} domain) into a register.
0x42XQMXreg: Register2 → 0Pop k then size; allocate an integer model with domain {0, …, k-1} into a register. Errors when k < 2.
0x43BSMXreg: Register1 → 0Pop size; allocate a binary sample ({0, 1} domain) into a register.
0x44SSMXreg: Register1 → 0Pop size; allocate a spin sample ({-1, +1} domain) into a register.
0x45XSMXreg: Register2 → 0Pop k then size; allocate an integer sample with domain {0, …, k-1} into a register. Errors when k < 2.
0x4AVECreg: Register0 → 0Create an empty vec<int> in a register, identical to VECI.
0x4BVECIreg: Register0 → 0Create an empty vec<int> in a register.
0x4CVECXreg: Register0 → 0Create an empty vec<xqmx> in a register.

Vector Operations

CodeMnemonicOperandsStackDescription
0x50VECPUSHreg: Register1 → 0Pop a value; append it to the register’s vec.
0x51VECGETreg: Register1 → 1Pop index; push vec[index] from the register’s vec.
0x52VECSETreg: Register2 → 0Pop value and index; set vec[index] in the register’s vec.
0x53VECLENreg: Register0 → 1Push the length of the register’s vec onto the stack.
0x54SLACKindices: Register, coeffs: Register2 → 0Pop capacity and start_index; append slack variable indices and power-of-two coefficients to two register vecs.

Index Math

CodeMnemonicOperandsStackDescription
0x5AIDXGRID–3 → 1Pop cols, col, row; push the flat grid index row * cols + col.
0x5BIDXTRIU–2 → 1Pop j and i; push the upper-triangular index for the unordered pair (i, j).

XQMX Coefficient Access

CodeMnemonicOperandsStackDescription
0x60GETLINEreg: Register1 → 1Pop i; push linear[i] from the register’s model, or a sample’s assignment at i; raises IndexOutOfBounds outside [0, size).
0x61SETLINEreg: Register2 → 0Pop value and i; set linear[i] in the register’s model, or a sample’s assignment at i; a sample value outside its domain raises SampleOutOfDomain.
0x62ADDLINEreg: Register2 → 0Pop delta and i; add delta to linear[i] in the register’s model, or a sample’s assignment at i; a sample result outside its domain raises SampleOutOfDomain.
0x63GETQUADreg: Register2 → 1Pop j and i; push quadratic[i, j] from the register’s model; requires MODEL mode – a sample register raises TypeMismatch; raises IndexOutOfBounds outside [0, size).
0x64SETQUADreg: Register3 → 0Pop value, j, and i; set quadratic[i, j] in the register’s model; requires MODEL mode – a sample register raises TypeMismatch; raises IndexOutOfBounds outside [0, size).
0x65ADDQUADreg: Register3 → 0Pop delta, j, and i; add delta to quadratic[i, j] in the register’s model; requires MODEL mode – a sample register raises TypeMismatch; raises IndexOutOfBounds outside [0, size).

XQMX Grid

CodeMnemonicOperandsStackDescription
0x66RESIZEreg: Register2 → 0Pop cols and rows; set the grid dimensions of the register’s model or sample.
0x67ROWFINDreg: Register2 → 1Pop value and row; push the first column where the value matches, or -1.
0x68COLFINDreg: Register2 → 1Pop value and col; push the first row where the value matches, or -1.
0x69ROWSUMreg: Register1 → 1Pop row; push the sum of all linear values in that grid row.
0x6ACOLSUMreg: Register1 → 1Pop col; push the sum of all linear values in that grid column.

XQMX High-Level Constraints

CodeMnemonicOperandsStackDescription
0x70ONEHOTRreg: Register2 → 0Pop penalty and row; add a one-hot constraint over the grid row.
0x71ONEHOTCreg: Register2 → 0Pop penalty and col; add a one-hot constraint over the grid column.
0x72EXCLUDEreg: Register3 → 0Pop penalty, j, and i; add a mutual-exclusion constraint between variables i and j; raises IndexOutOfBounds outside [0, size).
0x73IMPLIESreg: Register3 → 0Pop penalty, j, and i; add an implication constraint from variable i to variable j; raises IndexOutOfBounds outside [0, size).
0x74EQUALITYmodel: Register, indices: Register, coeffs: Register2 → 0Pop penalty and target; expand weighted equality constraint into QUBO terms on a model.
0x75ATLEASTmodel: Register, indices: Register2 → 0Pop penalty and k; allocate slack variables and apply at-least-k constraint.
0x76ATLEASTWmodel: Register, indices: Register, coeffs: Register2 → 0Pop penalty and k; allocate slack variables and apply weighted at-least-k constraint.
0x77REDUCEmodel: Register3 → 1Pop P_aux, var_b, var_a; allocate auxiliary variable and add Rosenberg enforcement terms; push aux index.
0x7FENERGYmodel: Register, sample: Register0 → 1Compute the Hamiltonian energy of a sample against a model; push the result.

Special

CodeMnemonicOperandsStackDescription
0xF0NOP–0 → 0No operation.
0xFFHALT–0 → 0Stop execution.

Bytecode Format

A .xqb file is the binary form of an XQVM program: a fixed 15-byte XQBC header followed immediately by the raw instruction stream. This page covers that wire format. For the human-readable .xqasm source format and how it compiles down to this, see Assembly.

The normative source is spec/xqvm/ENCODING.md. The Rust implementation of the header is Program::encode/Program::decode.

The XQBC header

OffsetWidthFieldDescription
0..44 bytesMagicThe ASCII bytes XQBC
41 byteVersionFormat version, currently 0x01
51 byteinput_slotsCount of INPUT instructions in the program (calldata arity)
61 byteoutput_slotsCount of OUTPUT instructions in the program
7..114 bytescode_lenByte length of the instruction stream, u32 big-endian
11..154 bytescrc32CRC-32/ISO-HDLC checksum of the instruction stream, u32 big-endian
15+–Instruction streamRaw opcode and operand bytes

input_slots and output_slots are informational and are not validated by the decoder. Each counts instructions of that kind in the stream. Neither is a slot count: a program with three OUTPUT instructions that all write slot 0 records output_slots = 3 against a required slot count of 1, and a program with one OUTPUT inside a loop that writes slots 0 through 9 records output_slots = 1 against a required count of 10. Neither byte can therefore be used to pre-size a calldata or output-slot array. The host fixes both counts before the run – Vm::set_calldata and Vm::set_output_slots – and a slot outside them raises CallDataIndex or OutputIndex at run time. Both counts saturate at 255 (u8::MAX): a program with 300 INPUT instructions still encodes input_slots as 255, and there is no error path for the overflow. The count is also best-effort in another sense – it is produced by walking the instruction stream and skipping any instruction that fails to decode, so a malformed stream still yields a (possibly incomplete) count rather than aborting the walk.

A decoder rejects a file if any of the following hold:

  1. It is shorter than 15 bytes.
  2. Its first four bytes are not XQBC.
  3. Its version byte is not 0x01.
  4. The instruction-stream length does not match code_len.
  5. The CRC-32/ISO-HDLC of the instruction stream does not match crc32.

The instruction stream

Every instruction is an opcode byte followed by zero to eight operand bytes:

[ opcode : 1 byte ] [ operand bytes : 0-8 bytes ]

Instruction length is fixed per opcode – it is never encoded in the stream itself, so a decoder needs the opcode table, not a length prefix, to know how many operand bytes follow a given opcode byte. Register operands are a single u8 (0-255); PUSH1-PUSH8 operands are 1 to 8 bytes of big-endian signed two’s complement; label operands are a u8 (JUMP1/JUMPI1) or a u16 big-endian (JUMP2/JUMPI2). Multi-operand and multi-register opcodes concatenate their operands in the order the opcode table lists them – ENERGY r0 r1 encodes as 0x7F 0x00 0x01.

The opcode byte occupies 0x00-0x7F for the normal instruction space, plus two single-byte opcodes outside that range: 0xF0 (NOP) and 0xFF (HALT). Every other byte value in 0x80-0xFF, and any unassigned gap below 0x80, is rejected by the decoder as an unknown opcode.

TARGET and the label pre-scan

TARGET (0x00) has no operand – the opcode byte is the whole instruction. It marks a jump destination and does nothing at runtime; its only job is to exist at a fixed byte position so a decoder can find it.

A decoder builds an id-to-offset lookup by scanning the instruction stream once for TARGET opcodes: the first one encountered is assigned id 0, the second id 1, and so on in program order, each recorded against the byte offset where it starts. JUMP1/JUMP2/JUMPI1/JUMPI2 operands carry these sequential ids – not byte offsets, and not the .N label token that appears in .xqasm source. .N is assembler-only syntax used to resolve jump references before encoding; it is never emitted into the bytecode. In the Rust implementation this scan produces a JumpTable value (the JumpTable::scan associated function), and Program::new runs it once when a program is constructed from raw bytes.

This pre-scan is why a decoder cannot fully validate a single instruction in isolation: whether JUMP1 .3 is well-formed depends on how many TARGET opcodes exist anywhere in the program, which is only known once the whole stream has been scanned. See Verifier for how an out-of-range label is rejected before execution.

Instruction lengths by opcode

LengthOpcodes
1 byte (opcode only)TARGET, NEXT, RANGE, NOP, HALT, POP, SCLR, SWAP, COPY, ADD, SUB, MUL, DIV, MOD, SQR, ABS, NEG, MIN, MAX, INC, DEC, BITLEN, EQ, LT, GT, LTE, GTE, NOT, AND, OR, XOR, BAND, BOR, BXOR, BNOT, SHL, SHR, IDXGRID, IDXTRIU
2 bytes (opcode + 1)JUMP1, JUMPI1, LIDX, LVAL, ITER, LOAD, STOW, DROP, INPUT, OUTPUT, PUSH1, VEC, VECI, VECX, BQMX, SQMX, XQMX, BSMX, SSMX, XSMX, VECPUSH, VECGET, VECSET, VECLEN, GETLINE, SETLINE, ADDLINE, GETQUAD, SETQUAD, ADDQUAD, RESIZE, ROWFIND, COLFIND, ROWSUM, COLSUM, ONEHOTR, ONEHOTC, EXCLUDE, IMPLIES, REDUCE
3 bytes (opcode + 2)JUMP2, JUMPI2, PUSH2, ENERGY, ATLEAST, SLACK
4 bytesPUSH3, EQUALITY, ATLEASTW
5 bytesPUSH4
6 bytesPUSH5
7 bytesPUSH6
8 bytesPUSH7
9 bytesPUSH8

XQMX and XSMX are exceptions worth flagging: each takes one register operand (2 bytes on the wire) but additionally pops two values off the value stack at runtime. Their bytecode length is 2, not 3 – the popped stack values never appear in the encoding.

Examples

InstructionBytes
NOP0xF0
HALT0xFF
TARGET0x00
PUSH1 420x11 0x2A
PUSH2 -10x12 0xFF 0xFF
LOAD r50x0A 0x05
JUMP1 .1000x01 0x64
JUMP2 .10000x03 0x03 0xE8
JUMPI1 .50x02 0x05
ENERGY r0 r10x7F 0x00 0x01
BQMX r20x40 0x02

The PUSHn rows show the encoded instruction, not source you can type: the assembler accepts the PUSH <value> sugar (and its PUSHC alias) and selects the width itself. The JUMPn/JUMPIn forms are typeable but interact badly with the unused-label check – see Control Flow. Write JUMP/JUMPI and let the assembler pick the width.

Where this is implemented

Verifier

The verifier performs static analysis over a program before it runs, so structural and semantic errors are caught at load time rather than partway through execution. It runs as a fixed pipeline of phases; each phase makes one pass over the program and the pipeline is fail-fast, stopping at the first violation any phase finds. A program with two independent defects reports only the first one encountered.

The Rust implementation is xqvm::verifier, exposed to Python via xqffi.verifier and re-exported as xquad.verifier.

This page is the reference: what each phase checks and what each error means. For why a specific program was rejected and how to change it, see Verification.

What passing verification guarantees

Verifier::default() builds the standard pipeline described below. Calling its run method against a program returns Ok(()) if every phase passes, or the first VerifierError encountered otherwise. A program that passes is guaranteed structurally sound: every opcode decodes, every jump lands on a real TARGET, every loop is balanced, and every register is read only after it is written on every reachable path. For the value stack, phase 4 found no underflow, no join-point depth disagreement, and no basic block whose depth exceeds the 8,192-item limit. Every instruction on every reachable path has at least as many values on the stack as it pops, counted before any of its pushes, so a program that passes cannot underflow the value stack.

Every phase reasons about types, control flow and depths. None reasons about values, so no phase can decide a question whose answer the program computes at run time. An allocator size, a grid extent, a loop bound, a shift amount, a calldata or output index and every arithmetic operand are all ordinary popped stack values, which is why InvalidAllocation, InvalidGridDimensions, InvalidIntegerK, InvalidShift, ArithmeticOverflow, IndexOutOfBounds, SampleOutOfDomain, LoopStackOverflow and the two budget faults exist only at runtime and have no verifier counterpart. SampleOutOfDomain is the clearest of them: SETLINE on a sample is well-typed whatever it writes, and only the value the program computes decides whether the write is in domain. That is a boundary rather than a gap: an embedder gets its bound from the step and allocation budgets it sets, not from a verification pass.

Pipeline

OrderPhaseChecks
1aStructuralTruncated instruction bytes, unknown opcodes
1bJump targetA jump label id at or past the program’s target count
1cLoop nestingRANGE/ITER/NEXT balance, loop-only instructions used outside a loop
2Register type-stateControl-flow-based read-before-write and register type checks
3Must-initControl-flow AND-meet analysis: a register read that is unset on at least one incoming path
4Stack depthControl-flow-based stack underflow, overflow risk, and join-point depth mismatches

Phases 1a-1c share one linear scan over the instruction bytes – the same scan that produces the program’s id-to-offset lookup for TARGET labels (see Bytecode Format). Phases 2 through 4 share one control-flow graph of basic blocks, built once, and each runs its own forward worklist analysis over it. The control-flow graph correctly accounts for unreachable code, conditional branches, and loop back-edges, catching errors that a purely linear scan would miss.

Phase 1 – structural, jump target, loop nesting

1a, structural. Every byte in the instruction stream must decode to a known opcode with the correct number of operand bytes. An unrecognised opcode byte or a truncated operand sequence is rejected immediately. Errors: TruncatedInstruction, BadOpcode.

1b, jump target. Every JUMP1, JUMPI1, JUMP2, and JUMPI2 instruction must reference a label id produced by the TARGET pre-scan; an id greater than or equal to the program’s target count is invalid. Error: UndefinedJumpTarget.

1c, loop nesting. RANGE and ITER each open a loop frame; NEXT closes one. The verifier rejects NEXT, LVAL, or LIDX reached with no open loop frame, and any loop frame still open at the end of the program (reported at the byte offset of the outermost unmatched opener). Errors: NoActiveLoop, UnmatchedLoop.

Phase 2 – register type-state

A control-flow-based forward analysis over 256 register slots. Each register starts Unset. An instruction that writes a register advances its slot to a concrete type (Int, VecInt, VecXqmx, Model, Sample) or to Any; an instruction that reads a register is checked against what that instruction requires.

At a join point – two or more incoming control-flow paths – the per-register meet is permissive: disagreeing types, including a register unset on one path and Int on the other, resolve to Any rather than an immediate error. That permissiveness avoids false positives for a register written on only one branch of a conditional; Phase 3 is what actually catches that case. Reading an Unset register is rejected, and so is a type mismatch, for example reading a Model register where an instruction requires Int.

A join is held back from that permissiveness, because Any satisfies every requirement. Joining a model with a sample gives model or sample, and joining the two vector types gives vec<int> or vec<xqmx>; each satisfies only what its two members have in common – the grid operations and the linear coefficient trio for the first, ITER and VECLEN for the second. Any other pair of types gives conflicting types, which satisfies nothing but a read that accepts any set register, such as OUTPUT. A program that allocates a model on one branch and a sample on the other and then writes a quadratic coefficient is rejected, which is the point: it would fault at run time on one of its two paths. Answering unrelated pairs this way is also what keeps the result the same however many branches meet, and whatever order they are in.

DROP is a special case worth knowing. At runtime it resets the register to Unset, and the verifier models that faithfully: DROP resets the register’s tracked type to Unset, so a read after DROP is rejected as ReadUnsetRegister and would also fault at runtime as UnsetRegister. Unreachable blocks are skipped, so dead code after an unconditional jump never produces a false positive.

Errors: ReadUnsetRegister, RegisterTypeMismatch.

Phase 3 – must-init analysis

A control-flow-based forward analysis using an AND-meet over a bitmap of which registers are definitely initialised. At program entry every register is un-written. At a join point, a register’s bit is cleared unless every incoming path wrote it – so a register written on only one branch of a conditional is flagged as possibly-unset at any read after the join, even though Phase 2’s permissive meet let the same register pass its type check. Unreachable blocks are skipped.

Error: ReadUnsetRegister.

Phase 4 – stack depth

A control-flow-based forward analysis over the abstract depth of the value stack. Each basic block is characterised by the minimum depth it needs on entry and its effect on exit; the analysis propagates the most conservative depth to each successor.

Checks run in this order:

  1. Loop stack imbalance. A separate pass walks each loop body, ignoring the back-edge, and compares the depth at NEXT against the depth at the matching RANGE/ITER opener. A non-zero net effect is an imbalance, reported before anything else in that loop is checked.

  2. Stack depth mismatch. After the pass converges, every join block – two or more incoming edges – has the arrival depths of all its reachable incoming edges compared; any two that differ mean the stack state at that point is not well-defined.

    Program entry counts as an incoming edge of the entry block, arriving at depth 0. So a back-edge targeting the entry block makes it a join block even though it has only one predecessor block, and the loop body’s exit depth is compared against entry’s depth 0.

  3. Underflow and overflow risk. A block whose converged depth would fall below zero, or exceed 8,192, is flagged. The block’s entry requirement is the lowest depth any of its instructions’ pops reaches: each instruction’s pops are counted before its pushes, so PUSH 1 / SWAP needs one more value than it has, even though SWAP leaves the depth unchanged.

SCLR resets the abstract depth unconditionally to zero, which the analysis treats as a reset rather than a delta. A reset does not undo an underflow before it: SCLR / POP / SCLR / HALT is rejected, because the POP faults before the second SCLR runs. If the entry depth was N > 0 and the exit depth is 0, that reset is itself the imbalance: inside a loop it is reported directly as LoopStackImbalance by check 1, ahead of everything else in that loop. Outside a loop, the same reset is only a problem if it creates a discrepancy between incoming edges at a join, in which case check 2 catches it as StackDepthMismatch.

Errors: LoopStackImbalance, StackDepthMismatch, StackUnderflow, StackOverflowRisk.

Error reference

ErrorPhaseMeaning
TruncatedInstruction1aThe instruction stream ends mid-operand
BadOpcode1aAn opcode byte does not map to a known instruction
UndefinedJumpTarget1bA jump references a label id at or past the program’s target count
NoActiveLoop1cNEXT, LVAL, or LIDX reached with no open loop frame
UnmatchedLoop1cA loop frame is still open at the end of the program
ReadUnsetRegister2 or 3A register is read before it is written, or before it is written on every path
RegisterTypeMismatch2A register holds a different RegVal variant than the instruction expects
LoopStackImbalance4A loop body’s entry and exit stack depths differ
StackDepthMismatch4Two control-flow paths reach a join point with different stack depths
StackUnderflow4Stack depth would go negative at a reachable instruction
StackOverflowRisk4Stack depth would exceed 8,192 at a reachable instruction

Every VerifierError variant carries the byte offset of the offending instruction; several (RegisterTypeMismatch, UndefinedJumpTarget, and others) also carry the register number or label id involved, so a diagnostic can point at the exact spot in the bytecode.

When a program fails

A verifier failure is a load-time rejection: nothing in the program has executed. xquad vm check runs the pipeline from the command line without running the program; see CLI. For what to change in your source when a specific error fires, see Verification, which covers the same errors from the side of fixing them rather than defining them.

Sources

Limits and Errors

XQVM has two separate error surfaces. The verifier is static analysis that runs before a program executes; xquad vm check runs it without running the program. The VM is runtime: xquad vm run does not verify automatically, so a program that was never checked can reach the VM and fault there instead. Several conditions – an out-of-range jump label, in particular – have both a verifier error and a distinct VM runtime error for this reason.

Fixed Limits

LimitValueEnforced by
Stack depth8,192 itemsVM: StackOverflow. Verifier: StackUnderflow/StackOverflowRisk (Phase 4)
Register count256 slots (r0-r255), statically allocated–
Jump label range0-65,535 (u16), 65,536 labels maxAssembler: TooManyTargets. Verifier: UndefinedJumpTarget. VM: InvalidLabel
Shift amount0-63 bitsVM: InvalidShift
Grid dimensionsrows and cols must be > 0, and rows * cols must not exceed the register’s declared sizeVM: InvalidGridDimensions
XQMX size4,294,967,295 (2^32 - 1) variables, and the allocation budgetVM: InvalidAllocation past the maximum, MemoryLimitExceeded when the budget cannot pay. Applies to a size an allocator was given and to one ATLEAST, ATLEASTW, REDUCE or EQUALITY grew a model to
Loop nesting8,192 framesVM: LoopStackOverflow
Integer domain size (XQMX/XSMX)k >= 2VM: InvalidIntegerK
Sample assignment valuea member of the register’s domain: {0, 1} binary, {-1, +1} spin, {0, ..., k-1} integerVM: SampleOutOfDomain on SETLINE/ADDLINE. Model coefficients are unbounded and never raise it

Configurable Limits

LimitLibrary defaultMethodVM error when exceeded
Step budget10,000,000Vm::set_step_limit(n) / Vm::set_unlimited_steps()StepLimitExceeded
Allocation budget1 GiBVm::set_memory_limit(bytes)MemoryLimitExceeded
Calldata slots0Vm::set_calldata(vec)CallDataIndex
Output slots0Vm::set_output_slots(n)OutputIndex

The library defaults above apply to Vm::new() directly; the xquad vm run CLI sets its own defaults before handing control to the VM – 16 output slots unless --outputs overrides it. See xquad vm run for the CLI’s own default table.

The step limit is exact: set_step_limit(0) permits no instructions at all, not unlimited ones. To remove the bound, call Vm::set_unlimited_steps(). Nothing else removes it, and nothing removes it by default. Until 0.4.0 0 was the sentinel for “unlimited”, which made a zero budget the most dangerous value a caller could pass rather than the safest.

The step budget

A step is a unit of metered execution cost, not an instruction: every instruction charges a base cost before dispatch, and opcodes whose work scales with data the program controls – evaluating a model, expanding a constraint, copying a register that holds a model – charge more before they do that work. An instruction that cannot pay the charge fails with StepLimitExceeded and does none of the work it would have charged for. Vm::steps() reports the metered total; Vm::instructions() reports the plain dispatch count, which is always less than or equal to steps(). See spec/xqvm/METERING.md for the full cost model, including the per-opcode charges and the constants they use.

The allocation budget

Every instruction that allocates a sample buffer, declares model variables, grows a vector, or expands constraint coefficients is charged against the budget before it allocates. An instruction that cannot pay fails with MemoryLimitExceeded and allocates nothing, leaving its target register untouched. Vm::memory_used() reports what a run spent.

Charges are cumulative rather than a high-water mark of live memory: bytes are charged when they are allocated and are never refunded, so a loop that allocates and discards cannot spend more than the budget in total. Each run() starts from zero. There is no sentinel for “unlimited” – pass u64::MAX.

ChargedRate
BQMX, SQMX, XQMX (declared model size)8 bytes per variable
BSMX, SSMX, XSMX (sample buffer)8 bytes per variable
VECPUSH, SLACK (vector growth)16 bytes per element
SETLINE, ADDLINE on a model32 bytes per coefficient
SETQUAD, ADDQUAD, EXCLUDE, IMPLIES48 bytes per coefficient
ONEHOTR, ONEHOTC, EQUALITY, ATLEAST, ATLEASTWworst-case expansion: one linear term per variable and one quadratic term per pair
REDUCEone auxiliary variable, three quadratic terms, one linear term
ITER on a vec<int> (slice copied into the loop frame)8 bytes per element
ITER on a vec<xqmx> (slice copied into the loop frame)one whole-model copy per element – see below

Models store their coefficients sparsely, so a declared model size costs nothing immediately; it is charged because every consumer of the model – the sample needed to evaluate it, each solver backend – has to materialise it. The expanding constraint opcodes are charged for their worst case, so an expansion whose indices collide can be charged more than it ultimately stores.

VEC, VECI and VECX install an empty vector and allocate nothing; their storage is charged as VECPUSH and SLACK create it.

ITER copies the slice it iterates into its loop frame, and a loop frame is released only by NEXT. A back-edge that re-enters an ITER without reaching its NEXT therefore accumulates copies, which is why the copy is charged. The loop frame itself is not charged, but it is bounded: the loop stack is capped at 8,192 frames and a program that grows it past that fails with LoopStackOverflow.

An element of a vec<xqmx> is charged the whole-model copy rate, because cloning a model clones its coefficient maps: 8 bytes per declared variable plus 32 per non-zero linear coefficient and 48 per non-zero quadratic one. A write that leaves a coefficient at 0 removes its entry, so a zeroed coefficient is not copied, though the write that zeroed it was still charged. The copy therefore never costs more than the allocator and the coefficient writes charged to build the model, and it is the same number wherever the copy happens – through an ITER, or through INPUT/OUTPUT copying a whole register across the host boundary. A model does not get cheaper by being duplicated through one opcode rather than another.

The charge schedule is defined over program-visible quantities – variables declared, elements appended, coefficients written – rather than over the interpreter’s internal representation, so every host rejects the same programs at the same instruction having charged the same bytes. xquad.vm.VM.set_memory_limit() sets the budget and VM.memory_used() reads the result back.

VM runtime errors

Produced by xqvm::Error (xqvm/src/error.rs) while a program is executing. Every variant that carries a byte position (pos) can be turned into a RuntimeDiagnostic via Error::into_diagnostic, which disassembles the program and points at the failing instruction.

ErrorCause
StackUnderflowPopping from an empty or too-shallow stack
StackOverflowPushing when the stack is already at 8,192 items
RegisterTypeInstruction expects a different RegVal variant than the register holds
IncompatibleTypeType mismatch reported without register context
UnsetRegisterLOAD or OUTPUT on a register that was never written, or was DROPped
DivisionByZeroDIV or MOD with divisor 0
ArithmeticOverflowAn i64 operation left the signed 64-bit range. Every operation the VM performs on a program’s behalf is checked, intermediates included, so a computation whose mathematical answer is representable still faults when a partial result is not
IndexOutOfBoundsAn index operand outside what it addresses: a vec index, a coefficient index against the model’s declared size, or a grid row or column against the extent RESIZE declared
NoActiveLoopNEXT, LVAL, or LIDX with no active loop
BadJumpTargetJump target lands outside the bytecode buffer
InvalidLabelJUMP/JUMPI references a label id the id-to-offset scan never resolved
BadOpcodeUnrecognised opcode byte
TruncatedInstructionBytecode ends mid-instruction
CallDataIndexINPUT index out of range
OutputIndexOUTPUT index out of range
SizeMismatchENERGY sample length does not match model size
VecLengthMismatchTwo parallel vectors used together (for example EQUALITY’s indices and coefficients) have different lengths
StepLimitExceededA step charge could not be paid: "step charge of {requested} exceeds the step limit of {limit} ({used} steps already charged)", where requested is 1 for the base per-instruction cost or the larger charge an opcode asked for before doing data-scaled work
MemoryLimitExceededAn allocating instruction exceeded the configured allocation budget
InvalidShiftSHL/SHR shift amount outside [0, 64)
InvalidGridDimensionsRESIZE with rows or cols <= 0, RESIZE with rows * cols past the register’s declared size, or a grid-reading opcode on a register with no grid
InvalidAllocationAn allocator given a negative size, or one past the maximum allocator size 2^32 - 1; also a constraint that would grow a model past it, since ATLEAST, ATLEASTW, REDUCE and EQUALITY all append variables. Both are properties of the operand rather than of the machine running it, so the same size is refused everywhere. Under an ordinary budget an oversized size raises MemoryLimitExceeded first, since the charge precedes the range check
LoopStackOverflowRANGE/ITER nesting past 8,192 frames
InvalidIntegerKXQMX/XSMX called with k < 2 – k counts the values in {0, ..., k-1}, so k = 1 leaves a single value and no decision to make
SampleOutOfDomainSETLINE/ADDLINE wrote a value outside a sample’s domain. ADDLINE checks the result of the addition rather than the delta. Sample registers only: a model’s linear[i] is a bias, not an assignment, and is unbounded
UnmatchedLoopA RANGE/ITER skip-forward scan reached the end of the stream without a matching NEXT
TraceFailedA tracer callback returned an error (for example an I/O write failure)

Verifier errors

Produced by xqvm::verifier::VerifierError (xqvm/src/verifier/error.rs) before a program runs, by the four-phase pipeline described in Verifier: structural, jump-target and loop-nesting checks; register type-state; must-init analysis; and stack depth.

The eleven variants, which phase raises each one, and what each one means are catalogued in Verifier’s error reference rather than repeated here, since that page also explains the phase that produces each one. See Verification for how to fix a rejected program.

Substrate pallet fixture limits

fixtures/pallet-xqvm (excluded from the main Cargo workspace build; see Embedding Overview) adds two additional bounds on top of the ones above, enforced by the runtime’s Config trait rather than the VM:

LimitConfig itemPurpose
Program sizeMaxProgramSizeMaximum bytecode byte length accepted by submit_program
Calldata and output countMaxCalldataShared bound on both the calldata vector and the output-slot vector

submit_program takes the step budget as an extrinsic argument rather than reading a pallet constant, so a caller names the bound the VM may spend. The bound is exact, and a step_limit of 0 fails rather than succeeding vacuously. The fixture sets no allocation budget, so the VM’s 1 GiB default applies – far too generous for a runtime that has to price what it admits. See Substrate Pallet.

Embedding Overview

Everything so far in this book drives XQuad from Python or the xquad CLI. This chapter is for embedding the Rust crates directly. That means building bytecode with a fluent Rust API instead of .xqasm text, running it without a Python process in the loop, and compiling for no_std targets.

Crate Map

CrateRoleno_std
xqvmOpcode table, InstructionBuilder, the wire codec, and the Vm interpreterYes, with --no-default-features
xqasmParses .xqasm text into an xqvm::ProgramNo

A Rust program that only needs to build and run bytecode depends on xqvm alone; add xqasm to parse .xqasm source instead of building bytecode by hand with InstructionBuilder.

The no_std Story

xqvm builds no_std + alloc. Two layers are always compiled without std:

  • Bytecode – the opcode table, Instruction, Register, InstructionBuilder, Program, and the wire codec.
  • Interpreter core – Vm, Error, RegVal, XqmxModel / XqmxSample, the bytecode verifier, and the Tracer trait with its no-op implementation.

The std feature, enabled by default, adds three things a no_std embedder gives up: the disassembler (disasm::Disassembly), miette-backed runtime diagnostics (RuntimeDiagnostic), and the JsonTracer / TextTracer implementations. None of these change what a program computes; they change how a fault or a trace is reported. Build for a no_std target with:

cargo add xqvm --no-default-features

This is exercised, not just claimed: fixtures/xqvm-wasm is a freestanding wasm32-unknown-unknown test crate depending on xqvm with default-features = false, and CI’s test:wasm job runs cargo build -p xqvm --target wasm32v1-none --no-default-features followed by wasm-pack test --node against it. The job is path-gated: it runs on merge requests and feature branches whenever the diff can reach xqvm or the fixture, and unconditionally on protected refs and release tags.

Running In-Process, Without .xqasm

Builder API covers InstructionBuilder in full: emitting instructions one at a time, resolving forward and backward labels, and reading back the resulting JumpTable. It is the API a no_std embedder uses when parsing text is not an option.

On-Chain: The Pallet Fixture

Pallet Fixture documents fixtures/pallet-xqvm, a Substrate FRAME pallet that embeds xqvm inside a runtime as an integration gate, not a production deployment path. It is a real, CI-tested example of a no_std embedder, and the one currently in this repository.

On-Chain: Which Opcodes a Chain May Admit

On-Chain Admissibility answers which of the 93 opcodes a runtime embedding xqvm may accept from an untrusted account. The answer is all of them, because chain compatibility is a property of the instruction set rather than a per-embedder decision; the page states the bar an opcode has to clear, where each half of it is enforced, what every family charges, and which parts of that boundary the tree does not yet enforce.

Checking an Implementation Against the Spec

Conformance covers the vector suite that holds xqvm to the behaviour spec/xqvm/ prescribes. Read it if you are embedding xqvm somewhere that needs to trust its output matches the specification.

Builder API

InstructionBuilder is a fluent Rust API for constructing XQVM bytecode programmatically, without going through the text assembler. It lives in the xqvm crate.

Basic Usage

#![allow(unused)]
fn main() {
use xqvm::InstructionBuilder;

let mut b = InstructionBuilder::new();
b.emit_push(10)
 .emit_push(32)
 .emit_add()
 .emit_halt();

let program = b.build().unwrap();
assert_eq!(program.code().len(), 6);
}

Labels

Labels are opaque handles. Create them with label(), anchor them with place(), and reference them in emit_jump()/emit_jump_if(). Both forward and backward references work. place() returns a Result – placing the same label twice, or placing a label from a different builder, is an error.

Backward Reference

#![allow(unused)]
fn main() {
use xqvm::InstructionBuilder;

let mut b = InstructionBuilder::new();
let loop_top = b.label();

b.emit_push(3);
b.place(loop_top).unwrap();  // anchor label at this position
b.emit_push(-1);
b.emit_add();
b.emit_copy();
b.emit_jump_if(loop_top);    // backward jump to loop_top
b.emit_pop();
b.emit_halt();

let program = b.build().unwrap();
}

Forward Reference

#![allow(unused)]
fn main() {
use xqvm::InstructionBuilder;

let mut b = InstructionBuilder::new();
let done = b.label();

b.emit_push(0);
b.emit_jump_if(done);         // forward jump -- target not yet placed
b.emit_push(42);
b.place(done).unwrap();       // anchor here
b.emit_halt();

let program = b.build().unwrap();
}

PUSH Auto-Sizing

emit_push(val) automatically selects the smallest PUSH1–PUSH8 instruction:

#![allow(unused)]
fn main() {
use xqvm::InstructionBuilder;

let mut b = InstructionBuilder::new();
b.emit_push(0);         // emits PUSH1 (2 bytes)
b.emit_push(42);        // emits PUSH1 (2 bytes)
b.emit_push(1000);      // emits PUSH2 (3 bytes)
b.emit_push(i64::MAX);  // emits PUSH8 (9 bytes)
b.emit_halt();

let program = b.build().unwrap();
assert_eq!(program.code().len(), 17);  // 2 + 2 + 3 + 9 + 1 (HALT)
}

Register Operations

Most register instructions have a corresponding method:

#![allow(unused)]
fn main() {
use xqvm::{InstructionBuilder, Register};

let mut b = InstructionBuilder::new();
b.emit_push(42)
 .emit_stow(Register(0))     // r0 <- Int(42)
 .emit_load(Register(0))     // push r0's value back onto the stack
 .emit_bqmx(Register(1))     // pop 42 as size; allocate a binary model in r1
 .emit_halt();

let program = b.build().unwrap();
}

DROP is available as emit_drop(). It resets the register to Unset, the same state an unwritten register starts in – not Int(0):

#![allow(unused)]
fn main() {
use xqvm::{InstructionBuilder, Register};

let mut b = InstructionBuilder::new();
b.emit_drop(Register(5));  // r5 <- Unset
b.emit_halt();

let program = b.build().unwrap();
}

ENERGY

The emit_energy() method takes two register operands:

#![allow(unused)]
fn main() {
use xqvm::{InstructionBuilder, Register};

let mut b = InstructionBuilder::new();
b.emit_energy(Register(0), Register(1));  // ENERGY r0 r1
b.emit_halt();

let program = b.build().unwrap();
}

Raw Instruction Emit

For instructions without a dedicated method, use emit():

#![allow(unused)]
fn main() {
use xqvm::{InstructionBuilder, Instruction};

let mut b = InstructionBuilder::new();
b.emit(Instruction::Copy {})
 .emit(Instruction::Halt {});

let program = b.build().unwrap();
assert_eq!(program.code().len(), 2);
}

Build Errors

build() validates all labels and returns errors for:

  • UnplacedLabel – a label was used in a JUMP/JUMPI but never placed.
  • UnusedLabel – a label was placed but never referenced by any jump. The label at byte offset 0 is exempt from this check: the builder treats offset 0 as the implicit entry point, so a label placed there and never jumped to does not count as unused. (The l0/l1 example in the next section relies on exactly this – l0 sits at offset 0 and is never referenced by a jump, yet build() succeeds.)
  • TooManyTargets – more than u16::MAX + 1 (65,536) labels were placed in the program, exceeding the wire-format limit on sequential TARGET ids.
  • FixupOutOfBounds – a fixup site falls outside the assembled buffer. This indicates a bug in the builder itself, not a mistake in caller code.

Two more Error variants exist but are returned by place(), not build(): DuplicateLabel (a label placed more than once) and ForeignLabel (a label passed to a builder that did not create it).

#![allow(unused)]
fn main() {
use xqvm::InstructionBuilder;

let mut b = InstructionBuilder::new();
let ghost = b.label();
b.emit_jump(ghost).emit_halt();
assert!(b.build().is_err());  // UnplacedLabel
}

Labels and the JumpTable

Every place() call anchors a label to the current write position and emits an inline TARGET opcode there. build() resolves each JUMP/JUMPI fixup against the placed TARGET positions, renumbers labels into stream order, and returns a Program. The result of that resolution is queryable afterward: Program::jump_table() returns a JumpTable, mapping each TARGET’s sequential id (0, 1, 2, ... in program order) to its byte offset.

JumpTable::len() equals the number of distinct labels placed, and JumpTable::get(id) returns the byte offset for a sequential id:

#![allow(unused)]
fn main() {
use xqvm::InstructionBuilder;

let mut b = InstructionBuilder::new();
let l0 = b.label();
let l1 = b.label();
b.place(l0).unwrap()
 .emit_nop()
 .emit_jump(l1)
 .place(l1).unwrap()
 .emit_halt();

let program = b.build().unwrap();
assert_eq!(program.jump_table().len(), 2);
assert_eq!(program.jump_table().get(0), Some(0));  // first TARGET, byte 0
assert_eq!(program.jump_table().get(1), Some(4));  // second TARGET, byte 4
}

This mapping is built once, at load time, by a single scan of the instruction stream for TARGET opcodes – it is not part of the .xqb wire format. See Execution for how the VM’s run loop resolves a Jump result against it.

Pallet Fixture

pallet-xqvm embeds xqvm inside a Substrate FRAME runtime, as a compile-time and runtime integration gate. Its own module documentation states the purpose plainly: if xqvm’s public API changes in a way that breaks Substrate pallet integration, CI fails here first. fixtures/pallet-xqvm is an excluded member of the main Cargo workspace, declared in Cargo.toml, because it pulls in a heavy polkadot-sdk git dependency. Treat this chapter as a reference integration and a fixture to build on, not as a supported deployment path with its own release cycle. Which opcodes a runtime that embeds the VM may admit, and what it has to enforce to admit them, is stated separately in On-Chain Admissibility.

CI still runs it, and blocks on it. test:substrate in .gitlab/ci/test.yml is path-gated: on a merge request or a feature branch it runs only when the diff touches xqvm, the fixture, or the files that define the job itself, and it runs unconditionally on protected refs and release tags. It carries no allow_failure:, so it blocks the pipeline whenever it is created. make test-substrate-fixture runs cargo test --manifest-path fixtures/pallet-xqvm/Cargo.toml, exercising eight pallet tests plus two runtime-integrity checks FRAME generates from construct_runtime!. Every claim below is checked against fixtures/pallet-xqvm/src/lib.rs.

Configuration

The pallet is configured via the Config trait:

#[pallet::config]
pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
    /// Maximum byte length of an uploaded XQBC program.
    #[pallet::constant]
    type MaxProgramSize: Get<u32>;

    /// Maximum number of calldata input slots and output slots.
    #[pallet::constant]
    type MaxCalldata: Get<u32>;
}

Two associated types, both bounds rather than tunable behaviour. The mock runtime used by the fixture’s own tests sets MaxProgramSize = 65_536 (64 KiB) and MaxCalldata = 256, matching the VM’s 256-slot register file. A single MaxCalldata bound caps both the calldata a caller supplies and the outputs the pallet can return – there is no separate output-side limit.

Storage

#[pallet::storage]
pub type StoredProgram<T: Config> =
    StorageValue<_, BoundedVec<u8, T::MaxProgramSize>, OptionQuery>;

One value, not a map. StoredProgram holds the bytecode of the most recently executed program; calling the pallet’s extrinsic again overwrites it. There is no per-program storage keyed by hash, and no record of which account submitted which program beyond the event stream below.

The submit_program Extrinsic

The pallet exposes exactly one dispatchable, call index 0:

#[pallet::call_index(0)]
pub fn submit_program(
    origin: OriginFor<T>,
    bytecode: BoundedVec<u8, T::MaxProgramSize>,
    calldata: BoundedVec<i64, T::MaxCalldata>,
    output_slots: u32,
    step_limit: u64,
    memory_limit: u64,
) -> DispatchResult
ParameterTypeDescription
bytecodeBoundedVec<u8, T::MaxProgramSize>XQBC-encoded program, produced by InstructionBuilder::build().encode() or by assembling .xqasm source.
calldataBoundedVec<i64, T::MaxCalldata>Integer values injected as RegVal::Int into the VM’s calldata slots, in order.
output_slotsu32Output slots to reserve. The caller declares its own arity; see step 3. May not exceed MaxCalldata.
step_limitu64Instructions the run may execute. The bound is exact: 0 executes nothing and the call fails with ExecutionFailed rather than succeeding vacuously.
memory_limitu64Bytes the run may allocate. Charged before each allocation, so an over-large one returns ExecutionFailed instead of reaching the allocator.

Decode, execute, store, in that order:

  1. Require a signed origin (ensure_signed); an unsigned call is rejected with BadOrigin before pallet logic runs.

  2. Decode bytecode as an xqvm::Program. A decode failure returns Error::BytecodeInvalid and nothing is stored.

  3. Take the output-slot count from the caller’s output_slots argument, rejecting a request above MaxCalldata with OutputSlotsTooLarge.

    Do not derive it from program.output_slots(). That header byte is a saturating count of OUTPUT instructions, not of slots, so one OUTPUT inside a loop that writes slots 0 and 1 records 1; sizing the output vec from it makes the slot-1 write raise OutputIndex and the call fail with ExecutionFailed, rejecting a program the VM and the verifier both accept. See the bytecode format, which states that neither header count may be used to pre-size a slot array.

  4. Build a fresh Vm, set the calldata, the output-slot count and the caller’s two budgets, and run the program. Any VM fault returns Error::ExecutionFailed; the pallet does not distinguish which fault occurred, so a budget exhausted one instruction short of HALT is indistinguishable from a decode-clean program that divided by zero.

  5. Collect the Int outputs (any other RegVal variant in an output slot is silently dropped from the result) and bound them to T::MaxCalldata. Exceeding that bound returns Error::OutputOverflow.

  6. Store bytecode in StoredProgram, overwriting any previous value, and emit Event::ProgramExecuted { who, outputs }.

There is no separate store-then-execute split and no program lookup by hash. Both budgets are extrinsic arguments rather than pallet constants because that is the shape the real pallet has to take: what a caller pre-pays for is what the VM may spend, and a budget the caller cannot name is a threat model the fixture cannot express.

The allocation budget is the one that has to be named on-chain rather than inherited. Vm::new() installs 1 GiB, which is far more than a wasm32 runtime heap, so the heap gives out long before the budget does and the allocator traps the whole execution instead of returning a fault the pallet can report as ExecutionFailed. A caller-supplied memory_limit, capped by MaxMemoryLimit, is what keeps an over-large allocation a reportable dispatch error. The mock sets the cap to the VM’s own 1 GiB so that only the caller’s ability to name something smaller is under test; a production runtime sets it to a figure its heap can actually honour.

What the fixture does not do is call xqvm::verifier::verify: it decodes the bytecode and runs it, so every static check the verifier performs is skipped, and the faults those checks would have caught surface at runtime as ExecutionFailed instead. A production pallet must verify before it executes, for the reasons set out in On-Chain Admissibility.

Off-chain, produce the bytecode however you like; the assembler CLI is xquad vm asm.

The cargo profile the runtime is built with is a second default an operator must not inherit without reading it. Do not compile the runtime that carries this pallet with overflow-checks = true until every #[expect(clippy::arithmetic_side_effects, ...)] entry in xqvm has been re-read as a claim about that build: the VM already raises ArithmeticOverflow for the arithmetic it performs on a program’s behalf, so the flag adds nothing there and instead turns the host-side operations that allow-list records into panics, and a panic inside block execution is a block-production fault, strictly worse than a wrong answer. The flag would not touch the VM’s deliberate wraps, which are method calls (SHL’s wrapping_shl, SLACK’s wrapping_mul); it acts on the bare operator sites, which are exactly the ones the allow-list claims cannot leave range. That is why the reading is the gate, and it does not expire once the allow-list exists.

Weight

#[pallet::weight(Weight::from_parts(10_000, 0).saturating_add(T::DbWeight::get().writes(1)))]

A fixed placeholder, not a metered cost model: the same weight is charged regardless of program size or step count actually used. The extrinsic neither computes an actual weight from vm.steps() nor refunds any difference via PostDispatchInfo. The pallet’s own source comment says as much: a production integration must supply real benchmarks. This weight exists only so the extrinsic compiles and the fixture’s tests can dispatch it.

Events and Errors

EventFieldsDescription
ProgramExecutedwho, outputsEmitted once, after a successful run. outputs holds only the Int-valued output slots, in slot order.
ErrorMeaning
BytecodeInvalidbytecode failed XQBC decode.
ExecutionFailedThe VM faulted at runtime – stack underflow, an unresolved jump, or any other xqvm::Error variant, all mapped to this one case.
OutputOverflowThe program produced more Int outputs than MaxCalldata allows.
StepLimitTooLargestep_limit exceeds MaxStepLimit. Checked before the decode, so an over-budget request never pays to parse the program it would have run.
MemoryLimitTooLargememory_limit exceeds MaxMemoryLimit. Checked before the decode, for the same reason.
OutputSlotsTooLargeoutput_slots exceeds MaxCalldata.

What the Fixture’s Tests Check

fixtures/pallet-xqvm/src/tests.rs covers five groups, sixteen tests in all:

  • Happy paths – an arithmetic program with no calldata, a calldata passthrough, and a two-value sum.
  • Error paths – invalid bytecode, a stack underflow that reaches ExecutionFailed, and an unsigned origin rejected by ensure_signed.
  • The step budget – a request above MaxStepLimit, one exactly at it, a zero budget, and a budget shorter than the program.
  • The allocation budget – a request above MaxMemoryLimit, one exactly at it, and a budget a BQMX outgrows. The third is the one the argument exists for: it fails outright if submit_program stops calling set_memory_limit, which is how the gap this closes went unnoticed.
  • The output-slot count – one OUTPUT inside a loop writing a slot per iteration, the same program with the count the header byte would have supplied, and a count above MaxCalldata. The middle test asserts program.output_slots() == 1 directly, so the pair records what that header byte is and is not good for.

Running make test-substrate-fixture against this tree passes eighteen: the sixteen above plus two FRAME-generated checks, a genesis-config build and a construct_runtime! integrity test.

Calldata Limitations

submit_program only accepts i64 calldata (not models, vectors, or samples), because BoundedVec<i64, T::MaxCalldata> is the extrinsic’s parameter type. A program that needs a richer input must construct it internally rather than receive it from the caller.

On-Chain Admissibility

A runtime that embeds xqvm runs bytecode it did not write, submitted by an account it does not trust, inside a block whose weight it has to declare in advance. The question this page answers is which of the VM’s 93 opcodes such a runtime may admit.

The answer is all of them, and not as a concession each runtime makes separately. On-chain verifiable deterministic execution is what XQVM is for, so chain compatibility is a property of the instruction set rather than a per-embedder admission decision. An operation that cannot be made to meet the bar below is not an opcode that needs gating: it is functionality that belongs in xqcp, xqsa, the xquad API or a helper library, and it never enters the ISA at all.

This page is the reader-facing summary of that rule. All six clauses are specified where a third implementation reads them: clause 1 in spec/xqvm/SPEC.md’s Type System, which makes checked arithmetic normative with no implementation-defined alternative; clause 2 in the same file’s Determinism section, which states that an instruction’s result is a function of the program, the calldata and the VM’s own state, and that no instruction reads host state the calldata did not carry; clause 3 in HLF.md’s ordering rules, covering delta application, sparse-table accumulation and the grid fold; clause 4 in SPEC.md’s Allocation budget section, which charges every allocation before it happens; clause 5 in METERING.md, which fixes the step schedule, its charging points and its counting units; and clause 6 in SPEC.md’s Faults section, which gives every fault a normative identity that ISA.md is bound to name faults by, together with the conformance vectors that pin the behaviour itself. One row of that section records an identity that is not yet settled; the section on what is not enforced says which.

The gate that holds a proposed opcode against all six belongs to the contribution process rather than to the specification. It is stated in docs/guide/development-workflow.md under “The opcode-addition gate”: adding a row to the opcode table requires the merge request to argue each of the six clauses in its description, and an opcode that cannot clear all six does not ship. There is deliberately no CI guard for it, because the gate asks for a correctness argument a reviewer weighs rather than a string a script can find.

The bar

An XQVM opcode has to satisfy all of the following.

  1. No floating point. Integer arithmetic only, with every operation range-checked so that it faults rather than wrapping.
  2. No host I/O, wall-clock, or ambient state. An instruction’s result is a function of the program, the calldata and the VM’s own state.
  3. No nondeterministic iteration order. Anything that walks a collection walks it in an order the spec fixes.
  4. Bounded allocation. Every allocation is charged against a budget before it happens, and the budget is not escapable by a value the submitting account controls.
  5. Bounded per-instruction work. The work one instruction performs is charged against the step budget before it happens, at a rate that scales with the data the program controls, so that a step is a unit of cost rather than a unit of dispatch.
  6. Specified behaviour, pinned by a vector. The result and every fault the opcode can raise are specified normatively in spec/xqvm/, and a conformance vector covers the behaviour, failure paths included.

Rules 4 and 5 are the two halves of bounded work, and rule 5 is the one that is easy to lose: an allocation budget bounds how much an instruction keeps, not how long it takes. Rule 6 is the one that is easy to skip. A chain does not need determinism against a single implementation; it needs behaviour that a reader of the specification can predict, because a divergence between the specification and an implementation is unspecified behaviour with a plausible result, and no memory budget makes that safe. spec/xqvm/ and the vectors derived from it are the whole of rule 6: the vectors run the VM against outcomes the specification prescribes, and a mismatch fails CI.

Because the bar is a property of the instruction set, the denied set is empty by construction. A proposed opcode that cannot clear it does not ship, so no embedder inherits a per-opcode decision.

Why not an allowlist

An earlier framing of this question asked which opcode families to admit and which to gate behind a later runtime upgrade, on the theory that the integer core is cheap and the model-building surface is not. That framing is no longer the right one: the model-building opcodes are exactly the reason to run XQVM on chain at all, and the two properties that made them unsafe – unbounded allocation and divergence from the specification – were defects with owners rather than inherent properties of the family.

So a runtime should not carry a static allowlist scanned over the decoded instruction stream. With the bar above there is no denied set for such a scan to find, and an allowlist would be code that has to be revisited on every addition to the instruction set, silently rejecting valid programs until someone remembers to update it. The static half of enforcement is the verifier, which checks properties of the program rather than membership of its opcodes.

Where the bar is enforced

LayerRunsEnforces
Staticxqvm::verifier::verify at program submissionStructural integrity, jump targets, loop balance, register type-state, must-init, stack depth
RuntimeVm::set_memory_limit before executionThe allocation budget, charged before each allocation
RuntimeVm::set_step_limit before executionThe step budget, charged before each unit of work
WeightThe extrinsic’s declared and refunded weightThe block’s share of the above

The static layer is what makes a rejection cheap: a program that cannot run correctly is refused before it consumes execution weight. The runtime layer is what makes admission safe: the verifier does not bound allocation or running time, and cannot, because both depend on values computed at runtime. Neither layer substitutes for the other.

The budgets belong in the runtime’s configuration rather than in the VM defaults. The library defaults documented in Limits and Errors – 10,000,000 steps and a 1 GiB allocation budget – are sized for an off-chain host, and a 1 GiB budget admits a sample of 134 million variables. A chain has to set both from its own Config, sized so that the worst case a submitted program can reach still fits the block.

Family by family

The table below uses the same fourteen categories as the Instruction Set Reference, so the two can be read side by side. Cost is what an instruction charges against the allocation budget; the full schedule, with rates, is in Limits and Errors. Several categories charge different rates for different opcodes, and the cells say which. The step budget is a separate schedule with its own rates, specified in spec/xqvm/METERING.md; an opcode free against one budget is not necessarily free against the other. ENERGY and the four grid readers are the clearest cases: each allocates nothing and each charges the step budget per unit of work it does.

FamilyCountCostNotes
Control flow12ITER charges for the slice it copies – 8 bytes per element of a vec<int>, a whole-model copy per element of a vec<xqmx>; the rest are freeLoop balance is checked statically; RANGE and ITER skip an empty body rather than running it once. The loop frame itself is uncharged but capped: the loop stack holds 8,192 frames and a program that grows past that faults with LoopStackOverflow. LVAL copies the current loop value without charging the allocation budget – see below. NOP and HALT live here
Register I/O5INPUT and OUTPUT charge for the value they copy across the host boundary, at the schedule’s own rates; the rest are freeBoth are also bounded by the calldata and output-slot counts the runtime sets. A model copies at the whole-model rate ITER pays, but a vec<int> costs 16 bytes per element here against ITER’s 8: the copy rate prices a Vec that grew by doubling, which is what VECPUSH charged for, while ITER allocates its slice at exact capacity
Stack manipulation12FreeDepth is bounded at 8,192 items, checked statically and at runtime
Arithmetic13FreeInteger only; every operation is range-checked and faults rather than wrapping
Comparison5Free
Logical4Free
Bitwise6FreeShift amounts outside [0, 64) fault
Allocators9BQMX, SQMX, XQMX: 8 bytes per declared variable. BSMX, SSMX, XSMX: 8 bytes per variable. VEC, VECI, VECX: freeThe three charges are not the same kind. A model stores coefficients sparsely, so its charge is a proxy for what every consumer of the model has to materialise; a sample allocates a dense buffer at that rate, so its charge buys real bytes. An empty vec allocates nothing and its storage is charged as it grows
Vector operations5VECPUSH: 16 bytes per element. SLACK: 16 bytes per element appended, two per entry. VECGET, VECSET, VECLEN: freeSLACK appends to two vecs and is charged for both, so an entry costs 32 bytes; the three access forms neither grow nor allocate
Index math2FreePure index arithmetic on the stack
Coefficient access6SETLINE, ADDLINE: 32 bytes per linear coefficient. SETQUAD, ADDQUAD: 48 bytes per quadraticReads are free, and so are writes into a sample, whose buffer was charged when it was allocated. Both rates are literals rather than derived from the host’s pointer width, so a wasm32 runtime charges what a native build and the book state
Grid operations5FreeA grid reinterprets variables the program already declared and paid for: RESIZE rejects extents whose product exceeds the model’s size, which bounds memory. The step budget bounds the work: ROWSUM, COLSUM, ROWFIND and COLFIND charge GRID_CELL_STEPS per cell of the axis they scan, after validating the operand and before walking it, whether or not the scan short-circuits
High-level constraints8EXCLUDE: one quadratic term. IMPLIES: one linear and one quadratic. REDUCE: one variable, three quadratic terms and one linear. ONEHOTR, ONEHOTC, EQUALITY, ATLEAST, ATLEASTW: worst-case expansion, one linear term per variable and one quadratic term per pairOnly the last five expand; see below
Energy1FreeAllocates nothing, and charges the step budget for the sample it copies and every model term it accumulates

Ninety-three opcodes across the fourteen categories, all of them integer-only by construction: the ISA has no floating-point type, no floating-point opcode and no way to produce one. What the conformance vectors pin is the stronger property that makes this useful for consensus – the checked-arithmetic rule, under which every result is the exact integer or a fault, and every overflow faults at the same instruction on every host.

The expanding constraints deserve a second look

Three sets are worth separating, because Limits and Errors separates them and a weight model needs each:

  • Quadratic in a stack-controlled operand. ONEHOTR, ONEHOTC, EQUALITY, ATLEAST and ATLEASTW write a number of coefficients that grows with the square of a term count the caller chooses.
  • Allocating variables. EQUALITY, ATLEAST, ATLEASTW and REDUCE add variables to the model. REDUCE adds exactly one.
  • Both at once, from a single operand. EQUALITY, ATLEAST and ATLEASTW.

EXCLUDE, IMPLIES and REDUCE charge constant amounts and do not expand. Every opcode in the first set is charged for its worst case before it writes anything, so the aggregate budget does bound them. A runtime that wants a submitted program to fail early and cheaply rather than after spending most of its budget in one instruction should consider a per-instruction cap on top of the aggregate one. That is a pricing decision, not an admissibility one.

What is not enforced today

The bar above is what the instruction set is designed to, and most of it is now backed by code. The v0.4.0 hardening pass bounded grid extents to the allocation a program already paid for, made the coefficient rates target-independent literals, charged the INPUT/OUTPUT copy across the host boundary, capped the loop stack, and closed three divergences from the specification in the model-building surface. Step metering then made a step a unit of cost rather than a unit of dispatch: every instruction charges a base cost before dispatch, the opcodes whose work scales with caller-controlled data charge for that work before doing it, and the forward scan of an empty-loop skip charges for each instruction it consumes. The four grid scans charge GRID_CELL_STEPS per cell of the axis they walk, which was the last place a memory bound stood in for a work bound: RESIZE bounds the extent by an allocation the program paid for, and that bounds what the grid holds, not what a scan over it costs. An embedder pricing WeightPerStep * steps is pricing work rather than dispatches.

The same pass closed the sample value domain, which had been documented as a producer convention rather than a rule: SETLINE and ADDLINE now raise SampleOutOfDomain for a write outside the domain the register’s allocator declared. It is a bounded per-instruction check on a value already on the stack, so it costs nothing against the bar above, and it removes a way for a program to hand a solver an assignment that is not an assignment.

What is left is narrow.

A tracer copies registers outside the step budget. The Rust VM clones every register an instruction reads and writes around each dispatch when a tracer is attached – O(model) per instruction, charged nothing, because the observable step count must not depend on whether a tracer is attached. This is a constraint on the embedder rather than a hole in the meter: a chain runs NoopTracer and the branch compiles out. A runtime that attaches a real tracer meters untrusted programs untraced, or accounts for the tracing copy outside the step budget. Stated normatively in spec/xqvm/METERING.md under Conformance.

LVAL copies without charging the allocation budget. The step budget now prices the copy – LVAL charges for the coefficients it clones out of a loop frame – but the bytes that copy occupies are not charged, so a vec<xqmx> iterated with LVAL in the body holds one unaccounted model copy per live register beyond what ITER paid for. Bounded by the 256-slot register file, so a constant factor rather than an unbounded obligation, but one a memory bound has to include.

The reference integration does not verify. The fixture takes both budgets from the caller and bounds them with MaxStepLimit and MaxMemoryLimit, which is the shape a real pallet needs, so the runtime layer of the boundary is now exercised end to end. The static layer is not: it never calls xqvm::verifier::verify – it decodes and runs, so every static check is skipped and the faults those checks would have caught surface at runtime as ExecutionFailed. Running the verifier on chain is QUI-1055; pricing the budgets into a benchmarked weight rather than the fixed placeholder is QUI-1012.

Agreement with the specification, meanwhile, is close to where the bar wants it. The xqvm build checks its opcode table against xqvm/opcodes.yaml, make conformance runs every vector on the VM, and xqvm/tests/metering_spec.rs checks the metering constants against spec/xqvm/METERING.md value for value. The vectors now cover the model-building surface that the arithmetic-only suite once missed, including failure paths: constraints without a grid, invalid grid dimensions, integer allocation, index-math operand ordering, and accumulation overflow in energy and grid sums.

Operand validation order is part of that agreement and is normative: spec/xqvm/METERING.md fixes it under Conformance, and the VM validates each register completely before looking at the next. The identity of a mode fault is normative too: spec/xqvm/SPEC.md’s Faults section fixes a sample in a model slot as TypeMismatch, and the vectors pin it. See Conformance for how the suite is structured and what adding a vector involves.

Conformance

spec/xqvm/ says what an XQVM program must do, and the Rust xqvm crate this book documents implements it. The specification vectors – xqvm/tests/vectors/ – are the mechanical check that the crate does what the spec says. This page explains what their result means for you: whether you are embedding xqvm or deciding how much to trust it.

What a Vector Is

A conformance vector is a directory holding a fixed test case: a canonical .xqasm program, the calldata it runs with, and the outputs and residual stack it is expected to produce. The runner assembles the program, runs it on the VM, and asserts the observed result matches the recorded expectation.

Each vector directory holds three files:

  • program.xqasm – the canonical, human-readable source. The assembler is authoritative: whatever xqasm::assemble_source() produces from this file is the canonical bytecode, assembled in-process on every run. No pre-assembled bytecode artifact is committed.
  • inputs.json – calldata, output slot count and the two budgets the run is given, for example {"calldata": [6, 7], "output_slots": 16}. Every key but calldata is optional: output_slots defaults to 16, step_limit to 10000000 and memory_limit to 1073741824, each matching xquad vm run. The runner resolves the defaults itself, so a budget is a property of the vector rather than of whichever default the runner carries. Set one only for a vector that is about that budget.
  • expected.json – the recorded result, for example {"outputs": [42], "final_stack": [], "steps": 11}. outputs is a sparse map: each entry is an i64 written by OUTPUT, or null for a slot that was reserved but never written; trailing unset slots are omitted entirely, so a program that writes slot 0 of 16 reserved slots produces [42], not [42, null, ...]. Explicitly-written zeroes are preserved – only slots OUTPUT never touched disappear. final_stack is the residual stack at HALT, bottom to top. steps is the run’s metered cost, per spec/xqvm/METERING.md, and every successful vector must assert it: it is the quantity the chain prices execution by. A vector that expects a fault writes {"error": "DIVISION_BY_ZERO"} instead, with neither steps nor final_stack.

Vectors are grouped, for human navigation, into eight directories under xqvm/tests/vectors/: arithmetic, constraints, control-flow, energy, index-math, metering, vector-ops, and xqmx-grid. These names are not the same vocabulary as opcodes.yaml’s own category field, which has 15 values and matches spec/xqvm/SPEC.md’s section names, not the vector directories. Only five names happen to coincide (arithmetic, control-flow, index-math, vector-ops, xqmx-grid); constraints, energy and metering have no matching YAML category at all – the constraint opcodes are catalogued under xqmx-high-level, and so is ENERGY itself, while a metering vector is about a budget rather than about any one opcode. Don’t expect a vector directory to line up with an opcodes.yaml category by name.

Running the Vectors Locally

# Every vector
cargo test -p xqvm --test vectors

# One category
cargo test -p xqvm --test vectors -- arithmetic

# One vector
cargo test -p xqvm --test vectors -- --exact vector::arithmetic::add_basic

The runner assembles each program with xqasm, which the published xqvm tarball does not carry, so the vectors run from a checkout of the repository rather than from the crate downloaded from crates.io.

What Passing Means

Every vector runs as its own named test in the xqvm crate’s vectors test target, which CI runs with the rest of the Rust integration tests. A passing vector means: for that specific program and that specific calldata, the VM produces every output value, the final stack and the step count the vector records. It says nothing about programs the vector does not cover.

Two more guarantees apply alongside the vectors, each checked separately:

  • The opcode table itself – a build-time assertion ties the Rust opcodes! macro to opcodes.yaml, so the VM cannot silently diverge from the published table on which opcodes exist or how many operands they take.
  • Bytecode encoding – owned by the xqasm crate’s own test suite. The vectors trust the assembler to produce correct bytecode from .xqasm source; they do not re-check the encoding independently, though each vector does run a second time after an encode/decode round trip and must produce the same result.

For what the VM actually does with a program, see the XQVM Reference.

Coverage Is Per-Opcode, and Incomplete

A vector exists only where someone wrote one, and xqvm/tests/vectors/ holds far more vectors than it covers distinct opcodes: bitlen_small and bitlen_negative both cover BITLEN; three separate slack_* vectors all cover SLACK; the xqmx-grid directory alone spends seventeen vectors on six opcodes. Where a vector is missing, the suite makes no claim about that opcode at all. Passing CI does not mean every opcode has been checked against the spec – only that every opcode a vector currently exercises has been.

Which opcodes those are is now computed rather than guessed. CI prints the report on every pipeline, as the last step of make test-rust (the test:rust job), and it runs locally with:

make conformance-coverage

The report gives two numbers, because one is not enough. Present counts opcodes appearing in some vector’s assembled program; reached counts those a vector executes to completion. Reached is the stronger measure and always the smaller one: an opcode behind an untaken branch is present but not reached, and so is the instruction an error vector exists to make fault – a faulting instruction never completes a step. Reporting only “reached” would call DIV uncovered despite arithmetic/div_by_zero; reporting only “present” would credit an opcode sitting in dead code.

Both numbers sit below the size of the opcode table, and the report names the shortfall rather than only counting it. Run make conformance-coverage for the current figures; this page does not repeat them, because they move whenever a vector lands. The opcodes in no vector at all are the real holes. Those present but never reached are a weaker signal worth knowing: each has an error vector pinning how it fails, and no vector pinning what it does when it succeeds.

Coverage reports; it does not gate on completeness, which would fail today. It does gate on regression – xqvm/tests/vector_suite/coverage.rs holds both numbers as floors that a merge request may raise and may not lower without saying why.

IDXTRIU is the worked example of what that costs. It had no vector, and its behaviour was wrong in two separate ways: on operand order, and on whether an intermediate that leaves i64 range faults. Both were found by reading the code against the spec rather than by any mechanical check, and both are now closed, with index-math/idxtriu_intermediate_overflow and index-math/idxgrid_intermediate_overflow pinning the second. Neither gap was exotic; both were simply in the part of the opcode table nothing had written a vector for – which is the hole the coverage report exists to make visible before someone has to find it by reading.

Treat a green vector run as evidence for the programs it actually tests, not as a blanket guarantee that the VM matches the spec on every opcode in every configuration.

Glossary

Terms as this book uses them, alphabetically. Each entry links to the page that treats it in full.

Backend. Where a model actually solves: locally on a CPU or GPU, on D-Wave’s cloud QPU, or on the Quip network. Choosing one does not change the model, the encoder, or the verifier. See Backends.

Calldata. The read-only array of values a host program supplies to a VM run before execution starts. INPUT reads from it by slot index. See Calldata and Outputs.

Chain strength. A D-Wave QPU parameter. It sets how strongly a group of physical qubits standing in for one logical variable are coupled together, so they read out as one value rather than breaking apart under the problem’s own couplings. See D-Wave QPU.

Conformance vector. A fixed test case – program, calldata, and expected outcome, written from the specification – that the vector suite runs on xqvm and checks against its recorded expectation. See Conformance.

Decoder. One of the three programs a problem compiles to: takes a sample and extracts the answer in the problem’s own terms (a tour, a partition, a set of selected items). See Three Programs.

Domain. The set of values a model’s variables take: binary (\({0, 1}\)), spin (\({-1, 1}\)), or integer (\({0, \ldots, k-1}\)). A model and the sample solving it must share a domain. See Quadratic Models.

Embedding (D-Wave). Mapping a model’s logical variables onto a QPU’s physical qubit graph, called minor embedding: each logical variable becomes a chain of one or more physical qubits held together by chain strength (above). SolverDWave builds this automatically. See D-Wave QPU.

Embedding (Rust). Using the xqvm and xqasm crates directly from a Rust program, without the Python packages. See Embedding Overview.

Encoder. One of the three programs a problem compiles to: reads runtime inputs from calldata and builds the model. See Three Programs.

Energy. The single number a model computes for a candidate assignment – the Hamiltonian evaluated at that point. A solver searches for the assignment that minimises it; ENERGY computes it directly so a program can check a solver’s answer instead of trusting it. See Quadratic Models.

Hamiltonian. The function a quadratic model represents: a sum of linear and quadratic terms over the variables, borrowed from the physics term for a system’s total energy. See Quadratic Models.

Ising model. The spin domain: variables take values \(-1\) or \(+1\), the domain quantum annealing hardware minimises natively. See Quadratic Models.

Model. An XqmxModel: two sparse coefficient maps, linear and quadratic, that together define a Hamiltonian over a fixed number of variables. Built with BQMX/SQMX/XQMX and the coefficient-access opcodes. See Quadratic Models and VM Architecture.

Output slot. The writable array a VM run populates via OUTPUT, read back by the host program after the run halts. See Calldata and Outputs.

Penalty. A term added to a model’s Hamiltonian that is zero when a constraint holds and positive when it does not. Minimising the combined Hamiltonian then tends to satisfy the constraint too. Sizing the penalty weight correctly is the real engineering problem in constraint-by-penalty modelling. See Quadratic Models and Constraints.

QUBO. Quadratic Unconstrained Binary Optimisation: the binary domain, where each variable is either selected or not. See Quadratic Models.

Register. One of 256 typed slots (r0-r255) holding a RegVal: an integer, a vector, a model, or a sample. See VM Architecture.

Sample. An XqmxSample: one candidate assignment, a single value per variable, produced by a solver or built by hand. Shares a domain and variable count with the model it answers, but not its shape. See Quadratic Models.

Solver. Anything implementing the xqsa interface: takes a model, returns a sample and its reported energy. xqsa ships five backends: a CPU annealer, CUDA and Metal GPU annealers (both SolverGPU), the D-Wave QPU, and the Quip network. See Solving Overview.

Step limit. The maximum number of instructions a VM run will execute before faulting with StepLimitExceeded, guarding against runaway programs. Configurable per run; defaults to 10,000,000. See Limits and Errors.

TARGET. The opcode marking a valid jump destination. Every label a program jumps to must have a TARGET at that position. The VM scans for them once at load time, rather than reading a precomputed mapping out of the wire format, and resolves jumps against that scan. See Execution and Builder API.

Verifier. Two related but distinct things in this book. The bytecode verifier is static analysis (xqvm::verifier) that checks a program’s structure before it runs: every jump lands on a TARGET, and every register is read only after it is written. See Verifier. The verifier program is one of the three programs a problem compiles to: it checks a sample against the encoder’s constraints and recomputes its energy independently. See Three Programs.

XQBC. The binary wire format: a 15-byte header (magic, version, slot counts, code length, checksum) followed by the raw instruction stream. See Bytecode Format.

XQCP. X-Quadratic Constraint Programming: the Python DSL that turns a problem description into the three XQVM programs. See Modelling Lifecycle.

XQMX. The matrix type the VM allocates and manipulates, in either of two modes: a model (XqmxModel, a Hamiltonian’s coefficients) or a sample (XqmxSample, one candidate assignment). See VM Architecture.

XQSA. X-Quadratic Solver Adapters: the Python package holding one adapter per solving backend. See Solving Overview.

XQVM. X-Quadratic Virtual Machine: the stack machine every XQuad problem compiles to, specified in spec/xqvm/ and implemented by the xqvm crate. See XQVM Reference.

Stability

XQuad is pre-1.0. The root README.md says so directly: the instruction set, the binary format, and the public API may still change before v1.0, and production use is not recommended yet. Read everything below in that light – it describes the project’s current process discipline, not a promise that anything is frozen.

What Is Versioned Together

Six packages ship from this repository, two to crates.io (xqvm, xqasm) and four to PyPI (xqffi, xqcp, xqsa, xquad). All six always carry the same version. They release together: one release MR bumps every version, one tag triggers the one pipeline that publishes all six, and the changelog is generated from that same tag. There is no independent release cadence per package today.

What CI Actually Guards

Two things fail the build if they drift, checked mechanically by default rather than caught only by review discipline; the second of the two has a deliberate, contributor-controlled way out, noted below:

  • VM behaviour against the spec. Conformance checks xqvm against the outcome spec/xqvm/ prescribes for every behaviour a vector covers. Coverage is real but partial – see that page for what “partial” means concretely.
  • Spec and implementation agreement. Any change to VM semantics – opcode table, control flow, stack depth, type system, or the high-level constraint expansions – must touch three things in the same change: the normative spec/xqvm/ files, the Rust implementation, and the conformance vectors. CI’s atomic-spec-MR guard rejects a change that touches only some of them, unless a commit in the range carries an Atomic-Spec-Exempt: <reason> trailer, which deliberately bypasses it for a one-sided change such as aligning the implementation to behaviour the spec already states. This keeps the three descriptions of VM behaviour from drifting apart silently by default; it does not keep the behaviour itself from changing, and the exemption is a contributor’s call, not a machine guarantee.

Both guarantee how a change to observable behaviour is made. Neither guarantees that observable behaviour stays fixed.

What Is Not Guaranteed

Nothing here promises binary compatibility across versions. Versions follow semver as Cargo reads it, where the leftmost non-zero field is the major: before 1.0 a breaking change bumps the minor (0.4 to 0.5) and a patch release (0.4.0 to 0.4.1) carries only compatible changes, fixes and features alike. That policy governs the crate versions – it is not a commitment that nothing observable will ever break before 1.0. Treat every 0.x release as a snapshot, not a foundation to build on without re-checking. The breaking changes in v0.5.0 are listed in Migrating from 0.4 to 0.5. If your use case needs a stability guarantee this project does not yet make, file an issue against the repository rather than assuming one.

Reading the Documentation for an Older Release

The published book tracks the most recent release. Every tagged release carries its own copy of these pages in the repository, so an older version is a checkout away:

git switch --detach v0.4.0
make build-docs      # renders to docs/book/build/
make serve-docs      # serves it and opens a browser

make serve-docs rebuilds on edit, so it is also the way to preview a change before opening a merge request. Return with git switch -.

Tags are listed with git tag --list 'v*' | sort -V, and the released packages for each are on crates.io and PyPI. Tags carrying -rc or -beta are pre-releases: they are on both registries, but the published book never describes one.

Migrating from 0.4 to 0.5

v0.5.0 is a breaking release. This page lists every change in it that can break code written against 0.4, each with what to write instead. Upgrade the Python packages together: xquad pins xqsa, xqcp and xqffi to its own version, and xqsa and xqcp pin xqffi the same way. On the Rust side, move xqvm and xqasm to 0.5 together.

One VM

The Python reference VM is gone. The Rust VM, reached through xqffi, is the only implementation, and the xqvm_py distribution has no 0.5 release.

0.40.5
from xqvm_py ...from xqffi.vm ..., or the re-exports in xquad.types
VM(backend=VMBackend.RUST), vm.backendVM(). VMBackend and the backend argument are gone
xquad.types.XQMX, XQMXDomain, XQMXMode, Vec, VecElemXqmxModel, XqmxSample and Domain from xquad.types (see Model types)
Outputs converted to XQMX and VecOutputs are the xqffi values, unconverted
SampleOutOfDomain from VM.set_calldata()ValueError where the XqmxSample is built or written; no out-of-domain sample can exist
--interpreter on the example runnersRemoved; every runner uses the Rust VM

Model Types

xqsa, xqcp and xquad all use the typed model API from xqffi.vm.

0.40.5
XQMX.binary_model(size=4)XqmxModel.binary(4)
XQMX.integer_model(size=3, k=4)XqmxModel.integer(3, k=4)
XqmxModel("binary", size=4), XqmxSample("spin", values=[...])XqmxModel.binary(4), XqmxSample.spin([...]), or XqmxModel(Domain.BINARY, 4)
A domain string, plus k= for an integer domainDomain.BINARY, Domain.SPIN, Domain.integer(k)
model.domain == "binary"model.domain == Domain.BINARY; the getter returns a Domain, with name and k
XQMX.binary_sample(size), spin_sample, integer_sampleXqmxSample.default(Domain.BINARY, size) and so on; XqmxSample.binary(values) builds one from values
set_quadratic, get_quadratic, add_quadraticset_quad, get_quad, add_quad
iter_linear(), iter_quadratic()linear_items(), quadratic_items()
xqmx.mode, is_model(), is_sample()Gone: XqmxModel and XqmxSample are separate types
The fields .linear, .quadratic and .integer_klinear_items(), quadratic_items() and get_linear(i) on a model, .values on a sample, and .k
grid_index(row, col)No replacement
domain_contains(domain, k, value)domain.contains(value)
compute_energy(model, sample)model.energy(sample)
"StepLimitExceeded" in str(e)except StepLimitExceeded. Vm.run raises one XqvmError subclass per fault, and the message is no longer the Debug form

The module functions of xqvm_py.xqmx, such as row_sum, col_find and expand_onehot, have no replacement. triu is in xquad.types.

A model or sample that no program could build is still refused when it is constructed, but with a different exception. A size past MAX_ALLOCATION_SIZE raises InvalidAllocation and a grid that RESIZE would refuse raises InvalidGridDimensions, both XqvmError subclasses and so RuntimeError, where 0.4 raised ValueError. Code that catches ValueError around these constructors has to catch the new types. The coefficient accessors (set_linear, get_linear, set_quad, get_quad) likewise raise IndexOutOfBounds, also an XqvmError, for an index outside 0..size, where 0.4 raised IndexError.

XQCP Domains

define_model() takes an xqcp.Domain only. 0.4 also accepted an XQMXDomain from xqvm_py and converted it; that type is gone with the Python VM. The member names BINARY, SPIN, INTEGER and CATEGORICAL are unchanged, but each value is now the domain’s name as a string: Domain.INTEGER.value == "integer", and CATEGORICAL is "categorical" rather than None. Code that read .value as an XQMXDomain breaks, and problem.model.domain is now the xqcp.Domain member rather than an XQMXDomain.

Solvers

The four per-hardware classes become three classes on one Solver base.

0.40.5
SolverDWaveCPU(...)SolverCPU(...)
SolverCudaGPU(...)SolverGPU(engine="cuda", ...)
SolverMetalGPU(...)SolverGPU(engine="metal", ...)
SolverDWaveQPU(...)SolverDWave(...); mode="qpu" is the default
strategy= on the GPU constructorsalgorithm=
solve(model, strategy=...)Not accepted: the algorithm is fixed when the solver is built, so build one solver per algorithm
metadata["params"]["strategy"]metadata["params"]["algorithm"]
from xqsa.registry import SOLVERS, DEFAULT_SOLVER, build_solverThe same names from xqsa or xqsa.solvers. A SOLVERS value is a callable that builds a solver, not always a class
Registry keys dwave-cpu, cuda-gpu, metal-gpu, dwave-qpucpu, cuda, metal, dwave; quip is unchanged and cpu is the default
from xqsa.dwave_cpu import ... and the other per-hardware modulesImport the solver classes from xqsa
An XQMX model in, an XQMX sample outAn XqmxModel in, an XqmxSample out

SolverQuip keeps its class and its constructor. These change for the other solvers:

  • The constructors take keyword arguments only, so a positional call such as SolverDWaveCPU(100, 1000) fails.
  • An unknown keyword to solve() raises TypeError. 0.4 ignored it, or for SolverDWaveQPU passed it on to Ocean’s sampler; SolverDWave forwards only chain_strength and auto_scale.
  • result.metadata holds exactly seed, reads and params. The algorithm, engine or mode the solver ran with is in params, and SolverDWave moves solver and qpu_timing under params as well.

A solver of your own that subclasses Solver also changes. Solver.__init__ now takes the implementation it runs, so a subclass that only overrides solve() fails with TypeError when it is built. Give it its own __init__ and keep overriding solve(), as SolverQuip does. The _model_to_bqm and _sample_to_xqmx methods are gone; xqsa.solver has model_to_bqm and sample_to_xqmx as module functions.

Verifier and Metering

The verifier and the VM refuse programs that 0.4 accepted:

  • The verifier checks stack underflow against each instruction’s pop count, and an underflow between two SCLR instructions stays an underflow. A program that passed the 0.4 verifier and then faulted on every run with a stack underflow is now rejected before it runs.
  • A step charge that would carry the step counter past 2^64 - 1 raises StepLimitExceeded, whatever the step limit.

In the xqvm crate, StackEffect::Delta and StackEffect::from_delta become StackEffect::Fixed { pops, pushes } and StackEffect::fixed.

Deprecated Names

The names that 0.4 deprecated still work in 0.5 and still warn, and they are removed in v0.6.0. The Deprecated APIs table lists each one with its replacement. The top-level commands xquad asm, dism, run and verify are among them; use xquad vm instead (see xquad vm).

Spec Index

The book teaches; spec/ is what XQuad actually is. Every file below is the normative source for its topic’s intent. The specification vectors check the Rust VM against these documents one program at a time, and its build checks the opcode table against xqvm/opcodes.yaml. The specification is the authority: where the VM diverges from it, the VM is wrong, and where a divergence is known, the relevant book page says so. Conformance explains why a green run is evidence about the programs a vector covers rather than a blanket guarantee.

The lists below are not closed. For anything they omit, check the book chapter covering the component you are working with, or search the spec files directly.

Top Level

  • spec/README.md – index of the three layers below, and the drift policy that ties spec to implementation.

XQVM

Normative for the virtual machine: the instruction set, the wire format, and the bytecode verifier.

  • spec/xqvm/README.md – document index for this layer.
  • spec/xqvm/SPEC.md – normative for the machine overview, the three-program architecture, the state model, determinism, the type system, and runtime limits.
  • spec/xqvm/ISA.md – normative for the instruction set: notation, per-category opcode tables, semantic notes, and reserved opcodes.
  • spec/xqvm/HLF.md – normative for the high-level constraint expansion formulas: the QUBO penalty terms ONEHOTR, ONEHOTC, EXCLUDE, IMPLIES, EQUALITY, ATLEAST, ATLEASTW, and REDUCE inject.
  • spec/xqvm/ENCODING.md – normative for both file formats: .xqasm assembly syntax and the .xqb binary encoding, including the XQBC header layout.
  • spec/xqvm/VERIFIER.md – normative for the bytecode verifier: its phase pipeline and every error it can raise.
  • spec/xqvm/METERING.md – normative for step metering: what a step is, the cost constants and how they were measured, the per-opcode charge table, the three formulas, and the conformance rules for step counts and operand validation order.

XQCP

Normative for the constraint-programming DSL that compiles a Problem into the three XQVM programs.

  • spec/xqcp/README.md – document index for this layer.
  • spec/xqcp/SPEC.md – normative for the DSL overview, the three-program architecture as xqcp implements it, the problem lifecycle, and the compilation contract.
  • spec/xqcp/TYPES.md – normative for the symbolic value types, the expression tree, the operator algebra, and the free functions built on top of it.
  • spec/xqcp/CONSTRAINTS.md – normative for the constraint taxonomy, each constraint method’s signature, and its cross-reference into the matching HLF expansion.
  • spec/xqcp/COMPILER.md – normative for the compilation pipeline: encoder/verifier/decoder generation, register allocation, and action recording.

XQSA

Normative for the solver-adapter interface between a model and a backend.

  • spec/xqsa/README.md – document index for this layer.
  • spec/xqsa/SPEC.md – normative for the architecture overview, where solving sits in the pipeline, and the plugin model new solvers implement.
  • spec/xqsa/INTERFACE.md – normative for the Solver base class, the solve() contract, the SolverResult type, and parameter-passing conventions.
  • spec/xqsa/ENERGY.md – normative for the energy computation formula, its precision contract, and the sparse representation solvers exchange it in.
  • spec/xqsa/DOMAINS.md – normative for the domain support matrix, sample encoding, grid metadata, and capability negotiation between a model and a solver.
  • spec/xqsa/SOLVERS.md – normative for the solver registry, its naming convention, algorithm families, and the dependency model behind each optional extra.

Reading a Spec File Like a Reference, Not a Tutorial

Every file above assumes the concepts this book explains from scratch – what a quadratic model is, why a problem becomes three programs, what a penalty weight does. Arrive from Concepts or the relevant chapter first; the spec files are where to go once you need the exact rule the book page summarised, not where to start.

Contributing to These Docs

This book is written and reviewed in the same repository as the code it documents. If something here is wrong, incomplete, or out of date, the fix goes through the same review process as a code change. Start from CONTRIBUTING.md for the general contribution rules – sign-off, commit format, and review expectations apply to documentation changes too.

Where the Pages Live

Every page you can read here comes from one Markdown file under docs/book/src/. The URL mirrors the path: this page is appendix/contributing.md.

Two files control the shape of the book rather than its content:

  • docs/book/src/SUMMARY.md is the table of contents and the sidebar. Every page must be linked from it, and every link in it must point at a page that exists. Continuous integration checks both directions, so adding a new page without adding its SUMMARY.md entry fails the build rather than producing an orphan.
  • book.toml, at the repository root, holds the renderer configuration: the theme, the output directory, and the preprocessor list.

Pages You Should Not Edit by Hand

Sixteen pages are generated, not written. Editing one of them directly works until the next regeneration silently reverts it. Each generated file opens with a DO NOT EDIT banner as its first line, which you will see immediately if you open it in an editor.

They are the instruction reference, generated from xqvm/opcodes.yaml, and the fifteen example pages, generated from examples/manifest.yaml together with each example’s own README.md.

To change one, edit its source and regenerate:

make regen-docs

Continuous integration runs the same generators in check mode and fails if the committed pages differ from freshly generated output, so a regenerated page has to be committed alongside the source that produced it.

Building the Book Locally

The renderer and its diagram preprocessor are pinned in scripts/cargo-tools.lock and installed by make deps. With those in place:

make build-docs   # build once into docs/book/build/
make serve-docs   # rebuild on save and open a browser

make build-docs also asserts that every diagram in the sources actually rendered, which catches a preprocessor failure that would otherwise show up as a page of raw diagram source.

Two more checks are worth running before you open a merge request:

make check-docs-generated   # generated pages match their sources
make check-docs-drift       # table-of-contents coverage and prose guards

Both run in continuous integration as well, so running them locally just saves you a round trip.

Reporting Something Instead

Not every problem is worth a merge request. If you have found an error but not the fix, or you want to argue that a page should exist at all, open an issue and say which page you mean.