Skip to content

Latest commit

 

History

History
213 lines (164 loc) · 6.71 KB

File metadata and controls

213 lines (164 loc) · 6.71 KB

XQuad Python API walkthrough

A guided tour through the XQuad Python surface — enough to take a user from zero to encode → sample → verify → decode on their own problems. Every snippet below is runnable end-to-end against an installed xquad wheel; a companion doctest suite lives at xquad/tests/test_program.py and xquad/tests/test_umbrella.py.

The XQuad toolchain ships as five peer PyPI distributions:

Package Purpose
xqffi pyo3 FFI bindings for the Rust VM + assembler (not user-facing)
xqcp Constraint-programming DSL
xqsa Solver adapters (dwave-samplers)
xqvm_py Python reference VM (conformance oracle)
xquad Umbrella meta-package — user-facing API surface

Install the umbrella (pip install xquad) for the full pipeline. The umbrella re-exports are identity, not copies: xquad.cp.Problem is xqcp.Problem holds, so both import styles interoperate cleanly. xqffi is an internal dependency — user code should import from xquad, not xqffi directly.

The two VM surfaces

xquad.program — the interactive surface

What REPL / notebook / script users reach for. A Program loads once, a Session handles mutable calldata and multiple runs, a RunResult presents outputs as a dict keyed by slot.

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
"""

# 1. Load the program once.
program = Program.from_source(src)
assert program.instruction_count == 11
assert program.source is not None   # retained for debugging
bytecode = program.bytecode()       # wire-format bytes, if you need them

# 2. Derive a session with 4 output slots. Sessions are mutable,
#    programs are not.
session = program.session(output_slots=4)

# 3. Run with calldata [40, 2] -> slot 0 = 42; slots 1..3 never written.
session.set_calldata([40, 2])
result = session.run()

# 4. Outputs are a dict. Unset slots are None (sparse semantics).
assert dict(result.outputs) == {0: 42, 1: None, 2: None, 3: None}
assert result.stack == []           # residual stack (bottom-to-top)
assert result.steps == 11           # instruction count for this run

# 5. 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}

xqffi.vm — the low-level surface

What the conformance harness drives. A single one-shot Vm object with a list-shaped outputs(). Use this when you want the smallest-possible wrapper over xqvm::Vm — typically conformance work, fuzzing, or benchmarks.

from xquad.vm import Vm
from xquad.asm import assemble_source

bytecode = assemble_source(src)
vm = Vm()
vm.set_calldata([40, 2])
vm.set_output_slots(4)
vm.run(bytecode)
assert vm.outputs() == [42, None, None, None]  # positional list

For anything else, prefer xquad.program.

Loading bytecode directly

Program.load(bytes) parses raw wire-format bytes (a .xqb blob produced by xquad asm or Program.bytecode()). No source is retained; the source attribute reads as None.

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

Malformed input surfaces at execution time (the wire format has no header to validate up-front), so Program.load itself is infallible; an error like TypeMismatch or StackUnderflow appears when the session actually tries to run.

Heterogeneous calldata

Calldata slots may hold any of:

  • int — scalar integer
  • list[int] — maps to VecInt
  • xquad.vm.XqmxModel — quadratic model
  • xquad.vm.XqmxSample — candidate solution
  • None — unset
from xquad.vm import XqmxModel, XqmxSample

model = XqmxModel("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])
# Now session.run() sees four typed input slots.

Inspecting models and samples

Standalone conversion functions in xquad.vm turn FFI objects into plain-Python dicts for serialisation, diffing, or handing to another library:

from xquad.vm import XqmxModel, model_as_dict, sample_as_dict

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

model_as_dict(model)
# {'domain': 'binary', 'size': 4, 'rows': 0, 'cols': 0, 'k': None,
#  'linear': {0: -1, 2: 3}, 'quadratic': {(0, 1): 2}}

The FFI __repr__ is intentionally minimal (XqmxModel(domain=binary, size=4)) — richer formatting belongs in the xquad layer when it lands.

End-to-end through the umbrella

The xquad umbrella groups all of the above under one import:

from xquad import asm, vm, cp, sa
from xquad.vm import XqmxModel, XqmxSample

# encode — xqcp DSL compiles to .xqasm
problem = cp.Problem("MyProblem")
# ... DSL body ...
programs = problem.compile()
encoder_bytecode = asm.assemble_source(programs.encoder)

# execute — xquad.vm runs it
v = vm.Vm()
v.set_calldata([...])
v.run(encoder_bytecode)

# sample — xqsa drives a solver
result = sa.SolverDWaveCPU().solve(model_from_outputs)

# verify / decode — problem-specific; typically more xqasm runs

Each subnamespace is an identity re-export: xquad.vm.Vm is xqffi.vm.Vm. Mixing direct-peer imports and umbrella imports in the same codebase is safe; isinstance works either way.

When to reach for which surface

If you want to... Use
Explore a program in a notebook xquad.program.Program / Session
Run one program thousands of times on different calldata xquad.program.Session (multi-run, no state leak)
Write a conformance test or fuzzer over bytecode xquad.vm.Vm (low overhead, one-shot)
Read / diff a model's coefficients xquad.vm.model_as_dict()
Build the full encode → sample → decode pipeline xquad umbrella

What this walkthrough does not cover

  • Tracing — step-by-step execution inspection lands as a follow-up (QUI-464 trace hooks on Session).
  • Keyword calldata (session.set_calldata(n=4, ...)) — requires input-slot labels on the program; xqcp is the natural emitter but the label channel is not wired yet. Positional list remains.
  • Jupyter-specific _repr_html_ — deferred; __repr__ is notebook-friendly for the common small-model case.
  • numpy to_numpy() helper — deferred to avoid a runtime numpy dependency; convert via model_as_dict()numpy.asarray(...) on the user side for now.

These follow-ups are tracked against QUI-464 and will land on top of this surface without API breaks.