Skip to content

Commit 0128d36

Browse files
committed
Add Zephyr symbol
1 parent 865d4dc commit 0128d36

File tree

8 files changed

+224
-2
lines changed

8 files changed

+224
-2
lines changed

dwave/optimization/include/dwave-optimization/nodes/quadratic_model.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,16 @@ class LatticeNode : public ScalarOutputMixin<ArrayNode> {
168168
/// Return a reference to the lattice structure of the node.
169169
const LatticeType& lattice() const noexcept { return lattice_; }
170170

171+
template<class... Args>
172+
static ssize_t lattice_num_edges(Args... args) {
173+
return LatticeType(args...).num_edges();
174+
}
175+
176+
template<class... Args>
177+
static ssize_t lattice_num_nodes(Args... args) {
178+
return LatticeType(args...).num_nodes();
179+
}
180+
171181
/// Get the linear bias associated with `u`. Returns `0` if `u` is out-of-bounds.
172182
double linear(int u) const noexcept;
173183

dwave/optimization/libcpp/__init__.pxd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from libcpp.functional cimport function
16+
1517
# As of Cython 3.0.8 these are not in Cython's libcpp
1618

19+
cdef extern from "<functional>" namespace "std" nogil:
20+
# We just do the overloads we need
21+
function[double(int)] bind_front(double(void*, int), void*)
22+
function[double(int, int)] bind_front(double(void*, int, int), void*)
23+
1724
cdef extern from "<span>" namespace "std" nogil:
1825
cdef cppclass span[T]:
1926
ctypedef size_t size_type

dwave/optimization/libcpp/nodes.pxd

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,14 @@ cdef extern from "dwave-optimization/nodes/quadratic_model.hpp" namespace "dwave
262262
cdef cppclass QuadraticModelNode(ArrayNode):
263263
QuadraticModel* get_quadratic_model()
264264

265+
cdef cppclass ZephyrNode(ArrayNode):
266+
@staticmethod
267+
Py_ssize_t lattice_num_nodes(Py_ssize_t m)
268+
@staticmethod
269+
Py_ssize_t lattice_num_edges(Py_ssize_t m)
270+
double linear(int v)
271+
double quadratic(int u, int v)
272+
265273

266274
cdef extern from "dwave-optimization/nodes/testing.hpp" namespace "dwave::optimization" nogil:
267275
cdef cppclass ArrayValidationNode(Node):

dwave/optimization/src/nodes/quadratic_model.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,12 +484,15 @@ LatticeNode<T>::LatticeNode(ArrayNode* x_ptr, T lattice, std::function<double(in
484484
// Add the nodes (with their weights) to the adjacency
485485
adj_.resize(lattice_.num_nodes());
486486
for (ssize_t u = 0, N = adj_.size(); u < N; ++u) {
487-
adj_[u].bias = linear(u);
487+
const double bias = linear(u);
488+
if (!std::isfinite(bias)) throw std::invalid_argument("biases must be finite");
489+
adj_[u].bias = bias;
488490
}
489491

490492
// Add the edges (with their weights) to the adjacency
491493
for (const auto& [u, v] : lattice_.edges()) {
492494
const double bias = quadratic(u, v);
495+
if (!std::isfinite(bias)) throw std::invalid_argument("biases must be finite");
493496
adj_[u].neighbors.emplace_back(v, bias);
494497
adj_[v].neighbors.emplace_back(u, bias);
495498
}

dwave/optimization/symbols.pyx

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ from libcpp.unordered_map cimport unordered_map
3434
from libcpp.utility cimport move
3535
from libcpp.vector cimport vector
3636

37-
from dwave.optimization.libcpp cimport get, holds_alternative, span
37+
from dwave.optimization.libcpp cimport bind_front, get, holds_alternative, span
3838
from dwave.optimization.libcpp.array cimport (
3939
Array as cppArray,
4040
SizeInfo as cppSizeInfo,
@@ -108,6 +108,7 @@ from dwave.optimization.libcpp.nodes cimport (
108108
SumNode as cppSumNode,
109109
WhereNode as cppWhereNode,
110110
XorNode as cppXorNode,
111+
ZephyrNode as cppZephyrNode,
111112
)
112113
from dwave.optimization.model cimport ArraySymbol, _Graph, Symbol
113114
from dwave.optimization.states cimport States
@@ -172,6 +173,7 @@ __all__ = [
172173
"Sum",
173174
"Where",
174175
"Xor",
176+
"Zephyr",
175177
]
176178

177179
# We would like to be able to do constructions like dynamic_cast[cppConstantNode*](...)
@@ -4003,3 +4005,110 @@ cdef class Xor(ArraySymbol):
40034005
cdef cppXorNode* ptr
40044006

40054007
_register(Xor, typeid(cppXorNode))
4008+
4009+
4010+
cdef double _read_linear(void* obj, int v) noexcept nogil:
4011+
with gil:
4012+
try:
4013+
return (<object>obj)(v)
4014+
except Exception as ex:
4015+
(<object>obj).ex = ex
4016+
# If we got here that means an exception was raised. So we trigger C++'s
4017+
# error handling by returning a non-finite value.
4018+
return float('nan')
4019+
4020+
cdef double _read_quadratic(void* obj, int u, int v) noexcept nogil:
4021+
# object must be a Python callable!
4022+
with gil:
4023+
try:
4024+
return (<object>obj)(u, v)
4025+
except Exception as ex:
4026+
(<object>obj).ex = ex
4027+
# If we got here that means an exception was raised. So we trigger C++'s
4028+
# error handling by returning a non-finite value.
4029+
return float('nan')
4030+
4031+
# Currently we don't have a matching dwave.optimization.mathematical function
4032+
# so we document it better here than we usually do.
4033+
cdef class Zephyr(ArraySymbol):
4034+
"""A quadratic model with biases arranged according to a Zephyr lattice.
4035+
4036+
Encode a quadratic model arranged according to a :term:`Zephyr` lattice.
4037+
4038+
Args:
4039+
x: A 1D array symbol giving the assignments to the variables.
4040+
m: Grid parameter for the Zephyr lattice.
4041+
linear:
4042+
A function that will be called for each node in the Zephyr lattice.
4043+
Must accept a single ``int`` giving the node index.
4044+
Must return a finite number giving the linear bias.
4045+
Will be called once for each node.
4046+
quadratic:
4047+
A function that will be called for each edge in the Zephyr lattice.
4048+
Must accept two ``int`` giving the indices of the nodes that share an edge.
4049+
Must return a finite number giving the quadratic bias.
4050+
Will be called once for each edge.
4051+
4052+
See also:
4053+
:func:`dwave_networkx.zephyr_graph()`
4054+
4055+
..versionadded:: 0.6.2
4056+
"""
4057+
def __init__(self, ArraySymbol x, Py_ssize_t m, *, object linear, object quadratic):
4058+
cdef _Graph model = x.model
4059+
4060+
# todo: we could accept dictionaries directly (probably adding uv + vu for quadratic)
4061+
if not callable(linear):
4062+
raise TypeError("linear must be a callable that accepts one int and returns a float")
4063+
if not callable(quadratic):
4064+
raise TypeError("quadratic must be a callable that accepts two ints and returns a float")
4065+
4066+
# We have to do a bit of convolution in order to do error handling
4067+
# We start by making some aliases of our input functions so that we
4068+
# can add an .ex attribute that will signal if an exception was raised
4069+
# in the C++ code.
4070+
def _linear(v):
4071+
return linear(v)
4072+
_linear.ex = None
4073+
def _quadratic(u, v):
4074+
return quadratic(u, v)
4075+
_quadratic.ex = None
4076+
4077+
try:
4078+
self.ptr = model._graph.emplace_node[cppZephyrNode](
4079+
x.array_ptr,
4080+
m,
4081+
bind_front(_read_linear, <void*>_linear),
4082+
bind_front(_read_quadratic, <void*>_quadratic),
4083+
)
4084+
except Exception as ex:
4085+
# Determine if the error was raised by the Python function or by C++
4086+
if _linear.ex:
4087+
raise _linear.ex from None
4088+
if _quadratic.ex:
4089+
raise _quadratic.ex from None
4090+
raise ex
4091+
4092+
self.initialize_arraynode(model, self.ptr)
4093+
4094+
@staticmethod
4095+
def lattice_num_edges(Py_ssize_t m):
4096+
"""Return the number of edges in a Zephyr lattice with grid parameter ``m``."""
4097+
return cppZephyrNode.lattice_num_edges(m)
4098+
4099+
@staticmethod
4100+
def lattice_num_nodes(Py_ssize_t m):
4101+
"""Return the number of nodes in a Zephyr lattice with grid parameter ``m``."""
4102+
return cppZephyrNode.lattice_num_nodes(m)
4103+
4104+
def linear(self, Py_ssize_t v):
4105+
"""Return the linear bias associated with node ``v``, or ``0`` if the node
4106+
is not in the model."""
4107+
return self.ptr.linear(v)
4108+
4109+
def quadratic(self, Py_ssize_t u, Py_ssize_t v):
4110+
"""Return the quadratic bias associated with edge ``u, v``, or ``0`` if
4111+
the edge is not in the model."""
4112+
return self.ptr.quadratic(u, v)
4113+
4114+
cdef cppZephyrNode* ptr
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
features:
3+
- Add Python ``Zephyr`` symbol and C++ ``ZephyrNode``.

tests/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
dimod==0.12.17
2+
dwave-networkx==0.8.17

tests/test_symbols.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2936,3 +2936,84 @@ def test_array_xor(self):
29362936

29372937
np.testing.assert_array_equal(ab.state(0), [0, 1, 0, 1])
29382938
np.testing.assert_array_equal(ac.state(0), [1, 0, 1, 0])
2939+
2940+
2941+
class TestZephyr(unittest.TestCase):
2942+
def test(self):
2943+
from dwave.optimization.symbols import Zephyr
2944+
2945+
model = Model()
2946+
2947+
num_nodes = Zephyr.lattice_num_nodes(2)
2948+
2949+
x = model.binary(num_nodes)
2950+
z = Zephyr(x, 2, linear=lambda v: 5, quadratic=lambda u, v: 1)
2951+
2952+
def test_exceptions(self):
2953+
from dwave.optimization.symbols import Zephyr
2954+
2955+
model = Model()
2956+
x = model.binary(Zephyr.lattice_num_nodes(2))
2957+
2958+
def _raise(*args):
2959+
raise ValueError("boom")
2960+
2961+
with self.assertRaisesRegex(ValueError, "boom"):
2962+
Zephyr(x, 2, linear=_raise, quadratic=lambda u, v: 1)
2963+
with self.assertRaisesRegex(ValueError, "boom"):
2964+
Zephyr(x, 2, linear=lambda u: 1, quadratic=_raise)
2965+
2966+
def _infinite(*args):
2967+
return float('inf')
2968+
with self.assertRaisesRegex(ValueError, "biases must be finite"):
2969+
Zephyr(x, 2, linear=_infinite, quadratic=lambda u, v: 1)
2970+
2971+
# and, very importantly, there shouldn't be any side effects nodes
2972+
self.assertEqual(model.num_symbols(), 1)
2973+
self.assertEqual(len(list(x.iter_successors())), 0)
2974+
2975+
def test_graph(self):
2976+
try:
2977+
import dwave_networkx as dnx
2978+
except ImportError:
2979+
return self.skipTest("no dwave_networkx")
2980+
2981+
from dwave.optimization.symbols import Zephyr
2982+
2983+
model = Model()
2984+
2985+
for m in range(1, 4):
2986+
with self.subTest(m=m):
2987+
G = dnx.zephyr_graph(m)
2988+
2989+
for v in G.nodes:
2990+
G.nodes[v]["count"] = 0
2991+
for u, v in G.edges:
2992+
G.edges[u, v]["count"] = 0
2993+
2994+
x = model.binary(len(G.nodes))
2995+
2996+
def linear(v):
2997+
if not G.has_node(v):
2998+
raise ValueError("no node")
2999+
G.nodes[v]["count"] += 1
3000+
return v
3001+
3002+
def quadratic(u, v):
3003+
if not G.has_edge(u, v):
3004+
raise ValueError("no edge")
3005+
G.edges[u, v]["count"] += 1
3006+
return u*v
3007+
3008+
z = Zephyr(x, m, linear=linear, quadratic=quadratic)
3009+
3010+
# each node/edge should have been called exactly once
3011+
self.assertTrue(all(G.nodes[v]["count"] == 1 for v in G.nodes))
3012+
self.assertTrue(all(G.edges[u, v]["count"] == 1 for u, v in G.edges))
3013+
3014+
# and they got the bias we expected
3015+
for v in G.nodes:
3016+
self.assertEqual(z.linear(v), v)
3017+
for u, v in G.edges:
3018+
self.assertEqual(z.quadratic(u, v), u * v)
3019+
self.assertEqual(z.quadratic(v, u), u * v)

0 commit comments

Comments
 (0)