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/quadraticcoefficients, sign included. - Constraints – folding a rule into the objective as a penalty.
- Control Flow –
range,iter, andbranch. - Outputs and Decoding – reading a solved sample back into your problem’s own terms.
- Compiling – what
problem.compile()actually produces.