Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion dwave/cloud/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1603,10 +1603,71 @@ def estimate_qpu_access_time(self,
return sampling_time + programming_time


class QCDLSolver(BaseUnstructuredSolver):
"""Class for D-Wave QCDL gate-model solvers.

This class provides the :term:`QCDL` upload and submit methods and
encapsulates the solver description returned from the D-Wave cloud API.

Args:
client (:class:`~dwave.cloud.client.Client`):
Client that manages access to this solver.

data (`dict`):
Data from the server describing this solver.
"""

_handled_problem_types = {"qcdl"}
_handled_encoding_formats = {"binary-ref"}

def _encode_problem_for_upload(self, qcdl, **kwargs):
return orjson.dumps(qcdl)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if a try/except here might help some users debug in the future.

OTOH we don't have evidence that this is a common failure path, so until we do probably better to leave it alone. Ok, I talked myself out of it, but leaving the comment for posterity 😄

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we integrate QCDL generation into Ocean, we'll be able to do better checks here. Or even defer serialization completely. Just like we do with NL (and require dwave-optimization) or BQM/CQM/DQM (and require dimod).


def sample_qcdl(self, qcdl, label=None, **params):
"""Sample from the specified :term:`QCDL`.

Args:
qcdl (dict/str):
A quantum circuit in a Quantum Circuit Description Language
(QCDL) dict, or a reference to one (Problem ID returned by the
:meth:`.upload_qcdl` method).

label (str, optional):
Problem label you can optionally tag submissions with for ease
of identification.

**params:
Parameters for the sampling method, solver-specific.

Returns:
:class:`~dwave.cloud.computation.Future`

"""
return self.sample_problem(qcdl, label=label, **params)

def upload_qcdl(self, qcdl):
r"""Upload the specified :term:`QCDL` circuit to SAPI, returning a
Problem ID that can be used to submit the circuit to this solver.

Args:
qcdl (dict/bytes-like/file-like):
A quantum circuit QCDL dict, given either as a ``dict``, or as
raw data (encoded serialized circuit) in either a file-like or
a bytes-like object.

Returns:
:class:`concurrent.futures.Future`\ [str]:
Problem ID in a Future. Problem ID can be used to submit
problems by reference.

"""
return self.upload_problem(qcdl)


# for backwards compatibility:
Solver = StructuredSolver
UnstructuredSolver = BQMSolver

# list of all available solvers, ordered according to loading attempt priority
# (more specific first)
available_solvers = [StructuredSolver, BQMSolver, CQMSolver, DQMSolver, NLSolver]
available_solvers = [StructuredSolver, BQMSolver, CQMSolver, DQMSolver, NLSolver, QCDLSolver]
17 changes: 17 additions & 0 deletions dwave/cloud/testing/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
'qpu_chimera_solver_data', 'qpu_pegasus_solver_data', 'qpu_zephyr_solver_data',
'unstructured_solver_data', 'hybrid_bqm_solver_data', 'hybrid_dqm_solver_data',
'hybrid_cqm_solver_data', 'hybrid_nl_solver_data', 'qpu_problem_timing_data',
'qcdl_solver_data',
]


Expand Down Expand Up @@ -439,3 +440,19 @@ def hybrid_nl_solver_data(**kwargs) -> dict:
)
params.update(**kwargs)
return unstructured_solver_data(**params)


def qcdl_solver_data(**kwargs) -> dict:
params = dict(
name="qcdl_mock_solver",
description="QCDL mock solver",
supported_problem_types=["qcdl"],
# solver-specific properties (mock values)
max_shots=10000,
parameters=dict(
shots="Number of times the circuit is executed",
time_limit="Maximum run time in seconds",
),
)
params.update(**kwargs)
return unstructured_solver_data(**params)
4 changes: 4 additions & 0 deletions releasenotes/notes/add-qcdl-support-58a1a047aa28afc7.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
features:
- |
Add support for gate-model quantum circuit (QCDL) solvers in SAPI.
1 change: 1 addition & 0 deletions tests/fixtures/qcdl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"program":{"statements":[{"op":"h","name":null,"args":["q0"]},{"op":"cx","name":null,"args":["q0","q1"]},{"op":"measure","name":null,"args":["q0"],"kwargs":{"log":true}},{"op":"measure","name":null,"args":["q1"],"kwargs":{"log":true}}],"statement_hash":"69cfd115e2bb5df585538c50b79a605c","signature":{"qcdl_operator":null,"qubits":["q0","q1"],"qubits_used":["q0","q1"],"args":[],"kwargs":{}}},"procedures":{},"next_indices":{}}
96 changes: 94 additions & 2 deletions tests/test_mock_unstructured_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import io
import unittest
import zlib
from pathlib import Path
from unittest import mock

import numpy
Expand All @@ -25,9 +27,10 @@
from dwave.cloud.client import Client
from dwave.cloud.solver import (
StructuredSolver, BaseUnstructuredSolver, UnstructuredSolver,
BQMSolver, CQMSolver, DQMSolver, NLSolver)
BQMSolver, CQMSolver, DQMSolver, NLSolver, QCDLSolver)
from dwave.cloud.concurrency import Present
from dwave.cloud.testing.mocks import qpu_pegasus_solver_data, hybrid_nl_solver_data
from dwave.cloud.testing.mocks import (
qpu_pegasus_solver_data, hybrid_nl_solver_data, qcdl_solver_data)

from tests.api.mocks import choose_reply

Expand Down Expand Up @@ -413,6 +416,95 @@ def mock_upload(_, file, **kwargs):
self.assertEqual(fut.answer_data.read(), mock_answer_data)


class TestQCDLSolver(unittest.TestCase):

def setUp(self):
self.mock_qcdl_solver = QCDLSolver(client=None, data=qcdl_solver_data())
self.mock_qpu_solver = StructuredSolver(client=None, data=qpu_pegasus_solver_data(2))
self.solvers = [self.mock_qpu_solver, self.mock_qcdl_solver]
self.client = Client(endpoint='endpoint', token='token')
self.client._fetch_solvers = lambda **kw: self.solvers

def shutDown(self):
self.client.close()

def assertSolvers(self, container, members):
self.assertEqual(set(container), set(members))

def test_get_solvers(self):
self.assertSolvers(self.client.get_solvers(), self.solvers)

def test_nl_solver_selection(self):
solvers = self.client.get_solvers(supported_problem_types__issubset={'qcdl'})
self.assertSolvers(solvers, [self.mock_qcdl_solver])

def test_sample_qcdl_smoke_test(self):
# a simple qcdl
with open(Path(__file__).parent / 'fixtures/qcdl.json') as fp:
qcdl = orjson.loads(fp.read())

problem_type = 'qcdl'
timing_info = {'charge_time': 1, 'run_time': 2}
num_shots = 10
mock_answer_data = orjson.dumps({
'num_shots': num_shots,
'num_qubits': 2,
'measurements': {},
'executed_qcdl': qcdl
})

# use a global mocked session, so we can modify it on the fly
session = mock.Mock()
setattr(session, '__enter__', mock.Mock())
setattr(session, '__exit__', mock.Mock())
session.__enter__.return_value = session
session.__exit__.return_value = None
session.get.return_value.iter_content.return_value = iter([mock_answer_data])

# mock the upload worker on client only, still testing the solver upload path
mock_problem_id = 'mock-problem-id'
def mock_upload(_, file, **kwargs):
self.assertEqual(kwargs, {})
return Present(result=mock_problem_id)

def mock_post(path, data, headers=None, **kwargs):
# decode submitted data
if headers is None:
headers = {}
encoding = headers.get('Content-Encoding', 'identity').lower()
if encoding == 'deflate':
data = zlib.decompress(data)
problems = orjson.loads(data)

# verify request
self.assertTrue(isinstance(problems, list) and len(problems) == 1)
self.assertEqual(problems[0].get('params', {}).get('num_shots'), num_shots)
self.assertEqual(problems[0].get('type'), problem_type)

# return mock answer data
answer_url = f'/problems/{mock_problem_id}/answer/data/'
return choose_reply(path, {
'problems/': complete_reply_binary_ref(
answer_url, id_=mock_problem_id, type_=problem_type,
timing=timing_info),
answer_url: mock_answer_data,
})

session.post = mock_post

# construct a functional solver by mocking client and api response data
with mock.patch.multiple(Client, create_session=lambda self: session,
upload_problem_encoded=mock_upload):
with Client(endpoint='endpoint', token='token') as client:
solver = QCDLSolver(client, qcdl_solver_data())

# verify decoding works
fut = solver.sample_qcdl(qcdl, num_shots=num_shots)
self.assertEqual(fut.problem_type, problem_type)
self.assertEqual(fut.timing, timing_info)
self.assertEqual(fut.answer_data.read(), mock_answer_data)


class TestProblemLabel(unittest.TestCase):

class PrimaryAssertionSatisfied(Exception):
Expand Down
5 changes: 5 additions & 0 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,11 @@ def test_unstructured_solver_data(self):
self.assertIn('maximum_number_of_nodes', data['properties'])
self.assertIn('maximum_number_of_states', data['properties'])

data = mocks.qcdl_solver_data()
self.assertListEqual(data['properties']['supported_problem_types'], ['qcdl'])
self.assertIn('max_shots', data['properties'])
self.assertIn('time_limit', data['properties']['parameters'])


if __name__ == '__main__':
unittest.main()