|
16 | 16 | A :std:doc:`dimod sampler <oceandocs:docs_dimod/reference/samplers>` for Leap's hybrid solvers.
|
17 | 17 | """
|
18 | 18 |
|
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 |
24 | 21 | from collections import abc
|
| 22 | +from numbers import Number |
| 23 | +from typing import Any, Dict, List, NamedTuple, Optional |
25 | 24 |
|
26 | 25 | import dimod
|
27 |
| - |
| 26 | +import dwave.optimization |
| 27 | +import numpy |
28 | 28 | from dwave.cloud import Client
|
| 29 | + |
29 | 30 | from dwave.system.utilities import classproperty, FeatureFlags
|
30 | 31 |
|
31 | 32 |
|
32 | 33 | __all__ = ['LeapHybridSampler',
|
33 | 34 | 'LeapHybridBQMSampler',
|
34 | 35 | 'LeapHybridDQMSampler',
|
35 | 36 | 'LeapHybridCQMSampler',
|
| 37 | + 'LeapHybridNLSampler', |
36 | 38 | ]
|
37 | 39 |
|
38 | 40 |
|
39 | 41 | class LeapHybridSampler(dimod.Sampler):
|
40 | 42 | """A class for using Leap's cloud-based hybrid BQM solvers.
|
41 | 43 |
|
42 |
| - Leap’s quantum-classical hybrid BQM solvers are intended to solve arbitrary |
| 44 | + Leap's quantum-classical hybrid BQM solvers are intended to solve arbitrary |
43 | 45 | application problems formulated as binary quadratic models (BQM).
|
44 | 46 |
|
45 | 47 | You can configure your :term:`solver` selection and usage by setting parameters,
|
@@ -273,15 +275,15 @@ def min_time_limit(self, bqm):
|
273 | 275 | """
|
274 | 276 |
|
275 | 277 | 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] |
277 | 279 |
|
278 | 280 | LeapHybridBQMSampler = LeapHybridSampler
|
279 | 281 |
|
280 | 282 |
|
281 | 283 | class LeapHybridDQMSampler:
|
282 | 284 | """A class for using Leap's cloud-based hybrid DQM solvers.
|
283 | 285 |
|
284 |
| - Leap’s quantum-classical hybrid DQM solvers are intended to solve arbitrary |
| 286 | + Leap's quantum-classical hybrid DQM solvers are intended to solve arbitrary |
285 | 287 | application problems formulated as **discrete** quadratic models (DQM).
|
286 | 288 |
|
287 | 289 | 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
|
474 | 476 | # (and internal) file-like object for now
|
475 | 477 |
|
476 | 478 | if compressed is not None:
|
477 |
| - warn( |
| 479 | + warnings.warn( |
478 | 480 | "Argument 'compressed' is deprecated and in future will raise an "
|
479 | 481 | "exception; please use 'compress' instead.",
|
480 | 482 | DeprecationWarning, stacklevel=2
|
481 |
| - ) |
| 483 | + ) |
482 | 484 | compress = compressed or compress
|
483 | 485 |
|
484 | 486 | with dqm.to_file(compress=compress, ignore_labels=True) as f:
|
@@ -522,15 +524,15 @@ def min_time_limit(self, dqm):
|
522 | 524 | """
|
523 | 525 | ec = (dqm.num_variable_interactions() * dqm.num_cases() /
|
524 | 526 | 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]) |
527 | 529 | return max([5, t])
|
528 | 530 |
|
529 | 531 |
|
530 | 532 | class LeapHybridCQMSampler:
|
531 | 533 | """A class for using Leap's cloud-based hybrid CQM solvers.
|
532 | 534 |
|
533 |
| - Leap’s quantum-classical hybrid CQM solvers are intended to solve |
| 535 | + Leap's quantum-classical hybrid CQM solvers are intended to solve |
534 | 536 | application problems formulated as
|
535 | 537 | :ref:`constrained quadratic models (CQM) <cqm_sdk>`.
|
536 | 538 |
|
@@ -781,3 +783,205 @@ def min_time_limit(self, cqm: dimod.ConstrainedQuadraticModel) -> float:
|
781 | 783 | num_constraints_multiplier * num_variables * num_constraints,
|
782 | 784 | minimum_time_limit
|
783 | 785 | )
|
| 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 | + ) |
0 commit comments