From 221200b5b6c340830d4d040c6f8c4bc80533727e Mon Sep 17 00:00:00 2001 From: Mathieu Gras Date: Mon, 18 May 2026 15:16:16 +0200 Subject: [PATCH 01/11] feat: added adjust measure --- .../measurement/expectation_value.py | 43 +----- mpqp/execution/providers/aws.py | 13 +- mpqp/execution/providers/ibm.py | 3 + mpqp/execution/runner.py | 113 ++++++++++++--- mpqp/tools/circuit.py | 9 +- mpqp/tools/maths.py | 135 +++++++++++++++++- .../instruction/gates/test_custom_gate.py | 16 ++- .../measurement/test_expectation_value.py | 17 --- tests/test_doc.py | 2 + tests/tools/test_math.py | 39 ++++- 10 files changed, 297 insertions(+), 93 deletions(-) diff --git a/mpqp/core/instruction/measurement/expectation_value.py b/mpqp/core/instruction/measurement/expectation_value.py index d09a9c0da..fcd8d56cc 100644 --- a/mpqp/core/instruction/measurement/expectation_value.py +++ b/mpqp/core/instruction/measurement/expectation_value.py @@ -9,12 +9,10 @@ class to define your observable, and a :class:`ExpectationMeasure` to perform from numbers import Real from typing import TYPE_CHECKING, Literal, Optional, Union, overload from typing_extensions import Never -from warnings import warn import numpy as np import numpy.typing as npt -from mpqp.core.instruction.gates.native_gates import SWAP from mpqp.core.instruction.measurement.measure import Measure from mpqp.core.instruction.measurement.pauli_string import ( CommutingTypes, @@ -484,47 +482,8 @@ def _check_targets_order(self): """Ensures target qubits are ordered and contiguous, rearranging them if necessary (private).""" - if len(self.targets) == 0: - self._pre_measure: list[Gate] = [] - return - - if self.nb_qubits != self.observables[0].nb_qubits: - raise NumberQubitsError( - f"Target size {self.nb_qubits} doesn't match observable size " - f"{self.observables[0].nb_qubits}." - ) - + self.rearranged_targets = list(self.targets) self._pre_measure: list[Gate] = [] - """List of Gates added before the expectation measurement to correctly swap - target qubits when their are not ordered or contiguous.""" - targets_is_ordered = all( - [self.targets[i] > self.targets[i - 1] for i in range(1, len(self.targets))] - ) - tweaked_tgt = copy.copy(self.targets) - if ( - max(tweaked_tgt) - min(tweaked_tgt) + 1 != len(tweaked_tgt) - or not targets_is_ordered - ): - warn( - "Non contiguous or non sorted observable target will introduce " - "additional CNOTs." - ) - - for t_index, target in enumerate(tweaked_tgt): # sort the targets - min_index = tweaked_tgt.index(min(tweaked_tgt[t_index:])) - if t_index != min_index: - self._pre_measure.append(SWAP(target, tweaked_tgt[min_index])) - tweaked_tgt[t_index] = tweaked_tgt[min_index] - tweaked_tgt[min_index] = target - for t_index, target in enumerate(tweaked_tgt): # compact the targets - if t_index == 0: - continue - if target != tweaked_tgt[t_index - 1] + 1: - self._pre_measure.append(SWAP(target, tweaked_tgt[t_index - 1] + 1)) - tweaked_tgt[t_index] = tweaked_tgt[t_index - 1] + 1 - self.rearranged_targets = tweaked_tgt - """Adjusted list of target qubits when they are not initially sorted and - contiguous.""" @property def pre_measure(self) -> list[Gate]: diff --git a/mpqp/execution/providers/aws.py b/mpqp/execution/providers/aws.py index 8c83f6a98..267a83209 100644 --- a/mpqp/execution/providers/aws.py +++ b/mpqp/execution/providers/aws.py @@ -187,11 +187,14 @@ def run_braket_observable(job: Job): if job.measure.pre_transpiled is None: grouping = job.measure.get_pauli_grouping() + pre_measure = [ + QCircuit(find_qubitwise_rotations(group)) for group in grouping + ] + for circuit in pre_measure: + for instr in circuit.instructions: + instr.targets[0] = job.measure.targets[instr.targets[0]] transpiled_pre_measures = [ - QCircuit(find_qubitwise_rotations(group)).to_other_language( - Language.BRAKET - ) - for group in grouping + pre_m.to_other_language(Language.BRAKET) for pre_m in pre_measure ] eigenvalues = [ {monom.name: pauli_monomial_eigenvalues(monom) for monom in group} @@ -226,7 +229,7 @@ def run_braket_observable(job: Job): ) result = local_result.result() assert isinstance(result, GateModelQuantumTaskResult) - length = 2**job.circuit.nb_qubits + length = 2**job.measure.nb_qubits sorted_values: list[float] = [] for i in range(length): binary_state = f"{bin(i)[2:].zfill(len(bin(length))- 3)}" diff --git a/mpqp/execution/providers/ibm.py b/mpqp/execution/providers/ibm.py index e355c875e..4a2aa4533 100644 --- a/mpqp/execution/providers/ibm.py +++ b/mpqp/execution/providers/ibm.py @@ -106,6 +106,9 @@ def compute_expectation_value( if TYPE_CHECKING: assert isinstance(translated, SparsePauliOp) qiskit_observables.append(translated) + qiskit_observables = [ + obs.apply_layout(ibm_circuit.layout) for obs in qiskit_observables + ] if isinstance(job.device, StaticIBMSimulatedDevice) or nb_shots != 0: from qiskit_ibm_runtime import EstimatorV2 as Runtime_Estimator diff --git a/mpqp/execution/runner.py b/mpqp/execution/runner.py index abc2bb3ad..ed624350e 100644 --- a/mpqp/execution/runner.py +++ b/mpqp/execution/runner.py @@ -55,14 +55,16 @@ def adjust_measure(measure: ExpectationMeasure, circuit: QCircuit): + # TODO: to enhance docs + """We allow the measure to not span the entire circuit, but providers usually do not support this behavior. To make this work, we tweak the measure this function to match the expected behavior. In order to do this, we add identity measures on the qubits not targeted by - the measure. In addition to this, some swaps are automatically added so the - the qubits measured are ordered and contiguous (though this is done in - :func:`generate_job`) + the measure. pauli observables are directly embeded on their target qubits, + while matrix observables are padded with identity matrices when the targets + are ordered and contiguous, otherwise are embedded through their pauli decomposition. Args: measure: The expectation measure, potentially incomplete. @@ -78,25 +80,83 @@ def adjust_measure(measure: ExpectationMeasure, circuit: QCircuit): if measure.targets == list(range(circuit.nb_qubits)): return measure - tweaked_observables = [] - n_before = measure.rearranged_targets[0] - n_after = circuit.nb_qubits - measure.rearranged_targets[-1] - 1 + nb_qubits = circuit.nb_qubits + targets = measure.targets + + targets_is_ordered = all( + [targets[i] > targets[i - 1] for i in range(1, len(targets))] + ) + if not targets_is_ordered: + ordered_targets = sorted(targets) + contiguous_targets = [targets.index(t) for t in ordered_targets] + for obs in measure.observables: + if ( + obs._matrix is None # pyright: ignore[reportPrivateUsage] + or measure.optimize_measurement + ): # Order pauli string + from mpqp.tools.maths import rearrange_pauli_string + + obs._pauli_string = ( # pyright: ignore[reportPrivateUsage] + rearrange_pauli_string(obs.pauli_string, contiguous_targets) + ) + else: # Order the matrix + from mpqp.tools.maths import rearrange_matrix + + obs.matrix = rearrange_matrix(obs.matrix, contiguous_targets) + + measure.targets = sorted(targets) + targets_is_contiguous = ( + len(targets) > 0 + and targets_is_ordered + and (targets[-1] - targets[0] + 1 == len(targets)) + ) + + tweaked_observables: list[Observable] = [] + for obs in measure.observables: - if obs._pauli_string is not None: # pyright: ignore[reportPrivateUsage] - from mpqp.measures import pI + from mpqp.core.instruction.measurement.pauli_string import ( + PauliString, + PauliStringMonomial, + ) + from mpqp.measures import pI + + if ( + obs._pauli_string is None # pyright: ignore[reportPrivateUsage] + and targets_is_contiguous + ): + n_before = targets[0] + n_after = nb_qubits - targets[-1] - 1 + + full_matrix = obs.matrix - pauli = pI(n_before - 1) @ obs.pauli_string @ pI(n_after - 1) - tweaked_observables.append(Observable(pauli)) - else: Id_before = np.eye(2**n_before) Id_after = np.eye(2**n_after) + + if n_before > 0: + full_matrix = np.kron(Id_before, full_matrix) + + if n_after > 0: + full_matrix = np.kron(full_matrix, Id_after) + tweaked_observables.append( Observable( - np.kron( - np.kron(Id_before, obs.matrix), Id_after - ) # pyright: ignore[reportArgumentType] + full_matrix, label=obs.label # pyright: ignore[reportArgumentType] ) ) + continue + + pauli = obs.pauli_string + embedded = PauliString() + + for mono in pauli.monomials: + full_register = [pI] * nb_qubits + + for local_idx, target in enumerate(targets): + full_register[target] = mono.atoms[local_idx] + + embedded += PauliStringMonomial(mono.coef, full_register) + + tweaked_observables.append(Observable(embedded.simplify(), label=obs.label)) tweaked_measure = ExpectationMeasure( tweaked_observables, @@ -104,7 +164,9 @@ def adjust_measure(measure: ExpectationMeasure, circuit: QCircuit): measure.shots, measure.commuting_type, measure.grouping_method, + label=measure.label, optimize_measurement=measure.optimize_measurement, + optim_diagonal=measure.optim_diagonal, ) return tweaked_measure @@ -145,14 +207,21 @@ def generate_job( else: job = Job(JobType.SAMPLE, circuit, device) elif isinstance(measurement, ExpectationMeasure): - m = adjust_measure(measurement, circuit) - c = circuit.without_measurements(deep_copy=False) - c.add(m) - job = Job( - JobType.OBSERVABLE, - c, - device, - ) + if measurement.optimize_measurement and isinstance(device, AWSDevice): + job = Job( + JobType.OBSERVABLE, + circuit, + device, + ) + else: + m = adjust_measure(measurement, circuit) + c = circuit.without_measurements(deep_copy=False) + c.add(m) + job = Job( + JobType.OBSERVABLE, + c, + device, + ) else: raise NotImplementedError( f"Measurement type {type(measurement)} not handled" diff --git a/mpqp/tools/circuit.py b/mpqp/tools/circuit.py index 16b1fbe2b..41ba26847 100644 --- a/mpqp/tools/circuit.py +++ b/mpqp/tools/circuit.py @@ -41,6 +41,7 @@ def random_circuit( gate_classes: Optional[Sequence[type[Gate]]] = None, nb_qubits: int = 5, nb_gates: Optional[int] = None, + use_all_qubits: bool = False, seed: Optional[int] = None, ): """This function creates a QCircuit with a specified number of qubits and gates. @@ -86,6 +87,12 @@ def random_circuit( qcircuit = QCircuit(nb_qubits) for _ in range(nb_gates): qcircuit.add(random_gate(gate_classes, nb_qubits, rng)) + + if use_all_qubits: # used in case we want to test braket + from mpqp.gates import H + + for i in range(nb_qubits): + qcircuit.add(H(i)) return qcircuit @@ -107,7 +114,7 @@ def statevector_from_random_circuit( Examples: >>> print(statevector_from_random_circuit(2, seed=123)) # doctest: +NORMALIZE_WHITESPACE - [0.70710678+0.j 0. -0.j 0.26893257-0.65396886j 0. -0.j ] + [-0.07978925-0.49359262j -0.07978925+0.49359262j -0.07978925-0.49359262j -0.07978925+0.49359262j] """ from mpqp.execution import IBMDevice, Result, run diff --git a/mpqp/tools/maths.py b/mpqp/tools/maths.py index dfa5e0534..1918ffa1e 100644 --- a/mpqp/tools/maths.py +++ b/mpqp/tools/maths.py @@ -11,10 +11,11 @@ import numpy as np import numpy.typing as npt from scipy.linalg import inv, sqrtm +from typeguard import typechecked if TYPE_CHECKING: from sympy import Expr - + from mpqp.core.instruction.measurement.pauli_string import PauliString from mpqp.tools.generics import Matrix @@ -478,3 +479,135 @@ def is_power_of_two(n: int) -> bool: """ return n >= 1 and (n & (n - 1)) == 0 + + +@typechecked +def rearrange_matrix(m: Matrix, targets: list[int], copy: bool = True) -> Matrix: + """Function to reorder the rows and columns of a matrix in order to change the targets of a gate. + The intended order for a gate is having continuous targets in growing order. + + For example the targets for a 3 qubit gate should be [1,2,3], changing it for [3,2,1] would + reverse the effects on the qubits 3 and 1 (akin to a SWAP gate on those qubits). + + Note: This function's goal is not to move around a gate in a circuit but to shuffle the targets in a sense. + + Args: + m: The matrix for which we want to reorder the targets. + targets: The targets + copy: If True performs the copy of the matrix, to prevent overwriting the original matrix. + + Returns: + The shuffled matrix according to the given targets. + + Example: + >>> I = np.eye(2) + >>> X = np.array([[0,1], [1,0]]) + >>> matrix = np.kron(I, X) + >>> pprint(matrix) + [[0, 1, 0, 0], + [1, 0, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0]] + >>> pprint(rearrange_matrix(matrix, [1,0])) + [[0, 0, 1, 0], + [0, 0, 0, 1], + [1, 0, 0, 0], + [0, 1, 0, 0]] + """ + from copy import deepcopy + + if copy: + matrix = deepcopy(m) + else: + matrix = m + l = len(targets) + targets = deepcopy(targets) + shuffled = sorted(targets) + for index in range(l - 1): + if targets[index] == index: + continue + # If no swaps happened of the target then shuffled_index = targets[index] + shuffled_index = shuffled.index(targets[index]) + + i = 1 << (l - 1 - shuffled_index) + j = 1 << (l - 1 - index) + for change in range(1 << l): + current = bin(change)[2:].zfill(l) + if current[shuffled_index - l] == "0" and current[index - l] == "1": + current = int(current, 2) + conjugate = current + i - j + for k in range(len(matrix)): + hold = matrix[k][current] + matrix[k][current] = matrix[k][conjugate] + matrix[k][conjugate] = hold + + for k in range(len(matrix)): + hold = matrix[current][k] + matrix[current][k] = matrix[conjugate][k] + matrix[conjugate][k] = hold + + # keeps tracks of the position of the targets in the matrix + + shuffled[index], shuffled[targets[index]] = ( + shuffled[targets[index]], + shuffled[index], + ) + + i = targets.index(index) + targets[i], targets[index] = ( + targets[index], + targets[i], + ) + return matrix + + +def rearrange_pauli_string( + ps: PauliString, targets: list[int], copy: bool = True +) -> PauliString: + """ + This function aims at reorderring a pauli string's monomials according to unordered targets. + + Note: The targets must be contiguous, otherwise the algorithm won't work. + Args: + ps: The PauliString to reorder. + targets: The list of unordered targets. + copy: If set at True will deepcopy the initial string ps. + + Examples: + >>> ps = pX @ pI + >>> print(rearrange_pauli_string(ps, [1,0])) + pI@pX + >>> ps2 = pX @ pI + pI @ pX + >>> print(rearrange_pauli_string(ps2, [1,0])) + pI@pX + pX@pI + """ + if copy: + from copy import deepcopy + + pauli = deepcopy(ps) + else: + pauli = ps + + l = len(targets) + shuffled = sorted(targets) + for index in range(l): + if targets[index] == index: + continue + shuffled_index = shuffled.index(targets[index]) + for monom in pauli.monomials: + atoms = monom.atoms + atoms[shuffled_index], atoms[shuffled[index]] = ( + atoms[shuffled[index]], + atoms[shuffled_index], + ) + shuffled[index], shuffled[targets[index]] = ( + shuffled[targets[index]], + shuffled[index], + ) + + i = targets.index(index) + targets[i], targets[index] = ( + targets[index], + targets[i], + ) + return pauli diff --git a/tests/core/instruction/gates/test_custom_gate.py b/tests/core/instruction/gates/test_custom_gate.py index 37c6c3aa8..146402c06 100644 --- a/tests/core/instruction/gates/test_custom_gate.py +++ b/tests/core/instruction/gates/test_custom_gate.py @@ -1,4 +1,5 @@ from functools import reduce +from typing import Optional import numpy as np import pytest @@ -64,7 +65,10 @@ def exec_random_orthogonal_matrix(circ_size: int, device: AvailableDevice): result = run(c, device) # we reduce the precision because of approximation errors coming from CustomGate usage - assert isinstance(result, Result) + from mpqp.tools import pprint + + pprint(result.amplitudes) + pprint(exp_state_vector) assert matrix_eq(result.amplitudes, exp_state_vector, 1e-5, 1e-5) @@ -124,7 +128,9 @@ def test_custom_gate_with_random_circuit_qiskit(circ_size: int): @pytest.mark.provider("braket") @pytest.mark.parametrize("circ_size", range(1, 6)) def test_custom_gate_with_random_circuit_braket(circ_size: int): - exec_custom_gate_with_random_circuit(circ_size, AWSDevice.BRAKET_LOCAL_SIMULATOR) + exec_custom_gate_with_random_circuit( + circ_size, AWSDevice.BRAKET_LOCAL_SIMULATOR, True + ) @pytest.mark.provider("cirq") @@ -139,8 +145,10 @@ def test_custom_gate_with_random_circuit_myqlm(circ_size: int): exec_custom_gate_with_random_circuit(circ_size, ATOSDevice.MYQLM_PYLINALG) -def exec_custom_gate_with_random_circuit(circ_size: int, device: AvailableDevice): - random_circ = random_circuit(nb_qubits=circ_size) +def exec_custom_gate_with_random_circuit( + circ_size: int, device: AvailableDevice, use_all_qubits: bool = False +): + random_circ = random_circuit(nb_qubits=circ_size, use_all_qubits=use_all_qubits) matrix = random_circ.to_matrix() custom_gate_circ = QCircuit([CustomGate(matrix, list(range(circ_size)))]) diff --git a/tests/core/instruction/measurement/test_expectation_value.py b/tests/core/instruction/measurement/test_expectation_value.py index 82ae65e36..9ba7a3e54 100644 --- a/tests/core/instruction/measurement/test_expectation_value.py +++ b/tests/core/instruction/measurement/test_expectation_value.py @@ -24,23 +24,6 @@ def test_expectation_measure_right_targets(targets: list[int]): ExpectationMeasure(obs, targets) -@pytest.mark.parametrize( - "targets, expected_swaps", - [ - ([1, 3, 4], [{2, 3}, {3, 4}]), - ([1, 0, 2], [{1, 0}]), - ([2, 0, 3], [{0, 2}, {1, 2}, {2, 3}]), - ], -) -def test_expectation_measure_wrong_targets( - targets: list[int], expected_swaps: list[tuple[int, int]] -): - obs = Observable(np.diag([1] * 2 ** len(targets))) - with pytest.warns(UserWarning): - measure = ExpectationMeasure(obs, targets) - assert [set(swap.targets) for swap in measure.pre_measure] == expected_swaps - - # TODO: complete this @pytest.fixture def list_to_cirq_pauli() -> ( diff --git a/tests/test_doc.py b/tests/test_doc.py index cc1179ea4..dd561b69d 100644 --- a/tests/test_doc.py +++ b/tests/test_doc.py @@ -133,6 +133,8 @@ rand_product_local_unitaries, rand_unitary_2x2_matrix, rand_unitary_matrix, + rearrange_matrix, + rearrange_pauli_string, ) from mpqp.tools.operators import * from mpqp.tools.pauli_grouping import CommutingTypes, pauli_grouping_greedy diff --git a/tests/tools/test_math.py b/tests/tools/test_math.py index f1b3df9e8..f42b448e8 100644 --- a/tests/tools/test_math.py +++ b/tests/tools/test_math.py @@ -1,9 +1,11 @@ +from cirq import PauliString import numpy as np import pytest from sympy import symbols from mpqp.tools.generics import Matrix -from mpqp.tools.maths import is_hermitian, rand_hermitian_matrix +from mpqp.tools.maths import is_hermitian, rand_hermitian_matrix, rand_unitary_matrix +from mpqp.core.instruction.measurement.pauli_string import pX, pI, pY, pZ x = symbols("x", real=True) @@ -25,3 +27,38 @@ def test_is_hermitian(matrix: Matrix, isHermitian: bool): def test_rand_hermitian(): assert is_hermitian(rand_hermitian_matrix(3)) + + +@pytest.mark.parametrize( + ("matrix", "targets"), + [ + (rand_unitary_matrix(4), [1, 0]), + (rand_unitary_matrix(8), [1, 0, 2]), + (rand_unitary_matrix(8), [2, 0, 1]), + ], +) +def test_rearrange_matrix(matrix: Matrix, targets: list[int]): + from mpqp.gates import CustomGate + from mpqp.tools.maths import rearrange_matrix, matrix_eq + from mpqp import QCircuit + + g = CustomGate(matrix, targets) + m = rearrange_matrix(matrix, targets) + g2 = CustomGate(m, sorted(targets)) + assert matrix_eq(QCircuit([g]).to_matrix(), g2.to_matrix()) + + +@pytest.mark.parametrize( + ("ps", "targets", "expected"), + [ + (pX @ pY, [1, 0], pY @ pX), + (pX @ pI @ pZ, [1, 0, 2], pI @ pX @ pZ), + (pX @ pY @ pZ, [2, 0, 1], pY @ pZ @ pX), + ], +) +def test_rearrange_pauli_string( + ps: PauliString, targets: list[int], expected: PauliString +): + from mpqp.tools.maths import rearrange_pauli_string + + assert rearrange_pauli_string(ps, targets) == expected From f3ba11f159e0fe6f4e96f9b40e416b7443070207 Mon Sep 17 00:00:00 2001 From: Mathieu Gras Date: Mon, 18 May 2026 16:16:15 +0200 Subject: [PATCH 02/11] fix: pyright and tests --- mpqp/core/instruction/measurement/expectation_value.py | 1 - mpqp/tools/circuit.py | 2 +- tests/core/instruction/gates/test_custom_gate.py | 1 - tests/tools/test_math.py | 3 +-- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/mpqp/core/instruction/measurement/expectation_value.py b/mpqp/core/instruction/measurement/expectation_value.py index fcd8d56cc..d25de7619 100644 --- a/mpqp/core/instruction/measurement/expectation_value.py +++ b/mpqp/core/instruction/measurement/expectation_value.py @@ -22,7 +22,6 @@ class to define your observable, and a :class:`ExpectationMeasure` to perform ) from mpqp.core.languages import Language from mpqp.tools.display import one_lined_repr -from mpqp.tools.errors import NumberQubitsError from mpqp.tools.generics import Matrix from mpqp.tools.maths import is_diagonal, is_hermitian, is_power_of_two diff --git a/mpqp/tools/circuit.py b/mpqp/tools/circuit.py index 41ba26847..6437ae224 100644 --- a/mpqp/tools/circuit.py +++ b/mpqp/tools/circuit.py @@ -118,7 +118,7 @@ def statevector_from_random_circuit( """ from mpqp.execution import IBMDevice, Result, run - mpqp_circ = random_circuit(None, nb_qubits, None, seed) + mpqp_circ = random_circuit(None, nb_qubits, None, seed=seed) res = run(mpqp_circ, IBMDevice.AER_SIMULATOR_STATEVECTOR) if TYPE_CHECKING: assert isinstance(res, Result) diff --git a/tests/core/instruction/gates/test_custom_gate.py b/tests/core/instruction/gates/test_custom_gate.py index 146402c06..a9d7dcefe 100644 --- a/tests/core/instruction/gates/test_custom_gate.py +++ b/tests/core/instruction/gates/test_custom_gate.py @@ -1,5 +1,4 @@ from functools import reduce -from typing import Optional import numpy as np import pytest diff --git a/tests/tools/test_math.py b/tests/tools/test_math.py index f42b448e8..9d81276f6 100644 --- a/tests/tools/test_math.py +++ b/tests/tools/test_math.py @@ -1,11 +1,10 @@ -from cirq import PauliString import numpy as np import pytest from sympy import symbols from mpqp.tools.generics import Matrix from mpqp.tools.maths import is_hermitian, rand_hermitian_matrix, rand_unitary_matrix -from mpqp.core.instruction.measurement.pauli_string import pX, pI, pY, pZ +from mpqp.core.instruction.measurement.pauli_string import pX, pI, pY, pZ, PauliString x = symbols("x", real=True) From c7d8dc4dcef194a10b25e5eb80138ea32ab82ee4 Mon Sep 17 00:00:00 2001 From: Mathieu Gras Date: Mon, 18 May 2026 16:23:14 +0200 Subject: [PATCH 03/11] fix: test and cleanup --- mpqp/execution/runner.py | 7 ++----- mpqp/tools/circuit.py | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/mpqp/execution/runner.py b/mpqp/execution/runner.py index ed624350e..3e32645b8 100644 --- a/mpqp/execution/runner.py +++ b/mpqp/execution/runner.py @@ -104,11 +104,8 @@ def adjust_measure(measure: ExpectationMeasure, circuit: QCircuit): obs.matrix = rearrange_matrix(obs.matrix, contiguous_targets) - measure.targets = sorted(targets) - targets_is_contiguous = ( - len(targets) > 0 - and targets_is_ordered - and (targets[-1] - targets[0] + 1 == len(targets)) + targets_is_contiguous = len(targets) > 0 and ( + targets[-1] - targets[0] + 1 == len(sorted(targets)) ) tweaked_observables: list[Observable] = [] diff --git a/mpqp/tools/circuit.py b/mpqp/tools/circuit.py index 6437ae224..8bde64bdb 100644 --- a/mpqp/tools/circuit.py +++ b/mpqp/tools/circuit.py @@ -114,7 +114,8 @@ def statevector_from_random_circuit( Examples: >>> print(statevector_from_random_circuit(2, seed=123)) # doctest: +NORMALIZE_WHITESPACE - [-0.07978925-0.49359262j -0.07978925+0.49359262j -0.07978925-0.49359262j -0.07978925+0.49359262j] + [0.70710678+0.j 0. -0.j 0.26893257-0.65396886j + 0. -0.j ] """ from mpqp.execution import IBMDevice, Result, run From e8b7d912640134196d0c0d18286dd6f9667b8760 Mon Sep 17 00:00:00 2001 From: Mathieu Gras Date: Tue, 19 May 2026 15:30:46 +0200 Subject: [PATCH 04/11] fix: deleted dead code and rework doc --- mpqp/execution/providers/ibm.py | 3 --- mpqp/tools/circuit.py | 7 +++++-- mpqp/tools/maths.py | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/mpqp/execution/providers/ibm.py b/mpqp/execution/providers/ibm.py index 4a2aa4533..e355c875e 100644 --- a/mpqp/execution/providers/ibm.py +++ b/mpqp/execution/providers/ibm.py @@ -106,9 +106,6 @@ def compute_expectation_value( if TYPE_CHECKING: assert isinstance(translated, SparsePauliOp) qiskit_observables.append(translated) - qiskit_observables = [ - obs.apply_layout(ibm_circuit.layout) for obs in qiskit_observables - ] if isinstance(job.device, StaticIBMSimulatedDevice) or nb_shots != 0: from qiskit_ibm_runtime import EstimatorV2 as Runtime_Estimator diff --git a/mpqp/tools/circuit.py b/mpqp/tools/circuit.py index 8bde64bdb..19e12a68c 100644 --- a/mpqp/tools/circuit.py +++ b/mpqp/tools/circuit.py @@ -13,6 +13,7 @@ TOF, CRk, P, + OneQubitNoParamGate, Rk, RotationGate, Rx, @@ -89,10 +90,12 @@ def random_circuit( qcircuit.add(random_gate(gate_classes, nb_qubits, rng)) if use_all_qubits: # used in case we want to test braket - from mpqp.gates import H + gates = np.array( + [g for g in NATIVE_GATES if issubclass(g, OneQubitNoParamGate)] + ) for i in range(nb_qubits): - qcircuit.add(H(i)) + qcircuit.add(rng.choice(gates)(i)) return qcircuit diff --git a/mpqp/tools/maths.py b/mpqp/tools/maths.py index 1918ffa1e..c821725bd 100644 --- a/mpqp/tools/maths.py +++ b/mpqp/tools/maths.py @@ -501,18 +501,18 @@ def rearrange_matrix(m: Matrix, targets: list[int], copy: bool = True) -> Matrix Example: >>> I = np.eye(2) - >>> X = np.array([[0,1], [1,0]]) - >>> matrix = np.kron(I, X) + >>> Z = np.array([[1,0], [0,-1]]) + >>> matrix = np.kron(I, Z) >>> pprint(matrix) - [[0, 1, 0, 0], - [1, 0, 0, 0], - [0, 0, 0, 1], - [0, 0, 1, 0]] + [[1, 0, 0 , 0 ], + [0, 1, 0 , 0 ], + [0, 0, -1, 0 ], + [0, 0, 0 , -1]] >>> pprint(rearrange_matrix(matrix, [1,0])) - [[0, 0, 1, 0], - [0, 0, 0, 1], - [1, 0, 0, 0], - [0, 1, 0, 0]] + [[1, 0 , 0, 0 ], + [0, -1, 0, 0 ], + [0, 0 , 1, 0 ], + [0, 0 , 0, -1]] """ from copy import deepcopy From ae7db31eb037d3dcd7998e9567941fbeb14256fa Mon Sep 17 00:00:00 2001 From: Mathieu Gras Date: Tue, 19 May 2026 15:37:23 +0200 Subject: [PATCH 05/11] fix: test --- mpqp/tools/maths.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mpqp/tools/maths.py b/mpqp/tools/maths.py index c821725bd..ab32f441e 100644 --- a/mpqp/tools/maths.py +++ b/mpqp/tools/maths.py @@ -504,15 +504,15 @@ def rearrange_matrix(m: Matrix, targets: list[int], copy: bool = True) -> Matrix >>> Z = np.array([[1,0], [0,-1]]) >>> matrix = np.kron(I, Z) >>> pprint(matrix) - [[1, 0, 0 , 0 ], - [0, 1, 0 , 0 ], - [0, 0, -1, 0 ], - [0, 0, 0 , -1]] - >>> pprint(rearrange_matrix(matrix, [1,0])) [[1, 0 , 0, 0 ], [0, -1, 0, 0 ], [0, 0 , 1, 0 ], [0, 0 , 0, -1]] + >>> pprint(rearrange_matrix(matrix, [1,0])) + [[1, 0, 0 , 0 ], + [0, 1, 0 , 0 ], + [0, 0, -1, 0 ], + [0, 0, 0 , -1]] """ from copy import deepcopy From 4dfbb16b068a21de406d7b25179854f4d9500b1a Mon Sep 17 00:00:00 2001 From: Mathieu Gras Date: Wed, 27 May 2026 16:30:39 +0200 Subject: [PATCH 06/11] fix: deepcopy targets and deleted check targets order --- mpqp/core/circuit.py | 2 -- mpqp/core/instruction/measurement/expectation_value.py | 10 ++-------- mpqp/tools/maths.py | 5 +++-- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/mpqp/core/circuit.py b/mpqp/core/circuit.py index a681b988a..b875192f7 100644 --- a/mpqp/core/circuit.py +++ b/mpqp/core/circuit.py @@ -383,8 +383,6 @@ def _update_targets_components(self, component: Instruction | NoiseModel): if isinstance(component, Barrier): component.size = self.nb_qubits component.targets = list(range(self.nb_qubits)) - elif isinstance(component, ExpectationMeasure): - component._check_targets_order() # pyright: ignore[reportPrivateUsage] elif isinstance(component, DimensionalNoiseModel): component.check_dimension() elif isinstance(component, BasisMeasure): diff --git a/mpqp/core/instruction/measurement/expectation_value.py b/mpqp/core/instruction/measurement/expectation_value.py index d25de7619..dbb6e13a8 100644 --- a/mpqp/core/instruction/measurement/expectation_value.py +++ b/mpqp/core/instruction/measurement/expectation_value.py @@ -467,7 +467,8 @@ def __init__( if targets is None: self.targets = list(range(observable[0].nb_qubits)) - self._check_targets_order() + self.rearranged_targets = list(self.targets) + self._pre_measure: list[Gate] = [] @property def nb_observables(self) -> int: @@ -477,13 +478,6 @@ def nb_observables(self) -> int: def observables_labels(self) -> list[str]: return [o.label for o in self.observables if o.label is not None] - def _check_targets_order(self): - """Ensures target qubits are ordered and contiguous, rearranging them if - necessary (private).""" - - self.rearranged_targets = list(self.targets) - self._pre_measure: list[Gate] = [] - @property def pre_measure(self) -> list[Gate]: return self._pre_measure diff --git a/mpqp/tools/maths.py b/mpqp/tools/maths.py index ab32f441e..ac341a12c 100644 --- a/mpqp/tools/maths.py +++ b/mpqp/tools/maths.py @@ -581,14 +581,15 @@ def rearrange_pauli_string( >>> print(rearrange_pauli_string(ps2, [1,0])) pI@pX + pX@pI """ - if copy: - from copy import deepcopy + from copy import deepcopy + if copy: pauli = deepcopy(ps) else: pauli = ps l = len(targets) + targets = deepcopy(targets) shuffled = sorted(targets) for index in range(l): if targets[index] == index: From 59fd820a3b67356f8d53117f4894f68d7faf61ac Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Thu, 28 May 2026 11:34:08 +0200 Subject: [PATCH 07/11] chore: clean up old swap related fields --- .../instruction/measurement/expectation_value.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/mpqp/core/instruction/measurement/expectation_value.py b/mpqp/core/instruction/measurement/expectation_value.py index dbb6e13a8..39cd5e216 100644 --- a/mpqp/core/instruction/measurement/expectation_value.py +++ b/mpqp/core/instruction/measurement/expectation_value.py @@ -8,10 +8,10 @@ class to define your observable, and a :class:`ExpectationMeasure` to perform import copy from numbers import Real from typing import TYPE_CHECKING, Literal, Optional, Union, overload -from typing_extensions import Never import numpy as np import numpy.typing as npt +from typing_extensions import Never from mpqp.core.instruction.measurement.measure import Measure from mpqp.core.instruction.measurement.pauli_string import ( @@ -35,8 +35,6 @@ class to define your observable, and a :class:`ExpectationMeasure` to perform from qiskit.quantum_info import SparsePauliOp from sympy import Expr - from mpqp.core.instruction.gates.custom_controlled_gate import Gate - class Observable: """Class defining an observable, used for evaluating expectation values. @@ -334,7 +332,7 @@ def to_other_language( return QLMObservable(self.nb_qubits, matrix=self.matrix) elif language == Language.BRAKET: if self._pauli_string: - from braket.circuits.observables import TensorProduct, Sum + from braket.circuits.observables import Sum, TensorProduct obs = self.pauli_string.to_other_language(Language.BRAKET) if isinstance(obs, TensorProduct): @@ -466,9 +464,7 @@ def __init__( self.observables.append(new_obs) if targets is None: - self.targets = list(range(observable[0].nb_qubits)) - self.rearranged_targets = list(self.targets) - self._pre_measure: list[Gate] = [] + self.targets = list(range(self.observables[0].nb_qubits)) @property def nb_observables(self) -> int: @@ -478,10 +474,6 @@ def nb_observables(self) -> int: def observables_labels(self) -> list[str]: return [o.label for o in self.observables if o.label is not None] - @property - def pre_measure(self) -> list[Gate]: - return self._pre_measure - def get_pauli_grouping(self) -> list[list[PauliStringMonomial]]: """Return the grouped monomials of the Pauli string of the observable. The grouping is done according to the grouping method of the expectation From 9ddb86bed559543a85b57d2c021b23d338c281a0 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Thu, 28 May 2026 12:36:59 +0200 Subject: [PATCH 08/11] fix: AWS pre-measure target remapping --- mpqp/execution/providers/aws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpqp/execution/providers/aws.py b/mpqp/execution/providers/aws.py index 267a83209..542430a77 100644 --- a/mpqp/execution/providers/aws.py +++ b/mpqp/execution/providers/aws.py @@ -192,7 +192,7 @@ def run_braket_observable(job: Job): ] for circuit in pre_measure: for instr in circuit.instructions: - instr.targets[0] = job.measure.targets[instr.targets[0]] + instr.targets = [job.measure.targets[t] for t in instr.targets] transpiled_pre_measures = [ pre_m.to_other_language(Language.BRAKET) for pre_m in pre_measure ] From 6b12bcc8dce3a4dc1933a686c1c77872f476d6bb Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Thu, 28 May 2026 14:11:54 +0200 Subject: [PATCH 09/11] test: remove debugging print --- tests/core/instruction/gates/test_custom_gate.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/core/instruction/gates/test_custom_gate.py b/tests/core/instruction/gates/test_custom_gate.py index a9d7dcefe..70d91b45c 100644 --- a/tests/core/instruction/gates/test_custom_gate.py +++ b/tests/core/instruction/gates/test_custom_gate.py @@ -64,10 +64,6 @@ def exec_random_orthogonal_matrix(circ_size: int, device: AvailableDevice): result = run(c, device) # we reduce the precision because of approximation errors coming from CustomGate usage - from mpqp.tools import pprint - - pprint(result.amplitudes) - pprint(exp_state_vector) assert matrix_eq(result.amplitudes, exp_state_vector, 1e-5, 1e-5) From 01a2724199563b6bfebacd22d6bd78942bc1425d Mon Sep 17 00:00:00 2001 From: Mathieu Gras Date: Fri, 29 May 2026 11:40:38 +0200 Subject: [PATCH 10/11] fix: refacto --- .../instruction/measurement/pauli_string.py | 50 +++++++++++++++++++ .../measurement/test_pauli_string.py | 15 ++++++ tests/tools/test_math.py | 17 ------- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/mpqp/core/instruction/measurement/pauli_string.py b/mpqp/core/instruction/measurement/pauli_string.py index adc0fa4ca..8fbd2fea0 100644 --- a/mpqp/core/instruction/measurement/pauli_string.py +++ b/mpqp/core/instruction/measurement/pauli_string.py @@ -943,6 +943,56 @@ def is_diagonal(self) -> bool: return all([all([a == pI or a == pZ for a in m.atoms]) for m in self.monomials]) + def rearrange(self, targets: list[int], copy: bool = True) -> "PauliString": + """ + This function aims at reorderring a pauli string's monomials according to unordered targets. + + Note: The targets must be contiguous, otherwise the algorithm won't work. + Args: + ps: The PauliString to reorder. + targets: The list of unordered targets. + copy: If set at True will deepcopy the initial string ps. + + Examples: + >>> ps = pX @ pI + >>> print(ps.rearrange([1,0])) + pI@pX + >>> ps2 = pX @ pI + pI @ pX + >>> print(ps2.rearrange([1,0])) + pI@pX + pX@pI + """ + from copy import deepcopy + + if copy: + pauli = deepcopy(self) + else: + pauli = self + + l = len(targets) + targets = deepcopy(targets) + rearranged = sorted(targets) + for index in range(l): + if targets[index] == index: + continue + shuffled_index = rearranged.index(targets[index]) + for monom in pauli.monomials: + atoms = monom.atoms + atoms[shuffled_index], atoms[rearranged[index]] = ( + atoms[rearranged[index]], + atoms[shuffled_index], + ) + rearranged[index], rearranged[targets[index]] = ( + rearranged[targets[index]], + rearranged[index], + ) + + i = targets.index(index) + targets[i], targets[index] = ( + targets[index], + targets[i], + ) + return pauli + class PauliStringMonomial(PauliString): """Represents a monomial in a Pauli string, consisting of a coefficient and diff --git a/tests/core/instruction/measurement/test_pauli_string.py b/tests/core/instruction/measurement/test_pauli_string.py index 4afc9c76e..5ec6b295e 100644 --- a/tests/core/instruction/measurement/test_pauli_string.py +++ b/tests/core/instruction/measurement/test_pauli_string.py @@ -826,3 +826,18 @@ def test_pauli_monomial_from_atom( ): result = atom(prefix) if postfix is None else atom(prefix, postfix) assert result == expected_ps + + +@pytest.mark.parametrize( + ("ps", "targets", "expected"), + [ + (pX @ pY, [1, 0], pY @ pX), + (pX @ pI @ pZ, [1, 0, 2], pI @ pX @ pZ), + (pX @ pY @ pZ, [2, 0, 1], pY @ pZ @ pX), + ], +) +def test_rearrange_pauli_string( + ps: PauliString, targets: list[int], expected: PauliString +): + + assert ps.rearrange(targets) == expected diff --git a/tests/tools/test_math.py b/tests/tools/test_math.py index 9d81276f6..18bb0f46c 100644 --- a/tests/tools/test_math.py +++ b/tests/tools/test_math.py @@ -4,7 +4,6 @@ from mpqp.tools.generics import Matrix from mpqp.tools.maths import is_hermitian, rand_hermitian_matrix, rand_unitary_matrix -from mpqp.core.instruction.measurement.pauli_string import pX, pI, pY, pZ, PauliString x = symbols("x", real=True) @@ -45,19 +44,3 @@ def test_rearrange_matrix(matrix: Matrix, targets: list[int]): m = rearrange_matrix(matrix, targets) g2 = CustomGate(m, sorted(targets)) assert matrix_eq(QCircuit([g]).to_matrix(), g2.to_matrix()) - - -@pytest.mark.parametrize( - ("ps", "targets", "expected"), - [ - (pX @ pY, [1, 0], pY @ pX), - (pX @ pI @ pZ, [1, 0, 2], pI @ pX @ pZ), - (pX @ pY @ pZ, [2, 0, 1], pY @ pZ @ pX), - ], -) -def test_rearrange_pauli_string( - ps: PauliString, targets: list[int], expected: PauliString -): - from mpqp.tools.maths import rearrange_pauli_string - - assert rearrange_pauli_string(ps, targets) == expected From 75586aa5b809bb5714db4658738de2b9bf354357 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Mon, 1 Jun 2026 14:53:34 +0200 Subject: [PATCH 11/11] doc: update adjust_measure docs --- mpqp/execution/runner.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/mpqp/execution/runner.py b/mpqp/execution/runner.py index 3e32645b8..7c3a92395 100644 --- a/mpqp/execution/runner.py +++ b/mpqp/execution/runner.py @@ -55,25 +55,22 @@ def adjust_measure(measure: ExpectationMeasure, circuit: QCircuit): - # TODO: to enhance docs - - """We allow the measure to not span the entire circuit, but providers + """A measure can be incomplete and not span the entire circuit, but providers usually do not support this behavior. To make this work, we tweak the measure this function to match the expected behavior. - In order to do this, we add identity measures on the qubits not targeted by - the measure. pauli observables are directly embeded on their target qubits, - while matrix observables are padded with identity matrices when the targets - are ordered and contiguous, otherwise are embedded through their pauli decomposition. + In order to do this, we place identity operators on the qubits not targeted by + the measure. If the targets are not ordered, each observable is first reordered so that + its local qubit order matches the sorted traget order. Pauli observables are directly embedded + on their target qubits, while matrix observables are padded with identity matrices + when the targets are ordered and contiguous, and are otherwise embedded through their pauli decomposition. Args: measure: The expectation measure, potentially incomplete. - circuit: The circuit to which will be added the potential swaps allowing - the user to get the expectation value of the qubits in an arbitrary - order (this part is not handled by this function). + circuit: The circuit defining the full qubit register. Returns: - The measure padded with identities before and after. + A measure targeting all circuit qubits, with observables embedded into the full register. """ # TODO: use this only for specific provider