From 86b2992ad4bb1338de850028af96405ab8432f6b Mon Sep 17 00:00:00 2001 From: Valentin Macheret Date: Fri, 14 Nov 2025 13:54:11 +0100 Subject: [PATCH 1/2] feat(sampler): fix sampling base conversion --- qiskit_scaleway/backends/base_job.py | 2 +- qiskit_scaleway/primitives/sampler.py | 84 ++++++++++++++++++++++++++- tests/test_aer_multiple_circuits.py | 38 ++---------- tests/test_aqt_multiple_circuits.py | 38 ++---------- 4 files changed, 94 insertions(+), 68 deletions(-) diff --git a/qiskit_scaleway/backends/base_job.py b/qiskit_scaleway/backends/base_job.py index 44d1c5d..fd2781a 100644 --- a/qiskit_scaleway/backends/base_job.py +++ b/qiskit_scaleway/backends/base_job.py @@ -80,7 +80,7 @@ def submit(self, session_id: str) -> None: options = self._config.copy() shots = options.pop("shots") - memory = options.pop("memory") + memory = options.pop("memory", False) programs = map(lambda c: QuantumProgram.from_qiskit_circuit(c), self._circuits) diff --git a/qiskit_scaleway/primitives/sampler.py b/qiskit_scaleway/primitives/sampler.py index d01e231..85fa34d 100644 --- a/qiskit_scaleway/primitives/sampler.py +++ b/qiskit_scaleway/primitives/sampler.py @@ -11,7 +11,24 @@ # 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 BackendSamplerV2 +import numpy as np +import re + +from qiskit.primitives.backend_sampler_v2 import ( + BackendSamplerV2, + _MeasureInfo, + _samples_to_packed_array, + QiskitError, + ResultMemory, +) + +from qiskit.primitives.containers import ( + BitArray, + DataBin, + SamplerPubResult, +) + +_NON_BINARY_CHARS = re.compile(r"[^01]") class Sampler(BackendSamplerV2): @@ -37,3 +54,68 @@ def __init__( backend=backend, options=options, ) + + def _postprocess_pub( + self, + result_memory: list[ResultMemory], + shots: int, + shape: tuple[int, ...], + meas_info: list[_MeasureInfo], + max_num_bytes: int, + circuit_metadata: dict, + meas_level: int | None, + ) -> SamplerPubResult: + """Converts the memory data into a sampler pub result + + For level 2 data, the memory data are stored in an array of bit arrays + with the shape of the pub. For level 1 data, the data are stored in a + complex numpy array. + """ + if meas_level == 2 or meas_level is None: + arrays = { + item.creg_name: np.zeros( + shape + (shots, item.num_bytes), dtype=np.uint8 + ) + for item in meas_info + } + memory_array = _memory_array(result_memory, max_num_bytes) + + for samples, index in zip(memory_array, np.ndindex(*shape)): + for item in meas_info: + ary = _samples_to_packed_array(samples, item.num_bits, item.start) + arrays[item.creg_name][index] = ary + + meas = { + item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) + for item in meas_info + } + elif meas_level == 1: + raw = np.array(result_memory) + cplx = raw[..., 0] + 1j * raw[..., 1] + cplx = np.reshape(cplx, (*shape, *cplx.shape[1:])) + meas = {item.creg_name: cplx for item in meas_info} + else: + raise QiskitError(f"Unsupported meas_level: {meas_level}") + return SamplerPubResult( + DataBin(**meas, shape=shape), + metadata={"shots": shots, "circuit_metadata": circuit_metadata}, + ) + + +def _memory_array(results: list[list[str]], num_bytes: int) -> np.NDArray[np.uint8]: + """Converts the memory data into an array in an unpacked way.""" + lst = [] + # Heuristic: check only the first result format + if len(results) > 0 and len(results[0]) > 0: + base = 16 if _NON_BINARY_CHARS.search(results[0][0]) else 2 + + for memory in results: + if num_bytes > 0: + data = b"".join(int(i, base).to_bytes(num_bytes, "big") for i in memory) + data = np.frombuffer(data, dtype=np.uint8).reshape(-1, num_bytes) + else: + # no measure in a circuit + data = np.zeros((len(memory), num_bytes), dtype=np.uint8) + lst.append(data) + ary = np.asarray(lst) + return np.unpackbits(ary, axis=-1, bitorder="big") diff --git a/tests/test_aer_multiple_circuits.py b/tests/test_aer_multiple_circuits.py index 450614c..eac5c57 100644 --- a/tests/test_aer_multiple_circuits.py +++ b/tests/test_aer_multiple_circuits.py @@ -12,42 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. import os -import numpy as np import random from qiskit import QuantumCircuit from qiskit_scaleway import ScalewayProvider - -def _random_qiskit_circuit(size: int) -> QuantumCircuit: - num_qubits = size - num_gate = size - - qc = QuantumCircuit(num_qubits) - - for _ in range(num_gate): - random_gate = np.random.choice(["unitary", "cx", "cy", "cz"]) - - if random_gate == "cx" or random_gate == "cy" or random_gate == "cz": - control_qubit = np.random.randint(0, num_qubits) - target_qubit = np.random.randint(0, num_qubits) - - while target_qubit == control_qubit: - target_qubit = np.random.randint(0, num_qubits) - - getattr(qc, random_gate)(control_qubit, target_qubit) - else: - for q in range(num_qubits): - random_gate = np.random.choice(["h", "x", "y", "z"]) - getattr(qc, random_gate)(q) - - qc.measure_all() - - return qc +from qio.utils.circuit import random_square_qiskit_circuit def test_aer_multiple_circuits(): - provider = ScalewayProvider( project_id=os.environ["QISKIT_SCALEWAY_PROJECT_ID"], secret_key=os.environ["QISKIT_SCALEWAY_SECRET_KEY"], @@ -69,10 +42,10 @@ def test_aer_multiple_circuits(): assert session_id is not None try: - qc1 = _random_qiskit_circuit(20) - qc2 = _random_qiskit_circuit(15) - qc3 = _random_qiskit_circuit(21) - qc4 = _random_qiskit_circuit(17) + qc1 = random_square_qiskit_circuit(20) + qc2 = random_square_qiskit_circuit(15) + qc3 = random_square_qiskit_circuit(21) + qc4 = random_square_qiskit_circuit(17) run_result = backend.run( [qc1, qc2, qc3, qc4], @@ -126,7 +99,6 @@ def _simple_one_state_circuit(): def test_aer_with_noise_model(): - provider = ScalewayProvider( project_id=os.environ["QISKIT_SCALEWAY_PROJECT_ID"], secret_key=os.environ["QISKIT_SCALEWAY_SECRET_KEY"], diff --git a/tests/test_aqt_multiple_circuits.py b/tests/test_aqt_multiple_circuits.py index 2d0ec7d..3207d2e 100644 --- a/tests/test_aqt_multiple_circuits.py +++ b/tests/test_aqt_multiple_circuits.py @@ -12,39 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. import os -import numpy as np import random -from qiskit import QuantumCircuit from qiskit_scaleway import ScalewayProvider - -def _random_qiskit_circuit(size: int) -> QuantumCircuit: - num_qubits = size - num_gate = size - - qc = QuantumCircuit(num_qubits) - - for _ in range(num_gate): - random_gate = np.random.choice(["unitary", "cx", "cy", "cz"]) - - if random_gate == "cx" or random_gate == "cy" or random_gate == "cz": - control_qubit = np.random.randint(0, num_qubits) - target_qubit = np.random.randint(0, num_qubits) - - while target_qubit == control_qubit: - target_qubit = np.random.randint(0, num_qubits) - - getattr(qc, random_gate)(control_qubit, target_qubit) - else: - for q in range(num_qubits): - random_gate = np.random.choice(["h", "x", "y", "z"]) - getattr(qc, random_gate)(q) - - qc.measure_all() - - return qc - +from qio.utils.circuit import random_square_qiskit_circuit def test_aqt_multiple_circuits(): provider = ScalewayProvider( @@ -66,10 +38,10 @@ def test_aqt_multiple_circuits(): assert session_id is not None try: - qc1 = _random_qiskit_circuit(10) - qc2 = _random_qiskit_circuit(12) - qc3 = _random_qiskit_circuit(9) - qc4 = _random_qiskit_circuit(10) + qc1 = random_square_qiskit_circuit(10) + qc2 = random_square_qiskit_circuit(12) + qc3 = random_square_qiskit_circuit(9) + qc4 = random_square_qiskit_circuit(10) run_result = backend.run( [qc1, qc2, qc3, qc4], From 501a10d87ce4d334a070b63908f7ef80afd59bfa Mon Sep 17 00:00:00 2001 From: Valentin Macheret Date: Fri, 14 Nov 2025 13:57:19 +0100 Subject: [PATCH 2/2] feat(sampler): fix sampling base conversion --- qiskit_scaleway/primitives/sampler.py | 4 +++- tests/test_aqt_multiple_circuits.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/qiskit_scaleway/primitives/sampler.py b/qiskit_scaleway/primitives/sampler.py index 85fa34d..a6361c8 100644 --- a/qiskit_scaleway/primitives/sampler.py +++ b/qiskit_scaleway/primitives/sampler.py @@ -14,6 +14,8 @@ import numpy as np import re +from numpy.typing import NDArray + from qiskit.primitives.backend_sampler_v2 import ( BackendSamplerV2, _MeasureInfo, @@ -102,7 +104,7 @@ def _postprocess_pub( ) -def _memory_array(results: list[list[str]], num_bytes: int) -> np.NDArray[np.uint8]: +def _memory_array(results: list[list[str]], num_bytes: int) -> NDArray[np.uint8]: """Converts the memory data into an array in an unpacked way.""" lst = [] # Heuristic: check only the first result format diff --git a/tests/test_aqt_multiple_circuits.py b/tests/test_aqt_multiple_circuits.py index 3207d2e..a4194e4 100644 --- a/tests/test_aqt_multiple_circuits.py +++ b/tests/test_aqt_multiple_circuits.py @@ -18,6 +18,7 @@ from qio.utils.circuit import random_square_qiskit_circuit + def test_aqt_multiple_circuits(): provider = ScalewayProvider( project_id=os.environ["QISKIT_SCALEWAY_PROJECT_ID"],