---
title: SQOA Solver by JIJ
description: Solve large combinatorial optimization problems with SQOA Solver, a quantum optimization function by JIJ for IBM Quantum hardware.
source: https://quantum.cloud.ibm.com/docs/en/functions/jij-sqoa-solver
---

# SQOA Solver by JIJ

*See the [API reference](/docs/functions/jij-sqoa-solver/api-reference).*

> **Note**
>
> Quantum Functions are a beta feature available only to IBM Quantum® Premium Plan, Flex Plan, and On-Prem (through the IBM Quantum Platform API) Plan users. Beta features are subject to change.

## Overview

With SQOA Solver by JIJ, you can solve large combinatorial optimization problems on IBM® quantum computers. The solver is based on the Sampling-based Quantum Optimization Algorithm with Quantum Relaxation (SQOA-QR) [\[1\]](#references). Unlike quantum algorithms that use a one-to-one mapping between classical decision variables and qubits, SQOA Solver can handle optimization problems with more variables than the number of qubits available on current quantum devices. Its sampling-based design also makes it less sensitive to the physical noise on current quantum hardware. Together, these features make SQOA Solver well suited to optimization problems at this stage of quantum computing.

The solver handles Quadratic Unconstrained Binary Optimization (QUBO) problems given as a classical mathematical formulation, and it automatically maps the classical formulation into the quantum setting. You can also convert constrained problems into unconstrained ones by using the penalty method.

The following sections describe the solver's workflow, benchmark results, and end-to-end examples that model problems with [qiskit-addon-opt-mapper](https://qiskit.github.io/qiskit-addon-opt-mapper/) and, optionally, Open Mathematical prograMming eXchange (OMMX). [OMMX](https://ommx-en-book.readthedocs.io/en/latest/) is an open data format and SDK that simplifies the exchange of optimization problems between software tools.

## Description

SQOA Solver implements the Sampling-based Quantum Optimization Algorithm with Quantum Relaxation (SQOA-QR) and surrounds it with classical preprocessing and postprocessing. This section explains the SQOA framework and how it differs from the Quantum Approximate Optimization Algorithm (QAOA).

The following figure shows a schematic of the SQOA Solver workflow.

![Schematic of the SQOA Solver workflow, from classical preprocessing through SQOA sampling to postprocessing and local search](https://quantum.cloud.ibm.com/docs/images/guides/jij-sqoa-solver/sqoa-workflow.svg)

### The SQOA framework

SQOA is a sampling-based framework for combinatorial optimization [\[1\]](#references). It combines two techniques: Quantum Random Access Optimization (QRAO) [\[2\]](#references), which encodes the problem in a quantum relaxation, and sample-based quantum diagonalization (SQD) [\[3\]](#references), also known as quantum-selected configuration interaction (QSCI) [\[4\]](#references), which uses the quantum computer only to sample bitstrings and finds the low-energy state on a classical computer. SQOA is a framework in the sense that it fixes this combination but leaves the details to each implementation, such as how the variables are assigned to qubits and which circuits generate the samples.

- **Quantum relaxation with QRAO.** QAOA and other Ising-based approaches represent each binary variable by the $Z$ operator of its qubit. QRAO also uses the $X$ and $Y$ operators. With a (3,1) quantum random access code (QRAC), up to three binary variables share one qubit, and each variable is represented by a different Pauli operator.

  The QUBO then becomes a relaxed Hamiltonian that needs fewer qubits than the Ising Hamiltonian. Variables that interact in the QUBO must be placed on different qubits. If they shared a qubit, the product of their Pauli operators would reduce to a single operator on that qubit (for example, $XY = iZ$), and the relaxed Hamiltonian would no longer represent their interaction.

  The assignment of variables to qubits therefore determines both the number of qubits and the quality of the relaxation, and SQOA Solver designs this assignment in its preprocessing.
- **Sample-based quantum diagonalization (SQD).** The quantum computer first prepares a quantum state that overlaps with low-energy states of the relaxed Hamiltonian and then samples bitstrings from it. A classical computer projects the relaxed Hamiltonian onto the subspace spanned by the sampled bitstrings and diagonalizes it to approximate the ground state.

  Because the relaxed Hamiltonian is non-diagonal, the diagonalization combines the sampled bitstrings into a superposition. With a diagonal Ising Hamiltonian, every bitstring is already an eigenstate, and the diagonalization would only select the best sample. The same approach has been applied to large-scale problems in quantum chemistry.

The approximate ground state obtained by SQD does not directly correspond to a classical solution of the original problem, so the classical solution must be decoded from it. Because this state is computed on a classical computer, the decoding, called rounding, is classical postprocessing. SQOA Solver uses decoding techniques designed to obtain better classical solutions heuristically.

The following table compares SQOA with QAOA, a common variational approach to the same class of problems.

|                                 | QAOA                                                                                           | SQOA                                                                                                                |
| ------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Qubits for $n$ binary variables | $n$                                                                                            | One qubit holds up to three of the variables. The exact number depends on the problem structure                     |
| Use of the QPU                  | Repeatedly estimates the energy of parameterized circuits inside a classical optimization loop | Only samples bitstrings. No circuit parameters are trained on the QPU                                               |
| From samples to a solution      | Selects the best measured bitstring                                                            | Diagonalizes the relaxed Hamiltonian in the sampled subspace on a classical computer and rounds the resulting state |

Around SQOA-QR, SQOA Solver adds classical processing. Its preprocessing fixes variables whose optimal values can be determined in advance and designs the assignment of variables to qubits. Its postprocessing refines the rounded solution with a local search, which you can turn off with `postprocessing_level=0`.

## Benchmarks

> **Disclaimer**
>
> Performance can depend on both the problem instance and subsequent processing steps. In some cases, classical samples and quantum-generated samples might achieve similar final solution quality after equivalent postprocessing. Evaluation should therefore consider the complete optimization workflow.

The following benchmark metrics provide a general indication of solution quality, qubit compression, and execution time across selected problem sizes and structures. The table lists a selection of [Maximum Independent Set instances](https://github.com/ZIB-AOPT/QOBLIB/tree/main/07-independentset) from the Quantum Optimization Benchmarking Library (QOBLIB) and two 3-regular MaxCut instances. You can load the QOBLIB instances as `ommx.v1.Instance` objects, together with their optimal solutions, by using the [ommx-quantum-benchmarks](https://jij-inc.github.io/OmmxQuantumBenchmarks/) package.

Each instance was run 10 times on an IBM Heron r2 processor. `Number of qubits` counts the qubits actually used on hardware after preprocessing and QRAC encoding. The compression ratio is instance-dependent. `Approximation ratio` (AR) reports the quality of the best solution found over the 10 runs:

$$
\mathrm{AR} = \frac{C_{\textrm{best}}}{C_{\textrm{opt}}},
$$

where $C_{\textrm{best}}$ is the best objective value found and $C_{\textrm{opt}}$ is the known optimal objective value. All instances are maximization problems with a positive optimal value, so AR is at most 100%, and 100% in the `Approximation ratio` column indicates that the solver found an optimal solution. `Total time` is the average combined QPU and CPU time per run. `QPU time` is the average IBM Quantum Compute usage per run.

| Instance           | Number of variables | Number of qubits | Approximation ratio | Total time (seconds) | QPU time (seconds) |
| ------------------ | :-----------------: | :--------------: | :-----------------: | :------------------: | :----------------: |
| maxcut\_384\_nodes |         384         |        129       |        98.5%        |         62.1         |        12.0        |
| gen200\_p0-9\_44   |         200         |        67        |         100%        |         38.0         |        12.0        |
| es60fst02          |         186         |        42        |         100%        |         24.7         |        12.0        |
| es60fst04          |         162         |        29        |         100%        |         17.9         |        12.0        |
| maxcut\_150\_nodes |         150         |        51        |         100%        |         23.1         |        12.0        |
| sloane\_1dc\_128   |         128         |        37        |        93.8%        |         19.1         |        12.0        |
| sloane\_2dc\_128   |         128         |         8        |         100%        |         12.3         |        11.5        |
| C125-9             |         125         |        43        |         100%        |         22.3         |        12.0        |
| es60fst01          |         123         |        12        |         100%        |         13.6         |        12.0        |
| es60fst03          |         113         |        26        |         100%        |         14.9         |        12.0        |
| sloane\_1dc\_64    |          64         |        16        |         100%        |         13.4         |        11.9        |
| chesapeake         |          39         |        13        |         100%        |         13.1         |        12.0        |
| football           |          35         |         6        |         100%        |         12.5         |        12.0        |
| karate             |          34         |         2        |         100%        |          6.2         |         6.0        |
| ibm32              |          32         |        11        |         100%        |         12.9         |        12.0        |
| farm               |          17         |         4        |         100%        |          8.5         |         8.0        |

## Get started

Find access information for this quantum function in the [Quantum Functions Catalog](/functions).

This section shows how to authenticate and load the function, and how to prepare a problem in the dictionary format that SQOA Solver accepts by using `OptimizationProblem` from [qiskit-addon-opt-mapper](https://qiskit.github.io/qiskit-addon-opt-mapper/). The examples that follow run the function and read the result.

### Authenticate and load the function

First, authenticate with your [IBM Quantum API key](/). (The following code assumes that you already [saved your account](/docs/ibm-quantum-platform/functions-get-started#install-qiskit-functions-catalog-client) to your local environment.) Then, create a catalog client as follows:

```python
from qiskit_ibm_catalog import FunctionsCatalog

catalog = FunctionsCatalog(channel="ibm_quantum_platform")

# verify that you have access to the function
catalog.list()
```

Output:

```
[QiskitFunction(jij/sqoa-solver)]
```

```python
solver = catalog.load("jij/sqoa-solver")
```

### Prepare the input with `qiskit-addon-opt-mapper`

This guide models problems with the `OptimizationProblem` class of [qiskit-addon-opt-mapper](https://qiskit.github.io/qiskit-addon-opt-mapper/). SQOA Solver accepts a QUBO problem as a coefficient dictionary in the tuple-string format used by IBM optimization functions, so every example in this guide follows the same client-side preparation pattern:

1. **Write the model.** Create an `OptimizationProblem`, add binary decision variables, and set the objective and any constraints.
2. **Convert to QUBO.** Convert the problem with `OptimizationProblemToQubo`, which adds any constraints to the objective as penalty terms and returns an equivalent unconstrained minimization problem.
3. **Convert to a dictionary.** Pass the QUBO to the `to_problem_dict()` helper function defined in the next section, and pass the result to `solver.run()` as `problem`, together with `variable_type="binary"`.

SQOA Solver always minimizes a dictionary problem, which matches the minimization problem that `OptimizationProblemToQubo` returns. For a maximization problem, the returned `objective_value` is therefore the negative of the original objective. To read the result in terms of the original problem, evaluate the returned assignment with `OptimizationProblem`, as the examples show. For the dictionary format details, see [Accepted problem formats](/docs/functions/jij-sqoa-solver/api-reference#accepted-problem-formats) in the API reference.

```python
# %pip install qiskit-addon-opt-mapper
```

Define the following helper function once and reuse it in every example. It converts the QUBO returned by `OptimizationProblemToQubo` into the dictionary format. The keys use the variable indices of the QUBO, so the solution keys `x_0`, `x_1`, and so on in the result correspond to those indices. Every variable receives a linear entry, even with a zero coefficient, so that the returned solution covers all variables.

```python
from qiskit_addon_opt_mapper import OptimizationProblem
from qiskit_addon_opt_mapper.converters import OptimizationProblemToQubo


def to_problem_dict(qubo: OptimizationProblem) -> dict[str, float]:
    """Convert a QUBO from OptimizationProblemToQubo to the SQOA Solver dictionary format."""
    objective = qubo.objective
    payload = {"()": objective.constant}
    # Keep every variable in the problem, even with a zero linear coefficient.
    payload.update({f"({i},)": 0.0 for i in range(qubo.get_num_vars())})
    for i, coefficient in objective.linear.to_dict().items():
        payload[f"({i},)"] += coefficient
    for (i, j), coefficient in objective.quadratic.to_dict().items():
        # Fold a binary diagonal term x_i * x_i = x_i into the linear term.
        key = f"({i},)" if i == j else f"({i}, {j})"
        payload[key] = payload.get(key, 0.0) + coefficient
    return {key: float(value) for key, value in payload.items()}
```

## Function sizes

Every job runs at one size: `"S"`, `"M"`, or `"L"`. If the size is too small, the job is rejected. If it is larger than needed, the job runs on a bigger compute profile than the work requires. See [Function sizes](/docs/managed-workflows/managed-workflow-compute-profiles#best-practice-match-sizes-to-problem-sizes) for how sizes work in general.

The size comes from two properties of the QUBO that you submit: how many variables it has, and how densely they interact, that is, the fraction of all variable pairs that appear together in a quadratic term with a nonzero coefficient.

| Size | Smallest size for variables and interaction density | Compute profile | vCPU | RAM    |
| ---- | --------------------------------------------------- | --------------- | ---- | ------ |
| `S`  | At most 50 variables and density at most 0.1        | `8x32`          | 8    | 32 GB  |
| `M`  | At most 100 variables and density at most 0.25      | `16x128`        | 16   | 128 GB |
| `L`  | Either above the `M` bound                          | `48x768`        | 48   | 768 GB |

A problem must fit **both** bounds of a size, which is why a small but tightly coupled problem can be `L`: ten variables all coupled to each other is a density of 1.0. These are the same bounds for every optimization function, so a size means the same difficulty of problem whichever one you call.

Converting a constrained problem to a QUBO raises the density more than it might appear. `OptimizationProblemToQubo` replaces each constraint with a penalty term, and squaring the constraint couples every pair of variables in it, so a one-hot constraint over four variables contributes all six pairs.

The default size, used when a job does not request one, is `"M"`. You can also read the mapping from the function itself with `solver.sizes_map`, and the default size with `solver.default_size`.

### Calculate the size before submitting

Run the following calculator on your own problem. It reads the problem and nothing else: no job is submitted and nothing is reserved, so you can call it as often as you like while you reduce a problem to a smaller size. It performs the same calculation that the function runs when a job arrives, so a job submitted at the calculated size is not rejected as too small.

For a coefficient dictionary, pass `calculate_function_size()` a dictionary with the same `problem` and `variable_type` that you pass to `solver.run()`. For an OMMX instance, pass the `ommx.v1.Instance` itself, not the Base64 string. The calculator uses OMMX in both cases, so install the `ommx` package first:

```python
# %pip install ommx
```

```python
# Calculate the function size a problem needs, before submitting it.
# calculate_function_size(problem) returns "S", "M", or "L" and submits
# nothing. The size comes from the number of variables and how densely
# they interact.

from collections.abc import Iterator
from itertools import combinations
from typing import Any, Literal

from qiskit_addon_opt_mapper.problems import OptimizationProblem

Size = Literal["S", "M", "L"]
_ORDER = {"S": 0, "M": 1, "L": 2}

# Limits are provided by IBM
_SMALL_NUMVARS, _SMALL_DENSITY = 50, 0.1
_MEDIUM_NUMVARS, _MEDIUM_DENSITY = 100, 0.25


def calculate_function_size(problem: Any, **kwargs: Any) -> Size:
    """Return the size ("S", "M" or "L") needed to run ``problem``."""
    metrics = _measure(problem, **kwargs)
    if _is_small(metrics):
        return "S"
    if _is_medium(metrics):
        return "M"
    return "L"


def to_optimization_problem(problem: Any) -> OptimizationProblem:
    """Convert SQOA input into an ``OptimizationProblem``.

    Accept an OMMX ``Instance`` directly, or a dictionary containing a coefficient
    dictionary as ``problem`` and ``"binary"`` or ``"spin"`` as ``variable_type``.
    Convert to a binary minimization problem with consecutive variable IDs from zero.
    Leave the input instance unchanged.
    Return an existing ``OptimizationProblem`` unchanged.
    """
    if isinstance(problem, OptimizationProblem):
        return problem
    import ast
    import math
    import re

    from ommx.v1 import DecisionVariable, Instance, Kind

    _TERM_ID = r"(?:0|[1-9][0-9]*)"
    _TERM_SPACE = r"[ \t\r\n]*"
    _TERM_KEY_PATTERN = (
        rf"^\({_TERM_SPACE}(?:{_TERM_ID}{_TERM_SPACE},"
        rf"{_TERM_SPACE}(?:{_TERM_ID}{_TERM_SPACE}(?:,{_TERM_SPACE})?)?)?\)(?![\s\S])"
    )

    def _term_ids(key: str) -> tuple[int, ...]:
        if (
            not isinstance(key, str)
            or re.fullmatch(_TERM_KEY_PATTERN, key) is None
        ):
            raise ValueError("invalid term key")
        ids = ast.literal_eval(key)
        if len(set(ids)) != len(ids):
            raise ValueError("repeated variable ID")
        return tuple(sorted(ids))

    def _dictionary_to_instance(
        problem: dict[str, object], variable_type: str
    ) -> Instance:
        terms: dict[tuple[int, ...], float] = {}
        variable_ids: set[int] = set()
        for key, value in problem.items():
            ids = _term_ids(key)
            if type(value) not in (int, float):
                raise ValueError("invalid coefficient")
            coefficient = float(value)
            if not math.isfinite(coefficient):
                raise ValueError("invalid coefficient")
            variable_ids.update(ids)
            terms[ids] = terms.get(ids, 0.0) + coefficient
            if not math.isfinite(terms[ids]):
                raise ValueError("coefficient overflow")
        variables = {
            i: DecisionVariable.binary(i) for i in sorted(variable_ids)
        }
        expressions = {
            i: 1 - 2 * variable if variable_type == "spin" else variable
            for i, variable in variables.items()
        }
        objective = 0.0
        for ids, coefficient in terms.items():
            term = coefficient
            for i in ids:
                term = term * expressions[i]
            objective = objective + term
        return Instance.from_components(
            decision_variables=list(variables.values()),
            objective=objective,
            constraints=[],
            sense=Instance.MINIMIZE,
        )

    if isinstance(problem, Instance):
        instance = problem
    else:
        if (
            not isinstance(problem, dict)
            or not {"problem", "variable_type"} <= problem.keys()
        ):
            raise ValueError(
                "SQOA sizing requires an OMMX Instance or a coefficient input envelope "
                "with 'problem' and 'variable_type'"
            )
        variable_type = problem["variable_type"]
        if variable_type not in ("binary", "spin"):
            raise ValueError("variable_type must be binary or spin")
        if not isinstance(problem["problem"], dict):
            raise ValueError(
                "problem must be a coefficient dictionary; pass OMMX Instances directly"
            )
        instance = _dictionary_to_instance(problem["problem"], variable_type)

    variables = list(instance.decision_variables)
    degree = instance.objective.degree()
    if not variables:
        raise ValueError(
            "problem must contain at least one decision variable"
        )
    if any(variable.kind == Kind.Continuous for variable in variables):
        raise ValueError(
            "problem must not contain Continuous decision variables"
        )
    if degree > 2:
        raise ValueError("problem objective degree must be at most two")
    # Copy the instance so minimization does not modify the caller's original.
    normalized = Instance.from_bytes(instance.to_bytes())
    normalized.as_minimization_problem()
    qubo, offset = normalized.as_qubo_format()
    if not math.isfinite(offset) or any(
        not math.isfinite(v) for v in qubo.values()
    ):
        raise ValueError("problem objective coefficients must all be finite")

    ids = [variable.id for variable in variables]
    dense_ids = {
        variable_id: index for index, variable_id in enumerate(sorted(ids))
    }
    linear = {}
    quadratic = {}
    for (first, second), coefficient in qubo.items():
        i, j = dense_ids[first], dense_ids[second]
        if i == j:
            linear[i] = linear.get(i, 0.0) + coefficient
        else:
            pair = (min(i, j), max(i, j))
            quadratic[pair] = quadratic.get(pair, 0.0) + coefficient

    model = OptimizationProblem()
    model.binary_var_list(len(ids), name="x")
    model.minimize(
        constant=offset,
        linear=linear,
        quadratic=quadratic,
    )
    return model


def _measure(problem: Any, **kwargs: Any) -> dict[str, float]:
    """Return the number of variables and the interaction density."""
    model = to_optimization_problem(problem)
    num_vars = model.get_num_vars()

    pairs = set()
    for term in _interactions(model):
        pairs.update(frozenset(pair) for pair in combinations(set(term), 2))

    max_pairs = num_vars * (num_vars - 1) // 2
    return {
        "num_vars": num_vars,
        "density": len(pairs) / max_pairs if max_pairs else 0.0,
    }


def _interactions(problem: OptimizationProblem) -> Iterator[tuple[int, ...]]:
    """Yield the variables of every term that couples two or more of them.

    A constraint contributes every variable in it, because on hardware it becomes
    a penalty whose square couples them all.
    """
    objective = problem.objective
    yield from objective.quadratic.to_dict()
    for expression in objective.higher_order.values():
        yield from expression.to_dict()

    for constraint in problem.linear_constraints:
        yield _constraint_support(constraint)
    for constraint in problem.quadratic_constraints:
        yield _constraint_support(constraint)
    for constraint in problem.higher_order_constraints:
        yield _constraint_support(constraint)


def _constraint_support(constraint: Any) -> tuple[int, ...]:
    """Return every variable appearing in a constraint's left-hand side."""
    variables = set(constraint.linear.to_dict())
    if hasattr(constraint, "quadratic"):
        for term in constraint.quadratic.to_dict():
            variables.update(term)
    if hasattr(constraint, "higher_order"):
        for expression in constraint.higher_order.values():
            for term in expression.to_dict():
                variables.update(term)
    return tuple(variables)


def _is_small(metrics: dict[str, float]) -> bool:
    """Determine if the metrics fit T-size 'S'."""
    return (
        metrics["num_vars"] <= _SMALL_NUMVARS
        and metrics["density"] <= _SMALL_DENSITY
    )


def _is_medium(metrics: dict[str, float]) -> bool:
    """Determine if the metrics fit T-size 'M'."""
    return (
        metrics["num_vars"] <= _MEDIUM_NUMVARS
        and metrics["density"] <= _MEDIUM_DENSITY
    )
```

If the size is too small, the job fails with error `4600`, and the message names the size to use instead. If it is larger than needed, the job runs normally and records a `4601` advisory event. See [Function sizes](/docs/managed-workflows/managed-workflow-compute-profiles#best-practice-match-sizes-to-problem-sizes) to learn more. The examples that follow calculate the size before every submission.

## Example 1: MaxCut on a 3-regular graph

Many graph problems, such as MaxCut, are NP-hard and are natural candidates for quantum optimization. Install `networkx` as an additional dependency to run this example:

```python
# %pip install networkx
```

### 1. Write the model

Assign a binary variable $x_v \in \{0, 1\}$ to each vertex of a graph $G = (V, E)$ to indicate its side of the cut. An edge is cut when its endpoints are on different sides, so the cut size is:

$$
\mathrm{Cut}(x) = \sum_{(u,v) \in E} \left(x_u + x_v - 2 x_u x_v\right).
$$

This objective is already a QUBO without constraints, so `OptimizationProblemToQubo` only turns the maximization into the equivalent minimization and keeps the variables unchanged. The `Maxcut` application class in `qiskit_addon_opt_mapper.applications` builds the same model with `Maxcut(G).to_optimization_problem()`.

```python
import networkx as nx

G = nx.random_regular_graph(3, 40, seed=42)

linear = {v: 0 for v in G.nodes}
quadratic = {}
for u, v in G.edges:
    linear[u] += 1
    linear[v] += 1
    quadratic[(u, v)] = -2

maxcut = OptimizationProblem("MaxCut")
maxcut.binary_var_list(G.number_of_nodes(), name="x")
maxcut.maximize(linear=linear, quadratic=quadratic)
```

### 2. Run the solver

Convert the model to a QUBO and then to a dictionary, and submit it. `problem` and `variable_type` are required. `backend_name` is optional. When it is omitted or `None`, the function selects the least busy operational QPU available to your instance.

The `options` dictionary accepts `shots`, `seed`, `session`, `postprocessing_level`, and `job_tags`. For descriptions and defaults, see the [API reference](/docs/functions/jij-sqoa-solver/api-reference).

The `function_size` argument selects the computing resources for the job. Pass it directly to `solver.run()`, outside `options`. Calculate it with `calculate_function_size()` from [Function sizes](#function-sizes). The graph has 40 nodes and 60 edges, so the QUBO has 40 variables and a density of about 0.08, which fits `"S"`.

```python
qubo = OptimizationProblemToQubo().convert(maxcut)
problem = to_problem_dict(qubo)

size = calculate_function_size(
    {"problem": problem, "variable_type": "binary"}
)
print(f"Function size: {size}")
```

Output:

```
Function size: S
```

```python
job = solver.run(
    problem=problem,
    variable_type="binary",
    backend_name="ibm_fez",
    options={
        "shots": 4096,
        "seed": 42,
    },
    function_size=size,
)
```

### 3. Retrieve the result

`job.result()` returns a dictionary with four keys:

- `solution` is the complete variable assignment, keyed by `x_i`, where `i` is the variable index (for example, `x_3`).
- `objective_value` and `feasible` are the objective and feasibility of that assignment, evaluated on the submitted dictionary.
- `metadata` holds `solver_info` and `resource_usage`.

Because the QUBO is the minimization form of MaxCut, `objective_value` is the negative of the cut size. Evaluate the assignment on the original `OptimizationProblem` to read the cut size directly.

```python
result = job.result()

x = [result["solution"][f"x_{i}"] for i in range(maxcut.get_num_vars())]
print(f"Cut value: {maxcut.objective.evaluate(x)}")
print(f"Nodes on side 1: {[v for v, value in enumerate(x) if value > 0.5]}")
```

Output:

```
Cut value: 55.0
Nodes on side 1: [5, 7, 9, 11, 14, 16, 17, 19, 21, 23, 24, 26, 30, 31, 33, 34, 36, 37, 38]
```

### 4. Check the resource usage

`result["metadata"]["resource_usage"]` reports the time spent in each execution phase. Phases other than `RUNNING: EXECUTING_QPU` report `CPU_TIME`, the wall-clock time of the phase in seconds, when it is available.

The `RUNNING: EXECUTING_QPU` phase reports `QPU_TIME`, the QPU usage of all Quantum Compute jobs of the run, if the final usage is available for every job. For details, see the [API reference](/docs/functions/jij-sqoa-solver/api-reference).

```python
for phase, metrics in result["metadata"]["resource_usage"].items():
    line = f"{phase:36s}"
    if "CPU_TIME" in metrics:
        line += f" CPU {metrics['CPU_TIME']:8.1f} s"
    if "QPU_TIME" in metrics:
        line += f" QPU {metrics['QPU_TIME']:8.1f} s"
    print(line)
```

Output:

```
RUNNING: MAPPING                     CPU      3.2 s
RUNNING: OPTIMIZING_FOR_HARDWARE     CPU      5.0 s
RUNNING: WAITING_FOR_QPU             CPU      1.1 s
RUNNING: EXECUTING_QPU               QPU      3.0 s
RUNNING: POST_PROCESSING             CPU      0.9 s
```

## Example 2: Convert a constrained problem to QUBO

SQOA Solver accepts unconstrained problems. This example shows how to convert a constrained problem into a QUBO with `OptimizationProblemToQubo` and map the result back to the variables of the original problem.

```python
selection = OptimizationProblem("ConstrainedExample")
selection.binary_var_list(2, name="x")
selection.maximize(linear={"x0": 1, "x1": 1})
selection.linear_constraint({"x0": 1, "x1": 1}, "<=", 1, name="at_most_one")

print(selection.prettyprint())
```

Output:

```
Problem name: ConstrainedExample

Maximize
  x0 + x1

Subject to
  Linear constraints (1)
    x0 + x1 <= 1  'at_most_one'

  Binary variables (2)
    x0 x1

```

`OptimizationProblemToQubo` adds the constraints to the objective as penalty terms and converts the problem to minimization. The appropriate penalty weight is problem-dependent.

After the run, `interpret()` maps the QUBO assignment back to the variables of the original problem.

Evaluate the objective and the feasibility on the original problem. For a dictionary problem, the `feasible` field of the result refers only to the variable domain, because the dictionary has no constraints.

The two variables of this QUBO interact, so its density is 1, and `calculate_function_size()` returns `"L"`.

```python
converter = OptimizationProblemToQubo(penalty=4.0)
qubo = converter.convert(selection)
problem = to_problem_dict(qubo)
size = calculate_function_size(
    {"problem": problem, "variable_type": "binary"}
)

job = solver.run(
    problem=problem,
    variable_type="binary",
    backend_name="ibm_fez",
    function_size=size,
)
result = job.result()

x_qubo = [result["solution"][f"x_{i}"] for i in range(qubo.get_num_vars())]
x = converter.interpret(x_qubo)
print("Objective:", selection.objective.evaluate(x))
print("Feasible:", selection.is_feasible(x))
print("x values:", x)
```

Output:

```
Objective: 1.0
Feasible: True
x values: [1. 0.]
```

## Example 3: Submit an OMMX instance

SQOA Solver also accepts an `ommx.v1.Instance` from the [OMMX](https://ommx-en-book.readthedocs.io/en/latest/) SDK, which keeps variable names, subscripts, and conversion metadata together with the problem. Compared with a dictionary, an OMMX input has the following advantages:

- The result also contains the canonical `ommx.v1.Solution` in `solver_info["ommx_solution"]`.
- `objective_value` is evaluated in the sense of the instance, so the objective of a maximization problem is not negated.
- For an instance converted in place with [`Instance.to_qubo()`](https://jij-inc.github.io/ommx/python/ommx/autoapi/ommx/v1/index.html#ommx.v1.Instance.to_qubo), `feasible` is evaluated against the original constraints.

This example builds the MaxCut model from Example 1 with [JijModeling](https://jij-inc-jijmodeling-tutorials-en.readthedocs-hosted.com/en/latest/introduction.html), a mathematical optimization modeler that compiles models into OMMX instances. Install JijModeling, which also installs the `ommx` package:

```python
# %pip install "jijmodeling~=2.8.0"
```

SQOA Solver exchanges OMMX data as standard padded Base64 strings. Define the following helper functions for this conversion:

- `to_problem_payload()` serializes an `ommx.v1.Instance` into the string that you pass as `problem`.
- `to_ommx_solution()` decodes the canonical `ommx.v1.Solution` from the result.

```python
import base64

from ommx.v1 import Instance, Solution


def to_problem_payload(instance: Instance) -> str:
    """Serialize an OMMX instance into the `problem` string for SQOA Solver."""
    return base64.b64encode(instance.to_bytes()).decode()


def to_ommx_solution(result: dict) -> Solution:
    """Decode the canonical OMMX solution from a SQOA Solver result."""
    payload = result["metadata"]["solver_info"]["ommx_solution"]
    return Solution.from_bytes(base64.b64decode(payload, validate=True))
```

### 1. Write the model with JijModeling

The usual spin formulation of MaxCut on a graph $G = (V, E)$ assigns
$s_v \in \{-1, +1\}$ to each vertex. An edge is cut when $s_u s_v = -1$, so the cut size is:

$$
\operatorname{Cut}(s) = \sum_{(u,v) \in E} \frac{1 - s_u s_v}{2}.
$$

JijModeling does not provide a native spin decision variable, and SQOA Solver accepts binary QUBO
variables. Therefore, declare $b_v \in \{0, 1\}$ and define the spin-valued expression
$s_v = 1 - 2b_v$ inside the model. With this expression, the objective stays in the standard spin form, and the
compiled instance remains a binary QUBO.

```python
import jijmodeling as jm


@jm.Problem.define("MaxCut", sense=jm.ProblemSense.MAXIMIZE)
def maxcut_model(problem: jm.DecoratedProblem):
    V = problem.Natural()  # number of nodes
    E = problem.Graph(dtype=V)  # edge list
    b = problem.BinaryVar(shape=(V,))
    s = 1 - 2 * b  # spin-valued expression

    problem += jm.sum((1 - s[u] * s[v]) / 2 for u, v in E)
```

If you display `maxcut_model` in a notebook, it shows the following formulation:

$$
\begin{array}{rl}
\text{Problem}\colon &\text{MaxCut}\\
\displaystyle \max &\displaystyle \sum _{\left\langle u,v\right\rangle \in E}{\frac{1-{\left(1-2\cdot b\right)}_{u}\cdot {\left(1-2\cdot b\right)}_{v}}{2}}\\
&\\
\text{where}&\\
&\text{Decision Variables:}\\
&\qquad \begin{alignedat}{2}{b}_{i}&\in \left\{0,1\right\}&\qquad &\text{a }1\text{-dim array of }\text{binary}\text{ decision variables}\\
&\forall i\in \left\{0,\ldots ,V-1\right\}&&\\
\end{alignedat}\\
&\\
&\text{Placeholders:}\\
&\qquad \begin{alignedat}{2}{E}_{i}&\in V\times V&\qquad &\text{a }1\text{-dim array of placeholders with elements in }V\times V\\
&\forall i\in \left\{0,\ldots ,\mathop{\mathtt{len\_{}at}}\left(E,0\right)-1\right\}&&\\
&&&\\
V&\in \mathbb{N}&\qquad &\text{a scalar placeholder in }\mathbb{N}\\
\end{alignedat}\end{array}
$$

### 2. Compile the instance and run the solver

Bind the graph from Example 1 to compile an `ommx.v1.Instance`, and submit it with `variable_type="binary"`. An OMMX instance is always submitted as a binary problem. Pass the instance itself to `calculate_function_size()`, before you encode it as Base64. The instance has the same variables and interactions as the QUBO in Example 1, so it also fits `"S"`.

```python
instance = maxcut_model.eval(
    {"V": G.number_of_nodes(), "E": [list(edge) for edge in G.edges]}
)
size = calculate_function_size(instance)

job = solver.run(
    problem=to_problem_payload(instance),
    variable_type="binary",
    backend_name="ibm_fez",
    function_size=size,
)
```

### 3. Retrieve the result

For an OMMX input, `objective_value` is the cut size, and `to_ommx_solution()` decodes the canonical solution so that you can read the assignment by variable name.

```python
result = job.result()
print(f"Cut value: {result['objective_value']}")

solution = to_ommx_solution(result)
b_values = solution.extract_decision_variables("b")
print(
    f"Nodes on side 1: {sorted(v for (v,), value in b_values.items() if value > 0.5)}"
)
```

Output:

```
Cut value: 55.0
Nodes on side 1: [5, 7, 9, 11, 14, 16, 17, 19, 21, 23, 24, 26, 30, 31, 33, 34, 36, 37, 38]
```

## Get support

For support, email [info@j-ij.com](mailto:info@j-ij.com).

## Next steps

> **Recommendations**
>
> - Request a free trial of SQOA Solver by completing this [form](https://www.j-ij.com/en/qiskit-function-access).
> - Visit the [API reference](/docs/functions/jij-sqoa-solver/api-reference) for this quantum function.
> - Try an end-to-end example in the [maximum independent set problem](/docs/tutorials/maximum-independent-set-jij-sqoa) tutorial.
> - Learn how to model optimization problems with [qiskit-addon-opt-mapper](https://qiskit.github.io/qiskit-addon-opt-mapper/).
> - Learn how to model and exchange optimization problems with [JijModeling](https://jij-inc-jijmodeling-tutorials-en.readthedocs-hosted.com/en/latest/introduction.html) and [OMMX](https://ommx-en-book.readthedocs.io/en/latest/).
> - Read the SQOA-QR paper [\[1\]](#references) to learn more about the SQOA framework.

## References

\[1] Matsuyama, H., & Yamashiro, Y. (2025). Sampling-based quantum optimization algorithm with quantum relaxation. 2025 IEEE International Conference on Quantum Computing and Engineering (QCE), 1, 2323–2333. [DOI](https://doi.org/10.1109/QCE65121.2025.00253), [arXiv:2504.12629](https://arxiv.org/abs/2504.12629)

\[2] Fuller, B., Hadfield, C., Glick, J. R., Imamichi, T., Itoko, T., Thompson, R. J., Jiao, Y., Kagele, M. M., Blom-Schieber, A. W., Raymond, R., et al. (2024). Approximate solutions of combinatorial problems via quantum relaxations. IEEE Transactions on Quantum Engineering, 5, 1–15. [DOI](https://doi.org/10.1109/TQE.2024.3421294), [arXiv:2111.03167](https://arxiv.org/abs/2111.03167)

\[3] Robledo-Moreno, J., Motta, M., Haas, H., Javadi-Abhari, A., Jurcevic, P., Kirby, W., et al. (2025). Chemistry beyond the scale of exact diagonalization on a quantum-centric supercomputer. Science Advances, 11(25), eadu9991. [DOI](https://doi.org/10.1126/sciadv.adu9991), [arXiv:2405.05068](https://arxiv.org/abs/2405.05068)

\[4] Kanno, K., Kohda, M., Imai, R., Koh, S., Mitarai, K., Mizukami, W., & Nakagawa, Y. O. (2026). Quantum-selected configuration interaction: Classical diagonalization of Hamiltonians in subspaces selected by quantum computers. Physical Review Research, 8(2), 023268. [DOI](https://doi.org/10.1103/dmn4-snfx), [arXiv:2302.11320](https://arxiv.org/abs/2302.11320)


## Contents

- 
  - [Documentation home](/docs/functions/jij-sqoa-solver)
- Tutorials
  - [Solve a maximum independent set problem using JIJ SQOA Solver](/docs/tutorials/maximum-independent-set-jij-sqoa)
- API reference
  - [Python API reference](/docs/functions/jij-sqoa-solver/api-reference)
