Repository: dwavesystems/dwave-system
Pull request #568 (2025-04-29 19:26:14)

Add `anneal_schedule_with_offset()` utility function
Will also need an update to [this SDK page](https://docs.dwavequantum.com/en/latest/quantum_research/annealing.html)


Patch:
diff --git a/docs/conf.py b/docs/conf.py
index 4853867..2f9b88b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -81,5 +81,6 @@ html_sidebars = {"**": ["search-field", "sidebar-nav-bs"]}  # remove ads
 
 intersphinx_mapping = {'python': ('https://docs.python.org/3', None),
     'networkx': ('https://networkx.github.io/documentation/stable/', None),
+    'numpy': ('http://numpy.org/doc/stable/', None),
     'dwave': ('https://docs.dwavequantum.com/en/latest/', None),
     }
diff --git a/docs/utilities.rst b/docs/utilities.rst
index 5f97e40..3eac69e 100644
--- a/docs/utilities.rst
+++ b/docs/utilities.rst
@@ -11,6 +11,7 @@ Utilities
 .. autosummary::
    :toctree: generated/
 
+   anneal_schedule_with_offset
    common_working_graph
 
 .. currentmodule:: dwave.system.coupling_groups
@@ -20,7 +21,6 @@ Utilities
 
    coupling_groups
 
-
 Temperature and Unit-Conversion Utilities
 -----------------------------------------
 
diff --git a/dwave/system/utilities.py b/dwave/system/utilities.py
index 63fa8af..6e6989b 100644
--- a/dwave/system/utilities.py
+++ b/dwave/system/utilities.py
@@ -17,10 +17,38 @@
 import os
 import json
 import networkx as nx
+import numpy as np
 import warnings
 
-__all__ = ['common_working_graph', 'classproperty']
+from typing import Union
 
+__all__ = [
+    'anneal_schedule_with_offset',
+    'classproperty',
+    'common_working_graph',
+    ]
+
+
+def _asarray(
+    argname: str,
+    array_like: np.typing.ArrayLike,
+    num_columns: int = 1,
+    ) -> np.ndarray:
+    "Coerce array-like input into a NumPy array."
+    try:
+        array = np.asarray_chkfinite(array_like)
+    except (ValueError, TypeError) as err:
+        raise ValueError(f"{argname!r}: {err}") from err
+
+    if not np.issubdtype(array.dtype, np.number):
+        raise TypeError(f"{argname!r} must be an array-like of numbers")
+
+    if (array.ndim > 1 and array.shape[1] != num_columns
+        or array.ndim == 1 and array.ndim != num_columns):
+
+        raise ValueError(f"{argname!r} must be a {num_columns}D array-like")
+
+    return array
 
 # taken from https://stackoverflow.com/a/39542816, licensed under CC BY-SA 3.0
 # not needed in py39+
@@ -89,3 +117,114 @@ class FeatureFlags:
     @classproperty
     def hss_solver_config_override(cls):
         return cls.get('hss_solver_config_override')
+
+
+def anneal_schedule_with_offset(
+        anneal_offset: float = 0.0,
+        anneal_schedule: Union[np.typing.ArrayLike, list, list[list[float]], None] = None,
+        s: Union[np.typing.ArrayLike, list, None] = None,
+        A: Union[np.typing.ArrayLike, list, None] = None,
+        B: Union[np.typing.ArrayLike, list, None] = None,
+        c: Union[np.typing.ArrayLike, list, None] = None
+    ) -> np.ndarray:
+    r"""Calculates the anneal schedule for a given anneal offset.
+
+    The standard annealing trajectory, published for each quantum computer on
+    :ref:`this <qpu_solver_properties_specific>` page, lowers :math:`A(s)`, the
+    tunneling energy, and raises :math:`B(s)`, the problem energy, identically
+    for all qubits. :ref:`Anneal offsets <qpu_qa_anneal_offsets>` enable you to
+    adjust the standard annealing path per qubit.
+
+    This function accepts a quantum computer's anneal schedule and an offset
+    value, and returns the advanced or delayed schedule.
+
+    Args:
+        anneal_offset:
+            Anneal-offset value for a single qubit.
+
+        anneal_schedule:
+            Anneal schedule, as a 4-column |array-like|_, with column values for
+            :math:`s, A, B, c` as provided by (and typically taken from) the
+            spreadsheet columns of the published
+            :ref:`Per-QPU Solver Properties and Schedules <qpu_solver_properties_specific>`
+            page. If set, ``anneal_offset`` is the only additional parameter
+            allowed.
+
+        s: Normalized anneal fraction, :math:`\frac{t}{t_a}`, which ranges from
+            0 to 1, where :math:`t_a` is the annealing duration, as a
+            1-dimensional |array-like|_. If set ``anneal_schedule`` must be
+            ``None`` and values must be provided for ``A``, ``B``, and ``c``.
+
+        A: Transverse or tunneling energy, :math:`A(s)`, as a 1-dimensional
+            |array-like|_. If set ``anneal_schedule`` must be ``None`` and
+            values must be provided for ``s``, ``B``, and ``c``.
+
+        B: Energy applied to the problem Hamiltonian, :math:`B(s)`, as a
+            1-dimensional |array-like|_. If set ``anneal_schedule`` must be
+            ``None`` and values must be provided for ``s``, ``A``, and ``c``.
+
+        c: Normalized annealing bias, :math:`c(s)`, as a 1-dimensional
+            |array-like|_. If set ``anneal_schedule`` must be ``None`` and
+            values must be provided for ``s``, ``A``, and ``B``.
+
+    Returns:
+        Offset schedules :math:`A(s), B(s)`, and :math:`c(s)`, as a
+        :std:doc:`NumPy <numpy:index>` array with columns :math:`s, A, B, c`.
+
+    Note:
+        You can prepare the input schedule by downloading the schedule for your
+        selected quantum computer on the
+        :ref:`Per-QPU Solver Properties and Schedules <qpu_solver_properties_specific>`
+        page, saving the schedule tab in CSV format, and using NumPy's
+        :func:`~numpy.loadtxt` function, as here:
+
+        .. doctest::
+            :skipif: True
+
+            >>> import numpy as np
+            >>> schedule = np.loadtxt(schedule_csv_filename, delimiter=",", skiprows=1)
+
+    Examples:
+
+        For a schedule provided as array :code:`schedule`, this example
+        returns the schedule with an offset of 0.2.
+
+        >>> from dwave.system import anneal_schedule_with_offset
+        ...
+        >>> offset = 0.2
+        >>> schedule_offset = anneal_schedule_with_offset(offset, schedule)  # doctest: +SKIP
+
+    """
+    if anneal_schedule is not None and (
+        s is not None or A is not None or B is not None or c is not None):
+
+        raise ValueError("Either `anneal_schedule` or `s, A, B, c`"
+            f" can be specified. Got both inputs.")
+
+    if anneal_schedule is None and (
+        s is None or A is None or B is None or c is None):
+
+        raise ValueError("If `anneal_schedule` is unspecified, you must"
+            f" specify all of `s, A, B, c`. Not all were specified.")
+
+    if anneal_schedule is not None:
+
+        schedule = _asarray('anneal_schedule', anneal_schedule, 4)
+
+        s = schedule[:, 0]
+        A = schedule[:, 1]
+        B = schedule[:, 2]
+        c = schedule[:, 3]
+
+    else:
+
+        s = _asarray('s', s, 1)
+        A = _asarray('A', A, 1)
+        B = _asarray('B', B, 1)
+        c = _asarray('c', c, 1)
+
+    A_offset = np.interp(c + anneal_offset, c, A)
+    B_offset = np.interp(c + anneal_offset, c, B)
+    c_offset= c + anneal_offset
+
+    return np.column_stack((s, A_offset, B_offset, c_offset))


Test patch:
diff --git a/tests/test_utilities.py b/tests/test_utilities.py
index 03d7003..5ff7ba8 100644
--- a/tests/test_utilities.py
+++ b/tests/test_utilities.py
@@ -12,13 +12,16 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 
+import os
 import unittest
 
+import numpy as np
+
 import dwave_networkx as dnx
 
 from dwave.cloud.testing import isolated_environ
 
-from dwave.system import common_working_graph
+from dwave.system import anneal_schedule_with_offset, common_working_graph
 from dwave.system.utilities import FeatureFlags
 
 
@@ -96,3 +99,111 @@ class TestFeatureFlagSupport(unittest.TestCase):
             self.assertTrue(FeatureFlags.hss_solver_config_override)
         with isolated_environ(remove=('DWAVE_FEATURE_FLAGS',)):
             self.assertFalse(FeatureFlags.hss_solver_config_override)
+
+
+class TestAnnealScheduleWithOffset(unittest.TestCase):
+    def test_doc_file(self):
+
+        anneal_schedule = [
+            [0.1, 10, 1, 0.02],
+            [0.2, 6, 3, 0.25],
+            [0.3, 4, 7, 0.34],
+            [0.4, 2, 12, 0.399]
+            ]
+
+        offset=0.05
+
+        # Schedule with  offset=0.05
+        expected_schedule = [
+            [0.1, 9.13043478, 1.43478261, 0.07],
+            [0.2, 4.88888889, 5.22222222, 0.3],
+            [0.3, 2.30508475, 11.23728814, 0.39],
+            [0.4, 2, 12, 0.449]
+            ]
+
+        # Anneal schedule as a list
+        schedule_offset = anneal_schedule_with_offset(
+            offset,
+            anneal_schedule,
+            )
+
+        np.testing.assert_allclose(schedule_offset,
+                                   np.asarray(expected_schedule),
+                                   atol=0.1,
+                                   )
+
+        # Anneal schedule as an array
+        schedule_offset = anneal_schedule_with_offset(
+            offset,
+            np.asarray(anneal_schedule),
+            )
+
+        np.testing.assert_allclose(schedule_offset,
+                                   np.asarray(expected_schedule),
+                                   atol=0.1,
+                                   )
+
+        # Vector inputs as lists
+        schedule_offset = anneal_schedule_with_offset(
+            offset,
+            s= [a[:1][0] for a in anneal_schedule],
+            A = [a[1:2][0] for a in anneal_schedule],
+            B = [a[2:3][0] for a in anneal_schedule],
+            c = [a[3:][0] for a in anneal_schedule],
+            )
+
+        np.testing.assert_allclose(schedule_offset,
+                                   np.asarray(expected_schedule),
+                                   atol=0.1,
+                                   )
+
+        # Vector inputs as 1D arrays
+        schedule_offset = anneal_schedule_with_offset(
+            offset,
+            s= np.asarray(anneal_schedule)[:,0],
+            A = np.asarray(anneal_schedule)[:,1],
+            B = np.asarray(anneal_schedule)[:,2],
+            c = np.asarray(anneal_schedule)[:,3],
+            )
+
+        np.testing.assert_allclose(schedule_offset,
+                                   np.asarray(expected_schedule),
+                                   atol=0.1,
+                                   )
+
+        # Error case: missing vector
+        with self.assertRaises(ValueError):
+            schedule_offset = anneal_schedule_with_offset(
+                offset,
+                s= np.asarray(anneal_schedule)[:,0],
+                B = np.asarray(anneal_schedule)[:,2],
+                c = np.asarray(anneal_schedule)[:,3],
+                )
+
+        # Error case: schedule and vectors
+        with self.assertRaises(ValueError):
+            schedule_offset = anneal_schedule_with_offset(
+                offset,
+                anneal_schedule,
+                s= np.asarray(anneal_schedule)[:,0],
+                A = np.asarray(anneal_schedule)[:,1],
+                B = np.asarray(anneal_schedule)[:,2],
+                c = np.asarray(anneal_schedule)[:,3],
+                )
+
+        # Error case: schedule not 4D
+        with self.assertRaises(ValueError):
+            schedule_offset = anneal_schedule_with_offset(
+                offset,
+                anneal_schedule=np.asarray(anneal_schedule)[:,0],
+                )
+
+        # Error case: vector not 1D
+        with self.assertRaises(ValueError):
+            schedule_offset = anneal_schedule_with_offset(
+                offset,
+                s= np.asarray(anneal_schedule)[:,0],
+                A = np.asarray(anneal_schedule)[:,1:3],
+                B = np.asarray(anneal_schedule)[:,2],
+                c = np.asarray(anneal_schedule)[:,3],
+                )
\ No newline at end of file