diff --git a/examples/run_aqt_estimator.py b/examples/run_aqt_estimator.py index a5f46cb..b36018f 100644 --- a/examples/run_aqt_estimator.py +++ b/examples/run_aqt_estimator.py @@ -19,7 +19,7 @@ from qiskit.circuit.library import n_local from qiskit_scaleway import ScalewayProvider -from qiskit_scaleway.primitives import EstimatorV1 +from qiskit_scaleway.primitives import Estimator provider = ScalewayProvider( project_id="", @@ -33,7 +33,7 @@ session_id = backend.start_session(name="test-session", deduplication_id="my-workshop") # Create an estimator (v1) with the backend and the session -estimator = EstimatorV1(backend=backend, session_id=session_id) +estimator = Estimator(backend=backend, session_id=session_id) # Specify the problem Hamiltonian hamiltonian = SparsePauliOp.from_list( @@ -56,7 +56,7 @@ def cost_function( params, ansatz: QuantumCircuit, hamiltonian: BaseOperator, - estimator: EstimatorV1, + estimator: Estimator, ) -> float: """Cost function for the VQE. @@ -64,7 +64,7 @@ def cost_function( on the state prepared by the Ansatz circuit. """ return float( - estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0] + estimator.run([(ansatz, hamiltonian, params)]).result().values[0] ) diff --git a/qiskit_scaleway/backends/aer/backend.py b/qiskit_scaleway/backends/aer/backend.py index 4f4691a..a3330c9 100644 --- a/qiskit_scaleway/backends/aer/backend.py +++ b/qiskit_scaleway/backends/aer/backend.py @@ -11,7 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from qiskit.providers import Options, convert_to_target +from typing import List +from qiskit.providers import Options +from qiskit.transpiler import Target from qiskit_aer.backends.aer_simulator import BASIS_GATES, AerBackendConfiguration from qiskit_aer.backends.aerbackend import NAME_MAPPING @@ -49,9 +51,8 @@ def __init__(self, provider, client: QaaSClient, platform: QaaSPlatform): } ) self._properties = None - self._target = convert_to_target( - self._configuration, self._properties, None, NAME_MAPPING - ) + + self._target = self._convert_to_target() def __repr__(self) -> str: return f"" @@ -71,6 +72,7 @@ def _default_options(self): shots=1000, memory=False, seed_simulator=None, + noise_model=None, method="automatic", precision="double", max_shot_size=None, @@ -105,3 +107,23 @@ def _default_options(self): fusion_max_qubit=None, fusion_threshold=None, ) + + def _convert_to_target(self) -> Target: + args_lis: List[str] = [ + "basis_gates", + "num_qubits", + "coupling_map", + "instruction_durations", + "concurrent_measurements", + "dt", + "timing_constraints", + "custom_name_mapping", + ] + + conf_dict = self._configuration.to_dict() + if conf_dict.get("custom_name_mapping") is None: + conf_dict["custom_name_mapping"] = NAME_MAPPING + + return Target.from_configuration( + **{k: conf_dict[k] for k in args_lis if k in conf_dict} + ) diff --git a/qiskit_scaleway/backends/base_job.py b/qiskit_scaleway/backends/base_job.py index 1d8ff44..1391ffd 100644 --- a/qiskit_scaleway/backends/base_job.py +++ b/qiskit_scaleway/backends/base_job.py @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. import time +import zlib import httpx import json +import numpy as np import randomname from typing import List, Union, Optional, Dict @@ -34,6 +36,8 @@ QaaSJobRunData, QaaSJobBackendData, QaaSCircuitSerializationFormat, + QaaSNoiseModelData, + QaaSNoiseModelSerializationFormat, ) @@ -90,6 +94,22 @@ def submit(self, session_id: str) -> None: ), ) + noise_model = options.pop("noise_model", None) + if noise_model: + noise_model_dict = _encode_numpy_complex(noise_model.to_dict(False)) + noise_model = QaaSNoiseModelData( + serialization_format=QaaSNoiseModelSerializationFormat.AER_COMPRESSED_JSON, + noise_model_serialization=zlib.compress( + json.dumps(noise_model_dict).encode() + ), + ) + ### Uncomment to use standard JSON serialization, provided there is no more issue with AER deserialization logic + # noise_model = QaaSNoiseModelData( + # serialization_format = QaaSNoiseModelSerializationFormat.JSON, + # noise_model_serialization = json.dumps(noise_model.to_dict(True)).encode() + # ) + ### + backend_data = QaaSJobBackendData( name=self.backend().name, version=self.backend().version, @@ -105,6 +125,7 @@ def submit(self, session_id: str) -> None: backend=backend_data, run=run_data, client=client_data, + noise_model=noise_model, ) ) @@ -196,3 +217,25 @@ def _wait_for_result( raise JobError("Job error") time.sleep(fetch_interval) + + +def _encode_numpy_complex(obj): + """ + Recursively traverses a structure and converts numpy arrays and + complex numbers into a JSON-serializable format. + """ + if isinstance(obj, np.ndarray): + return { + "__ndarray__": True, + "data": _encode_numpy_complex(obj.tolist()), # Recursively encode data + "dtype": obj.dtype.name, + "shape": obj.shape, + } + elif isinstance(obj, (complex, np.complex128)): + return {"__complex__": True, "real": obj.real, "imag": obj.imag} + elif isinstance(obj, dict): + return {key: _encode_numpy_complex(value) for key, value in obj.items()} + elif isinstance(obj, (list, tuple)): + return [_encode_numpy_complex(item) for item in obj] + else: + return obj diff --git a/qiskit_scaleway/primitives/__init__.py b/qiskit_scaleway/primitives/__init__.py index 8af5392..97e0f61 100644 --- a/qiskit_scaleway/primitives/__init__.py +++ b/qiskit_scaleway/primitives/__init__.py @@ -12,5 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. from .estimator import Estimator -from .estimator_v1 import Estimator as EstimatorV1 from .sampler import Sampler diff --git a/qiskit_scaleway/primitives/estimator.py b/qiskit_scaleway/primitives/estimator.py index d75b7a1..8659564 100644 --- a/qiskit_scaleway/primitives/estimator.py +++ b/qiskit_scaleway/primitives/estimator.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from qiskit.primitives import BackendEstimatorV2 -from qiskit.primitives.backend_estimator import ( +from qiskit.primitives.backend_estimator_v2 import ( _prepare_counts, _run_circuits, ) diff --git a/qiskit_scaleway/primitives/estimator_v1.py b/qiskit_scaleway/primitives/estimator_v1.py deleted file mode 100644 index ecc8ac9..0000000 --- a/qiskit_scaleway/primitives/estimator_v1.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2025 Scaleway -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from qiskit.primitives import BackendEstimator - - -class Estimator(BackendEstimator): - def __init__( - self, - backend, - session_id: str, - options: dict | None = None, - abelian_grouping: bool = True, - skip_transpilation: bool = False, - ): - if not session_id: - raise Exception("session_id must be not None") - - self._session_id = session_id - - super().__init__( - backend, - bound_pass_manager=backend.get_pass_manager(), - options=options, - abelian_grouping=abelian_grouping, - skip_transpilation=skip_transpilation, - ) - - def _run(self, circuits, observables, parameter_values, **run_options): - run_options["session_id"] = self._session_id - return super()._run(circuits, observables, parameter_values, **run_options) diff --git a/requirements.txt b/requirements.txt index 7669c08..2abc369 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -qiskit==1.4.2 -qiskit-aer==0.17 +qiskit~=2.1 +qiskit-aer~=0.17 randomname>=0.2.1 dataclasses-json>=0.6.4 dataclasses>=0.6 -scaleway-qaas-client>=0.1.16 \ No newline at end of file +scaleway-qaas-client>=0.1.23 \ No newline at end of file diff --git a/setup.py b/setup.py index c72513c..bafa476 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ setup( name="qiskit_scaleway", - version="0.2.12", + version="0.3.0", project_urls={ "Documentation": "https://www.scaleway.com/en/quantum-as-a-service/", "Source": "https://github.com/scaleway/qiskit-scaleway", diff --git a/tests/test_aer_multiple_circuits.py b/tests/test_aer_multiple_circuits.py index 01a99e6..450614c 100644 --- a/tests/test_aer_multiple_circuits.py +++ b/tests/test_aer_multiple_circuits.py @@ -47,13 +47,16 @@ def _random_qiskit_circuit(size: int) -> QuantumCircuit: def test_aer_multiple_circuits(): + provider = ScalewayProvider( project_id=os.environ["QISKIT_SCALEWAY_PROJECT_ID"], secret_key=os.environ["QISKIT_SCALEWAY_SECRET_KEY"], url=os.getenv("QISKIT_SCALEWAY_API_URL"), ) - backend = provider.get_backend("aer_simulation_pop_c16m128") + backend = provider.get_backend( + os.getenv("QISKIT_SCALEWAY_BACKEND_NAME", "aer_simulation_pop_c16m128") + ) assert backend is not None @@ -86,3 +89,89 @@ def test_aer_multiple_circuits(): assert result.success finally: backend.delete_session(session_id) + + +def _get_noise_model(): + import qiskit_aer.noise as noise + + # Error probabilities (exaggerated to get a noticeable effect for demonstration) + prob_1 = 0.01 # 1-qubit gate + prob_2 = 0.1 # 2-qubit gate + + # Depolarizing quantum errors + error_1 = noise.depolarizing_error(prob_1, 1) + error_2 = noise.depolarizing_error(prob_2, 2) + + # Add errors to noise model + noise_model = noise.NoiseModel() + noise_model.add_all_qubit_quantum_error(error_1, ["rz", "sx", "x"]) + noise_model.add_all_qubit_quantum_error(error_2, ["cx"]) + + return noise_model + + +def _bell_state_circuit(): + qc = QuantumCircuit(2, 2) + qc.h(0) + qc.cx(0, 1) + qc.measure_all() + return qc + + +def _simple_one_state_circuit(): + qc = QuantumCircuit(1, 1) + qc.x(0) + qc.measure_all() + return qc + + +def test_aer_with_noise_model(): + + provider = ScalewayProvider( + project_id=os.environ["QISKIT_SCALEWAY_PROJECT_ID"], + secret_key=os.environ["QISKIT_SCALEWAY_SECRET_KEY"], + url=os.getenv("QISKIT_SCALEWAY_API_URL"), + ) + + backend = provider.get_backend( + os.getenv("QISKIT_SCALEWAY_BACKEND_NAME", "aer_simulation_pop_c16m128") + ) + + assert backend is not None + + session_id = backend.start_session( + name="my-aer-session-autotest", + deduplication_id=f"my-aer-session-autotest-{random.randint(1, 1000)}", + max_duration="15m", + ) + + assert session_id is not None + + try: + qc1 = _bell_state_circuit() + qc2 = _simple_one_state_circuit() + + run_ideal_result = backend.run( + [qc1, qc2], + shots=1000, + max_parallel_experiments=0, + session_id=session_id, + ).result() + + run_noisy_result = backend.run( + [qc1, qc2], + shots=1000, + max_parallel_experiments=0, + session_id=session_id, + noise_model=_get_noise_model(), + ).result() + + ideal_results = run_ideal_result.results + noisy_results = run_noisy_result.results + + assert len(ideal_results) == len(noisy_results) == 2 + + for i, ideal_result in enumerate(ideal_results): + assert len(ideal_result.data.counts) < len(noisy_results[i].data.counts) + finally: + backend.delete_session(session_id) diff --git a/tests/test_estimator.py b/tests/test_estimator.py index 061f871..ba6938f 100644 --- a/tests/test_estimator.py +++ b/tests/test_estimator.py @@ -32,7 +32,9 @@ def test_estimator(): url=os.getenv("QISKIT_SCALEWAY_API_URL"), ) - backend = provider.get_backend("aer_simulation_pop_c16m128") + backend = provider.get_backend( + os.getenv("QISKIT_SCALEWAY_BACKEND_NAME", "aer_simulation_pop_c16m128") + ) assert backend is not None diff --git a/tests/test_qsim_simple_circuit.py b/tests/test_qsim_simple_circuit.py index 6940bfa..c127648 100644 --- a/tests/test_qsim_simple_circuit.py +++ b/tests/test_qsim_simple_circuit.py @@ -25,7 +25,9 @@ def test_qsim_simple_circuit(): url=os.getenv("QISKIT_SCALEWAY_API_URL"), ) - backend = provider.get_backend("qsim_simulation_pop_c16m128") + backend = provider.get_backend( + os.getenv("QSIM_SCALEWAY_BACKEND_NAME", "qsim_simulation_pop_c16m128") + ) assert backend is not None diff --git a/tests/test_sampler.py b/tests/test_sampler.py index 6eacc70..2ca5a7b 100644 --- a/tests/test_sampler.py +++ b/tests/test_sampler.py @@ -15,8 +15,7 @@ import numpy as np import random -from qiskit import transpile -from qiskit.circuit.library import IQP +from qiskit.circuit.library import iqp from qiskit.quantum_info import random_hermitian from qiskit_scaleway import ScalewayProvider @@ -30,7 +29,9 @@ def test_sampler(): url=os.getenv("QISKIT_SCALEWAY_API_URL"), ) - backend = provider.get_backend("aer_simulation_pop_c16m128") + backend = provider.get_backend( + os.getenv("QISKIT_SCALEWAY_BACKEND_NAME", "aer_simulation_pop_c16m128") + ) assert backend is not None @@ -47,11 +48,10 @@ def test_sampler(): n_qubits = 10 mat = np.real(random_hermitian(n_qubits, seed=1234)) - circuit = IQP(mat) + circuit = iqp(mat) circuit.measure_all() - isa_circuit = transpile(circuit, backend=backend, optimization_level=1) - job = sampler.run([isa_circuit], shots=100) + job = sampler.run([circuit], shots=100) result = job.result() assert result is not None