Skip to main content

High-level constraint programming DSL compiling to XQVM assembly.

Project description

XQCP — X-Quadratic Constraint Programmer

A constraint programming DSL that compiles high-level problem descriptions into three XQVM assembly programs: encoder, verifier, and decoder.

Overview

Instead of writing .xqasm by hand, define your problem in Python using symbolic expressions, loops, and model operations. XQCP records these operations and compiles them into assembly that the XQVM executor can run directly.

from xqcp import Problem, Types
from xqvm_py import XQMXDomain

problem = Problem("MyProblem")

n = problem.input("n", type=Types.Int)
problem.define_model(size=n, domain=XQMXDomain.BINARY)

# ... define objective, constraints, decoder ...

programs = problem.compile()
# programs.encoder   -> str (.xqasm source)
# programs.verifier  -> str
# programs.decoder   -> str

Architecture

XQCP uses a record-then-compile pattern:

  1. Record — DSL calls (input, define_model, range, stow, model.linear[i].add(w), etc.) append Action objects to an ordered list.
  2. Compile — The compiler walks the action list and emits assembly for each of the three programs, with automatic register allocation and loop nesting.

Module Structure

Module Purpose
problem.py Problem container, action recording, register allocator
expression.py Symbolic expression tree nodes that emit assembly
symbols.py Symbolic references: InputRef, LoopVar, ModelRef, SampleRef, OutputRef, CoefficientRef, LinearProxy, QuadraticProxy
compiler.py Three compiler functions: compile_encoder, compile_verifier, compile_decoder
__init__.py Public API re-exports and xq_* helper functions

DSL Reference

Inputs

Declare runtime inputs with a name and type:

n = problem.input("n", type=Types.Int)           # integer scalar
edges = problem.input("edges", type=Types.Vec)    # integer vector

InputRef supports arithmetic (+, -, *, //, %, unary -) and vector operations:

edges.get(i)        # VECGET: access element at index i
edges.veclen()      # VECLEN: get vector length

Model

Define the XQMX model the encoder will build:

# 1D model (flat variables)
problem.define_model(size=n, domain=XQMXDomain.BINARY)

# 2D model (grid with rows and columns)
problem.define_model(
    size=n * n, domain=XQMXDomain.BINARY,
    rows=n, cols=n,
)

Note: Discrete domain (XQMXDomain.DISCRETE) is not yet supported in the CP layer.

Model Coefficients

Access linear and quadratic coefficients via subscript proxies:

# Linear coefficients
model.linear[i]              # get (returns Expr → GETLINE)
model.linear[i] = weight     # set (→ SETLINE)
model.linear[i].add(weight)  # add (→ ADDLINE)

# Quadratic coefficients
model.quadratic[i, j]              # get (returns Expr → GETQUAD)
model.quadratic[i, j] = weight     # set (→ SETQUAD)
model.quadratic[i, j].add(weight)  # add (→ ADDQUAD)

# 2D models accept tuple coordinates (auto-flattened via IDXGRID)
model.quadratic[(row_a, col_a), (row_b, col_b)].add(weight)

Constraints

# One-hot constraints (2D models)
model.apply_onehot_row(row, penalty=100)    # → ONEHOTR
model.apply_onehot_col(col, penalty=100)    # → ONEHOTC

# Mutual exclusion (variables i and j cannot both be 1)
model.apply_exclude(i, j, penalty=100)      # → EXCLUDE

# Implication (if variable i is 1, variable j must be 1)
model.apply_implies(i, j, penalty=100)      # → IMPLIES

# 2D models accept tuple coordinates for exclude/implies
model.apply_exclude((r1, c1), (r2, c2), penalty=100)

Loops

problem.range(start, end) emits a RANGE loop:

with problem.range(0, n) as i:
    with problem.range(i + 1, n) as j:
        # body executes for all (i, j) pairs where i < j

problem.iter(vec, start, end) emits an ITER loop over vec elements:

# Always yields (idx, val) — unpack as needed
with problem.iter(distances, 0, n) as (idx, val):
    model.linear[idx].add(val)

with problem.iter(distances, 0, n) as (_, val):
    # discard index, use value only

The yielded LoopVars support full arithmetic and can be used as indices, coordinates, or operands.

Stow

Evaluate an expression and store it in a register for reuse:

dist = problem.stow("dist", edges.get(xq_triu(i, j)))
w = problem.stow("w", edges.get(offset + 2))

Returns a RegLoad that can be used in subsequent expressions. Pass an existing RegLoad to overwrite:

weight = problem.stow("weight", 0)       # allocate new register
problem.stow(weight, dist * 10)           # overwrite same register

Branch

Conditional multi-arm branching with first-match semantics:

problem.branch(
    dist > threshold, lambda: model.linear[i].add(dist * 10),  # if
    dist > 0,         lambda: model.linear[i].add(dist),       # elif
    None,                                                       # else (do nothing)
)
  • Variadic (condition, callable) pairs followed by a mandatory default (callable or None)
  • Minimum 3 arguments: (cond, callable, default)
  • Conditions use comparison operators (>, <, ==, >=, <=)

Outputs

Declare decoder outputs:

tour = problem.output("tour", type=Types.Vec)

with problem.range(0, n) as pos:
    tour.append(problem.sample.colfind(col=pos, value=1))   # VECPUSH
    # or
    tour.append(problem.sample.getline(pos))                 # VECPUSH

# Random-access write/read
tour[i] = value    # VECSET
val = tour[i]      # VECGET

Sample

problem.sample provides read access to the solution sample in the decoder:

problem.sample.colfind(col=pos, value=1)   # find row where column has value (2D)
problem.sample.rowfind(row=pos, value=1)   # find col where row has value (2D)
problem.sample.getline(index)               # read variable by index (1D)
problem.sample.rowsum(row)                  # sum all values in row
problem.sample.colsum(col)                  # sum all values in column

Operators

All symbolic types (InputRef, LoopVar, RegLoad, Literal) support:

Arithmetic

Operator Assembly Notes
a + b ADD Optimizes to INC when adding 1
a - b SUB Optimizes to DEC when subtracting 1
a * b MUL
a // b DIV Integer division
a % b MOD
-a NEG Unary negation

Comparison

Operator Assembly
a == b EQ
a < b LT
a > b GT
a <= b LTE
a >= b GTE

Bitwise

Operator Assembly
a & b BAND
a | b BOR
a ^ b BXOR
~a BNOT
a << b SHL
a >> b SHR

Free Functions

from xqcp import (
    xq_triu, xq_grid, xq_sqr, xq_abs, xq_min, xq_max,
    xq_not, xq_and, xq_or, xq_xor, xq_bnot,
)

Index Math

Function Assembly Description
xq_triu(i, j) IDXTRIU Upper triangular index
xq_grid(row, col, cols) IDXGRID Grid flat index (row * cols + col)

Arithmetic

Function Assembly Description
xq_sqr(x) SQR Square
xq_abs(x) ABS Absolute value
xq_min(a, b) MIN Minimum
xq_max(a, b) MAX Maximum

Logical (Python keywords and/or/not can't be overloaded)

Function Assembly Description
xq_not(x) NOT Logical NOT
xq_and(a, b) AND Logical AND
xq_or(a, b) OR Logical OR
xq_xor(a, b) XOR Logical XOR
xq_bnot(x) BNOT Bitwise NOT

Compiled Output

problem.compile() returns a CompiledPrograms dataclass with three .xqasm source strings:

  • Encoder — reads inputs, allocates the XQMX model, emits objective terms and constraints
  • Verifier — reads model + sample + N, checks validity (onehot or binary domain), computes energy
  • Decoder — reads sample + N, extracts the solution into output vectors

The verifier automatically selects the right validity check:

  • ROWSUM/COLSUM loops when onehot constraints are present
  • Binary domain check (GETLINE + 0-or-1 test) when no onehot constraints exist

Specification

Authoritative specification: ../spec/xqcp/SPEC.md. The spec is the source of truth; any divergence in this reference implementation is a bug.

Examples

See the working XQCP programs in top-level runnable examples:

  • TSP: examples/tsp/runner.py — 2D grid model, onehot constraints, COLFIND decoder
  • Max-Cut: examples/maxcut/runner.py — 1D model, edge iteration, GETLINE decoder

Each runner inlines the CP DSL, compiles to XQVM assembly, and runs the encoder → SA → verifier → decoder pipeline on either the Python reference VM or the Rust VM (via --interpreter).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

xqcp-0.3.1.tar.gz (24.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

xqcp-0.3.1-py3-none-any.whl (29.9 kB view details)

Uploaded Python 3

File details

Details for the file xqcp-0.3.1.tar.gz.

File metadata

  • Download URL: xqcp-0.3.1.tar.gz
  • Upload date:
  • Size: 24.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for xqcp-0.3.1.tar.gz
Algorithm Hash digest
SHA256 9371e26cb79cbac98a4b05c414453d8c8b0a00fea4fe3cc4f44b790cb43cbde0
MD5 bbebe5c18a9745b69de5bffa9fb110c7
BLAKE2b-256 fa4ffe8cc628a20485d67fbaca76c68a1cbf6e3e175c5ff1bbeeafa37c5eb791

See more details on using hashes here.

File details

Details for the file xqcp-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: xqcp-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 29.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for xqcp-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 674e61101fdaf7b78eb4910ac87d15d8f85ce0c31f9fcda1ab931873372fea23
MD5 4981faf95ff4706ac43b9b12ed9be23a
BLAKE2b-256 8f1ccc58c695ccf9b6824dcdd4bddc026ab76925ba215ad88b3aaa4717525d03

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page