Model inspection, conversions, and decoding
Human-readable model inspection
summary() returns concise text; pretty_print() displays and returns the same text. max_constraints limits only the displayed constraint list.
from qpiai_opt.opt_modeller import Binary, Model
x, y = Binary("x"), Binary("y")
model = Model("selection")
model.minimize(-3 * x - 2 * y)
model.add_constraint(x + y <= 1, label="capacity")
summary = model.summary(max_constraints=5)
displayed_summary = model.pretty_print(max_constraints=5)
Both values contain the detected problem class, variable-domain counts, objective, and named constraints. repr(model) remains a one-line identity view.
Transformation records
Operations that change representation record the applied variable maps, coefficients, penalties, offsets, and auxiliary variables.
qubo, offset = model.to_qubo_dict(matrix_format="upper")
steps = model.transformations()
latest_step = steps[-1]
Records can describe domain encodings, source-to-binary weights, sense and offset normalization, constraint penalties, slack and auxiliary variables, piecewise SOS2 formulation, QP curvature validation, QUBO/Ising substitution, and OMMX variable IDs. transformations() returns a detached copy; changing the returned dictionaries does not change the model.
Conversion map
| Source | Target | Operation | Returned value |
|---|---|---|---|
Model |
QUBO | model.to_qubo_dict() |
(qubo_dict, offset); decoding remains on model |
Model |
HUBO | model.to_hubo_dict() |
polynomial dictionary including its constant |
Model |
Ising | model.to_ising_dict() |
dict |
Model |
LP/MPS | model.to_lp_string(), model.to_mps_string() |
solver-neutral text |
Model |
canonical IR | to_ir(model) |
JSON-compatible dictionary |
Model or HUBOProblem |
qubit Hamiltonian | convert(value, "QUBIT_HAMILTONIAN") |
QubitHamiltonian |
Model |
OMMX | to_ommx(model) |
OMMX Instance |
| HUBO | Ising | hubo_to_ising() |
HUBOToIsingConvert |
| qubit Hamiltonian | QUBO or IR | convert(value, "QUBO"), convert(value, "IR") |
IsingToQUBOConvert or IR dictionary |
| HUBO | QUBO | hubo_to_qubo() |
HUBOToQUBOConvert |
| QUBO | MILP | qubo_to_milp() |
QUBOToMILPConvert |
MILP Model |
QUBO | milp_to_qubo() |
MILPToQUBOConvert |
| QUBO | Ising | qubo_to_ising() |
QUBOToIsingConvert |
| Ising | QUBO | ising_to_qubo() or ising.to_qubo() |
IsingToQUBOConvert |
A conversion object carries the transformed representation and the information needed to decode a target assignment. Preserve that object until the target result has been interpreted.
HUBO to QUBO
hubo_to_qubo() replaces higher-order products with shared binary auxiliaries and a validated product penalty.
from qpiai_opt.opt_modeller import hubo_to_qubo
source_hubo = {
(): 3.0,
("x",): -2.0,
("x", "y", "z"): 5.0,
}
reduced = hubo_to_qubo(
source_hubo,
sense="MIN",
matrix_format="upper",
)
source_sample = {"x": 1, "y": 0, "z": 1}
full_sample = reduced.complete_sample(source_sample)
auxiliaries_valid = reduced.verify_auxiliaries(full_sample)
source_value = reduced.original_objective(full_sample)
target_value = reduced.qubo_energy(full_sample)
decoded = reduced.decode(full_sample)
full_sample follows reduced.variable_names and includes implied auxiliary bits. For a valid auxiliary assignment, target_value equals the sense-normalized source energy and decoded contains only x, y, and z. strength=None chooses the minimum safe penalty. An explicit strength below that value raises ModelCompilationError.
For sense="MAX", the returned QUBO is a minimizing representation of the negated source polynomial. reduced.original_sense records "MAX" and reduced.original_objective(sample) evaluates the original, unnegated polynomial.
QUBO to MILP
qubo_to_milp() replaces every off-diagonal binary product with one auxiliary binary variable and exact linear product constraints.
import os
from qpiai_opt.opt_modeller import qubo_to_milp
from qpiai_opt.opt_solvers import Client, solve
client = Client(api_key=os.environ["QPIAI_OPT_API_KEY"])
source_qubo = {
(0, 0): -3.0,
(0, 1): 4.0,
(1, 1): -2.0,
}
linearized = qubo_to_milp(
source_qubo,
num_variables=2,
variable_names=("x", "y"),
constant=7.0,
sense="MIN",
)
milp_result = solve(linearized.model, client=client)
source_sample = linearized.decode(milp_result.solution)
The input is a direct polynomial dictionary: store each off-diagonal interaction once, or split it equally across mirrored keys. The returned model preserves the constant and MIN/MAX direction. decode() removes product variables and returns the original binary names.
MILP to QUBO
milp_to_qubo() supports linear objectives and relations over binary, bounded integer, fixed-point, and explicitly discretized bounded continuous variables. SOS, indicators, semi domains, and mixed domains are rejected.
from qpiai_opt.opt_modeller import Binary, Model, milp_to_qubo
from qpiai_opt.opt_solvers import solve
from qpiai_opt.opt_solvers.qubo import QUBOProblem
x = Binary("x")
y = Binary("y")
source_model = Model("binary_selection")
source_model.minimize(-3 * x - 2 * y)
source_model.add_constraint(x + y <= 1, label="capacity")
compiled = milp_to_qubo(source_model, matrix_format="upper")
target_problem = QUBOProblem(
num_variables=compiled.num_variables,
qubo_dict=compiled.qubo,
matrix_format="upper",
offset=compiled.offset,
sense="MIN",
variable_names=compiled.variable_names,
)
qubo_result = solve(target_problem, client=client)
source_solution = compiled.decode(qubo_result.solution)
source_feasible = source_model.is_feasible(source_solution)
source_objective = source_model.evaluate_objective(source_solution)
compiled.qubo is always the minimizing target energy. The conversion records the source direction in compiled.original_sense; use the retained source_model or compiled.model to evaluate the decoded source objective. source_feasible must be true before using the decoded candidate.
QUBO to Ising
qubo_to_ising() applies x = (1 + s) / 2 and preserves the constant, variable count, and names.
from qpiai_opt.opt_modeller import qubo_to_ising
from qpiai_opt.opt_modeller.formats import calculate_qubo_energy
upper_qubo = {
(0, 0): -3.0,
(0, 1): 4.0,
(1, 1): -2.0,
}
ising = qubo_to_ising(
upper_qubo,
num_variables=2,
constant=7.0,
variable_names=("x", "y"),
)
binary_sample = {"x": 1, "y": 0}
spin_sample = {name: 2 * value - 1 for name, value in binary_sample.items()}
qubo_value = calculate_qubo_energy(upper_qubo, [1, 0], constant=7.0)
ising_value = ising.energy(spin_sample)
qubo_value and ising_value are equal, subject only to floating-point roundoff. A symmetric QUBO input must contain half of each direct interaction on both mirrored keys.
Ising to QUBO
ising_to_qubo() and QUBOToIsingConvert.to_qubo() apply s = 2x - 1.
round_trip = ising.to_qubo(matrix_format="upper")
round_trip_value = round_trip.energy(binary_sample)
binary_assignment = round_trip.decode(binary_sample)
source_value = round_trip.source_objective(binary_sample)
round_trip_value equals ising_value. binary_assignment uses the original Ising names. source_objective() returns the original objective direction: it equals the energy for MIN and its negation for MAX.
High-level conversion
qpiai_opt.opt_solvers.convert() provides the same supported routes when a workflow selects a representation by name.
from qpiai_opt.opt_solvers import convert
model_ir = convert(source_model, "IR")
qubo_data = convert(source_model, "QUBO", matrix_format="upper")
hubo_data = convert(source_model, "HUBO")
ising_model = convert(target_problem, "ISING")
restored_qubo = convert(ising_model, "QUBO", matrix_format="upper")
Targets are case-insensitive. Supported Model targets are "IR", "QUBO", "HUBO", "ISING", "LP", "MPS", "QUBIT_HAMILTONIAN", and "OMMX". Hamiltonian export requires the gate-based optimization integration. A QUBOProblem converts to Ising, a HUBOProblem converts to QUBO, and an IsingProblem converts to QUBO. Unsupported source/target pairs and options raise ValueError. The conversion reference lists all native-problem routes and options.
Specialized exact QUBO penalties
The QUBO compiler recognizes three slack-free binary structures:
x <= y -> x(1-y)
sum(x[i]) <= 1 -> sum(i<j, x[i]x[j])
sum(x[i]) >= n-1 -> sum(i<j, (1-x[i])(1-x[j]))
Each penalty is zero exactly on feasible binary assignments and positive on every infeasible assignment. Other inequalities use the general slack encoding.
from qpiai_opt.opt_modeller import Binary, Model
a, b, c = Binary("a"), Binary("b"), Binary("c")
penalty_model = Model("at_most_one")
penalty_model.minimize(-a - b - c)
penalty_model.add_constraint(a + b + c <= 1)
penalty_model.to_qubo_dict(matrix_format="upper")
step = penalty_model.transformations()[-1]
rewrite = step["rewrite"]
auxiliary_variables = step["auxiliary_variables"]
For this at-most-one relation, the rewrite uses pairwise products and auxiliary_variables is empty.
Decode compiled model samples
A Model compiled directly to QUBO or HUBO retains its encoder map.
source_values = source_model.decode_qubo_sample(qubo_result.solution)
objective = source_model.evaluate_objective(source_values)
violations = source_model.constraint_violations(source_values)
feasible = source_model.is_feasible(source_values)
Use feasible and violations before accepting a heuristic candidate. objective is always evaluated in the source MIN/MAX direction.