Skip to content

Commit

Permalink
Merge pull request #1163 from arcondello/feature/speed-up-CQM.set_obj…
Browse files Browse the repository at this point in the history
…ective

Improve the performance of QM.update()
  • Loading branch information
arcondello authored Apr 12, 2022
2 parents 7b82011 + 0a12735 commit a973854
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 21 deletions.
25 changes: 25 additions & 0 deletions benchmarks/cqm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2022 D-Wave Systems Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import numpy as np

import dimod


class TimeSetObjective:
def setUp(self):
self.qm = dimod.QM.from_bqm(dimod.BQM(np.ones((500, 500)), 'SPIN'))

def time_qm(self):
dimod.CQM().set_objective(self.qm)
18 changes: 6 additions & 12 deletions dimod/constrained.py
Original file line number Diff line number Diff line change
Expand Up @@ -1488,19 +1488,13 @@ def set_objective(self, objective: Union[BinaryQuadraticModel,
"""
if isinstance(objective, Iterable):
objective = self._iterable_to_qm(objective)

# clear out current objective, keeping only the variables
self.objective.quadratic.clear() # there may be a more performant way...
for v in self.objective.variables:
self.objective.set_linear(v, 0)
# offset is overwritten later

# now add everything from the new objective
self._add_variables_from(objective)

for v in objective.variables:
self.objective.set_linear(v, objective.get_linear(v))
self.objective.add_quadratic_from(objective.iter_quadratic())
self.objective.offset = objective.offset
if not self.objective.is_linear():
self.objective.quadratic.clear() # there may be a more performant way...
self.objective.scale(0) # set all the remaining biases to 0

self.objective.update(objective)

def set_upper_bound(self, v: Variable, ub: float):
"""Set the upper bound for a variable.
Expand Down
8 changes: 8 additions & 0 deletions dimod/include/dimod/quadratic_model.h
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,10 @@ class BinaryQuadraticModel : public QuadraticModelBase<Bias, Index> {
vartype_ = vartype;
}

bias_type lower_bound(index_type v) const {
return vartype_info<bias_type>::default_min(this->vartype_);
}

/**
* Return the number of interactions in the quadratic model.
*
Expand Down Expand Up @@ -1039,6 +1043,10 @@ class BinaryQuadraticModel : public QuadraticModelBase<Bias, Index> {
}
}

bias_type upper_bound(index_type v) const {
return vartype_info<bias_type>::default_max(this->vartype_);
}

/// Return the vartype of the binary quadratic model.
const Vartype& vartype() const { return vartype_; }

Expand Down
3 changes: 3 additions & 0 deletions dimod/libcpp.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ cdef extern from "dimod/quadratic_model.h" namespace "dimod" nogil:
void change_vartype(cppVartype)
bint is_linear()
bias_type& linear(index_type)
const bias_type lower_bound(index_type)
bias_type& offset()
bias_type quadratic(index_type, index_type)
bias_type quadratic_at(index_type, index_type) except +
Expand All @@ -101,7 +102,9 @@ cdef extern from "dimod/quadratic_model.h" namespace "dimod" nogil:
void scale(bias_type)
void set_quadratic(index_type, index_type, bias_type) except +
void swap_variables(index_type, index_type)
const bias_type upper_bound(index_type)
cppVartype& vartype()
cppVartype& vartype(index_type)

cdef cppclass cppQuadraticModel "dimod::QuadraticModel" [Bias, Index]:
ctypedef Bias bias_type
Expand Down
72 changes: 72 additions & 0 deletions dimod/quadratic/cyqm/cyqm_template.pyx.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,22 @@ cimport cython

from cython.operator cimport preincrement as inc, dereference as deref
from libc.math cimport ceil, floor
from libcpp.vector cimport vector

from dimod.binary.cybqm cimport cyBQM
from dimod.cyutilities cimport as_numpy_float, ConstInteger, ConstNumeric, cppvartype
from dimod.libcpp cimport cppvartype_info
from dimod.quadratic cimport cyQM
from dimod.sampleset import as_samples
from dimod.variables import Variables
from dimod.vartypes import as_vartype, Vartype


ctypedef fused cyBQM_and_QM:
cyBQM
cyQM


cdef class cyQM_template(cyQMBase):
def __init__(self):
self.dtype = np.dtype(BIAS_DTYPE)
Expand Down Expand Up @@ -705,6 +712,71 @@ cdef class cyQM_template(cyQMBase):

self.cppqm.set_quadratic(ui, vi, bias)

def update(self, cyBQM_and_QM other):
# we'll need a mapping from the other's variables to ours
cdef vector[Py_ssize_t] mapping
mapping.reserve(other.num_variables())

cdef Py_ssize_t vi

# first make sure that any variables that overlap match in terms of
# vartype and bounds
for vi in range(other.num_variables()):
v = other.variables.at(vi)
if self.variables.count(v):
# there is a variable already
mapping.push_back(self.variables.index(v))

if self.cppqm.vartype(mapping[vi]) != other.data().vartype(vi):
raise ValueError(f"conflicting vartypes: {v!r}")

if self.cppqm.lower_bound(mapping[vi]) != other.data().lower_bound(vi):
raise ValueError(f"conflicting lower bounds: {v!r}")

if self.cppqm.upper_bound(mapping[vi]) != other.data().upper_bound(vi):
raise ValueError(f"conflicting upper bounds: {v!r}")
else:
# not yet present, let's just track that fact for now
# in case there is a mismatch so we don't modify our object yet
mapping.push_back(-1)

for vi in range(mapping.size()):
if mapping[vi] != -1:
continue # already added and checked

mapping[vi] = self.num_variables() # we're about to add a new one

v = other.variables.at(vi)
vartype = other.vartype(v)

self.add_variable(vartype, v,
lower_bound=other.data().lower_bound(vi),
upper_bound=other.data().upper_bound(vi),
)

# variables are in place!

# the linear biases
for vi in range(mapping.size()):
self._add_linear(mapping[vi], other.data().linear(vi))

# the quadratic biases
# dev note: for even more speed we could check that mapping is
# a range, and in that case can just add them without the indirection
# or the sorting.
it = other.data().cbegin_quadratic()
while it != other.data().cend_quadratic():
self.cppqm.add_quadratic(
mapping[deref(it).u],
mapping[deref(it).v],
deref(it).bias
)
inc(it)

# the offset
cdef bias_type *b = &(self.cppqm.offset())
b[0] += other.data().offset()

def upper_bound(self, v):
cdef Py_ssize_t vi = self.variables.index(v)
return as_numpy_float(self.cppqm.upper_bound(vi))
Expand Down
49 changes: 40 additions & 9 deletions dimod/quadratic/quadratic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import struct
import tempfile
import typing

from collections.abc import Callable, Set
from copy import deepcopy
Expand Down Expand Up @@ -1316,7 +1317,7 @@ def to_file(self, *,
file.seek(0)
return file

def update(self, other: 'QuadraticModel'):
def update(self, other: typing.Union[QuadraticModel, BinaryQuadraticModel]):
"""Update the quadratic model from another quadratic model.
Adds to the quadratic model the variables, linear biases, quadratic biases,
Expand Down Expand Up @@ -1346,23 +1347,53 @@ def update(self, other: 'QuadraticModel'):
>>> print(qm1.get_quadratic('s2', 's1'), qm1.get_quadratic('s3', 's1'))
-2.0 1.0
"""
# this can be improved a great deal with c++, but for now let's use
# python for simplicity
try:
return self.data.update(other.data)
except (AttributeError, TypeError):
pass

# looks like we have a model that either has object dtype or isn't
# a cython model we recognize, so let's fall back on python

# need a couple methods to be generic between bqm and qm
vartype = other.vartype if callable(other.vartype) else lambda v: other.vartype

def lower_bound(v: Variable) -> Bias:
try:
return other.lower_bound(v)
except AttributeError:
pass

if other.vartype is Vartype.SPIN:
return -1
elif other.vartype is Vartype.BINARY:
return 0
else:
raise RuntimeError # shouldn't ever happen

def upper_bound(v: Variable) -> Bias:
try:
return other.upper_bound(v)
except AttributeError:
pass

return 1

for v in other.variables:
if v not in self.variables:
continue
if self.vartype(v) != other.vartype(v):

if self.vartype(v) != vartype(v):
raise ValueError(f"conflicting vartypes: {v!r}")
if self.lower_bound(v) != other.lower_bound(v):
if self.lower_bound(v) != lower_bound(v):
raise ValueError(f"conflicting lower bounds: {v!r}")
if self.upper_bound(v) != other.upper_bound(v):
if self.upper_bound(v) != upper_bound(v):
raise ValueError(f"conflicting upper bounds: {v!r}")

for v in other.variables:
self.add_linear(self.add_variable(other.vartype(v), v,
lower_bound=other.lower_bound(v),
upper_bound=other.upper_bound(v)),
self.add_linear(self.add_variable(vartype(v), v,
lower_bound=lower_bound(v),
upper_bound=upper_bound(v)),
other.get_linear(v))

for u, v, bias in other.iter_quadratic():
Expand Down
12 changes: 12 additions & 0 deletions releasenotes/notes/CQM.set_objective-speedup-05801899c467408d.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
features:
- |
Add ``BinaryQuadraticModel::lower_bound()`` and
``BinaryQuadraticModel::upper_bound()`` methods to the C++ code.
- |
Improve the performance of the ``QuadraticModel.update()`` method.
- |
Improve the performance of the ``ConstrainedQuadraticModel.set_objective()`` method.
- |
``QuadraticModel.update()`` now accepts binary quadratic models in addition
to quadratic models.
22 changes: 22 additions & 0 deletions tests/test_constrained.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import os.path as path

import numpy as np

from dimod import BQM, Spin, Binary, CQM, Integer
from dimod.sym import Sense

Expand Down Expand Up @@ -902,6 +904,26 @@ def test_header(self):


class TestSetObjective(unittest.TestCase):
def test_bqm(self):
for dtype in [np.float32, np.float64, object]:
with self.subTest(dtype=np.dtype(dtype).name):
bqm = dimod.BQM({'a': 1}, {'ab': 4}, 5, 'BINARY', dtype=dtype)

cqm = dimod.CQM()

cqm.set_objective(bqm)

self.assertEqual(cqm.objective.linear, bqm.linear)
self.assertEqual(cqm.objective.quadratic, bqm.quadratic)
self.assertEqual(cqm.objective.offset, bqm.offset)

# doing it again should do nothing
cqm.set_objective(bqm)

self.assertEqual(cqm.objective.linear, bqm.linear)
self.assertEqual(cqm.objective.quadratic, bqm.quadratic)
self.assertEqual(cqm.objective.offset, bqm.offset)

def test_empty(self):
self.assertEqual(CQM().objective.num_variables, 0)

Expand Down
42 changes: 42 additions & 0 deletions tests/test_quadratic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,48 @@ def test_simple(self):
self.assertEqual((-i*j - x).to_polystring(), '-x - i*j')


class TestUpdate(unittest.TestCase):
def test_bqm(self):
for dtype in [np.float32, np.float64, object]:
with self.subTest(dtype=np.dtype(dtype).name):
bqm = dimod.BQM({'a': 1}, {'ab': 4}, 5, 'BINARY', dtype=dtype)

qm = dimod.QM()

qm.update(bqm)

self.assertEqual(bqm.linear, qm.linear)
self.assertEqual(bqm.quadratic, qm.quadratic)
self.assertEqual(bqm.offset, qm.offset)
self.assertEqual(qm.vartype('a'), dimod.BINARY)
self.assertEqual(qm.vartype('b'), dimod.BINARY)

# add it again, everything should double
qm.update(bqm)

self.assertEqual({'a': 2, 'b': 0}, qm.linear)
self.assertEqual({('a', 'b'): 8}, qm.quadratic)
self.assertEqual(2*bqm.offset, qm.offset)
self.assertEqual(qm.vartype('a'), dimod.BINARY)
self.assertEqual(qm.vartype('b'), dimod.BINARY)

def test_qm(self):
i = dimod.Integer('i', lower_bound=-5, upper_bound=10)
x, y = dimod.Binaries('xy')

other = i + 2*x * 3*y + 4*i*i + 5*i*x + 7

new = dimod.QM()
new.update(other)

self.assertTrue(new.is_equal(other))

# add it again
new.update(other)

self.assertTrue(new.is_equal(2*other))


class TestViews(unittest.TestCase):
@parameterized.expand([(np.float32,), (np.float64,)])
def test_empty(self, dtype):
Expand Down

0 comments on commit a973854

Please sign in to comment.