Skip to content

Commit 38365ca

Browse files
Merge pull request #32 from ColibrITD-SAS/feat-pauli-string
Feat pauli string
2 parents 95b8189 + dc5828c commit 38365ca

File tree

14 files changed

+872
-57
lines changed

14 files changed

+872
-57
lines changed

examples/scripts/observable_job.py

+24-19
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from mpqp.execution import run
77
from mpqp.measures import ExpectationMeasure, Observable
88
from mpqp.execution.devices import ATOSDevice, IBMDevice, AWSDevice
9+
from mpqp.core.instruction.measurement.pauli_string import I, Z
910

1011
obs = Observable(
1112
np.array(
@@ -19,18 +20,12 @@
1920
)
2021
)
2122

22-
obs2 = Observable(
23-
np.array(
24-
[
25-
[3.0, 0.0, 0.0, 0.0],
26-
[0.0, 1.0, 0.0, 0.0],
27-
[0.0, 0.0, -1.0, 0.0],
28-
[0.0, 0.0, 0.0, 0.0],
29-
],
30-
dtype=float,
31-
)
32-
)
23+
obs2 = Observable(1 * I @ Z + 1 * I @ I)
3324

25+
# Observable can be constructed from a Pauli string or a matrix
26+
print("Observable2:")
27+
print(obs2.pauli_string)
28+
print(obs2.matrix)
3429

3530
# Declaration of the circuit with the right size
3631
circuit = QCircuit(2, label="Observable test")
@@ -42,10 +37,15 @@
4237
print(circuit)
4338

4439
# Running the computation on myQLM and on Aer simulator, then retrieving the results
45-
results = run(circuit, [ATOSDevice.MYQLM_PYLINALG,
46-
IBMDevice.AER_SIMULATOR,
47-
ATOSDevice.MYQLM_CLINALG,
48-
AWSDevice.BRAKET_LOCAL_SIMULATOR])
40+
results = run(
41+
circuit,
42+
[
43+
ATOSDevice.MYQLM_PYLINALG,
44+
IBMDevice.AER_SIMULATOR,
45+
ATOSDevice.MYQLM_CLINALG,
46+
AWSDevice.BRAKET_LOCAL_SIMULATOR,
47+
],
48+
)
4949
print(results)
5050

5151

@@ -59,8 +59,13 @@
5959
print(circuit)
6060

6161
# Running the computation on myQLM and on Aer simulator, then retrieving the results
62-
results = run(circuit, [ATOSDevice.MYQLM_PYLINALG,
63-
ATOSDevice.MYQLM_CLINALG,
64-
IBMDevice.AER_SIMULATOR,
65-
AWSDevice.BRAKET_LOCAL_SIMULATOR])
62+
results = run(
63+
circuit,
64+
[
65+
ATOSDevice.MYQLM_PYLINALG,
66+
ATOSDevice.MYQLM_CLINALG,
67+
IBMDevice.AER_SIMULATOR,
68+
AWSDevice.BRAKET_LOCAL_SIMULATOR,
69+
],
70+
)
6671
print(results)

mpqp/core/circuit.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import numpy as np
88
import numpy.typing as npt
99
from braket.circuits import Circuit as braket_Circuit
10+
from cirq import Circuit as cirq_Circuit
1011
from matplotlib.figure import Figure
1112
from qat.core.wrappers.circuit import Circuit as myQLM_Circuit
1213
from qiskit.circuit import Operation, QuantumCircuit
@@ -27,6 +28,7 @@
2728
from mpqp.qasm import qasm2_to_myqlm_Circuit
2829
from mpqp.qasm.open_qasm_2_and_3 import open_qasm_2_to_3
2930
from mpqp.qasm.qasm_to_braket import qasm3_to_braket_Circuit
31+
from mpqp.qasm.qasm_to_cirq import qasm2_to_cirq_Circuit
3032
from mpqp.tools.errors import NumberQubitsError
3133
from mpqp.tools.maths import matrix_eq
3234

@@ -595,7 +597,7 @@ def without_measurements(self) -> QCircuit:
595597

596598
def to_other_language(
597599
self, language: Language = Language.QISKIT
598-
) -> Union[QuantumCircuit, myQLM_Circuit, braket_Circuit]:
600+
) -> Union[QuantumCircuit, myQLM_Circuit, braket_Circuit, cirq_Circuit,]:
599601
"""Transforms this circuit into the corresponding circuit in the language
600602
specified in the ``language`` arg.
601603
@@ -697,7 +699,10 @@ def to_other_language(
697699
)
698700

699701
return qasm3_to_braket_Circuit(circuit.to_qasm3())
700-
702+
elif language == Language.CIRQ:
703+
cirq_circuit = qasm2_to_cirq_Circuit(self.to_qasm2())
704+
return cirq_circuit
705+
701706
else:
702707
raise NotImplementedError(f"Error: {language} is not supported")
703708

mpqp/core/instruction/measurement/expectation_value.py

+143-25
Original file line numberDiff line numberDiff line change
@@ -7,61 +7,112 @@ class to define your observable, and a :class:`ExpectationMeasure` to perform
77

88
import copy
99
from numbers import Complex
10+
from typing import Optional, TYPE_CHECKING
1011
from warnings import warn
11-
from typing import Optional
1212

1313
import numpy as np
14-
import numpy.typing as npt
1514
from qiskit.circuit import Parameter
1615
from sympy import Expr
1716
from typeguard import typechecked
1817

18+
if TYPE_CHECKING:
19+
from cirq.circuits.circuit import Circuit as Cirq_Circuit
20+
1921
from mpqp.core.instruction.gates.native_gates import SWAP
2022
from mpqp.core.instruction.measurement.measure import Measure
23+
from mpqp.core.instruction.measurement.pauli_string import PauliString
2124
from mpqp.core.languages import Language
2225
from mpqp.tools.errors import NumberQubitsError
26+
from mpqp.tools.generics import Matrix, one_lined_repr
2327
from mpqp.tools.maths import is_hermitian
24-
from mpqp.tools.generics import one_lined_repr
2528

2629

2730
@typechecked
2831
class Observable:
2932
"""Class defining an observable, used for evaluating expectation values.
3033
31-
An observable can be defined by using a hermitian matrix, or using a combination of operators in a specific
32-
basis (Kraus, Pauli, ...).
33-
34-
For the moment, on can only define the observable using a matrix.
34+
An observable can be defined by using a Hermitian matrix, or using a combination of operators in a specific
35+
basis Pauli.
3536
3637
Example:
3738
>>> matrix = np.array([[1, 0], [0, -1]])
39+
>>> pauli_string = 3 * I @ Z + 4 * X @ Y
3840
>>> obs = Observable(matrix)
41+
>>> obs2 = Observable(pauli_string)
3942
4043
Args:
41-
matrix: Hermitian matrix representing the observable.
44+
observable : can be either a Hermitian matrix representing the observable or PauliString representing the observable.
45+
46+
Raises:
47+
ValueError: If the input matrix is not Hermitian or does not have a square shape.
48+
NumberQubitsError: If the number of qubits in the input observable does
49+
not match the number of target qubits.
50+
4251
"""
4352

44-
def __init__(self, matrix: npt.NDArray[np.complex64] | list[list[Complex]]):
45-
self.nb_qubits = int(np.log2(len(matrix)))
46-
"""Number of qubits of this observable."""
47-
self.matrix = np.array(matrix)
48-
"""See parameter description."""
53+
def __init__(self, observable: Matrix | PauliString):
54+
self._matrix = None
55+
self._pauli_string = None
4956

50-
basis_states = 2**self.nb_qubits
51-
if self.matrix.shape != (basis_states, basis_states):
52-
raise ValueError(
53-
f"The size of the matrix {self.matrix.shape} doesn't neatly fit on a"
54-
" quantum register. It should be a square matrix of size a power"
55-
" of two."
56-
)
57+
if isinstance(observable, PauliString):
58+
self.nb_qubits = observable.nb_qubits
59+
self._pauli_string = observable.simplify()
60+
else:
61+
self.nb_qubits = int(np.log2(len(observable)))
62+
"""Number of qubits of this observable."""
63+
self._matrix = np.array(observable)
64+
65+
basis_states = 2**self.nb_qubits
66+
if self.matrix.shape != (basis_states, basis_states):
67+
raise ValueError(
68+
f"The size of the matrix {self.matrix.shape} doesn't neatly fit on a"
69+
" quantum register. It should be a square matrix of size a power"
70+
" of two."
71+
)
5772

58-
if not is_hermitian(self.matrix):
59-
raise ValueError(
60-
"The matrix in parameter is not hermitian (cannot define an observable)"
61-
)
73+
if not is_hermitian(self.matrix):
74+
raise ValueError(
75+
"The matrix in parameter is not hermitian (cannot define an observable)"
76+
)
77+
78+
@property
79+
def matrix(self) -> Matrix:
80+
"""
81+
Returns the matrix representation of the observable.
82+
83+
Returns:
84+
np.ndarray: The matrix representation of the observable.
85+
"""
86+
if self._matrix is None:
87+
self._matrix = self.pauli_string.to_matrix()
88+
matrix = copy.deepcopy(self._matrix).astype(np.complex64)
89+
return matrix
90+
91+
@property
92+
def pauli_string(self) -> PauliString:
93+
"""
94+
Returns the PauliString representation of the observable.
95+
96+
Returns:
97+
PauliString: The PauliString representation of the observable.
98+
"""
99+
if self._pauli_string is None:
100+
self._pauli_string = PauliString.from_matrix(self.matrix)
101+
pauli_string = copy.deepcopy(self._pauli_string)
102+
return pauli_string
103+
104+
@pauli_string.setter
105+
def pauli_string(self, pauli_string: PauliString):
106+
self._pauli_string = pauli_string
107+
self._matrix = None
108+
109+
@matrix.setter
110+
def matrix(self, matrix: Matrix):
111+
self._matrix = matrix
112+
self._pauli_string = None
62113

63114
def __repr__(self) -> str:
64-
return f"Observable({one_lined_repr(self.matrix)})"
115+
return f"{type(self).__name__}({one_lined_repr(self.matrix)})"
65116

66117
def __mult__(self, other: Expr | Complex) -> Observable:
67118
"""3M-TODO"""
@@ -73,6 +124,70 @@ def subs(
73124
"""3M-TODO"""
74125
...
75126

127+
def to_other_language(
128+
self, language: Language, circuit: Optional[Cirq_Circuit] = None
129+
):
130+
"""
131+
Converts the observable to the representation of another quantum programming language.
132+
133+
Example:
134+
>>> obs = Observable(np.diag([0.7, -1, 1, 1]))
135+
>>> obs_qiskit = obs.to_other_language(Language.QISKIT)
136+
>>> print(obs_qiskit)
137+
<bound method Observable.to_qiskit_observable of Observable(array([[ 0.7, 0. , 0. , 0. ], [ 0. , -1. , 0. , 0. ], [ 0. , 0. , 1. , 0. ], [ 0. , 0. , 0. , 1. ]]))>
138+
139+
Args:
140+
language (str): The target programming language ('qiskit', 'pyquil', 'braket', 'cirq').
141+
circuit: The Cirq circuit associated with the observable (required for 'cirq' language).
142+
143+
Returns:
144+
Depends on the target language.
145+
"""
146+
if language == Language.QISKIT:
147+
from qiskit.quantum_info import Operator
148+
149+
return Operator(self.matrix)
150+
elif language == Language.MY_QLM:
151+
from qat.core.wrappers.observable import Observable as QLM_Observable
152+
153+
return QLM_Observable(self.nb_qubits, matrix=self.matrix)
154+
elif language == Language.BRAKET:
155+
from braket.circuits.observables import Hermitian
156+
157+
return Hermitian(self.matrix)
158+
elif language == Language.CIRQ:
159+
if circuit is None:
160+
raise ValueError("Circuit must be specified for cirq_observable.")
161+
from cirq.ops.identity import I as Cirq_I
162+
from cirq.ops.pauli_gates import X as Cirq_X, Y as Cirq_Y, Z as Cirq_Z
163+
164+
all_qubits = set(
165+
q for moment in circuit for op in moment.operations for q in op.qubits
166+
)
167+
all_qubits_list = sorted(all_qubits)
168+
169+
cirq_pauli_string = None
170+
pauli_gate_map = {"I": Cirq_I, "X": Cirq_X, "Y": Cirq_Y, "Z": Cirq_Z}
171+
for monomial in self.pauli_string.monomials:
172+
cirq_monomial = None
173+
for index, atom in enumerate(monomial.atoms):
174+
cirq_atom = pauli_gate_map[atom.label](all_qubits_list[index])
175+
cirq_monomial = (
176+
cirq_atom
177+
if cirq_monomial is None
178+
else cirq_monomial * cirq_atom
179+
)
180+
cirq_monomial *= monomial.coef
181+
cirq_pauli_string = (
182+
cirq_monomial
183+
if cirq_pauli_string is None
184+
else cirq_pauli_string + cirq_monomial
185+
)
186+
187+
return cirq_pauli_string
188+
else:
189+
raise ValueError(f"Unsupported language: {language}")
190+
76191

77192
@typechecked
78193
class ExpectationMeasure(Measure):
@@ -120,6 +235,7 @@ def __init__(
120235
super().__init__(targets, shots, label)
121236
self.observable = observable
122237
"""See parameter description."""
238+
# Raise an error if the number of target qubits does not match the size of the observable.
123239
if self.nb_qubits != observable.nb_qubits:
124240
raise NumberQubitsError(
125241
f"{self.nb_qubits}, the number of target qubit(s) doesn't match"
@@ -172,6 +288,8 @@ def to_other_language(
172288
) -> None:
173289
if qiskit_parameters is None:
174290
qiskit_parameters = set()
291+
#TODO : incoherence here, if the language is Qiskit we raise a NotImplementedError, and otherwise we say that
292+
# only qiskit is supported
175293
if language == Language.QISKIT:
176294
raise NotImplementedError(
177295
"Qiskit does not implement these kind of measures"

0 commit comments

Comments
 (0)