HUBO solver
QpiAI's native higher-order HUBO solver works directly with the problem's natural higher-order polynomial, preserving its energy landscape without quadratization or reduction-induced approximations. HUBOSolver validates and submits the polynomial, preserves source labels, and returns typed results.
Construct HUBOProblem
from qpiai_opt.opt_solvers.hubo import HUBOProblem
problem = HUBOProblem(
polynomial_dict={
(): 4.0,
("x",): -2.0,
("x", "y"): 3.0,
("x", "y", "z"): -5.0,
},
num_variables=3,
sense="MIN",
)
Polynomial rules:
- every key is a tuple;
()is the constant term;- labels are non-empty strings;
- coefficients are finite real numbers;
- repeated labels within a term are removed by binary idempotence;
- term labels are sorted deterministically;
- equivalent terms are aggregated and zero terms are removed;
num_variablesis a nonnegative integer;- without
variable_names, it must equal the distinct label count; - optional
variable_namesdeclares the complete register, including isolated variables.
Local execution supports zero and constant polynomials and isolated variables. The platform HUBO service requires a nonempty polynomial with an active term and every declared variable present in it; unsupported platform inputs are rejected before submission.
Thus:
canonical = HUBOProblem(
polynomial_dict={
("y", "x", "x"): 2,
("x", "y"): -1,
},
num_variables=2,
)
canonical.polynomial_dict # {("x", "y"): 1.0}
From an algebraic model
from qpiai_opt.opt_modeller import Binary, Model
x, y, z = Binary("x"), Binary("y"), Binary("z")
model = Model("cubic_assignment")
model.minimize(4 - 2 * x + 3 * x * y - 5 * x * y * z)
problem = HUBOProblem.from_model(model)
from_model() compiles the polynomial and retains an independent source snapshot for variable decoding, objective evaluation, and feasibility checks. Later edits to model do not change that problem. Encoding and penalty options follow HUBO compilation.
Problem fields
| Field | Default | Contract |
|---|---|---|
polynomial_dict |
required | Validated binary polynomial, including a zero polynomial locally. |
num_variables |
required | Strict nonnegative integer equal to the declared register size. |
variable_names |
None |
Optional complete, ordered, unique label tuple. |
sense |
"MIN" |
"MIN" minimizes the source polynomial; "MAX" maximizes it. Results are restored to this source sense. |
problem_type |
"HUBO" |
Fixed HUBO marker. |
vartype |
"BINARY" |
Only supported HUBO variable type. |
solver_config |
None |
Validated solver-parameter dictionary. |
Unknown fields are forbidden.
Client and solver construction
import os
from qpiai_opt.opt_solvers import Client
from qpiai_opt.opt_solvers.hubo import HUBOSolver
client = Client(api_key=os.environ["QPIAI_OPT_API_KEY"])
solver = HUBOSolver(client=client)
The solver can create its own client with api_key=..., url=..., experiment_id=..., or experiment_name=....
available = solver.health()
health() -> bool delegates to Client.health(). It performs an authenticated account request with a five-second timeout, creates no problem or job, and returns False on request or API failure.
Solver configuration
Configuration can be stored on the problem:
problem = HUBOProblem(
polynomial_dict={("x",): -1.0},
num_variables=1,
solver_config={
"num_reads": 100,
"num_sweeps": 1000,
"seed": 42,
},
)
or passed to solve()/submit():
result = solver.solve(
problem,
num_reads=100,
num_sweeps=1000,
seed=42,
)
Per-call values override the same keys in problem.solver_config.
| Parameter | Purpose | Validation |
|---|---|---|
num_sweeps |
Annealing sweeps per read. | Positive integer. |
num_reads |
Number of independent sampling reads. | Positive integer. |
num_threads |
Requested solver thread count. | Positive integer. |
beta_min, beta_max |
Lower and upper inverse-temperature endpoints for annealing. | Positive finite numbers; beta_min <= beta_max when both are present. |
temperature_schedule |
Linear or geometric progression between the annealing endpoints. | "LINEAR" or "GEOMETRIC", case-insensitive at input. |
seed |
Random seed for sampling. | Integer. |
Numeric settings reject Boolean values. Unknown keys raise before problem creation. solver_config=... may be passed as one explicit dictionary to a solver call; recognized direct keyword values are merged over it.
For qpiai_opt.opt_solvers.solve(problem, mode="local", client=client), the native local solver uses stored num_reads, num_sweeps, and seed; direct call values override them. The remaining settings in the table are platform-only and a local solve or inspection rejects them instead of ignoring them. Small local problems use exhaustive enumeration, so annealing settings do not affect that solve and are omitted from result.info().settings.
Blocking solve
Blocking solves read the current job state, then wait through Server-Sent Events (SSE) by default. If the stream cannot complete monitoring, the waiter falls back to polling; poll_interval controls that fallback interval in seconds.
result = solver.solve(
problem,
name="Cubic assignment",
experiment_name="Benchmarks",
num_reads=100,
seed=42,
timeout=3600,
request_timeout=60,
poll_interval=5,
)
solve():
- validates/canonicalizes the polynomial and solver options;
- creates the persisted HUBO problem;
- submits a job;
- waits for a terminal job state;
- retrieves and validates inline/storage-backed output;
- restores source labels when the map is available.
timeout is the waiting budget, starting after submission; it excludes problem creation, submission, and final result retrieval. request_timeout defaults to the client's timeout and supplies the connection/read timeout for those requests. Status reads use the client's timeout within the waiter's remaining budget. These HTTP timeouts bound connection and read inactivity, not total transfer duration; an in-flight request can overrun the waiting budget. A local timeout stops waiting without cancelling the platform job. Resume monitoring the same job ID rather than resubmitting. See request and wait timing.
Nonblocking lifecycle
This solver-instance submit() method is explicitly platform-only. Supplying its Client and calling it is the caller's choice to create one potentially paid platform job.
submission = solver.submit(
problem,
name="Cubic assignment",
num_reads=100,
seed=42,
timeout=60,
)
status = solver.get_status(submission.job_id, timeout=60)
if status.status == "completed":
result = status.result
submit() returns job_id, normalized initial status, acknowledgement message, and problem_id. It does not wait for solver execution.
result = solver.get_result(
submission.job_id,
timeout=60,
problem=problem,
)
solution = solver.get_solution(result=result)
get_result() requires a completed job and otherwise raises RuntimeError. get_solution(job_id=...) performs retrieval; pass result=... to avoid another request.
Label serialization
Labels may be nonempty single-line strings, including punctuation and Unicode, but cannot contain NUL, carriage return, or newline. Completed results restore the original labels when the solver has the source problem. For retrieval in another process, pass that problem explicitly:
result = solver.get_result(submission.job_id, problem=problem)
Without the source problem, the result retains the labels supplied by the platform and cannot reconstruct information that was not returned.
Result interpretation
| Field | Meaning |
|---|---|
problem_class |
Always "HUBO". |
execution_mode |
"local" or "platform". |
job_id |
Platform job identifier retained by blocking and retrieval workflows. |
problem_id |
Platform problem identifier associated with the job, when returned. |
num_variables |
Nonnegative compiled binary-variable count. |
solution |
Binary list in declared variable_names order, or canonical label order when names were omitted. |
variable_assignment |
Decoded source-name-to-value mapping for a model-derived problem; otherwise label-to-binary values. |
objective |
Source objective, including model decoding when available; otherwise the reported solver objective. |
compute_time_s, solver_time_s |
Compute and algorithm durations in seconds, or None when unavailable. |
compute_time_ms, compute_time_us, solver_time_ms, solver_time_us |
Runtime in milliseconds and microseconds. |
Use the common view when method or guarantee affects downstream use:
info = result.info()
solve_summary = (info.method, info.termination, info.guarantee)
Local method is "exhaustive-enumeration" or "simulated-annealing"; the platform method is "HUBO". For a source-feasible candidate, exact enumeration reports termination="optimal" and guarantee="global_optimum"; annealing and platform execution report "completed" and "best_found". A decoded candidate that violates its source model has no usable-solution guarantee. See common solve information.
The response validator requires exactly num_variables solution entries and binary values.
Verify the returned assignment locally:
from qpiai_opt.opt_modeller.formats import calculate_hubo_energy
energy = calculate_hubo_energy(
problem.polynomial_dict,
result.variable_assignment,
)
Compare this value with result.objective using an appropriate floating-point tolerance. Pass problem=problem when retrieving from another process so the SDK can restore labels and evaluate the source polynomial. When that metadata is available, a reported solver objective that disagrees with the returned assignment raises instead of producing a contradictory result. The typed solve_info field stores execution details in ordinary dumps. result.info() returns the common solver and guarantee view. Without the source problem, objective contains the reported solver objective while info().source_objective and info().transformed are None. get_status() separately returns asynchronous job lifecycle state and embeds this result only after completion.
For a source-aware result, every reported platform objective must agree with the returned assignment under round(abs(reported - recomputed)) <= max(1, 1e-7 * max(1, abs(reported), abs(recomputed))). A non-finite difference or a rounded difference above this tolerance raises RuntimeError. Python rounding uses ties to even. When the tolerance is one, a difference of 1.4 is accepted and 1.5 is rejected; the tolerance increases with objective magnitude.
get_status() returns job_id, normalized lifecycle state, optional result/error/timestamps, and the available compute_time_s and solver_time_s measurements.
Problem names, resources, and experiments
Problem names need not be unique. An omitted name becomes a UTC timestamped SDK HUBO problem - ... label.
Ordinary calls omit compute_resource_id; the platform automatically chooses an eligible HUBO resource. Advanced callers may pin a discovered resource ID. The ID is the opaque id of an active resource returned by solver discovery:
resources = client.list_solvers(method="HUBO", active_only=True)
resource = resources[0]
pinned_submission = solver.submit(
problem,
compute_resource_id=resource.id,
)
Pass the same value to HUBOSolver(..., compute_resource_id=resource.id) to make it the default for that solver instance. Per-call experiment_id and exact experiment_name override solver/client defaults. If no experiment is configured, the client uses Default Experiment.
Failure and cleanup behavior
All problem and solver configuration validation occurs locally.
A definitive non-transient 4xx submission rejection triggers cleanup of the new problem record. Timeout-like status codes, server failures, and transport errors have ambiguous acceptance; the SDK retains the problem and attaches its ID to the exception notes. Inspect existing jobs before resubmitting.
Blocking solve() can raise:
pydantic.ValidationErrorfor invalid problem/result models;ValueErrorfor unsupported solver parameters;AuthenticationErrororAPIErrorfor platform responses;- Requests exceptions for transport failures;
JobFailedErrororJobCancelledError;TimeoutErrorfor the total wait deadline.
The result tables above define the solver-specific schema; common status and failure semantics are in Automation. The problem construction section shows both a polynomial compiled by Model and a direct polynomial supplied without the modeller.