API reference¶
Stable solver API¶
New integrations should use the three top-level entry points and the shared result/config contracts:
import qqa
inspection = qqa.inspect("model.mps")
plan = qqa.plan("model.mps", profile="balanced", device="auto")
result = qqa.solve("model.mps", profile="balanced", budget=60)
print(result.status, result.best_obj, result.feasible)
print(result.violations.maximum_violation)
qqa.solve accepts an in-memory catalogue problem, AlgebraicModel,
ModelIR, or a supported file (MPS, LP, QPLIB, JSON, OPB, CNF/WCNF,
QUBO, or Ising). Configuration is strict: unknown options raise an error.
The default route is pure QQA; exact completion is enabled explicitly with
profile="certify" or exact_backend=....
budget also accepts duration strings such as "250ms", "30s", and
"2m". goal="best|feasible|prove|diverse|pareto" provides a compact
one-call policy. Run qqa.doctor(model) for strict bound, capability, scaling,
curvature, decomposition, route, and resource diagnostics before solving an
external model.
SolveResult keeps raw and repaired solutions separate. Its
objective_value is in the original objective direction,
internal_energy is the canonical minimisation value, and merit_value is
the quantity used by the search backend.
qqa.api
¶
Stable one-entry solve, inspect, and plan API.
inspect(model)
¶
Load if necessary and return solver-independent structural features.
doctor(model, *, replicas=128)
¶
Run deterministic model, route, scaling, and resource diagnostics.
plan(model, *, profile='balanced', budget=None, device='auto', config=None, goal='best', **overrides)
¶
Build an explainable plan without executing the solver.
solve(model, *, profile='balanced', budget=None, device='auto', config=None, initial_solution=None, warm_states=None, goal='best', checkpoint_path=None, checkpoint_interval=None, resume_from=None, **overrides)
¶
Solve any supported model through one QQA-centred, strict API.
Exact backends are opt-in through profile='certify' or
exact_backend=.... The default path remains pure QQA.
qqa.config
¶
Strict, shared configuration for the stable solve API.
SolverConfig
dataclass
¶
Single source of truth for Python, CLI, UI, and benchmark defaults.
Unknown keys are rejected by :meth:from_mapping; integrations must not
silently drop misspelled or unsupported options.
for_profile(profile='balanced', **overrides)
classmethod
¶
Create a profile and apply explicit, validated overrides.
from_mapping(values)
classmethod
¶
Build from a mapping and reject every unknown key.
resolved()
¶
Fill profile-dependent optional fields without changing explicit values.
anneal_kwargs()
¶
Translate to the legacy QQA engine at the explicit adapter boundary.
qqa.result
¶
Backend-independent solve result contract.
The legacy backend result classes remain available for compatibility. New
entry points adapt every backend to :class:SolveResult, where mathematical
objective, internal search energy, feasibility, repair, timing, and proof
information have distinct meanings.
SolveStatus
¶
Bases: str, Enum
Portable termination states shared by heuristic and exact backends.
GuaranteeLevel
¶
Bases: str, Enum
Strength of the claim made by a result, independent of termination.
CoordinateSpace
¶
Bases: str, Enum
Variable order used by a candidate at a solver boundary.
CandidateRecord
dataclass
¶
Auditable identity and verification summary for one candidate.
ConstraintViolation
dataclass
¶
One canonical constraint residual and its reporting tolerance.
ConstraintReport
dataclass
¶
Aggregate feasibility diagnostics without hiding individual rows.
TimingReport
dataclass
¶
Wall-clock phase breakdown in seconds.
ResourceReport
dataclass
¶
Portable resource metrics; machine names and filesystem paths are excluded.
Provenance
dataclass
¶
Reproducibility fields that are safe to serialise and publish.
CertificateMetadata
dataclass
¶
Portable pointer to a proof/certificate without embedding machine paths.
SolveResult
dataclass
¶
One unambiguous result contract for all QQA4CO solve routes.
objective_value is always the original mathematical objective.
internal_energy is the canonical minimisation quantity used by the
search backend. merit_value may additionally contain feasibility or
augmented-Lagrangian terms. Repair never overwrites raw_solution.
solution
property
¶
Preferred reported solution, using repair when available.
best_sol
property
¶
Compatibility alias for :attr:solution.
best_obj
property
¶
Compatibility alias for the original mathematical objective.
runtime
property
¶
Compatibility alias for total wall-clock time.
history
property
¶
Compatibility view of backend history for existing visualisations.
to_dict(*, include_solutions=False)
¶
Return a JSON-oriented, environment-neutral representation.
Canonical model and verification¶
ModelIR.verify_solution is the independent original-model boundary used by
result construction. It is separate from relaxation projection and search
merit, and returns objective finiteness, domain violations, row values,
row violations, and feasibility for every candidate.
qqa.model
¶
Canonical sparse model representation.
FactorBackend
dataclass
¶
One executable backend registration for a factor type.
Capability reports are assembled from these registrations, so a backend cannot be advertised merely because a factor class exists in the IR. Third-party packages may register additional implementations explicitly.
CompiledExecutionPlan
dataclass
¶
FactorExecutionBucket
dataclass
¶
Factors sharing one concrete backend and execution contract.
AssignmentFactor
dataclass
¶
Squared row/column residuals for a flattened assignment matrix.
BlackBoxFactor
dataclass
¶
capabilities
property
¶
Explicit execution declaration; black boxes are never proof-safe.
ClauseFactor
dataclass
¶
Weighted CNF clauses; positive signs mean x, negative mean not x.
ModelIR
dataclass
¶
Canonical sparse model with one objective-sense conversion boundary.
structured_block
property
¶
Return the one categorical/permutation block, when present.
domain_violations(values)
¶
Return the maximum variable-domain violation for every candidate.
Objective and factor evaluation intentionally accepts relaxed points. Feasibility does not: it independently checks finiteness, declared bounds, integrality, binary/spin membership, and structured one-hot or permutation semantics. Keeping these contracts separate prevents a relaxed objective probe from being mistaken for a verified incumbent.
verify_solution(values, *, domain_tolerance=1e-06)
¶
Verify candidates against the original objective, domains, and rows.
This path is independent from relaxation projection and search merit. It evaluates in float64 when possible, rejects non-finite objectives, and keeps every result aligned with the original variable order.
transformed(operation, **details)
¶
Return a copy with one reversible transformation ledger entry.
SolutionVerification
dataclass
¶
Independent original-model evaluation of one or more candidates.
VariableBlock
dataclass
¶
One named, contiguous variable block in original model order.
domain_value
property
¶
Canonical domain established during validation.
LogicalFactor
dataclass
¶
AND/OR/XOR relation where the final index is the output variable.
SubtourEliminationFactor
dataclass
¶
Penalty for explicit subsets in a flattened directed edge matrix.
PresolveInfeasibleError
¶
Bases: ValueError
Raised when a constant constraint proves the model infeasible.
PresolveResult
dataclass
¶
reduce(values)
¶
Project an original-space point into reduced variable order.
factor_backend_registrations(factor)
¶
Return deterministic registrations for a factor instance/type/name.
factor_capabilities(factor)
¶
Return conservative capabilities for one factor instance.
Third-party factors can expose a capabilities iterable containing enum
values or their string forms. Unknown factors remain representable when
they implement evaluate but are not assumed differentiable or safe for
an exact proof.
inspect_capabilities(model)
¶
Inspect every objective/constraint factor and every QQA bound.
register_factor_backend(backend, *, replace=False)
¶
Register an executable factor backend under an explicit unique name.
require_qqa_capabilities(model)
¶
Validate the pure-QQA route without silently changing semantics.
compile_execution_plan(model, *, device='cpu', dtype=torch.float32, strict=True)
¶
Select registered factor backends and optionally fuse the whole graph.
presolve_model(model, *, auto_scale=True)
¶
Apply safe reductions and return an explicit original-space decoder.
Generated module reference¶
Below is the auto-generated documentation for the public modules. The Backends reference page is a hand-curated comparison if you only need to pick one entry point.
Top-level¶
qqa
¶
Quasi-Quantum Annealing (QQA) for combinatorial and spin-glass optimization.
Reference
Y. Ichikawa, Y. Arai. "Optimization by Parallel Quasi-Quantum Annealing with Gradient-Based Sampling." ICLR 2025. https://openreview.net/forum?id=9EfBeXaXf0 (arXiv:2409.02135)
Typical usage::
import networkx as nx
import qqa
qqa.fix_seed(0)
g = nx.random_regular_graph(d=3, n=50, seed=0)
problem = qqa.MaximumIndependentSet(g, penalty=2)
result = qqa.anneal(problem, sol_size=100, num_epochs=1500)
print(result.best_obj, result.runtime)
Spin-glass example::
problem = qqa.SherringtonKirkpatrick(N=100, seed=0)
result = qqa.anneal(problem, sol_size=200, num_epochs=2000)
print("E_0 per spin:", result.best_obj / 100)
Legacy annealing result¶
qqa.anneal and the legacy-compatible solver backends return this result
contract:
qqa.annealing.AnnealResult
dataclass
¶
Result returned by :func:anneal.
Attributes¶
best_sol:
Tensor of the best discrete solution(s) found during annealing. Shape
depends on the problem: (N, ...) for one winning single-instance
state, or (num_instance, max_node) for batched-instance problems.
best_obj:
Best objective value observed. float for single-instance problems,
numpy.ndarray of shape (num_instance,) for batched-instance.
runtime:
Wall-clock time of the annealing loop in seconds.
history:
Dict of per-epoch metrics (loss_mean, penalty_mean,
diversity, bg). Empty if record_history=False.
callbacks:
List of callback instances that were active. Useful for retrieving
e.g. TrajectoryTracker.values.
score = field(default_factory=dict)
class-attribute
instance-attribute
¶
Human-readable problem-specific score produced by
:py:meth:COProblem.score_summary.
- Single-instance: standard dict
{label, value, unit, feasible, extra}with scalar fields. - Batched-instance (
problem.num_instance > 1): same keys, butvalueandfeasiblearenp.ndarrayof lengthnum_instance, andextracarries arrays plus afeasible_counttally.scoreis empty for batched problems whose class did not override :py:meth:COProblem.score_summary.
polished_sol = None
class-attribute
instance-attribute
¶
Domain-locally-optimal version of :attr:best_sol, populated when
:func:anneal is called with polish=True (the default) on a QUBO,
quadratic-spin, or categorical problem. best_sol / best_obj /
score are replaced only after a strict improvement.
final_population = None
class-attribute
instance-attribute
¶
Projected final replica population, populated only when
:func:anneal is called with return_population=True. Hybrid solvers
use it to pass several diverse QQA incumbents to exact solvers without
making ordinary results unnecessarily large.
diagnostics = field(default_factory=dict)
class-attribute
instance-attribute
¶
Solver-level diagnostics such as adaptive restart counts and the numerical-stability controls used for the run.
archive = None
class-attribute
instance-attribute
¶
Historical feasibility/quality/diversity archive retained across epochs.
Problems¶
qqa.problems.base
¶
Abstract problem base classes.
Every problem class in QQA exposes:
loss_fn(x)— the (continuous or discrete) objective, vectorised over the leading batch dimension thatqqa.annealuses for the parallel population.relaxation— a :class:~qqa.relaxation.Relaxationinstance describing how the variable is represented during annealing.
Binary QUBO problems return losses of shape (B,) for a single graph, or
(B, I) for batched-instance variants. Categorical and spin problems
return losses of shape (B,).
COProblem
¶
Bases: ABC
Abstract base class for any combinatorial optimisation problem.
score_summary(x_disc)
¶
Problem-specific, human-readable breakdown of a discrete solution.
The default implementation evaluates :meth:loss_fn and reports the
raw loss. Concrete subclasses should override to return a dict with
label / value / unit / feasible / extra so the
dashboard can display e.g. "IS size: 22" instead of "loss: -22".
normalize_graph(graph)
¶
Return a graph whose nodes are 0, 1, ..., N-1.
Many QUBO constructors use node labels directly as matrix/tensor indices
(Q[u, v] = ...), so a graph whose nodes are {10, 20, 30} (or
strings, or a subset of range(N)) silently produces a wrong QUBO or
raises an IndexError. This helper returns graph unchanged when
the labels are already a contiguous 0..N-1 range and returns a
relabelled copy otherwise. It does not mutate the input.
qqa.problems.qubo
¶
Binary QUBO problems: MIS, MaxClique, MaxCut.
All classes compute loss = x^T Q x on the continuous relaxation
x \in [0, 1]^N supplied by :class:~qqa.relaxation.BinaryRelaxation
(or its batched variant). Minimising the loss is equivalent to solving the
corresponding combinatorial problem.
The *Instance variants pack a list of graphs of (possibly different)
sizes into a single (num_instance, max_node, max_node) Q tensor so the
solver can attack all of them in one qqa.anneal call. Each instance
carries a pad_mask that the loss / score_summary multiplies in to keep
padded positions semantically inert — the optimiser may put anything in
x[i, n_i:] because the mask zeroes its contribution to both the loss
and the reported objective.
MaximumIndependentSet
¶
Bases: QUBOProblem
MIS as a QUBO: diag(-1) with penalty on each edge.
The loss x^T Q x is -|S| + penalty * (#violated edges), so when
all constraints are satisfied, -loss equals the independent-set size.
MaximumIndependentSetInstance
¶
Bases: COProblem
Batched-instance MIS, padded to max_node and masked in the loss.
Parameters¶
nx_graph_list
Heterogeneous list of NetworkX graphs (any sizes n_i <= max_node).
max_node
Padding width. If None (recommended), uses
max(g.number_of_nodes() for g in nx_graph_list).
penalty
Edge-violation penalty. Either a scalar (broadcast to all instances)
or a per-instance sequence of length I.
device
Torch device for the dense Q_tensor and pad_mask.
Loss¶
loss[b, i] = (m_i ⊙ x_{b,i})^T Q_i (m_i ⊙ x_{b,i})
where m_i is the per-instance pad mask. The mask makes padded
positions strictly inert: anything the optimiser writes into
x[:, i, n_i:] is squashed to zero before the einsum.
score_summary(x_disc)
¶
Per-instance IS sizes & feasibility for best_sol of shape (I, N).
value and feasible are 1-D np.ndarray of length
num_instance; extra carries plain-Python lists / ints so
the whole dict is json.dumps-able (the bench runner relies on this).
MaxClique
¶
MaxCut
¶
qqa.problems.categorical
¶
Categorical (one-hot) problems: balanced graph partitioning and coloring.
BalancedGraphPartition
¶
qqa.problems.spin
¶
Physics-flavoured spin problems for QQA.
All classes in this module use :class:~qqa.relaxation.SpinRelaxation, so the
x tensor fed in during annealing lives in [0, 1] while
problem.loss_fn sees the transformed spin s = 2x - 1 \in [-1, +1]
(and exactly \pm 1 after rounding).
Energies follow physics conventions (lower is better):
- Ising 1D:
E = -sum_<i,j> J_{ij} s_i s_j - h sum_i s_i - Edwards-Anderson / SK / Hopfield:
E = -0.5 s^T J swith symmetricJanddiag(J) = 0(so the full sum equals-sum_<i,j> J_{ij} s_i s_j). - Binary perceptron: a smooth surrogate for the number of mis-classified teacher-student patterns.
SpinProblem
¶
Bases: COProblem
Base class for \pm 1 spin problems.
Subclasses must populate self.num_spins and attach a
:class:SpinRelaxation. Most subclasses also build a symmetric coupling
matrix self.J and rely on :meth:quadratic_energy.
quadratic_energy(s)
¶
Compute E = -0.5 s^T J s - h . s for a batch of spin configs.
s has shape (B, N); returns a 1D tensor of shape (B,).
Ising1D
¶
Bases: SpinProblem
One-dimensional Ising chain with nearest-neighbour coupling J.
Energy: E = -J sum_i s_i s_{i+1} - h sum_i s_i.
For J > 0 and h = 0 with periodic boundaries, the ground state is
all spins aligned with energy -J * N.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
N
|
int
|
Number of spins. |
required |
J
|
float
|
Uniform nearest-neighbour coupling strength. |
1.0
|
h
|
float
|
Uniform external field. |
0.0
|
periodic
|
bool
|
Whether to close the chain ( |
True
|
EdwardsAnderson
¶
Bases: SpinProblem
Edwards-Anderson spin-glass on a hyper-cubic lattice.
Only nearest-neighbour bonds are coupled, with J_{ij} \sim N(0, \sigma^2)
drawn once at construction time. The energy is
E = -0.5 s^T J s with symmetric J.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
L
|
int
|
Lattice side length ( |
required |
dim
|
int
|
Spatial dimension ( |
3
|
seed
|
int
|
RNG seed for the couplings. |
0
|
periodic
|
bool
|
Whether to use periodic boundary conditions. |
True
|
sigma
|
float
|
Standard deviation of the Gaussian couplings. |
1.0
|
from_couplings_txt(path, N, *, L=None, dim=3, periodic=True, device='cpu')
classmethod
¶
Load an EA instance from a text file of i j J_ij rows.
Compatible with the couplings_L{L}_R1_seed{seed}.txt format
produced by related projects: rows of i j J_ij with 0-based
indices. No metadata is assumed; N must be provided. Pass L
explicitly when N != L**dim (the cubic-root fallback below only
yields a correct L for cubic lattices).
SherringtonKirkpatrick
¶
Bases: SpinProblem
Sherrington-Kirkpatrick mean-field spin glass.
All-to-all couplings with J_{ij} \sim N(0, 1/N) for i \ne j and
J_{ii} = 0. Energy: E = -0.5 s^T J s.
The standard normalisation J_{ij} \sim N(0, 1/N) makes the typical
ground-state energy density e_0 = E_0 / N converge to
\approx -0.7632 (Parisi).
PSpinGlass
¶
Bases: SpinProblem
Dense p-spin Sherrington-Kirkpatrick model.
Energy:
.. math::
E = -\sum_{i_1 < i_2 < \dots < i_p} J_{i_1 i_2 \dots i_p}
s_{i_1} s_{i_2} \dots s_{i_p}
with i.i.d. Gaussian couplings drawn from
J \sim \mathcal{N}(0,\, p!/(2 N^{p-1})) so that the typical
ground-state energy density is intensive (Crisanti & Sommers, 1992
<https://doi.org/10.1051/jphys:0199200530100128300>_).
For p = 2 this reduces to the classical Sherrington-Kirkpatrick
model (use :class:SherringtonKirkpatrick for the symmetric J
matrix form). For p \ge 3 the model exhibits a discontinuous
1RSB freezing transition and is a canonical hard benchmark for
annealing solvers — small instances already form rugged landscapes
with exponentially many metastable states.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
N
|
int
|
Number of spins. |
required |
p
|
int
|
Interaction order ( |
3
|
seed
|
int
|
RNG seed for the couplings. |
0
|
RandomFieldIsing
¶
Bases: SpinProblem
Random-Field Ising Model on a hyper-cubic lattice.
Energy:
.. math::
E = -J \sum_{\langle i, j \rangle} s_i s_j - \sum_i h_i s_i,
\qquad h_i \sim \mathcal{N}(0,\, \sigma_h^2)
The couplings are uniform ferromagnetic (J > 0) and the disorder
sits in the local fields. This is one of the cleanest models with
quenched randomness in equilibrium statistical physics: in 3D with
Gaussian fields it has a well-studied finite-temperature phase
transition, and the zero-temperature ground-state problem at finite
\sigma_h / J is a classical optimisation benchmark (max-flow
polynomial-time exact solvers exist — useful as a sanity check for
annealing solvers).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
L
|
int
|
Lattice side length ( |
required |
dim
|
int
|
Spatial dimension (default |
2
|
J
|
float
|
Uniform ferromagnetic coupling. |
1.0
|
h_std
|
float
|
Standard deviation of the Gaussian random field. |
1.0
|
seed
|
int
|
RNG seed for the fields. |
0
|
periodic
|
bool
|
Periodic boundary conditions. |
True
|
BinaryPerceptron
¶
Bases: SpinProblem
Teacher-student binary perceptron in the storage formulation.
Patterns \xi^\mu \in \{-1, +1\}^N are drawn uniformly, and a
teacher s^* \in \{-1, +1\}^N generates labels
\sigma^\mu = sign(\frac{1}{\sqrt N} \xi^\mu \cdot s^*).
The learning loss is the number of patterns on which the student s
disagrees with the teacher. During annealing this is replaced by a
smooth surrogate \sum_\mu \sigma(-k z^\mu) where
z^\mu = \sigma^\mu \frac{1}{\sqrt N} \xi^\mu \cdot s and
\sigma is the logistic sigmoid, so gradients are available. After
rounding, :meth:error_count returns the exact number of errors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
N
|
int
|
Input dimension. |
required |
alpha
|
float
|
Loading |
0.5
|
seed
|
int
|
RNG seed for patterns and teacher. |
0
|
sharpness
|
float
|
Sigmoid steepness |
10.0
|
error_count(s)
¶
Exact number of mis-classified patterns for each config in s.
A pattern is counted as an error when the pre-activation z is
strictly negative; exact ties (z == 0) occur only for the rare
case where the teacher inner product vanishes and are treated as
correct.
HopfieldMemory
¶
Bases: SpinProblem
Hopfield associative-memory model with Hebbian couplings.
Given P patterns \xi^\mu \in \{-1, +1\}^N, the couplings are
J_{ij} = (1/N) \sum_\mu \xi^\mu_i \xi^\mu_j for i \ne j and
J_{ii} = 0. Energy: E = -0.5 s^T J s.
At a stored pattern s = \xi^\mu and low loading \alpha = P/N,
E \approx -N/2.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
N
|
int
|
Number of spins. |
required |
patterns
|
int | ndarray | Tensor | Sequence[Sequence[int]]
|
Either an integer |
1
|
seed
|
int
|
RNG seed used when |
0
|
overlap(s)
¶
Normalised overlap with every stored pattern.
Returns a tensor of shape (B, P) with
m^\mu_b = (1/N) \sum_i \xi^\mu_i s_{b,i}.
qqa.problems.extras
¶
Extra problem catalog for QQA.
Eight classic discrete-optimization problems that plug into the unified
qqa.anneal loop alongside the graph / spin / categorical problems
already shipped.
Design¶
- Every class exposes
loss_fn(x) -> (B,)that is minimised by QQA (so e.g. Knapsack returns the negative value of the packed items). - Every class exposes
score_summary(x_disc) -> dictreturning a human-readable breakdown of the best solution so the dashboard can print e.g. "packed value: 138 / 150, feasible: True". - Variable sizing attributes are chosen to match the relaxation
that each problem consumes:
- :class:
BinaryRelaxationexpectsnum_nodes. - :class:
SpinRelaxationexpectsnum_spins. - :class:
CategoricalRelaxationexpectsnum_node+num_category.
- :class:
NumberPartitioning
¶
Bases: COProblem
Classic number partitioning of N positive integers.
Given a_1, ..., a_N the goal is to split them into two subsets
whose sums are as close as possible, i.e. minimise
.. math:: \Bigl(\sum_i a_i s_i\Bigr)^2, \quad s_i \in {-1, +1}.
Uses :class:SpinRelaxation internally so loss_fn receives
:math:s \in [-1, 1]^{B\times N} directly.
Knapsack
¶
Bases: COProblem
0/1 knapsack.
Maximise :math:\sum_i v_i x_i subject to
:math:\sum_i w_i x_i \le C. We minimise
.. math:: -\sum_i v_i x_i + \lambda\,\bigl[\max(0, \sum_i w_i x_i - C)\bigr]^2
so solutions that respect the capacity dominate.
VertexCover
¶
Bases: COProblem
Minimum vertex cover on an undirected graph.
Select a minimum-size vertex subset that touches every edge; we use the QUBO form
.. math:: H = \sum_i x_i + \lambda \sum_{(u,v)\in E} (1 - x_u)(1 - x_v)
GraphBisection
¶
Bases: COProblem
Balanced graph bisection.
Partition vertices into two equal-size sets minimising the cut:
.. math:: H = \sum_{(u,v)\in E} (x_u - x_v)^2 + \lambda \bigl(\sum_i x_i - N/2 \bigr)^2
MinimumDominatingSet
¶
Bases: COProblem
Minimum dominating set on an undirected graph.
Select a minimum-size vertex subset :math:S such that every vertex
is either in :math:S or adjacent to some vertex in :math:S. We
minimise
.. math:: H(x) = \sum_v x_v + \lambda \sum_v (1 - x_v)\,\prod_{u \in N(v)} (1 - x_u),
where :math:x \in \{0, 1\}^N. The product factor equals 1 exactly
when v is unselected and none of its neighbours are selected
(i.e. v is uncovered), so the penalty term counts uncovered
vertices.
During annealing we use the same form on the continuous relaxation
:math:x \in [0, 1]^N. Because :math:(1 - x_u) \in [0, 1], the
product is bounded in :math:[0, 1] and matches the discrete
indicator at the corners. To keep the cost :math:O(B \cdot |E|) per
forward pass (no per-vertex Python loop) we evaluate the log-product
on the edges:
.. math:: \prod_{u \in N(v)} (1 - x_u) = \exp!\left( \sum_{u \in N(v)} \log(1 - x_u + \varepsilon) \right).
SATClause
dataclass
¶
Signed literals of a 3-SAT clause. lits[k] = (var_idx, sign).
MaxSAT3
¶
Bases: COProblem
Random 3-SAT / MaxSAT.
Minimise the number of unsatisfied 3-CNF clauses. A clause is
represented by three signed literals. Let :math:L_i(x) equal
:math:x_i when the literal is positive and :math:1 - x_i otherwise.
The clause is violated iff
:math:(1-L_1)(1-L_2)(1-L_3) = 1, which we sum across all clauses as
the (exact) loss.
TSP
¶
Bases: COProblem
Symmetric travelling salesperson with permutation-aware relaxation.
Variable encoding follows Lucas (2014) "Ising formulations of many NP
problems": x ∈ {0, 1}^{N×N} with x[t, i] = 1 iff city i
occupies tour position t. Three additive terms make every
constraint visible inside loss_fn:
- distance — :math:
\sum_t \sum_{i \neq j} d_{ij}\, x_{t,i}\, x_{t+1,j}(cyclic, so positionN-1wraps back to0). - row penalty — :math:
\lambda_r \sum_t (\sum_i x_{t,i} - 1)^2so every position holds exactly one city. - column penalty — :math:
\lambda_c \sum_i (\sum_t x_{t,i} - 1)^2so every city appears exactly once.
Sinkhorn is the default because its continuous state respects assignment
structure. The explicit binary penalty formulation remains available as
relaxation="binary" for controlled comparisons.
objective_values(x)
¶
Return only the original tour-length objective, without penalties.
repair_solution(x_disc)
¶
Return the closest permutation without mutating the candidate.
Repair and scoring are deliberately separate: callers can retain the raw QQA state for diagnostics while explicitly selecting the repaired tour for reporting or local search.
qqa.problems.user
¶
User-defined problem helper.
Drop-in wrapper that lets a user plug an arbitrary differentiable loss into
:func:qqa.anneal without having to subclass :class:COProblem::
import torch, qqa
J = torch.randn(50, 50); J = (J + J.T) / 2; J.fill_diagonal_(0)
problem = qqa.UserProblem(
num_vars=50,
variable_kind="spin",
loss_fn=lambda s: -0.5 * torch.einsum("bi,ij,bj->b", s, J, s),
)
result = qqa.anneal(problem, sol_size=128, num_epochs=1000)
Three variable kinds are supported:
"binary"—x \in [0, 1](rounded to{0, 1})."spin"—s = 2 \, \text{clip}(x, 0, 1) - 1 \in [-1, +1]and projected to\{-1, +1\}."categorical"— one-hot simplexx \in \Delta^Kper variable.
For categorical kinds the num_vars argument sets the number of categorical
variables and num_category must also be passed.
Loss functions must accept a tensor whose leading axis is the parallel batch
B = sol_size and return a tensor of shape (B,).
UserProblem
¶
Bases: COProblem
Wrap an arbitrary loss function as a :class:COProblem.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_vars
|
int
|
Number of discrete variables ( |
required |
loss_fn
|
Callable[[Tensor], Tensor]
|
Callable mapping a batched tensor to a |
required |
variable_kind
|
VariableKind
|
|
'binary'
|
num_category
|
int | None
|
Required when |
None
|
relaxation
|
Relaxation | None
|
Advanced — pass a custom :class: |
None
|
name
|
str
|
Optional display name used by the GUI/CLI. |
'user-problem'
|
device
|
str | device
|
Torch device hint (only used by QQA's allocation path; the loss itself is device-agnostic as long as any constants it captures live on the right device). |
'cpu'
|
user_problem_from_source(source, num_vars, variable_kind='binary', num_category=None, name='inline', device='cpu', extra_globals=None, *, trusted=False)
¶
Build a :class:UserProblem by exec-ing a Python snippet.
The snippet must define a callable named loss_fn (single argument, the
batched configuration tensor, returning a (B,) loss tensor). The
namespace has torch, np (numpy), and any extra_globals entries
pre-loaded, and the defined loss_fn is closed over that namespace.
trusted=True is mandatory because this executes arbitrary Python.
Remote services and shared dashboards must keep it disabled.
load_problem_from_file(path, *, trusted=False)
¶
Load a user-provided problem from a Python file.
The file must either define a top-level variable problem that is a
:class:COProblem instance, or a callable make_problem() / build()
that returns one.
Mixed-variable optimisation¶
qqa.mixed
¶
Mixed binary/integer/real modelling API.
AdaptiveAugmentedLagrangian
dataclass
¶
Mutable, per-solve augmented-Lagrangian state.
ConstraintArchive
¶
Device-resident feasibility-first and feasible-objective incumbents.
objective_solution
property
¶
Return the feasible incumbent, synchronising only on explicit access.
Constraint
dataclass
¶
A differentiable scalar constraint evaluated for every replica.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
function
|
BatchFunction
|
Callable receiving named batched tensors. |
required |
sense
|
ConstraintSense
|
One of |
'<='
|
rhs
|
float
|
Right-hand side. |
0.0
|
weight
|
float
|
Squared-violation weight added to the optimisation loss. |
100.0
|
scale
|
float
|
Characteristic unit used to normalise the violation. |
1.0
|
tolerance
|
float
|
Raw-unit feasibility tolerance used for reporting. |
0.0001
|
name
|
str
|
Stable label shown in diagnostics and reports. |
'constraint'
|
violation(lhs)
¶
Return non-negative raw violation in the constraint's units.
MixedProblem
¶
Bases: COProblem
A GPU-vectorised optimisation model with heterogeneous variables.
objective and constraint functions receive a mapping from declared
names to tensors. The leading dimension is always the parallel population.
All objectives are minimised.
MixedRelaxation
¶
Relax binary, bounded-integer, and bounded-real variables together.
Every coordinate is represented internally in [0, 1]. Binary
coordinates use the standard QQA penalty, integer coordinates use a
periodic grid penalty, and real coordinates remain continuous.
BinaryVariable
dataclass
¶
One or more variables in {0, 1}.
IntegerVariable
dataclass
¶
One or more bounded integer variables.
RealVariable
dataclass
¶
One or more bounded real-valued variables.
VariableSpace
¶
Flatten typed variables into a stable tensor layout.
The solver works on a dense (..., D) tensor for GPU efficiency while
user objectives receive a mapping such as {"units": tensor, ...}.
decode(latent)
¶
Map normalised solver coordinates from [0, 1] to user units.
encode(values)
¶
Map user-unit values into normalised solver coordinates.
project(latent)
¶
Decode a latent tensor and enforce every declared variable domain.
unpack(values)
¶
Return named zero-copy views into a user-unit tensor.
pack(values, *, device=None, dtype=torch.float32)
¶
Pack one named solution into the solver's stable flat layout.
validate(values, *, atol=1e-06)
¶
Validate shape, bounds, and integrality of a user-unit solution.
describe()
¶
Return JSON-friendly variable metadata in tensor-column order.
__getstate__()
¶
Avoid serialising device-specific bound caches with a model.
VariableSpec
¶
Bases: Protocol
Structural type shared by all variable declarations.
choose_integer_encoding(lower, upper, *, local_lower=None, local_upper=None, categorical_limit=8, order_limit=32, radix=8, gray_limit=65536)
¶
Choose an encoding after applying SCIP's node-local domain.
repair_mixed_solution(problem, candidate, *, max_steps=150, learning_rate=0.03, elastic_weight=1000.0, proximity_weight=0.001, objective_weight=1e-06)
¶
Fix discrete coordinates and elastically repair continuous coordinates.
solve_mixed(problem, *, calibrate_penalty=True, calibration_points=256, penalty_safety_factor=50.0, max_penalty_multiplier=100000000.0, adaptive_augmented_lagrangian=True, al_update_interval=50, al_rho_growth=2.0, al_maximum_rho=10000000000.0, repair=True, repair_candidates=4, repair_steps=150, **kwargs)
¶
Solve a :class:MixedProblem with conservative mixed-domain defaults.
Explicit keyword arguments always win. The wrapper exists so a first-time
user can call problem.solve() without knowing binary-QUBO defaults.
qqa.mixed.problem
¶
Declarative mixed-integer/nonlinear optimisation problems.
Constraint
dataclass
¶
A differentiable scalar constraint evaluated for every replica.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
function
|
BatchFunction
|
Callable receiving named batched tensors. |
required |
sense
|
ConstraintSense
|
One of |
'<='
|
rhs
|
float
|
Right-hand side. |
0.0
|
weight
|
float
|
Squared-violation weight added to the optimisation loss. |
100.0
|
scale
|
float
|
Characteristic unit used to normalise the violation. |
1.0
|
tolerance
|
float
|
Raw-unit feasibility tolerance used for reporting. |
0.0001
|
name
|
str
|
Stable label shown in diagnostics and reports. |
'constraint'
|
violation(lhs)
¶
Return non-negative raw violation in the constraint's units.
qqa.mixed.variables
¶
Typed variable declarations and tensor packing for mixed optimisation.
VariableSpec
¶
Bases: Protocol
Structural type shared by all variable declarations.
BinaryVariable
dataclass
¶
One or more variables in {0, 1}.
IntegerVariable
dataclass
¶
One or more bounded integer variables.
RealVariable
dataclass
¶
One or more bounded real-valued variables.
VariableSpace
¶
Flatten typed variables into a stable tensor layout.
The solver works on a dense (..., D) tensor for GPU efficiency while
user objectives receive a mapping such as {"units": tensor, ...}.
decode(latent)
¶
Map normalised solver coordinates from [0, 1] to user units.
encode(values)
¶
Map user-unit values into normalised solver coordinates.
project(latent)
¶
Decode a latent tensor and enforce every declared variable domain.
unpack(values)
¶
Return named zero-copy views into a user-unit tensor.
pack(values, *, device=None, dtype=torch.float32)
¶
Pack one named solution into the solver's stable flat layout.
validate(values, *, atol=1e-06)
¶
Validate shape, bounds, and integrality of a user-unit solution.
describe()
¶
Return JSON-friendly variable metadata in tensor-column order.
__getstate__()
¶
Avoid serialising device-specific bound caches with a model.
qqa.reporting
¶
Portable result reports for notebooks, email, and experiment archives.
save_html_report(result, problem, path, *, title=None, include_plotlyjs=True)
¶
Write a self-contained interactive solver report.
The report includes a four-panel diagnostic dashboard and an embedded
machine-readable JSON payload. With the default include_plotlyjs=True
the HTML file works offline and can be shared as a single artifact.
Multi-objective optimisation¶
qqa.multiobjective
¶
Parallel multi-objective optimisation and Pareto visualisation.
MultiObjectiveProblem
¶
Bases: MixedProblem
Mixed binary/integer/real model with two or more objectives.
objective_matrix(values, *, minimize=False)
¶
Return shape (population, objectives).
With minimize=True, maximisation columns are sign-flipped so all
columns follow a common minimisation convention.
loss_fn(values)
¶
Reject accidental single-objective annealing.
score_summary(x_disc)
¶
Return all objectives, variables, and feasibility for one plan.
solve_pareto(**kwargs)
¶
Find a Pareto front with one parallel QQA run.
Objective
dataclass
¶
One named objective and its optimisation direction.
ParetoResult
dataclass
¶
Nondominated feasible solutions collected during one parallel run.
weights is row-aligned with solutions and records the reference
direction of the replica that produced each archived point.
search_reference_directions retains the complete set of directions
used by the parallel search, including replicas that did not contribute a
point to the final archive.
reference_directions
property
¶
Reference direction that produced each archived Pareto point.
minimize_objectives
property
¶
Objectives transformed to a common minimisation convention.
named_solutions(problem)
¶
Return variable-name views for every Pareto solution.
knee_index()
¶
Return a scale-invariant compromise point nearest the ideal vector.
select(weights=None)
¶
Select a compromise by normalised weighted achievement.
Passing no weights returns the geometric knee. Positive weights are normalised automatically and work for mixed min/max directions.
hypervolume(reference_point, *, samples=131072, seed=0)
¶
Return exact 2-D or deterministic Monte-Carlo many-objective HV.
reference_point uses the original reported objective directions.
It must be weakly worse than every point in the front.
r2_indicator(*, directions=256, seed=0)
¶
Return the minimisation R2 indicator over Sobol reference directions.
igd(reference_front)
¶
Inverted generational distance to a supplied original-direction front.
epsilon_indicator(reference_front)
¶
Unary additive epsilon indicator against a reference front.
to_frame(problem=None)
¶
Return objectives and, optionally, named decision variables.
pareto_anneal(problem, *, sol_size=256, num_epochs=1500, learning_rate=0.05, temp=0.0, min_bg=-0.5, max_bg=1.0, curve_rate=2, scalarization='augmented-tchebycheff', augmentation=0.05, div_param=0.01, archive_interval=25, archive_size=2048, dominance_chunk_size=1024, history_stride=10, constraint_strategy='adaptive', penalty_growth=2.0, penalty_progress_ratio=0.8, restart_patience=8, restart_fraction=0.15, restart_jitter=0.08, gradient_clip_norm=100.0, weight_decay=0.0, seed=0, device='cpu', time_limit=None, verbose=False)
¶
Find a diverse Pareto front in one GPU-parallel optimisation run.
A Powell–Hestenes–Rockafellar augmented Lagrangian treats inequalities with projected non-negative multipliers and equalities with signed multipliers. When the archive stagnates, weak non-anchor replicas are split between archive-centred and global restarts while the nondominated archive and objective-axis reference directions are preserved.
plot_pareto(result, *, backend='plotly', title='QQA Pareto front', show=True)
¶
Plot 2-D/3-D fronts or parallel coordinates for 4+ objectives.
The scale-invariant knee selected by :meth:ParetoResult.select is
highlighted so a user can distinguish a recommended compromise from the
objective-axis extremes without manually normalising mixed units.
plot_pareto_diagnostics(result, *, backend='plotly', title='QQA Pareto search diagnostics', show=True)
¶
Plot archive growth, feasibility, violation, penalty, and restarts.
This diagnostic view complements :func:plot_pareto: it makes adaptive
constraint enforcement and basin-recovery events auditable instead of
presenting only the final nondominated point cloud.
qqa.multiobjective.problem
¶
Declarative multi-objective mixed-variable models.
Objective
dataclass
¶
One named objective and its optimisation direction.
MultiObjectiveProblem
¶
Bases: MixedProblem
Mixed binary/integer/real model with two or more objectives.
objective_matrix(values, *, minimize=False)
¶
Return shape (population, objectives).
With minimize=True, maximisation columns are sign-flipped so all
columns follow a common minimisation convention.
loss_fn(values)
¶
Reject accidental single-objective annealing.
score_summary(x_disc)
¶
Return all objectives, variables, and feasibility for one plan.
solve_pareto(**kwargs)
¶
Find a Pareto front with one parallel QQA run.
qqa.multiobjective.solver
¶
One-shot parallel Pareto-front optimisation.
Every replica follows a distinct Sobol/Dirichlet reference direction. An augmented Tchebycheff scalarisation is used instead of a weighted sum so non-convex portions of the Pareto front remain reachable.
ParetoResult
dataclass
¶
Nondominated feasible solutions collected during one parallel run.
weights is row-aligned with solutions and records the reference
direction of the replica that produced each archived point.
search_reference_directions retains the complete set of directions
used by the parallel search, including replicas that did not contribute a
point to the final archive.
reference_directions
property
¶
Reference direction that produced each archived Pareto point.
minimize_objectives
property
¶
Objectives transformed to a common minimisation convention.
named_solutions(problem)
¶
Return variable-name views for every Pareto solution.
knee_index()
¶
Return a scale-invariant compromise point nearest the ideal vector.
select(weights=None)
¶
Select a compromise by normalised weighted achievement.
Passing no weights returns the geometric knee. Positive weights are normalised automatically and work for mixed min/max directions.
hypervolume(reference_point, *, samples=131072, seed=0)
¶
Return exact 2-D or deterministic Monte-Carlo many-objective HV.
reference_point uses the original reported objective directions.
It must be weakly worse than every point in the front.
r2_indicator(*, directions=256, seed=0)
¶
Return the minimisation R2 indicator over Sobol reference directions.
igd(reference_front)
¶
Inverted generational distance to a supplied original-direction front.
epsilon_indicator(reference_front)
¶
Unary additive epsilon indicator against a reference front.
to_frame(problem=None)
¶
Return objectives and, optionally, named decision variables.
nondominated_mask(values, *, tolerance=1e-08, chunk_size=1024)
¶
Return the Pareto-efficient rows of an all-minimisation matrix.
Comparisons are chunked along the candidate axis. This preserves exact
dominance while avoiding the O(points²*objectives) temporary tensor
that previously exhausted GPU memory on large archives.
scalarize_objectives(normalised, weights, *, method='augmented-tchebycheff', augmentation=0.05)
¶
Apply one row-aligned Pareto scalarisation to each replica.
pareto_anneal(problem, *, sol_size=256, num_epochs=1500, learning_rate=0.05, temp=0.0, min_bg=-0.5, max_bg=1.0, curve_rate=2, scalarization='augmented-tchebycheff', augmentation=0.05, div_param=0.01, archive_interval=25, archive_size=2048, dominance_chunk_size=1024, history_stride=10, constraint_strategy='adaptive', penalty_growth=2.0, penalty_progress_ratio=0.8, restart_patience=8, restart_fraction=0.15, restart_jitter=0.08, gradient_clip_norm=100.0, weight_decay=0.0, seed=0, device='cpu', time_limit=None, verbose=False)
¶
Find a diverse Pareto front in one GPU-parallel optimisation run.
A Powell–Hestenes–Rockafellar augmented Lagrangian treats inequalities with projected non-negative multipliers and equalities with signed multipliers. When the archive stagnates, weak non-anchor replicas are split between archive-centred and global restarts while the nondominated archive and objective-axis reference directions are preserved.
Black-box optimisation¶
qqa.blackbox
¶
Derivative-free optimisation for expensive mixed-variable functions.
AsynchronousEvaluationScheduler
¶
Submit independent points and expose non-blocking completion events.
EvaluationDatabase
¶
SQLite observations keyed by full experimental identity.
Point, problem, seed, fidelity, replicate, and evaluator version are all part of the identity. Repeated noisy observations therefore coexist rather than overwriting one another.
BlackBoxConstraint
dataclass
¶
A possibly non-differentiable constraint evaluated point by point.
BlackBoxProblem
¶
An expensive objective over binary, integer, real, or mixed variables.
The objective receives one plain-Python named point at a time, making it
suitable for simulators, remote services, subprocesses, and legacy code.
Independent points can be evaluated concurrently with workers > 1.
BlackBoxResult
dataclass
¶
Observations and incumbent from mixed-variable black-box optimisation.
to_frame(problem)
¶
Return every evaluated point as an analysis-ready DataFrame.
Study
¶
A resumable campaign whose acquisition batches are selected by QQA.
optimize(*, budget, **kwargs)
¶
Run or resume the campaign with QQA diverse-batch acquisition.
ask(*, fidelity='default', replicate=0)
¶
Reserve a deterministic projected point for distributed ask/tell use.
tell(trial, *, value=None, violations=(), state=TrialState.COMPLETE)
¶
Complete a reserved trial with strict finite observations.
blackbox_optimize(problem, *, budget=100, batch_size=4, initial_points=None, workers=1, candidate_pool=4096, exploration=2.0, acquisition='expected_improvement', acquisition_optimizer='pool', qqa_acquisition_epochs=60, qqa_acquisition_replicas=16, constraint_weight=10.0, initial_radius=0.35, min_radius=0.02, trust_regions=1, ridge=1e-06, noise=0.0, max_model_points=512, surrogate='rbf', rff_features=128, resume_from=None, surrogate_dtype='auto', evaluation_database=None, problem_fingerprint=None, fidelity='default', evaluation_timeout=None, seed=0, device='cpu', verbose=False)
¶
Optimise a costly mixed-variable function within an evaluation budget.
An exact RBF surrogate supplies mean and uncertainty. Candidate batches
combine global Sobol coverage with a success-adaptive local trust region;
expected improvement (or a lower confidence bound), probability of
feasibility, and greedy distance penalisation keep parallel evaluations
diverse. resume_from continues an expensive campaign without repeating
observations, while max_model_points bounds cubic surrogate cost.
plot_blackbox(result, *, backend='plotly', title='QQA black-box optimisation', show=True)
¶
Plot incumbent convergence, feasibility, and trust-region adaptation.
qqa.blackbox.problem
¶
User-facing black-box problem declarations.
BlackBoxConstraint
dataclass
¶
A possibly non-differentiable constraint evaluated point by point.
BlackBoxProblem
¶
An expensive objective over binary, integer, real, or mixed variables.
The objective receives one plain-Python named point at a time, making it
suitable for simulators, remote services, subprocesses, and legacy code.
Independent points can be evaluated concurrently with workers > 1.
qqa.blackbox.solver
¶
Batch Bayesian-style optimisation with an adaptive RBF trust region.
BlackBoxResult
dataclass
¶
Observations and incumbent from mixed-variable black-box optimisation.
to_frame(problem)
¶
Return every evaluated point as an analysis-ready DataFrame.
blackbox_optimize(problem, *, budget=100, batch_size=4, initial_points=None, workers=1, candidate_pool=4096, exploration=2.0, acquisition='expected_improvement', acquisition_optimizer='pool', qqa_acquisition_epochs=60, qqa_acquisition_replicas=16, constraint_weight=10.0, initial_radius=0.35, min_radius=0.02, trust_regions=1, ridge=1e-06, noise=0.0, max_model_points=512, surrogate='rbf', rff_features=128, resume_from=None, surrogate_dtype='auto', evaluation_database=None, problem_fingerprint=None, fidelity='default', evaluation_timeout=None, seed=0, device='cpu', verbose=False)
¶
Optimise a costly mixed-variable function within an evaluation budget.
An exact RBF surrogate supplies mean and uncertainty. Candidate batches
combine global Sobol coverage with a success-adaptive local trust region;
expected improvement (or a lower confidence bound), probability of
feasibility, and greedy distance penalisation keep parallel evaluations
diverse. resume_from continues an expensive campaign without repeating
observations, while max_model_points bounds cubic surrogate cost.
QQA × SCIP¶
The functions below require pip install "qqa[scip]".
qqa.hybrid
¶
Explicitly opt-in hybrid solvers combining QQA with exact optimisation.
The package facade is lazy so lightweight capability/configuration access does not import PySCIPOpt-facing implementations before a solver is requested.
qqa.hybrid.scip
¶
QQA + SCIP hybrid refinement for binary quadratic models.
QQA explores many basins on the selected Torch device. Diverse projected replicas are then installed as SCIP primal starts for an exact MIQP solve. SCIP can improve the incumbent and, when time permits, certify optimality.
SCIPHybridResult
dataclass
¶
solve_qqa_scip(problem, *, qqa_kwargs=None, time_limit=60.0, relative_gap=0.0, max_warm_starts=32, threads=1, verbose=False)
¶
Explore a QUBO with QQA and refine/certify it with SCIP.
The exact SCIP model preserves QQA's convention x.T @ Q @ x even
when Q is not symmetric: off-diagonal coefficients Q[i,j] and
Q[j,i] are combined into one binary-product term.
Sparse algebraic benchmarks¶
The QPLIB importer requires pip install "qqa[qplib]"; MPS execution and
SCIP-guided completion require pip install "qqa[scip]".
qqa.algebraic
¶
Sparse algebraic models used by external optimisation benchmarks.
AlgebraicConstraint
dataclass
¶
A ranged sparse algebraic constraint lower <= expression <= upper.
AlgebraicEvaluation
dataclass
¶
Objective and official-style feasibility diagnostics at one point.
AlgebraicModel
dataclass
¶
Sparse linear/quadratic model with original-space variable metadata.
SparseQuadratic
dataclass
¶
VariableType
¶
Bases: str, Enum
Domain type of one algebraic variable.
qqa.io
¶
Lazy importers for public optimisation benchmark formats.
qqa.presolve
¶
Lazy presolve state, reduction, and scaling exports.
qqa.decomposition
¶
Discrete proposal and continuous-completion decomposition.
benders_decompose(master_solver, subproblem_solver, *, maximum_iterations=100, tolerance=1e-06)
¶
Coordinate a master and scenario/recourse oracle until the gap closes.
complete_integer_assignment(template, variable_names, values, *, main_model=None, heuristic=None, algebraic=None, time_limit=1.0, node_limit=500, seed=0, minimum_relative_improvement=0.0, verbose=False)
¶
Fix an integer proposal in an independent sub-SCIP and complete it.
template must be an idle original-problem copy, normally produced by
:func:create_completion_template before the main solve begins. When a
main model is supplied, the full original-space solution is submitted via
trySol so SCIP remains responsible for feasibility and acceptance.
complete_integer_assignment_dive(model, variables, values, *, heuristic=None, algebraic=None, lp_iterations=500, anchor_values=None, change_order=None, max_repair_changes=12, minimum_relative_improvement=0.0)
¶
Complete an integer assignment with the active node LP in-place.
SCIP diving temporarily fixes transformed integer variables, reoptimises
the already loaded node LP, and restores the original node afterwards.
This avoids constructing a complete sub-SCIP for every QQA candidate. It
is an exact continuous completion for linear MIPs; trySol remains the
final authority for all original constraints and integrality conditions.
create_completion_template(model)
¶
Create an independent original-problem copy before SCIP starts solving.
Keeping an idle template avoids copying the actively solving transformed problem from inside a Python heuristic callback. Besides being portable across SCIP versions, this preserves the original variable names needed for safe postsolve injection.
detect_decomposition(model, *, maximum_linking_fraction=0.05)
¶
Find independent blocks or a small variable separator.
progressive_hedging(scenario_solvers, initial_consensus, *, probabilities=None, rho=1.0, tolerance=1e-05, maximum_iterations=100)
¶
Coordinate independent scenario oracles through nonanticipativity.
qqa.dual
¶
Primal-dual relaxation engines and bound-producing adapters.
crossover_lp(model, relaxation=None, *, time_limit=None, tolerance=1e-07)
¶
Crossover a linear relaxation to a basic solution with dual simplex.
SciPy's public HiGHS interface does not accept an LP warm start, so the optional PDHG point is checked for model alignment and retained as the semantic hand-off, while HiGHS performs a clean dual-simplex crossover on the identical sparse model. No coefficient or infinite bound is changed.
solve_lp_relaxation(model, *, device='auto', dtype=torch.float64, max_iterations=10000, tolerance=1e-06, restart_interval=200, time_limit=None)
¶
Solve the continuous linear relaxation and return primal/dual/KKT data.
Integrality is intentionally relaxed. Nonlinear rows are rejected rather than linearised silently, and a dual bound is returned only when every conjugate term is finite.
qqa.exact
¶
Optional CP/SAT and global-optimisation runtimes.
CPResult
dataclass
¶
scip_status
property
¶
Compatibility status consumed by the backend-neutral result adapter.
solve_cp_model_ir(model, *, time_limit=None, random_seed=0, workers=1, warm_start=None)
¶
Solve bounded binary/integer linear and scheduling models with CP-SAT.
A supplied warm start is only a hint. CP-SAT validates it against the original model and remains solely responsible for bounds and proofs.
solve_sat_model_ir(model, *, time_limit=None)
¶
Solve native clause factors with PySAT RC2/SAT and proof-safe semantics.
spatial_branch_and_bound(objective, lower, upper, *, relaxation_bound, tolerance=0.0001, maximum_nodes=10000)
¶
Globally search a bounded box using user-supplied valid lower bounds.
qqa.runtime
¶
Lazy event-driven runtime contracts shared by solvers, services, and UIs.
qqa.service
¶
Process-isolated, schema-only remote job service.
The service deliberately accepts portable ModelIR dictionaries, never Python
source, pickle payloads, server-side file paths, or arbitrary import names.
FastAPI is optional and imported only by :func:create_app.
qqa.templates
¶
Validated domain templates that compile to portable typed ModelIR models.
qqa.uncertainty
¶
Scenario, robust, chance-constrained, and CVaR factor aggregation.
ScenarioFactor
dataclass
¶
Aggregate matching factors evaluated under multiple scenarios.
ChanceConstraintFactor
dataclass
¶
Penalty when the empirical probability of violation exceeds a limit.
DistributionallyRobustChanceFactor
dataclass
¶
Total-variation ambiguity upper bound for a smoothed chance row.
WassersteinDROFactor
dataclass
¶
Lipschitz-certified Wasserstein worst-case expectation.
PhiDivergenceDROFactor
dataclass
¶
KL or chi-square ambiguity-set robust expectation.
MomentAmbiguityDROFactor
dataclass
¶
One-sided moment-ambiguity bound using a mean/std safety factor.
sample_average_confidence_interval(outcomes, *, confidence=0.95)
¶
Normal-approximation confidence interval along the scenario axis.
validate_out_of_sample(factor, solutions, *, tolerance=0.0)
¶
Evaluate held-out scenario cost/violation without modifying a solution.
reduce_scenarios(features, probabilities, *, count)
¶
Greedy probability-weighted k-medoids scenario reduction.
qqa.benchmarking
¶
Lazy public facade for the opt-in benchmark integration.
Importing :mod:qqa.benchmarking exposes the portable result types and
MIPLIB/QPLIB helpers without eagerly loading parsers, SCIP-facing runners, or
plotting dependencies. A concrete implementation is imported only when its
attribute is first requested.
TeX modelling¶
qqa.tex
¶
Compile TeX optimisation models through an OpenAI-compatible API.
LLMAPIError
¶
Bases: RuntimeError
An API transport or response-shape failure with secrets redacted.
OpenAICompatibleClient
¶
Call an OpenAI Responses-compatible or Anthropic Messages endpoint.
generate_model_json(prompt, *, system_prompt=None)
¶
Generate one model JSON document, preferring Structured Outputs.
TexSolveResult
dataclass
¶
The auditable spec, compiled problem, and numerical solver result.
ModelSpec
dataclass
¶
Validated, JSON-serialisable optimisation model.
validate_semantics()
¶
Preflight every expression on a small deterministic domain sample.
The safe AST validator prevents code execution, but syntax alone cannot establish the numerical contract required by QQA. Objectives and constraints must return exactly one finite scalar per candidate. This check intentionally includes declared bounds, because the relaxation and final projection may evaluate expressions there.
compile_tex(tex, *, client=None)
¶
Translate TeX into a validated declarative model specification.
problem_from_spec(spec)
¶
Compile a validated spec into a differentiable QQA problem.
solve_tex(tex, *, client=None, solver_kwargs=None)
¶
Translate and solve a TeX optimisation problem in one call.
qqa.tex.schema
¶
Strict JSON model schema shared by the API client and local compiler.
ModelSpec
dataclass
¶
Validated, JSON-serialisable optimisation model.
validate_semantics()
¶
Preflight every expression on a small deterministic domain sample.
The safe AST validator prevents code execution, but syntax alone cannot establish the numerical contract required by QQA. Objectives and constraints must return exactly one finite scalar per candidate. This check intentionally includes declared bounds, because the relaxation and final projection may evaluate expressions there.
qqa.tex.client
¶
Minimal Responses/Messages client with credential-safe failures.
LLMAPIError
¶
Bases: RuntimeError
An API transport or response-shape failure with secrets redacted.
Relaxations¶
qqa.relaxation
¶
Relaxation strategies for QQA.
A Relaxation defines how a combinatorial variable is represented as a
continuous tensor during annealing. It encapsulates:
- initialization of the relaxed variable,
- the transformation fed into
problem.loss_fn(forward), - the discrete projection used to evaluate the true objective (
project), - the quasi-quantum penalty function,
- the diversity term across the parallel batch,
- an in-place Langevin-style perturbation.
All relaxations operate on a leading batch dimension of size sol_size.
Relaxation
¶
Bases: Protocol
Protocol that any relaxation strategy must satisfy.
BinaryRelaxation
¶
Relaxation for binary variables x in [0, 1].
Used for QUBO problems (MIS, MaxClique, MaxCut) on either a single graph
(shape (sol_size, N)) or a batch of graphs via an instance problem
(shape (sol_size, I, N)).
encode(values)
¶
Map physical binary values back to latent coordinates.
StraightThroughBinaryRelaxation
¶
Bases: BinaryRelaxation
Opt-in logit relaxation with a straight-through hard forward pass.
The physical value seen by the objective is binary, while gradients flow
through the sigmoid probability. The default QQA route intentionally
remains :class:BinaryRelaxation; this class is useful for objectives
whose continuous extension is poorly conditioned or undefined.
StochasticBinaryRelaxation
¶
Bases: StraightThroughBinaryRelaxation
Straight-through Bernoulli relaxation for stochastic binary replicas.
SpinRelaxation
¶
Bases: BinaryRelaxation
Relaxation for ising-style spin variables s \in \{-1, +1\}.
Internally the latent representation x lives in [0, 1] (same as
:class:BinaryRelaxation), but :meth:forward maps it to the spin
s = 2 \, \text{clip}(x, 0, 1) - 1 so that problem.loss_fn can
safely work on real-valued spins in [-1, +1]. The discrete projection
thresholds at 0.5.
Because spin problems typically couple variables quadratically without a
convex QUBO structure, AdamW steps can push the latent x outside
[0, 1]; we clip before the forward so the effective spin stays in
[-1, +1], and :meth:perturb_ always clamps x back even when
temp == 0.
encode(values)
¶
Map physical spins in [-1, 1] back to [0, 1].
BinaryInstanceRelaxation
¶
Bases: BinaryRelaxation
Binary relaxation for batched instance problems.
Expects the problem to expose num_instance and max_node.
CategoricalRelaxation
¶
Relaxation for one-hot categorical variables.
Variable tensor shape: (sol_size, N, K). The forward pass normalises
across the category axis and project returns one-hot tensors.
encode(values)
¶
Use simplex/one-hot values directly as latent coordinates.
penalty_from_forward(x, x_fwd, curve_rate)
¶
Optimised path: reuse the already-normalised tensor from forward.
Used by :func:qqa.anneal to avoid the simplex normalisation
re-running every epoch (the legacy penalty calls forward
internally, which doubled the cost on the hot path).
SoftmaxCategoricalRelaxation
¶
Bases: CategoricalRelaxation
Logit/softmax categorical relaxation with temperature annealing support.
set_progress(progress)
¶
Update temperature by geometric endpoint-inclusive annealing.
EntropicCategoricalRelaxation
¶
SparseCategoricalRelaxation
¶
MirrorDescentCategoricalRelaxation
¶
Bases: CategoricalRelaxation
Simplex-native categorical relaxation for entropic mirror descent.
Select optimizer='mirror-descent' in :func:qqa.anneal. The latent
tensor stores probabilities directly and each optimizer step performs the
exponentiated-gradient update followed by exact simplex normalisation.
SinkhornRelaxation
¶
Bases: SoftmaxCategoricalRelaxation
Doubly-stochastic permutation relaxation.
project intentionally stays device-local and uses a row-wise hard
projection. Exact assignment repair belongs at the explicit repair
boundary after optimisation; running a CPU Hungarian solver in every
annealing epoch would otherwise dominate the hot loop and synchronise a
CUDA device repeatedly.
Schedules¶
qqa.schedule
¶
Annealing schedules for the QQA discretisation coefficient.
Every schedule implements schedule(epoch, num_epochs) -> float and uses
an endpoint-inclusive convention: for a run with two or more epochs, epoch
zero returns minimum and epoch num_epochs - 1 returns maximum.
Keeping that convention in one module prevents subtle differences between
the Python API, CLI, UI, and benchmark runner.
Schedule
¶
Bases: Protocol
Structural protocol shared by all QQA schedules.
LinearBGSchedule
dataclass
¶
Endpoint-inclusive linear schedule.
When min_bg < 0 and max_bg > 0, the penalty transitions from a
soft-centre curvature contribution to the discrete regime where binary
corners are favoured. Negative BG does not imply that an arbitrary
combined objective is globally convex or has a unique minimiser.
A one-epoch run returns max_bg so that even a smoke run performs a
discrete-facing update.
CosineBGSchedule
dataclass
¶
Cosine easing with zero slope at both endpoints.
ExponentialBGSchedule
dataclass
¶
Exponentially weighted interpolation that also supports negative BG.
SigmoidBGSchedule
dataclass
¶
Normalised logistic schedule.
PolynomialBGSchedule
dataclass
¶
Polynomial interpolation; powers above one delay discretisation.
PiecewiseBGSchedule
dataclass
¶
Piecewise-linear schedule through normalised (progress, value) knots.
CyclicBGSchedule
dataclass
¶
Triangular cycles around a linear trend for periodic exploration.
ReheatBGSchedule
dataclass
¶
Linear schedule with bounded reheating drops at configured progress.
AdaptiveBGSchedule
dataclass
¶
Cosine schedule with bounded, observation-driven reheating.
The annealer calls :meth:observe at a low-frequency control interval.
Stagnation or collapsed diversity temporarily delays discretisation;
successful windows gradually return to the base cosine path. The class
remains opt-in and never changes the default QQA dynamics.
observe(*, improved, diversity_ratio=None)
¶
Update the bounded reheat offset from one control window.
make_schedule(name, *, minimum=-2.0, maximum=0.1)
¶
Build a validated standard schedule by a stable public name.
Callbacks¶
qqa.callbacks
¶
Callbacks for the QQA annealing loop.
Callbacks receive a CallbackState snapshot at the end of every epoch and
can record metrics, adjust hyper-parameters, or track auxiliary objectives.
CallbackState
dataclass
¶
Mutable context passed to callbacks at each epoch.
The annealing loop writes fields here. Callbacks may read any field and
may write to extras or mutate hyperparams (e.g. div_param).
Callback
¶
Base class. Override on_epoch_end (and optionally other hooks).
HistoryRecorder
¶
Bases: Callback
Record loss / penalty / diversity statistics per epoch.
Performance notes¶
The recorder is on the hot path of every annealing step. Naively calling
tensor.item() for every metric forces a CUDA device->host sync at
each epoch and dominates the wall-clock when the kernels themselves are
cheap (small problems / GPU). To avoid that:
- Per-epoch scalars are written into preallocated device tensors. There
is no
.item()call inside :meth:on_epoch_end. - :meth:
on_train_endslices and transfers each buffer once, which costs one synchronisation regardless ofnum_epochs. strideskips intermediate epochs entirely (the last epoch is always recorded so that final-state observers see a non-empty history).
The exposed self.history dict is still a dict[str, list[float]]
(or list[list[float]] for best_obj of batched-instance problems),
so existing code that reads recorder.history["loss_mean"][-1] is
unchanged.
AutoDivTuner
¶
Bases: Callback
Adaptively tune div_param to target a desired diversity ratio.
At each epoch: ratio = diversity / N. The controller
nudges div_param by lr * (ratio - target) and clips to [0, 1].
Relaxation.diversity is already a standard deviation over the
population axis, so dividing by sol_size a second time would make the
measured ratio shrink as more replicas are added.
PopulationTracker
¶
Bases: Callback
Snapshot the parallel population for post-hoc parallel-search visualisation.
Records, every stride epochs:
loss— the(sol_size,)per-replica loss.x— optionally, the continuous variables (heavier but lets you reconstruct PCA trajectories or per-variable heatmaps).
Attributes:
| Name | Type | Description |
|---|---|---|
epochs |
list[int]
|
list of recorded epochs. |
loss |
list[Any]
|
list of |
x |
list[Any]
|
list of |
Visualization¶
qqa.visualization
¶
Visualisation helpers for :class:~qqa.annealing.AnnealResult.
Every plotting function accepts a backend argument:
"matplotlib"(default) — static figures, no optional deps."plotly"— interactive figures, requirespip install qqa[plotly].
If Plotly is not installed and backend="plotly" is requested, the
functions automatically fall back to matplotlib with a warning.
Plot catalog:
- :func:
plot_history— loss / penalty / diversity dynamics. - :func:
plot_best_trajectory— best objective value over epochs. - :func:
plot_schedule— the annealing schedulebg(epoch). - :func:
plot_run_comparison— overlay multiple runs. - :func:
plot_parallel_coordinates— hyper-parameter sweep view. - :func:
plot_solution_heatmap— spins / bits of the best solution. - :func:
plot_population_evolution— parallel-population loss heat-map. - :func:
plot_population_embedding— PCA trajectory of the population. - :func:
plot_result_dashboard— one-screen convergence / solution / constraint / schedule diagnostics. - :func:
plot_variable_solution— domain-aware mixed-variable view. - :func:
plot_constraint_diagnostics— feasibility and tolerance view.
plot_history(result, title='QQA dynamics', backend='matplotlib', show=True)
¶
Plot mean loss, mean penalty and diversity across epochs.
Returns the backend-native figure object ((fig, axes) for matplotlib,
go.Figure for plotly).
plot_best_trajectory(result, title='Best objective per epoch', backend='matplotlib', show=True)
¶
Plot best_obj vs epoch (monotonically non-increasing).
plot_schedule(schedule, num_epochs, title='Annealing schedule', backend='matplotlib', show=True)
¶
Visualise the bg annealing schedule over num_epochs.
plot_run_comparison(results, labels=None, title='Run comparison', backend='matplotlib', show=True)
¶
Overlay best_obj trajectories from multiple runs.
plot_parallel_coordinates(sweep_df, objective='best_obj', title='Hyperparameter sweep', backend='plotly', show=True)
¶
Parallel-coordinates plot of a hyper-parameter sweep.
sweep_df is expected to be a pandas.DataFrame (or any object with a
to_dict(orient="list") method) whose columns are the hyper-parameters
plus one objective column.
The Plotly backend produces a coloured interactive figure (recommended). Matplotlib falls back to a simple scatter-matrix-like rendering.
plot_solution_heatmap(result, problem=None, title='Best solution', backend='matplotlib', show=True)
¶
Render the best discrete solution as a 1D/2D heatmap.
For lattice spin problems (EdwardsAnderson with dim == 2) the
solution is reshaped to (L, L) automatically.
plot_population_evolution(tracker, title='Parallel population loss', backend='plotly', show=True)
¶
Render the parallel population's loss landscape across epochs.
tracker must be a :class:~qqa.callbacks.PopulationTracker instance
that captured snapshots during the run. Each column of the resulting
heat-map is one snapshot epoch, each row is one replica in the
sol_size population, and colour encodes loss. Rows are sorted once,
by final-epoch loss, to keep the panel readable.
The best trajectory is overlaid as a thin white curve.
plot_population_embedding(tracker, title='Population PCA trajectory', backend='plotly', show=True)
¶
2D PCA of the parallel population's continuous variables over time.
Projects every snapshot of tracker.x (shape (sol_size, N, ...))
onto the 2 principal components computed from the concatenation of all
snapshots, then draws the resulting trajectory as a scatter coloured by
epoch with replica paths drawn as light-grey lines.
Requires :class:~qqa.callbacks.PopulationTracker to have been run with
record_x=True.
plot_result_dashboard(result, problem=None, title='QQA optimisation diagnostics', backend='plotly', show=True)
¶
Plot convergence, solution values, constraints, and search dynamics.
plot_variable_solution(result, problem=None, title='Solution by variable domain', backend='plotly', show=True)
¶
Plot each value relative to its declared binary/integer/real bounds.
plot_constraint_diagnostics(result, problem=None, title='Constraint diagnostics', backend='plotly', show=True)
¶
Plot raw constraint violations against their feasibility tolerances.
qqa.visuals
¶
Advanced, responsibility-separated visualisation implementation.
constraint_rows(result, problem=None)
¶
Extract constraint diagnostics from a result score.
serialisable_summary(result, problem=None)
¶
Build a compact JSON-safe result payload.
solution_rows(result, problem=None)
¶
Flatten a solution into labelled, normalised plotting rows.
trajectory(result)
¶
Return an epoch axis and best-known scalar objective trajectory.
plot_optimization_cockpit(result, *, backend='plotly', title='QQA Optimization Cockpit', show=False)
¶
Render anytime primal/dual progress, feasibility, and phase timings.
plot_constraint_diagnostics(result, problem, *, backend, title, show)
¶
Plot raw constraint violations with feasibility thresholds.
plot_result_dashboard(result, problem, *, backend, title, show)
¶
Render convergence, variables, constraints, and search dynamics.
plot_variable_solution(result, problem, *, backend, title, show)
¶
Plot physical values and domains for every solution coordinate.
decision_explorer(result, model=None)
¶
Return JSON-ready stability and one-coordinate counterfactual records.
Optional PyG backends¶
The functions below require the pignn extra
(pip install "qqa[pignn]").
qqa.pignn
¶
Optional PyTorch Geometric backend: CRA-PI-GNN and CPRA trainers.
This subpackage provides self-contained re-implementations of two GNN-based unsupervised-learning combinatorial-optimization solvers:
-
CRA-PI-GNN — Y. Ichikawa, "Controlling Continuous Relaxation for Combinatorial Optimization," NeurIPS 2024 (https://openreview.net/forum?id=ykACV1IhJD). Single-replica continuous-relaxation annealing on a 2-layer GCN.
-
CPRA — Y. Ichikawa & H. Iwashita, "Continuous Parallel Relaxation for Finding Diverse Solutions in Combinatorial Optimization Problems," TMLR 2025 (https://openreview.net/forum?id=ix33zd5zCw). A multi-head extension of CRA-PI-GNN that returns
Rdiverse solutions in one training run, supporting both penalty- and variation-diversification.
Both reference releases use DGL. Because DGL's prebuilt wheels do
not yet target NVIDIA Blackwell (sm_100) and lag the latest PyTorch
/ CUDA combos, we ship ports written in PyTorch Geometric so QQA
users on modern GPUs can compare against either method without juggling
DGL.
Why this lives here, not in the main qqa namespace¶
- PyG and its transitive deps are heavy. We do not want
import qqato pay for them when the user only needsqqa.anneal. - The trainers are backend alternatives to
qqa.anneal, not building blocks of it. Keeping them isolated also makes the README's "QQA vs. CRA-PI-GNN vs. CPRA" comparison story clear.
Quickstart¶
::
pip install "qqa[pignn]"
CRA-PI-GNN (single solution per run)::
import networkx as nx
import qqa
from qqa.pignn import train_cra_pi_gnn
qqa.fix_seed(0)
g = nx.random_regular_graph(d=3, n=200, seed=0)
problem = qqa.MaximumIndependentSet(g, penalty=2)
result = train_cra_pi_gnn(problem, num_epochs=4000)
print(result.score)
CPRA (R diverse solutions per run, e.g. one per penalty level)::
from qqa.pignn import train_cpra_pi_gnn
penalties = [1.5, 2.0, 2.5, 3.0]
replicas = [qqa.MaximumIndependentSet(g, penalty=p) for p in penalties]
result = train_cpra_pi_gnn(
problem,
num_replicas=len(replicas),
replica_problems=replicas,
num_epochs=4000,
)
for record in result.score["extra"]["replicas"]:
print(record["score"])
Both functions return a :class:qqa.AnnealResult, so every downstream
helper that consumes AnnealResult (visualisation, CLI scoring) keeps
working.
See also¶
- CRA reference (DGL): https://github.com/Yuma-Ichikawa/CRA4CO
- CPRA reference (DGL): https://github.com/Yuma-Ichikawa/CPRA4CO
GCNNet
¶
Bases: Module
Two-layer GCN with a learnable node embedding (PI-GNN style).
Parameters¶
num_nodes:
Number of nodes in the graph (also the embedding table size).
in_feats:
Width of the input embedding. Defaults to floor(sqrt(N)) to
match the reference implementation.
hidden_dim:
Hidden width between the two GCN layers. Defaults to in_feats.
dropout:
Dropout probability applied after the first GCN layer. Defaults
to 0 — the original paper used 0 for the headline MIS results.
num_replicas:
Number of independent output channels. Defaults to 1,
which preserves the single-head CRA-PI-GNN behaviour and keeps
the forward output shape (N,). With num_replicas >= 2
the network becomes the CPRA multi-head backbone of
Ichikawa & Iwashita (TMLR 2025) — a single shared embedding +
GCN backbone produces R parallel continuous solutions in
one forward pass and the output shape is (N, R).
Notes¶
The forward pass takes only edge_index because the node
"features" are the learned embedding rows; they evolve through the
same backward pass as the convolution weights. This is the standard
PI-GNN trick from Schuetz et al. (Nature MI 2022). For
num_replicas >= 2 only the second convolution's output channels
grow — the embedding and first convolution are shared across
replicas, matching the CPRA shared-representation design.
forward(edge_index)
¶
Compute soft node assignments p \in (0, 1).
Parameters¶
edge_index:
(2, 2|E|) long tensor produced by
:func:qqa.pignn.graph.nx_to_edge_index.
Returns¶
torch.Tensor
(N,) tensor of probabilities when num_replicas == 1
(CRA-PI-GNN compatibility), or (N, num_replicas) when
num_replicas >= 2 (CPRA layout).
train_cpra_pi_gnn(problem, *, num_replicas=4, replica_problems=None, vari_param=0.0, nx_graph=None, in_feats=None, hidden_dim=None, dropout=0.0, learning_rate=0.0001, weight_decay=0.01, annealing=True, init_reg_param=-20.0, annealing_rate=0.001, curve_rate=2, num_epochs=100000, tol=0.0001, patience=1000, early_stop_disc_patience=None, check_interval=1000, device='cpu', seed=None, verbose=True, polish=True)
¶
Train a CPRA multi-head PI-GNN solver and return an :class:AnnealResult.
CPRA (Continual Parallel Relaxation Annealing) is the multi-replica
extension of CRA-PI-GNN introduced by Ichikawa & Iwashita,
Transactions on Machine Learning Research, 2025
(OpenReview <https://openreview.net/forum?id=ix33zd5zCw>_).
A single shared GCN backbone produces R continuous solutions in
one forward pass, and the loss combines a per-replica QUBO term, the
standard CRA penalty, and an optional inter-replica diversity term.
Two diversification regimes are supported:
- Penalty diversification — supply
replica_problems(lengthnum_replicas) where each problem instance differs in some hyperparameter (e.g.MaximumIndependentSet(g, penalty=p_r)for a sweep ofp_r). One training run yields one solution per penalty level, much cheaper than independent runs. - Variation diversification — leave
replica_problems=Noneso every replica solves the sameproblem, but setvari_param > 0to add the diversity term-R · Σ_i std_r(p_{i,r})(sign chosen so the loss decreases when between-replica spread grows). Replicas then converge to structurally different solutions.
Parameters¶
problem:
Base graph-based problem (used for graph extraction and as the
default score_summary provider when replica_problems is
None).
num_replicas:
Number of parallel continuous solutions R. Defaults to 4.
replica_problems:
Optional list of num_replicas problem instances. When
provided, replica_problems[r].loss_fn evaluates the cost for
replica r. Must share the same underlying graph as
problem (only the QUBO Q_mat may differ — typically via
a different penalty weight). When None, all replicas use
problem.loss_fn.
vari_param:
Coefficient of the diversity term. 0 (default) is pure
penalty diversification; positive values reward inter-replica
spread (used for variation diversification on a fixed problem).
nx_graph, in_feats, hidden_dim, dropout, learning_rate,
weight_decay, annealing, init_reg_param, annealing_rate, curve_rate,
num_epochs, tol, patience, check_interval, device, seed, verbose:
Identical semantics to :func:train_cra_pi_gnn.
Returns¶
qqa.AnnealResult
best_sol — the discrete (N,) assignment of the best
replica (lowest QUBO objective on its own loss_fn).
best_obj — that replica's float objective.
history — per-epoch loss, mean_cost, reg_term,
vari_term, reg_param arrays plus a per_replica_obj
array of shape (epochs, R) for downstream visualisation.
score['extra']['replicas'] — list of
{replica, obj, score, sol} dicts so the caller can inspect
every diversified solution, not only the best one.
Raises¶
ValueError
On invalid num_replicas, vari_param sign, or a
replica_problems list whose length does not match
num_replicas.
Notes¶
- Backbone vs. CPRA4CO. The reference CPRA implementation uses
DGL
GraphSAGE; this port reuses :class:GCNNet(a 2-layerGCNConvstack) for full parity with :func:train_cra_pi_gnnso head-to-head ablations across the two solvers measure the training objective, not the message-passing op. - Best-tracking. The reference CPRA loop returns the final iteration's discretised bits rather than the best-so-far solution. This trainer deliberately tracks the running best per replica — the QQA-side convention — to avoid losing a good early-epoch solution to a transient late spike.
- History keys differ from :func:
train_cra_pi_gnn.train_cra_pi_gnnreports per-epoch"cost"; CPRA reports"mean_cost"(per-replica average) because the raw cost scales linearly with R and is harder to compare across runs. When you mix the two trainers in a single plot, normalise by R yourself. - Replica collapse with
vari_param=0andreplica_problems=None. With identical losses on every replica and shared embedding+backbone gradients, the R output channels drift toward the same fixed point. They start different (random init) and remain visibly distinct for the first few hundred epochs, but eventually collapse. For real variation diversification on a fixed problem, setvari_param > 0(e.g.0.1to0.5works in practice).
train_cra_pi_gnn(problem, *, nx_graph=None, in_feats=None, hidden_dim=None, dropout=0.0, learning_rate=0.0001, weight_decay=0.01, annealing=True, init_reg_param=-20.0, annealing_rate=0.001, curve_rate=2, num_epochs=100000, tol=0.0001, patience=1000, early_stop_disc_patience=None, check_interval=1000, device='cpu', seed=None, verbose=True, polish=True)
¶
Train a CRA-PI-GNN solver and return a :class:qqa.AnnealResult.
Parameters¶
problem:
A graph-based binary QUBO problem from :mod:qqa — typically
:class:~qqa.MaximumIndependentSet, :class:~qqa.MaxClique,
:class:~qqa.MaxCut, :class:~qqa.VertexCover, or
:class:~qqa.GraphBisection. Anything that exposes
problem.nx_graph and problem.loss_fn works.
nx_graph:
Override for problem.nx_graph (rare; only needed for custom
problems that store the graph elsewhere).
in_feats, hidden_dim:
GCN widths. Both default to floor(sqrt(N)), matching the
reference paper.
dropout:
Dropout probability between the two GCN layers. 0 (default)
reproduces the headline numbers in the paper.
learning_rate, weight_decay:
AdamW hyper-parameters. Defaults match the reference.
annealing:
If False the trainer reduces to vanilla PI-GNN
(reg_param = 0 for every epoch).
init_reg_param, annealing_rate:
CRA schedule: reg_param = init_reg_param + epoch * annealing_rate.
Set init_reg_param < 0 so the early-epoch loss landscape is
concave (encourages exploration); annealing_rate > 0 linearly
ramps it through 0 toward the discrete-favouring regime.
curve_rate:
Penalty exponent (must be even). Defaults to 2.
num_epochs:
Hard upper bound on gradient steps. Early stopping (see
tol / patience) usually terminates earlier.
tol, patience:
Stop when both the loss and the penalty change by less than
tol for patience consecutive epochs.
check_interval:
How often the verbose log is printed.
device:
Torch device. Strings like "cuda" are validated up-front to
give a clear error if CUDA is unavailable.
seed:
If supplied, calls :func:qqa.fix_seed before allocating the
model and embedding (so the run is reproducible).
verbose:
If True print periodic progress and a final summary.
Notes¶
The defaults here match the NeurIPS 2024 paper and are tuned for
large instances (N >= 1000). For small graphs (N <= 200) the
paper defaults severely under-converge: try
init_reg_param=-2.0, annealing_rate=5e-4, learning_rate=1e-3
(or even learning_rate=1e-2) instead. This is a standard PI-GNN
quirk — the optimal annealing schedule is sub-linear in problem size
because the cost / penalty magnitudes scale very differently.
Returns¶
qqa.AnnealResult
With best_sol of shape (N,) (rounded {0, 1} tensor),
best_obj the float QUBO loss on that solution, and
history containing per-epoch arrays
loss, cost, reg_term, reg_param.
Raises¶
TypeError
If problem is not graph-based (no nx_graph attribute and
no nx_graph override).
RuntimeError
If device requests CUDA but torch.cuda.is_available() is
False.
ValueError
On obviously-wrong arguments (curve_rate odd, num_epochs
negative, ...).
qqa.pignn.trainer
¶
CRA-PI-GNN trainer (PyTorch Geometric).
Faithful port of the fit_model loop from the reference
NeurIPS 2024 implementation [#cra]_, with two intentional changes:
- The graph backend is :mod:
torch_geometricinstead of DGL, so the trainer runs on every PyTorch-supported GPU (including NVIDIA Blackwell /sm_100for which DGL has no prebuilt wheel as of April 2026). - The loss / penalty mathematics are expressed via the QQA primitives
:meth:
qqa.problems.QUBOProblem.loss_fnand :meth:qqa.relaxation.BinaryRelaxation.penalty, so the function returns a :class:qqa.AnnealResultand is a drop-in alternative to :func:qqa.anneal. Numerically the loss is identical to the original paper forcurve_rate=2(and for any evencurve_rate):
.. math::
L(p; \gamma) \;=\; p^\top Q\, p
\;+\; \gamma \sum_i \bigl(1 - (1 - 2p_i)^c\bigr)
matches CRA's cost + reg_param * Σ (1 - (2p - 1)^c) because
(1 - 2p)^c = (2p - 1)^c for even c.
.. [#cra] Y. Ichikawa, "Controlling Continuous Relaxation for Combinatorial Optimization," NeurIPS 2024. https://github.com/Yuma-Ichikawa/CRA4CO
train_cra_pi_gnn(problem, *, nx_graph=None, in_feats=None, hidden_dim=None, dropout=0.0, learning_rate=0.0001, weight_decay=0.01, annealing=True, init_reg_param=-20.0, annealing_rate=0.001, curve_rate=2, num_epochs=100000, tol=0.0001, patience=1000, early_stop_disc_patience=None, check_interval=1000, device='cpu', seed=None, verbose=True, polish=True)
¶
Train a CRA-PI-GNN solver and return a :class:qqa.AnnealResult.
Parameters¶
problem:
A graph-based binary QUBO problem from :mod:qqa — typically
:class:~qqa.MaximumIndependentSet, :class:~qqa.MaxClique,
:class:~qqa.MaxCut, :class:~qqa.VertexCover, or
:class:~qqa.GraphBisection. Anything that exposes
problem.nx_graph and problem.loss_fn works.
nx_graph:
Override for problem.nx_graph (rare; only needed for custom
problems that store the graph elsewhere).
in_feats, hidden_dim:
GCN widths. Both default to floor(sqrt(N)), matching the
reference paper.
dropout:
Dropout probability between the two GCN layers. 0 (default)
reproduces the headline numbers in the paper.
learning_rate, weight_decay:
AdamW hyper-parameters. Defaults match the reference.
annealing:
If False the trainer reduces to vanilla PI-GNN
(reg_param = 0 for every epoch).
init_reg_param, annealing_rate:
CRA schedule: reg_param = init_reg_param + epoch * annealing_rate.
Set init_reg_param < 0 so the early-epoch loss landscape is
concave (encourages exploration); annealing_rate > 0 linearly
ramps it through 0 toward the discrete-favouring regime.
curve_rate:
Penalty exponent (must be even). Defaults to 2.
num_epochs:
Hard upper bound on gradient steps. Early stopping (see
tol / patience) usually terminates earlier.
tol, patience:
Stop when both the loss and the penalty change by less than
tol for patience consecutive epochs.
check_interval:
How often the verbose log is printed.
device:
Torch device. Strings like "cuda" are validated up-front to
give a clear error if CUDA is unavailable.
seed:
If supplied, calls :func:qqa.fix_seed before allocating the
model and embedding (so the run is reproducible).
verbose:
If True print periodic progress and a final summary.
Notes¶
The defaults here match the NeurIPS 2024 paper and are tuned for
large instances (N >= 1000). For small graphs (N <= 200) the
paper defaults severely under-converge: try
init_reg_param=-2.0, annealing_rate=5e-4, learning_rate=1e-3
(or even learning_rate=1e-2) instead. This is a standard PI-GNN
quirk — the optimal annealing schedule is sub-linear in problem size
because the cost / penalty magnitudes scale very differently.
Returns¶
qqa.AnnealResult
With best_sol of shape (N,) (rounded {0, 1} tensor),
best_obj the float QUBO loss on that solution, and
history containing per-epoch arrays
loss, cost, reg_term, reg_param.
Raises¶
TypeError
If problem is not graph-based (no nx_graph attribute and
no nx_graph override).
RuntimeError
If device requests CUDA but torch.cuda.is_available() is
False.
ValueError
On obviously-wrong arguments (curve_rate odd, num_epochs
negative, ...).
train_cpra_pi_gnn(problem, *, num_replicas=4, replica_problems=None, vari_param=0.0, nx_graph=None, in_feats=None, hidden_dim=None, dropout=0.0, learning_rate=0.0001, weight_decay=0.01, annealing=True, init_reg_param=-20.0, annealing_rate=0.001, curve_rate=2, num_epochs=100000, tol=0.0001, patience=1000, early_stop_disc_patience=None, check_interval=1000, device='cpu', seed=None, verbose=True, polish=True)
¶
Train a CPRA multi-head PI-GNN solver and return an :class:AnnealResult.
CPRA (Continual Parallel Relaxation Annealing) is the multi-replica
extension of CRA-PI-GNN introduced by Ichikawa & Iwashita,
Transactions on Machine Learning Research, 2025
(OpenReview <https://openreview.net/forum?id=ix33zd5zCw>_).
A single shared GCN backbone produces R continuous solutions in
one forward pass, and the loss combines a per-replica QUBO term, the
standard CRA penalty, and an optional inter-replica diversity term.
Two diversification regimes are supported:
- Penalty diversification — supply
replica_problems(lengthnum_replicas) where each problem instance differs in some hyperparameter (e.g.MaximumIndependentSet(g, penalty=p_r)for a sweep ofp_r). One training run yields one solution per penalty level, much cheaper than independent runs. - Variation diversification — leave
replica_problems=Noneso every replica solves the sameproblem, but setvari_param > 0to add the diversity term-R · Σ_i std_r(p_{i,r})(sign chosen so the loss decreases when between-replica spread grows). Replicas then converge to structurally different solutions.
Parameters¶
problem:
Base graph-based problem (used for graph extraction and as the
default score_summary provider when replica_problems is
None).
num_replicas:
Number of parallel continuous solutions R. Defaults to 4.
replica_problems:
Optional list of num_replicas problem instances. When
provided, replica_problems[r].loss_fn evaluates the cost for
replica r. Must share the same underlying graph as
problem (only the QUBO Q_mat may differ — typically via
a different penalty weight). When None, all replicas use
problem.loss_fn.
vari_param:
Coefficient of the diversity term. 0 (default) is pure
penalty diversification; positive values reward inter-replica
spread (used for variation diversification on a fixed problem).
nx_graph, in_feats, hidden_dim, dropout, learning_rate,
weight_decay, annealing, init_reg_param, annealing_rate, curve_rate,
num_epochs, tol, patience, check_interval, device, seed, verbose:
Identical semantics to :func:train_cra_pi_gnn.
Returns¶
qqa.AnnealResult
best_sol — the discrete (N,) assignment of the best
replica (lowest QUBO objective on its own loss_fn).
best_obj — that replica's float objective.
history — per-epoch loss, mean_cost, reg_term,
vari_term, reg_param arrays plus a per_replica_obj
array of shape (epochs, R) for downstream visualisation.
score['extra']['replicas'] — list of
{replica, obj, score, sol} dicts so the caller can inspect
every diversified solution, not only the best one.
Raises¶
ValueError
On invalid num_replicas, vari_param sign, or a
replica_problems list whose length does not match
num_replicas.
Notes¶
- Backbone vs. CPRA4CO. The reference CPRA implementation uses
DGL
GraphSAGE; this port reuses :class:GCNNet(a 2-layerGCNConvstack) for full parity with :func:train_cra_pi_gnnso head-to-head ablations across the two solvers measure the training objective, not the message-passing op. - Best-tracking. The reference CPRA loop returns the final iteration's discretised bits rather than the best-so-far solution. This trainer deliberately tracks the running best per replica — the QQA-side convention — to avoid losing a good early-epoch solution to a transient late spike.
- History keys differ from :func:
train_cra_pi_gnn.train_cra_pi_gnnreports per-epoch"cost"; CPRA reports"mean_cost"(per-replica average) because the raw cost scales linearly with R and is harder to compare across runs. When you mix the two trainers in a single plot, normalise by R yourself. - Replica collapse with
vari_param=0andreplica_problems=None. With identical losses on every replica and shared embedding+backbone gradients, the R output channels drift toward the same fixed point. They start different (random init) and remain visibly distinct for the first few hundred epochs, but eventually collapse. For real variation diversification on a fixed problem, setvari_param > 0(e.g.0.1to0.5works in practice).
qqa.pignn.model
¶
GNN architectures for :mod:qqa.pignn.
Mirrors the reference GCN_dev from CRA4CO_:
- a learnable per-node :class:
torch.nn.Embeddingprovides the input features (no node attributes are assumed), - two stacked :class:
torch_geometric.nn.GCNConvlayers with a ReLU in-between and dropout, - a final :func:
torch.sigmoidso the outputp \in (0, 1)^Nis immediately compatible with the QUBO lossproblem.loss_fn(p) = p^T Q p.
.. _CRA4CO: https://github.com/Yuma-Ichikawa/CRA4CO
GCNNet
¶
Bases: Module
Two-layer GCN with a learnable node embedding (PI-GNN style).
Parameters¶
num_nodes:
Number of nodes in the graph (also the embedding table size).
in_feats:
Width of the input embedding. Defaults to floor(sqrt(N)) to
match the reference implementation.
hidden_dim:
Hidden width between the two GCN layers. Defaults to in_feats.
dropout:
Dropout probability applied after the first GCN layer. Defaults
to 0 — the original paper used 0 for the headline MIS results.
num_replicas:
Number of independent output channels. Defaults to 1,
which preserves the single-head CRA-PI-GNN behaviour and keeps
the forward output shape (N,). With num_replicas >= 2
the network becomes the CPRA multi-head backbone of
Ichikawa & Iwashita (TMLR 2025) — a single shared embedding +
GCN backbone produces R parallel continuous solutions in
one forward pass and the output shape is (N, R).
Notes¶
The forward pass takes only edge_index because the node
"features" are the learned embedding rows; they evolve through the
same backward pass as the convolution weights. This is the standard
PI-GNN trick from Schuetz et al. (Nature MI 2022). For
num_replicas >= 2 only the second convolution's output channels
grow — the embedding and first convolution are shared across
replicas, matching the CPRA shared-representation design.
forward(edge_index)
¶
Compute soft node assignments p \in (0, 1).
Parameters¶
edge_index:
(2, 2|E|) long tensor produced by
:func:qqa.pignn.graph.nx_to_edge_index.
Returns¶
torch.Tensor
(N,) tensor of probabilities when num_replicas == 1
(CRA-PI-GNN compatibility), or (N, num_replicas) when
num_replicas >= 2 (CPRA layout).
default_in_feats(num_nodes)
¶
Heuristic used by the original CRA paper: floor(sqrt(N)).
qqa.pignn.graph
¶
Graph extraction & PyG conversion utilities for :mod:qqa.pignn.
CRA-PI-GNN is a graph neural network method, so it only applies to QQA
problems whose underlying combinatorial structure is a graph (MIS,
MaxClique, MaxCut, VertexCover, GraphBisection). Spin-glass problems
defined by a coupling matrix J (Edwards–Anderson, SK, perceptron,
Hopfield, …) and pure categorical / permutation problems (TSP, QAP,
NQueens, Knapsack, …) have no node-edge structure to convolve over and
are silently rejected here with a clear TypeError.
extract_nx_graph(problem, override=None)
¶
Return the networkx graph backing a QQA problem.
Parameters¶
problem:
A :class:~qqa.problems.COProblem instance. The function checks
problem.nx_graph first (used by MaximumIndependentSet,
MaxClique, MaxCut) and falls back to problem.graph
(used by VertexCover, GraphBisection).
override:
If supplied, used directly (for the rare case where a custom
problem stores its graph elsewhere). The caller is then
responsible for ensuring node labels are 0..N-1 and that the
graph matches problem's QUBO matrix.
Returns¶
networkx.Graph
With node labels 0..N-1.
Raises¶
TypeError
If neither override nor any of the supported attribute names
on problem resolve to a networkx.Graph. The error lists
the supported problem families so the user can diagnose at a
glance.
nx_to_edge_index(graph, device='cpu')
¶
Convert a networkx graph to a symmetric PyG edge_index.
PyG's :class:~torch_geometric.nn.GCNConv expects edge_index of
shape (2, 2|E|) listing both (u, v) and (v, u) for every
undirected edge. Self-loops are not added here — :class:GCNConv
inserts them internally when add_self_loops=True (the default).
Parameters¶
graph:
Undirected networkx graph with node labels in 0..N-1.
device:
Target torch device for the returned tensor.
Returns¶
torch.Tensor
(2, 2|E|) long tensor on device.