Skip to content

Commit

Permalink
Revise OpenJij-Adapter APIs (#252)
Browse files Browse the repository at this point in the history
- Split out `response_to_samples` for better re-usability
- Only allow minimization problem as the input of `as_qubo_format`
  - We regards QUBO is always minimization problem
  • Loading branch information
termoshtt authored Dec 24, 2024
1 parent 47d2894 commit 6448ad7
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 16 deletions.
82 changes: 66 additions & 16 deletions python/ommx-openjij-adapter/ommx_openjij_adapter/__init__.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
from __future__ import annotations

from ommx.v1 import Instance, State, Samples
import openjij as oj


def sample_qubo_sa(instance: Instance, *, num_reads: int = 1) -> Samples:
def response_to_samples(response: oj.Response) -> Samples:
"""
Sampling QUBO with Simulated Annealing (SA) by [`openjij.SASampler`](https://openjij.github.io/OpenJij/reference/openjij/index.html#openjij.SASampler)
Note that input `instance` must be a QUBO (Quadratic Unconstrained Binary Optimization) problem, i.e.
- Every decision variables are binary
- No constraint
- Objective function is quadratic
You can convert a problem to QUBO via [`ommx.v1.Instance.penalty_method`](https://jij-inc.github.io/ommx/python/ommx/autoapi/ommx/v1/index.html#ommx.v1.Instance.penalty_method) or other corresponding method.
Convert OpenJij's `Response` to `ommx.v1.Samples`
"""
q, c = instance.as_qubo_format()
if instance.sense == Instance.MAXIMIZE:
q = {key: -val for key, val in q.items()}
sampler = oj.SASampler()
response = sampler.sample_qubo(q, num_reads=num_reads) # type: ignore

# Filling into ommx.v1.Samples
# Since OpenJij does not issue the sample ID, we need to generate it in the responsibility of this OMMX Adapter
sample_id = 0
entries = []

num_reads = len(response.record.num_occurrences)
for i in range(num_reads):
sample = response.record.sample[i]
state = State(entries=zip(response.variables, sample)) # type: ignore
Expand All @@ -35,3 +25,63 @@ def sample_qubo_sa(instance: Instance, *, num_reads: int = 1) -> Samples:
sample_id += 1
entries.append(Samples.SamplesEntry(state=state, ids=ids))
return Samples(entries=entries)


def sample_qubo_sa(
instance: Instance,
*,
beta_min: float | None = None,
beta_max: float | None = None,
num_sweeps: int | None = None,
num_reads: int | None = None,
schedule: list | None = None,
initial_state: list | dict | None = None,
updater: str | None = None,
sparse: bool | None = None,
reinitialize_state: bool | None = None,
seed: int | None = None,
) -> Samples:
"""
Sampling QUBO with Simulated Annealing (SA) by [`openjij.SASampler`](https://openjij.github.io/OpenJij/reference/openjij/index.html#openjij.SASampler)
The input `instance` must be a QUBO (Quadratic Unconstrained Binary Optimization) problem, i.e.
- Every decision variables are binary
- No constraint
- Objective function is quadratic
- Minimization problem
You can convert a problem to QUBO via [`ommx.v1.Instance.penalty_method`](https://jij-inc.github.io/ommx/python/ommx/autoapi/ommx/v1/index.html#ommx.v1.Instance.penalty_method) or other corresponding method.
:param instance: ommx.v1.Instance representing a QUBO problem
:param beta_min: minimal value of inverse temperature
:param beta_max: maximum value of inverse temperature
:param num_sweeps: number of sweeps
:param num_reads: number of reads
:param schedule: list of inverse temperature
:param initial_state: initial state
:param updater: updater algorithm
:param sparse: use sparse matrix or not.
:param reinitialize_state: if true reinitialize state for each run
:param seed: seed for Monte Carlo algorithm
Note that this is a simple wrapper function for `openjij.SASampler.sample_qubo` method.
For more advanced usage, you can use `ommx.v1.Instance.as_qubo_format` to get QUBO matrix,
and use OpenJij manually, and convert the `openjij.Response` via `response_to_samples` function.
"""
q, _offset = instance.as_qubo_format()
sampler = oj.SASampler()
response = sampler.sample_qubo(
q, # type: ignore
beta_min=beta_min,
beta_max=beta_max,
num_sweeps=num_sweeps,
num_reads=num_reads,
schedule=schedule,
initial_state=initial_state,
updater=updater,
sparse=sparse,
reinitialize_state=reinitialize_state,
seed=seed,
)
return response_to_samples(response)
1 change: 1 addition & 0 deletions python/ommx-openjij-adapter/tests/test_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def test_maximize():
constraints=[],
sense=Instance.MAXIMIZE,
)
instance.as_minimization_problem()
samples = adapter.sample_qubo_sa(instance, num_reads=1)
sample_set = instance.evaluate_samples(samples)

Expand Down
39 changes: 39 additions & 0 deletions python/ommx/ommx/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,45 @@ def partial_evaluate(self, state: State | Mapping[int, float]) -> Instance:
)
return Instance.from_bytes(out)

def as_minimization_problem(self):
"""
Convert the instance to a minimization problem.
If the instance is already a minimization problem, this does nothing.
Examples
=========
.. doctest::
>>> from ommx.v1 import Instance, DecisionVariable
>>> x = [DecisionVariable.binary(i) for i in range(3)]
>>> instance = Instance.from_components(
... decision_variables=x,
... objective=sum(x),
... constraints=[sum(x) == 1],
... sense=Instance.MAXIMIZE,
... )
>>> instance.sense == Instance.MAXIMIZE
True
>>> instance.objective
Function(x0 + x1 + x2)
Convert to a minimization problem
>>> instance.as_minimization_problem()
>>> instance.sense == Instance.MINIMIZE
True
>>> instance.objective
Function(-x0 - x1 - x2)
"""
if self.raw.sense == Instance.MINIMIZE:
return
self.raw.sense = Instance.MINIMIZE
obj = -self.objective
self.raw.objective.CopyFrom(obj.raw)

def as_qubo_format(self) -> tuple[dict[tuple[int, int], float], float]:
"""
Convert unconstrained quadratic instance to PyQUBO-style format.
Expand Down
23 changes: 23 additions & 0 deletions rust/ommx/src/convert/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ impl Instance {
if !self.constraints.is_empty() {
bail!("The instance still has constraints. Use penalty method or other way to translate into unconstrained problem first.");
}
if self.sense() == Sense::Maximize {
bail!("PUBO format is only for minimization problems.");
}
if !self
.objective()
.used_decision_variable_ids()
Expand All @@ -283,6 +286,17 @@ impl Instance {
Ok(out)
}

/// Convert the instance into a minimization problem.
///
/// This is based on the fact that maximization problem with negative objective function is equivalent to minimization problem.
pub fn as_minimization_problem(&mut self) {
if self.sense() == Sense::Minimize {
return;
}
self.sense = Sense::Minimize as i32;
self.objective = Some(-self.objective().into_owned());
}

/// Create QUBO (Quadratic Unconstrained Binary Optimization) dictionary from the instance.
///
/// Before calling this method, you should check that this instance is suitable for QUBO:
Expand All @@ -294,6 +308,9 @@ impl Instance {
/// - The degree of the objective is at most 2.
///
pub fn as_qubo_format(&self) -> Result<(BTreeMap<BinaryIdPair, f64>, f64)> {
if self.sense() == Sense::Maximize {
bail!("QUBO format is only for minimization problems.");
}
if !self.constraints.is_empty() {
bail!("The instance still has constraints. Use penalty method or other way to translate into unconstrained problem first.");
}
Expand Down Expand Up @@ -582,6 +599,9 @@ mod tests {

#[test]
fn test_pubo(instance in Instance::arbitrary_binary_unconstrained()) {
if instance.sense() == Sense::Maximize {
return Ok(());
}
let pubo = instance.as_pubo_format().unwrap();
for (_, c) in pubo {
prop_assert!(c.abs() > f64::EPSILON);
Expand All @@ -590,6 +610,9 @@ mod tests {

#[test]
fn test_qubo(instance in Instance::arbitrary_quadratic_binary_unconstrained()) {
if instance.sense() == Sense::Maximize {
return Ok(());
}
let (quad, _) = instance.as_qubo_format().unwrap();
for (ids, c) in quad {
prop_assert!(ids.0 <= ids.1);
Expand Down

0 comments on commit 6448ad7

Please # to comment.