From 7d701247a8cdf9d2c3261627ea5e747ca8b64b6c Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Tue, 12 May 2026 22:04:11 +0200 Subject: [PATCH 1/3] Add minimal implementation of the QCDL solver --- dwave/cloud/solver.py | 63 ++++++++++++++++++- .../add-qcdl-support-58a1a047aa28afc7.yaml | 4 ++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-qcdl-support-58a1a047aa28afc7.yaml diff --git a/dwave/cloud/solver.py b/dwave/cloud/solver.py index 59553740..906675ba 100644 --- a/dwave/cloud/solver.py +++ b/dwave/cloud/solver.py @@ -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) + + 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] diff --git a/releasenotes/notes/add-qcdl-support-58a1a047aa28afc7.yaml b/releasenotes/notes/add-qcdl-support-58a1a047aa28afc7.yaml new file mode 100644 index 00000000..ba0ed888 --- /dev/null +++ b/releasenotes/notes/add-qcdl-support-58a1a047aa28afc7.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for gate-model quantum circuit (QCDL) solvers in SAPI. From d0fc41fd9e5496664e752424cb2446a917e0c62a Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Wed, 13 May 2026 19:30:31 +0200 Subject: [PATCH 2/3] Add QCDL solver mock data generator to testing utils --- dwave/cloud/testing/mocks.py | 16 ++++++++++++++++ tests/test_testing.py | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/dwave/cloud/testing/mocks.py b/dwave/cloud/testing/mocks.py index e5f4f112..ede5995e 100644 --- a/dwave/cloud/testing/mocks.py +++ b/dwave/cloud/testing/mocks.py @@ -439,3 +439,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) diff --git a/tests/test_testing.py b/tests/test_testing.py index bc8e978c..2db9ed18 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -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() From d831938d9a8f597ae41c34729c726c2dd87daf4a Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Wed, 13 May 2026 19:31:07 +0200 Subject: [PATCH 3/3] Add QCDLSolver tests --- dwave/cloud/testing/mocks.py | 1 + tests/fixtures/qcdl.json | 1 + tests/test_mock_unstructured_submission.py | 96 +++++++++++++++++++++- 3 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/qcdl.json diff --git a/dwave/cloud/testing/mocks.py b/dwave/cloud/testing/mocks.py index ede5995e..b6e3f6a4 100644 --- a/dwave/cloud/testing/mocks.py +++ b/dwave/cloud/testing/mocks.py @@ -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', ] diff --git a/tests/fixtures/qcdl.json b/tests/fixtures/qcdl.json new file mode 100644 index 00000000..3f908d1e --- /dev/null +++ b/tests/fixtures/qcdl.json @@ -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":{}} diff --git a/tests/test_mock_unstructured_submission.py b/tests/test_mock_unstructured_submission.py index e235ae7d..a55b2084 100644 --- a/tests/test_mock_unstructured_submission.py +++ b/tests/test_mock_unstructured_submission.py @@ -16,6 +16,8 @@ import io import unittest +import zlib +from pathlib import Path from unittest import mock import numpy @@ -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 @@ -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):