Skip to content

Commit 5cd5f98

Browse files
authored
Merge pull request #518 from randomir/feature/add-nl-sampler
Add `LeapHybridNLSampler`
2 parents e8946dd + af01e1a commit 5cd5f98

File tree

4 files changed

+347
-16
lines changed

4 files changed

+347
-16
lines changed

dwave/system/samplers/leap_hybrid_sampler.py

+218-14
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,32 @@
1616
A :std:doc:`dimod sampler <oceandocs:docs_dimod/reference/samplers>` for Leap's hybrid solvers.
1717
"""
1818

19-
from typing import Any, Dict, List, Optional
20-
21-
import numpy as np
22-
from warnings import warn
23-
from numbers import Number
19+
import concurrent.futures
20+
import warnings
2421
from collections import abc
22+
from numbers import Number
23+
from typing import Any, Dict, List, NamedTuple, Optional
2524

2625
import dimod
27-
26+
import dwave.optimization
27+
import numpy
2828
from dwave.cloud import Client
29+
2930
from dwave.system.utilities import classproperty, FeatureFlags
3031

3132

3233
__all__ = ['LeapHybridSampler',
3334
'LeapHybridBQMSampler',
3435
'LeapHybridDQMSampler',
3536
'LeapHybridCQMSampler',
37+
'LeapHybridNLSampler',
3638
]
3739

3840

3941
class LeapHybridSampler(dimod.Sampler):
4042
"""A class for using Leap's cloud-based hybrid BQM solvers.
4143
42-
Leaps quantum-classical hybrid BQM solvers are intended to solve arbitrary
44+
Leap's quantum-classical hybrid BQM solvers are intended to solve arbitrary
4345
application problems formulated as binary quadratic models (BQM).
4446
4547
You can configure your :term:`solver` selection and usage by setting parameters,
@@ -273,15 +275,15 @@ def min_time_limit(self, bqm):
273275
"""
274276

275277
xx, yy = zip(*self.properties["minimum_time_limit"])
276-
return np.interp([bqm.num_variables], xx, yy)[0]
278+
return numpy.interp([bqm.num_variables], xx, yy)[0]
277279

278280
LeapHybridBQMSampler = LeapHybridSampler
279281

280282

281283
class LeapHybridDQMSampler:
282284
"""A class for using Leap's cloud-based hybrid DQM solvers.
283285
284-
Leaps quantum-classical hybrid DQM solvers are intended to solve arbitrary
286+
Leap's quantum-classical hybrid DQM solvers are intended to solve arbitrary
285287
application problems formulated as **discrete** quadratic models (DQM).
286288
287289
You can configure your :term:`solver` selection and usage by setting parameters,
@@ -474,11 +476,11 @@ def sample_dqm(self, dqm, time_limit=None, compress=False, compressed=None, **kw
474476
# (and internal) file-like object for now
475477

476478
if compressed is not None:
477-
warn(
479+
warnings.warn(
478480
"Argument 'compressed' is deprecated and in future will raise an "
479481
"exception; please use 'compress' instead.",
480482
DeprecationWarning, stacklevel=2
481-
)
483+
)
482484
compress = compressed or compress
483485

484486
with dqm.to_file(compress=compress, ignore_labels=True) as f:
@@ -522,15 +524,15 @@ def min_time_limit(self, dqm):
522524
"""
523525
ec = (dqm.num_variable_interactions() * dqm.num_cases() /
524526
max(dqm.num_variables(), 1))
525-
limits = np.array(self.properties['minimum_time_limit'])
526-
t = np.interp(ec, limits[:, 0], limits[:, 1])
527+
limits = numpy.array(self.properties['minimum_time_limit'])
528+
t = numpy.interp(ec, limits[:, 0], limits[:, 1])
527529
return max([5, t])
528530

529531

530532
class LeapHybridCQMSampler:
531533
"""A class for using Leap's cloud-based hybrid CQM solvers.
532534
533-
Leaps quantum-classical hybrid CQM solvers are intended to solve
535+
Leap's quantum-classical hybrid CQM solvers are intended to solve
534536
application problems formulated as
535537
:ref:`constrained quadratic models (CQM) <cqm_sdk>`.
536538
@@ -781,3 +783,205 @@ def min_time_limit(self, cqm: dimod.ConstrainedQuadraticModel) -> float:
781783
num_constraints_multiplier * num_variables * num_constraints,
782784
minimum_time_limit
783785
)
786+
787+
788+
class LeapHybridNLSampler:
789+
r"""A class for using Leap's cloud-based hybrid nonlinear-model solvers.
790+
791+
Leap's quantum-classical hybrid nonlinear-model solvers are intended to
792+
solve application problems formulated as
793+
:ref:`nonlinear models <nl_model_sdk>`.
794+
795+
You can configure your :term:`solver` selection and usage by setting
796+
parameters, hierarchically, in a configuration file, as environment
797+
variables, or explicitly as input arguments, as described in
798+
`D-Wave Cloud Client <https://docs.ocean.dwavesys.com/en/stable/docs_cloud/sdk_index.html>`_.
799+
800+
:ref:`dwave-cloud-client <sdk_index_cloud>`'s
801+
:meth:`~dwave.cloud.client.Client.get_solvers` method filters solvers you
802+
have access to by
803+
`solver properties <https://docs.dwavesys.com/docs/latest/c_solver_properties.html>`_
804+
``category=hybrid`` and ``supported_problem_type=nl``. By default, online
805+
hybrid nonlinear-model solvers are returned ordered by latest ``version``.
806+
807+
Args:
808+
**config:
809+
Keyword arguments passed to :meth:`dwave.cloud.client.Client.from_config`.
810+
811+
Examples:
812+
This example submits a model for a
813+
:class:`flow-shop-scheduling <dwave.optimization.generators.flow_shop_scheduling>`
814+
problem.
815+
816+
>>> from dwave.optimization.generators import flow_shop_scheduling
817+
>>> from dwave.system import LeapHybridNLSampler
818+
...
819+
>>> sampler = LeapHybridNLSampler() # doctest: +SKIP
820+
...
821+
>>> processing_times = [[10, 5, 7], [20, 10, 15]]
822+
>>> model = flow_shop_scheduling(processing_times=processing_times)
823+
>>> results = sampler.sample(model, label="Small FSS problem") # doctest: +SKIP
824+
>>> job_order = next(model.iter_decisions()) # doctest: +SKIP
825+
>>> print(f"State 0 of {model.objective.state_size()} has an "\ # doctest: +SKIP
826+
... f"objective value {model.objective.state(0)} for order " \ # doctest: +SKIP
827+
... f"{job_order.state(0)}.") # doctest: +SKIP
828+
State 0 of 8 has an objective value 50.0 for order [1. 2. 0.].
829+
"""
830+
831+
def __init__(self, **config):
832+
# strongly prefer hybrid solvers; requires kwarg-level override
833+
config.setdefault('client', 'hybrid')
834+
835+
# default to short-lived session to prevent resets on slow uploads
836+
config.setdefault('connection_close', True)
837+
838+
if FeatureFlags.hss_solver_config_override:
839+
# use legacy behavior (override solver config from env/file)
840+
solver = config.setdefault('solver', {})
841+
if isinstance(solver, abc.Mapping):
842+
solver.update(self.default_solver)
843+
844+
# prefer the latest hybrid NL solver available, but allow for an easy
845+
# override on any config level above the defaults (file/env/kwarg)
846+
defaults = config.setdefault('defaults', {})
847+
if not isinstance(defaults, abc.Mapping):
848+
raise TypeError("mapping expected for 'defaults'")
849+
defaults.update(solver=self.default_solver)
850+
851+
self.client = Client.from_config(**config)
852+
self.solver = self.client.get_solver()
853+
854+
# For explicitly named solvers:
855+
if self.properties.get('category') != 'hybrid':
856+
raise ValueError("selected solver is not a hybrid solver.")
857+
if 'nl' not in self.solver.supported_problem_types:
858+
raise ValueError("selected solver does not support the 'nl' problem type.")
859+
860+
self._executor = concurrent.futures.ThreadPoolExecutor()
861+
862+
@classproperty
863+
def default_solver(cls) -> Dict[str, str]:
864+
"""Features used to select the latest accessible hybrid nonlinear-model solver."""
865+
return dict(supported_problem_types__contains='nl',
866+
order_by='-properties.version')
867+
868+
@property
869+
def properties(self) -> Dict[str, Any]:
870+
"""Solver properties as returned by a SAPI query.
871+
872+
`Solver properties <https://docs.dwavesys.com/docs/latest/c_solver_properties.html>`_
873+
are dependent on the selected solver and subject to change.
874+
"""
875+
try:
876+
return self._properties
877+
except AttributeError:
878+
self._properties = properties = self.solver.properties.copy()
879+
return properties
880+
881+
@property
882+
def parameters(self) -> Dict[str, List[str]]:
883+
"""Solver parameters in the form of a dict, where keys
884+
are keyword parameters accepted by a SAPI query and values are lists of
885+
properties in :attr:`~dwave.system.samplers.LeapHybridNLSampler.properties`
886+
for each key.
887+
888+
`Solver parameters <https://docs.dwavesys.com/docs/latest/c_solver_parameters.html>`_
889+
are dependent on the selected solver and subject to change.
890+
"""
891+
try:
892+
return self._parameters
893+
except AttributeError:
894+
parameters = {param: ['parameters']
895+
for param in self.properties['parameters']}
896+
parameters.update(label=[])
897+
self._parameters = parameters
898+
return parameters
899+
900+
class SampleResult(NamedTuple):
901+
model: dwave.optimization.Model
902+
timing: dict
903+
904+
def sample(self, model: dwave.optimization.Model,
905+
time_limit: Optional[float] = None, **kwargs
906+
) -> 'concurrent.futures.Future[SampleResult]':
907+
"""Sample from the specified nonlinear model.
908+
909+
Args:
910+
model (:class:`~dwave.optimization.Model`):
911+
Nonlinear model.
912+
913+
time_limit (float, optional):
914+
Maximum runtime, in seconds, the solver should work on the
915+
problem. Should be at least the estimated minimum required for the
916+
problem, which is calculated and set by default.
917+
918+
:meth:`~dwave.system.samplers.LeapHybridNLMSampler.estimated_min_time_limit`
919+
estimates the minimum time for your problem. For ``time_limit `` values shorter
920+
than the estimated minimum, runtime (and charge time) is not guaranteed to be
921+
shorter than the estimated time
922+
923+
**kwargs:
924+
Optional keyword arguments for the solver, specified in
925+
:attr:`~dwave.system.samplers.LeapHybridNLMSampler.parameters`.
926+
927+
Returns:
928+
:class:`concurrent.futures.Future`[SampleResult]:
929+
Named tuple containing nonlinear model and timing info, in a Future.
930+
"""
931+
932+
if not isinstance(model, dwave.optimization.Model):
933+
raise TypeError("first argument 'model' must be a dwave.optimization.Model, "
934+
f"received {type(model).__name__}")
935+
936+
if time_limit is None:
937+
time_limit = self.estimated_min_time_limit(model)
938+
939+
num_states = len(model.states)
940+
max_num_states = min(
941+
self.solver.properties.get("maximum_number_of_states", num_states),
942+
num_states
943+
)
944+
problem_data_id = self.solver.upload_nlm(model, max_num_states=max_num_states).result()
945+
946+
future = self.solver.sample_nlm(problem_data_id, time_limit=time_limit, **kwargs)
947+
948+
def hook(model, future):
949+
# TODO: known dwave-optimization bug, don't check header for now
950+
model.states.from_file(future.answer_data, check_header=False)
951+
952+
model.states.from_future(future, hook)
953+
954+
def collect():
955+
timing = future.timing
956+
for msg in timing.get('warnings', []):
957+
# note: no point using stacklevel, as this is a different thread
958+
warnings.warn(msg, category=UserWarning)
959+
960+
return LeapHybridNLSampler.SampleResult(model, timing)
961+
962+
result = self._executor.submit(collect)
963+
964+
return result
965+
966+
def estimated_min_time_limit(self, nlm: dwave.optimization.Model) -> float:
967+
"""Return the minimum `time_limit`, in seconds, estimated for the given problem.
968+
969+
Runtime (and charge time) is not guaranteed to be shorter than this minimum time.
970+
"""
971+
972+
num_nodes_multiplier = self.properties.get('num_nodes_multiplier', 8.306792043756981e-05)
973+
state_size_multiplier = self.properties.get('state_size_multiplier', 2.8379674360396316e-10)
974+
num_nodes_state_size_multiplier = self.properties.get('num_nodes_state_size_multiplier', 2.1097317822863966e-12)
975+
offset = self.properties.get('offset', 0.012671678446550175)
976+
min_time_limit = self.properties.get('min_time_limit', 5)
977+
978+
nn = nlm.num_nodes()
979+
ss = nlm.state_size()
980+
981+
return max(
982+
num_nodes_multiplier * nn
983+
+ state_size_multiplier * ss
984+
+ num_nodes_state_size_multiplier * nn * ss
985+
+ offset,
986+
min_time_limit
987+
)

requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
--extra-index-url https://pypi.dwavesys.com/simple
22

33
dimod==0.12.13
4+
dwave-optimization==0.1.0rc1
45
dwave-preprocessing==0.6.4
5-
dwave-cloud-client==0.11.4
6+
dwave-cloud-client==0.12.0.dev0
67
dwave-networkx==0.8.10
78
dwave-drivers==0.4.4
89
dwave-samplers==1.2.0

setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525

2626

2727
install_requires = ['dimod>=0.12.7,<0.14.0',
28-
'dwave-cloud-client>=0.11.0,<0.13.0',
28+
'dwave-optimization>=0.1.0rc1,<0.3',
29+
'dwave-cloud-client>=0.12.0.dev0,<0.13.0',
2930
'dwave-networkx>=0.8.10',
3031
'dwave-preprocessing>=0.5.0',
3132
'homebase>=1.0.0,<2.0.0',

0 commit comments

Comments
 (0)