perceval-interop 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- perceval_interop/__init__.py +27 -0
- perceval_interop/abstract_converter.py +221 -0
- perceval_interop/cqasm/__init__.py +26 -0
- perceval_interop/cqasm/cqasm_converter.py +346 -0
- perceval_interop/myqlm/__init__.py +32 -0
- perceval_interop/myqlm/myqlm_converter.py +72 -0
- perceval_interop/myqlm/myqlm_helper.py +152 -0
- perceval_interop/myqlm/qpu_handler.py +147 -0
- perceval_interop/qiskit/__init__.py +35 -0
- perceval_interop/qiskit/circuit_to_graph_converter.py +160 -0
- perceval_interop/qiskit/converter_statevector.py +170 -0
- perceval_interop/qiskit/qiskit_converter.py +88 -0
- perceval_interop/qiskit/resources_estimator.py +82 -0
- perceval_interop/utils/__init__.py +26 -0
- perceval_interop/utils/converter_utils.py +127 -0
- perceval_interop/utils/exceptions.py +48 -0
- perceval_interop/utils/metadata.py +51 -0
- perceval_interop-1.2.0.dist-info/METADATA +126 -0
- perceval_interop-1.2.0.dist-info/RECORD +22 -0
- perceval_interop-1.2.0.dist-info/WHEEL +5 -0
- perceval_interop-1.2.0.dist-info/licenses/LICENSE +21 -0
- perceval_interop-1.2.0.dist-info/top_level.txt +5 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 Quandela
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
from .abstract_converter import AGateConverter
|
|
24
|
+
from .myqlm import MyQLMConverter, MyQLMHelper, QuandelaQPUHandler
|
|
25
|
+
from .qiskit import QiskitConverter, StatevectorConverter, CircuitToGraphConverter
|
|
26
|
+
from .cqasm import CQASMConverter
|
|
27
|
+
from .utils import *
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 Quandela
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from abc import ABC, abstractmethod
|
|
26
|
+
|
|
27
|
+
from perceval.components import Port, Circuit, Processor, catalog, AProcessor
|
|
28
|
+
from perceval.utils import P, BasicState, Encoding, global_params, PostSelect, NoiseModel
|
|
29
|
+
from perceval.utils.algorithms.optimize import optimize
|
|
30
|
+
from perceval.utils.algorithms.norm import frobenius
|
|
31
|
+
import perceval.components.unitary_components as comp
|
|
32
|
+
from perceval_interop.utils.converter_utils import label_cnots_in_gate_sequence
|
|
33
|
+
|
|
34
|
+
def _create_mode_map(c_idx: int, c_data: int) -> dict:
|
|
35
|
+
return {c_idx: 0, c_idx + 1: 1, c_data: 2, c_data + 1: 3}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UnknownGateError(Exception):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AGateConverter(ABC):
|
|
43
|
+
r"""
|
|
44
|
+
Converter class for gate based Circuits to perceval processor
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, backend_name: str = "SLOS", noise_model: NoiseModel = None):
|
|
48
|
+
self._converted_processor: AProcessor | None = None # TODO: replace by Experiment?
|
|
49
|
+
self._input_list = None # input state in list
|
|
50
|
+
self._noise_model = noise_model
|
|
51
|
+
self._backend_name = backend_name
|
|
52
|
+
|
|
53
|
+
# Define function handler to create complex components
|
|
54
|
+
# Users could override them
|
|
55
|
+
self.create_hcnot_processor = catalog["heralded cnot"].build_processor
|
|
56
|
+
self.create_hcz_processor = catalog["heralded cz"].build_processor
|
|
57
|
+
self.create_ppcnot_processor = catalog["postprocessed cnot"].build_processor
|
|
58
|
+
self.create_generic_2mode_circuit = catalog["generic 2 mode circuit"].build_circuit
|
|
59
|
+
self.create_lower_phase_circuit = lambda: Circuit(2) // (0, comp.PS(P("phi2")))
|
|
60
|
+
self.create_upper_phase_circuit = lambda: Circuit(2) // (1, comp.PS(P("phi1")))
|
|
61
|
+
self.create_2phase_circuit = lambda: Circuit(2) // (0, comp.PS(P("phi1"))) // (1, comp.PS(P("phi2")))
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def count_qubits(self, gate_circuit) -> int:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def _get_gate_sequence(self, gate_circuit) -> list[list]:
|
|
69
|
+
"""
|
|
70
|
+
Iterates over the gate circuit to return a list of Gate operations in the circuit to convert to LO circuit.
|
|
71
|
+
The information in each element of the list is - [gate name, gate qubit positions, gate parameters, gate unitary]
|
|
72
|
+
Note - Gate parameter or gate unitary can be None if value is not needed.
|
|
73
|
+
"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def _get_qubit_names(self, gate_circuit, n_qbits) -> list:
|
|
78
|
+
"""
|
|
79
|
+
Returns a list of Qubit names for a given 'n_qubits' gate circuit. Use 'Q' as the default name
|
|
80
|
+
unless the framework provides a custom name.
|
|
81
|
+
"""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def _configure_processor(self, gate_circuit, **kwargs):
|
|
85
|
+
"""
|
|
86
|
+
Sets port Encoding and default input state for the Processor
|
|
87
|
+
"""
|
|
88
|
+
n_qbits = self.count_qubits(gate_circuit)
|
|
89
|
+
qubit_names = self._get_qubit_names(gate_circuit, n_qbits)
|
|
90
|
+
|
|
91
|
+
n_moi = n_qbits * 2 # In dual rail, number of modes of interest = 2 * number of qbits
|
|
92
|
+
self._input_list = [0] * n_moi
|
|
93
|
+
self._converted_processor = Processor(self._backend_name, n_moi, noise=self._noise_model)
|
|
94
|
+
for i in range(n_qbits):
|
|
95
|
+
self._converted_processor.add_port(i * 2, Port(Encoding.DUAL_RAIL, qubit_names[i]))
|
|
96
|
+
self._input_list[i * 2] = 1
|
|
97
|
+
|
|
98
|
+
def apply_input_state(self):
|
|
99
|
+
default_input_state = BasicState(self._input_list)
|
|
100
|
+
self._converted_processor.min_detected_photons_filter(default_input_state.n)
|
|
101
|
+
self._converted_processor.with_input(default_input_state)
|
|
102
|
+
|
|
103
|
+
def convert(self, gate_circuit, use_postselection: bool = True) -> Processor:
|
|
104
|
+
"""Convert a gate-based quantum circuit into a `Processor`.
|
|
105
|
+
|
|
106
|
+
:param gate_circuit: gate-based quantum circuit (Qiskit, Myqlm or CQASM)
|
|
107
|
+
:param use_postselection: when True (default), uses optimized number of `postprocessed CNOT` and
|
|
108
|
+
'Heralded CNOT' gates. Otherwise, uses only `heralded CNOT`.
|
|
109
|
+
|
|
110
|
+
:return: the converted processor
|
|
111
|
+
"""
|
|
112
|
+
self._configure_processor(gate_circuit) # empty processor with ports initialized
|
|
113
|
+
gate_sequence = self._get_gate_sequence(gate_circuit)
|
|
114
|
+
|
|
115
|
+
return self._generate_converted_processor(gate_sequence, use_postselection=use_postselection)
|
|
116
|
+
|
|
117
|
+
def _generate_converted_processor(self, gate_sequence: list, use_postselection: bool) -> Processor:
|
|
118
|
+
# gate_inf -> [gate name, gate qubit pos, gate parameter if any or None, unitary Matrix or None]
|
|
119
|
+
|
|
120
|
+
optimized_gate_sequence = label_cnots_in_gate_sequence(gate_sequence)
|
|
121
|
+
|
|
122
|
+
for gate_index, gate_inf in enumerate(gate_sequence):
|
|
123
|
+
gate_name = gate_inf[0]
|
|
124
|
+
|
|
125
|
+
if len(gate_inf[1]) == 1:
|
|
126
|
+
if gate_name in catalog:
|
|
127
|
+
gate_param = gate_inf[2]
|
|
128
|
+
ins = self._create_catalog_1_qubit_gate(gate_name, param=gate_param)
|
|
129
|
+
else:
|
|
130
|
+
ins = self._create_generic_1_qubit_gate(gate_inf[3])
|
|
131
|
+
ins._name = gate_inf[0]
|
|
132
|
+
self._converted_processor.add(gate_inf[1][0] * 2, ins.copy())
|
|
133
|
+
|
|
134
|
+
else:
|
|
135
|
+
if len(gate_inf[1]) > 2:
|
|
136
|
+
raise NotImplementedError("2+ Qubit gates not implemented")
|
|
137
|
+
|
|
138
|
+
c_idx = gate_inf[1][0] * 2
|
|
139
|
+
c_data = gate_inf[1][1] * 2
|
|
140
|
+
self._create_2_qubit_gates_from_catalog(optimized_gate_sequence[gate_index], c_idx, c_data,
|
|
141
|
+
use_postselection)
|
|
142
|
+
self.apply_input_state()
|
|
143
|
+
return self._converted_processor
|
|
144
|
+
|
|
145
|
+
def _create_catalog_1_qubit_gate(self, gate_name: str, **kwargs) -> Processor:
|
|
146
|
+
param = kwargs.get("param", None)
|
|
147
|
+
if gate_name in ["rx", "ry", "rz"]:
|
|
148
|
+
return catalog[gate_name].build_processor(theta=param)
|
|
149
|
+
elif gate_name in ["ph"]:
|
|
150
|
+
return catalog[gate_name].build_processor(phi=param)
|
|
151
|
+
else:
|
|
152
|
+
return catalog[gate_name].build_processor()
|
|
153
|
+
|
|
154
|
+
def _create_generic_1_qubit_gate(self, u) -> Circuit:
|
|
155
|
+
# universal method, takes in unitary and approximates one using
|
|
156
|
+
# Frobenius method
|
|
157
|
+
if abs(u[1, 0]) + abs(u[0, 1]) < 2 * global_params["min_precision_gate"]:
|
|
158
|
+
# diagonal matrix - we can handle with phases, we consider that gate unitary parameters has
|
|
159
|
+
# limited numeric precision
|
|
160
|
+
if abs(u[0, 0] - 1) < global_params["min_precision_gate"]:
|
|
161
|
+
if abs(u[1, 1] - 1) < global_params["min_precision_gate"]:
|
|
162
|
+
return Circuit(2, name="I") # returns Identity/empty circuit
|
|
163
|
+
ins = self.create_upper_phase_circuit()
|
|
164
|
+
else:
|
|
165
|
+
if abs(u[1, 1] - 1) < global_params["min_precision_gate"]:
|
|
166
|
+
ins = self.create_lower_phase_circuit()
|
|
167
|
+
else:
|
|
168
|
+
ins = self.create_2phase_circuit()
|
|
169
|
+
optimize(ins, u, frobenius, sign=-1)
|
|
170
|
+
else:
|
|
171
|
+
ins = self.create_generic_2mode_circuit()
|
|
172
|
+
optimize(ins, u, frobenius, sign=-1)
|
|
173
|
+
return ins
|
|
174
|
+
|
|
175
|
+
def _create_2_qubit_gates_from_catalog(self, gate_name: str, c_idx: int, c_data: int, use_postselection: bool):
|
|
176
|
+
r"""
|
|
177
|
+
List of Gates implemented:
|
|
178
|
+
CNOT - Heralded and post-processed
|
|
179
|
+
CZ - Heralded
|
|
180
|
+
CRz - Heralded and post-processed (uses two CNOTs)
|
|
181
|
+
SWAP
|
|
182
|
+
"""
|
|
183
|
+
# Save and clear current post-selection data from the converted processor before adding the next gate
|
|
184
|
+
if self._converted_processor.post_select_fn is not None:
|
|
185
|
+
post_select_curr = self._converted_processor.post_select_fn
|
|
186
|
+
else:
|
|
187
|
+
post_select_curr = PostSelect() # save empty if I need to merge incoming PostSelect to it
|
|
188
|
+
self._converted_processor.clear_postselection() # clear current post-selection
|
|
189
|
+
|
|
190
|
+
gate_name = gate_name.upper()
|
|
191
|
+
if gate_name in ["POSTPROCESSED CNOT", "HERALDED CNOT"]:
|
|
192
|
+
if use_postselection and gate_name == "POSTPROCESSED CNOT":
|
|
193
|
+
cnot_processor = self.create_ppcnot_processor()
|
|
194
|
+
else:
|
|
195
|
+
cnot_processor = self.create_hcnot_processor()
|
|
196
|
+
|
|
197
|
+
self._converted_processor.add(_create_mode_map(c_idx, c_data), cnot_processor)
|
|
198
|
+
if self._converted_processor.post_select_fn is not None:
|
|
199
|
+
post_select_curr.merge(self._converted_processor.post_select_fn)
|
|
200
|
+
|
|
201
|
+
elif gate_name in ["CSIGN", "CZ"]:
|
|
202
|
+
# Controlled Z in myqlm is named CSIGN
|
|
203
|
+
cz_processor = self.create_hcz_processor()
|
|
204
|
+
self._converted_processor.add(_create_mode_map(c_idx, c_data), cz_processor)
|
|
205
|
+
elif gate_name == "SWAP":
|
|
206
|
+
# Works for any FIRST and LAST, everything in-between is unchanged.
|
|
207
|
+
c_first = min(c_idx, c_data)
|
|
208
|
+
c_last = max(c_idx, c_data)
|
|
209
|
+
n = (c_last - c_first) // 2 + 1
|
|
210
|
+
perm = [i for i in range(n * 2)]
|
|
211
|
+
perm[0] = c_last - c_first
|
|
212
|
+
perm[1] = perm[0] + 1
|
|
213
|
+
perm[perm[0]] = 0
|
|
214
|
+
perm[perm[1]] = 1
|
|
215
|
+
self._converted_processor.add(c_first, comp.PERM(perm))
|
|
216
|
+
else:
|
|
217
|
+
raise UnknownGateError(f"Gate not yet supported: {gate_name}")
|
|
218
|
+
|
|
219
|
+
# re-apply the cleared post-selection
|
|
220
|
+
self._converted_processor.set_postselection(post_select_curr)
|
|
221
|
+
return self._converted_processor
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 Quandela
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
try:
|
|
23
|
+
from .cqasm_converter import CQASMConverter
|
|
24
|
+
except ModuleNotFoundError:
|
|
25
|
+
from perceval_interop.utils import MissingDependency
|
|
26
|
+
CQASMConverter = MissingDependency("CQASMConverter", "cqasm")
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 Quandela
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
from perceval.components import Processor, catalog
|
|
24
|
+
from perceval.utils.logging import get_logger, channel
|
|
25
|
+
from perceval.utils import NoiseModel
|
|
26
|
+
from perceval_interop.abstract_converter import AGateConverter
|
|
27
|
+
from perceval_interop.utils import ConversionSyntaxError, ConversionUnsupportedFeatureError, ConversionBadVersionError
|
|
28
|
+
|
|
29
|
+
import cqasm.v3x as cqasm
|
|
30
|
+
import numpy as np
|
|
31
|
+
import re
|
|
32
|
+
|
|
33
|
+
# A collection of gates for which meaningful optical circuits (beam splitter,
|
|
34
|
+
# phase shifter, permutation) exist.
|
|
35
|
+
# Source:
|
|
36
|
+
# https://qutech-delft.github.io/cQASM-spec/language_specification/instructions/gates.html#standard-gate-set
|
|
37
|
+
|
|
38
|
+
_CQASM_1_QUBIT_GATES = {
|
|
39
|
+
"H", "X", "X90", "mX90", "Rx", "Y", "Y90", "mY90", "Ry", "S", "Sdag", "T", "Tdag", "Z", "Rz",
|
|
40
|
+
# For v1 compatibility
|
|
41
|
+
"I"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_CQASM_2_QUBIT_GATES = { "CNOT", "CZ"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CQASMConverter(AGateConverter):
|
|
48
|
+
r"""cQASM quantum circuit to perceval processor converter.
|
|
49
|
+
|
|
50
|
+
:param backend_name: backend name used in the converted processor (default SLOS)
|
|
51
|
+
"""
|
|
52
|
+
def __init__(self, backend_name: str = "SLOS", noise_model: NoiseModel = None):
|
|
53
|
+
super().__init__(backend_name, noise_model)
|
|
54
|
+
self._qubit_list = []
|
|
55
|
+
|
|
56
|
+
def count_qubits(self, ast) -> int:
|
|
57
|
+
return len(self._qubit_list)
|
|
58
|
+
|
|
59
|
+
def _collect_qubit_list(self, ast):
|
|
60
|
+
self._qubit_list = []
|
|
61
|
+
for variable in ast.variables:
|
|
62
|
+
if type(variable.typ) is cqasm.types.QubitArray:
|
|
63
|
+
for i in range(variable.typ.size):
|
|
64
|
+
self._qubit_list.append((variable.name, i))
|
|
65
|
+
elif type(variable.typ) is cqasm.types.Qubit:
|
|
66
|
+
self._qubit_list.append((variable.name, -1))
|
|
67
|
+
else:
|
|
68
|
+
raise ConversionUnsupportedFeatureError(f"Classical variable { variable.name } not supported")
|
|
69
|
+
|
|
70
|
+
def _operand_to_qubit_indices(self, operand):
|
|
71
|
+
name = operand.variable.name
|
|
72
|
+
if type(operand) is cqasm.values.VariableRef:
|
|
73
|
+
return [self._qubit_list.index((name, -1))]
|
|
74
|
+
elif type(operand) is cqasm.values.IndexRef:
|
|
75
|
+
return [self._qubit_list.index((name, index.value))
|
|
76
|
+
for index in operand.indices]
|
|
77
|
+
else:
|
|
78
|
+
raise ConversionUnsupportedFeatureError(f"Cannot map variable { name } to a declared qubit")
|
|
79
|
+
|
|
80
|
+
def _get_gate_info(self, statement):
|
|
81
|
+
# For each CQASM statement -> gate information - gate name, qubit position, parameter are returned
|
|
82
|
+
gate_name = statement.gate.name
|
|
83
|
+
num_operands = len(statement.operands)
|
|
84
|
+
|
|
85
|
+
# For now, assume the statement pattern is OP q
|
|
86
|
+
targets = self._operand_to_qubit_indices(statement.operands[0])
|
|
87
|
+
controls = []
|
|
88
|
+
parameter = None
|
|
89
|
+
|
|
90
|
+
# Match other statement patterns
|
|
91
|
+
if num_operands == 1 and len(statement.gate.parameters):
|
|
92
|
+
# Note: statement.gate.parameters is of type ConstFloat, but changes to MultiValueBase from 1.1.0
|
|
93
|
+
# get parameters for 1 qubit gates.
|
|
94
|
+
parameter = statement.gate.parameters[0].value
|
|
95
|
+
elif num_operands == 2:
|
|
96
|
+
if type(statement.operands[1]) is cqasm.values.ConstFloat:
|
|
97
|
+
# Statement pattern is OP(r) q. This case appears when converting 1 qubit gates from v1.
|
|
98
|
+
parameter = statement.operands[1].value
|
|
99
|
+
else:
|
|
100
|
+
# Statement pattern is OP q, q
|
|
101
|
+
controls = targets
|
|
102
|
+
targets = self._operand_to_qubit_indices(statement.operands[1])
|
|
103
|
+
elif num_operands == 3:
|
|
104
|
+
# Statement is OP(r) q, q
|
|
105
|
+
controls = targets
|
|
106
|
+
targets = self._operand_to_qubit_indices(statement.operands[1])
|
|
107
|
+
parameter = statement.operands[2].value
|
|
108
|
+
elif num_operands >= 4:
|
|
109
|
+
raise ConversionUnsupportedFeatureError(f"Statement with unsupported number of operands, n = { num_operands }")
|
|
110
|
+
|
|
111
|
+
return gate_name, controls, targets, parameter
|
|
112
|
+
|
|
113
|
+
def _get_gate_sequence(self, ast) -> list[list]:
|
|
114
|
+
# generates the gate sequence by converting ast statements
|
|
115
|
+
gate_sequence = []
|
|
116
|
+
for statement in ast.block.statements:
|
|
117
|
+
gate_name, controls, targets, parameter = self._get_gate_info(statement)
|
|
118
|
+
|
|
119
|
+
parameter = CQASMConverter._update_parameter_rotation_gate(gate_name, parameter)
|
|
120
|
+
# x90, mx90, y90, my90 are named gates in CQASM. Their parameter values are needed to build circuit in pcvl.
|
|
121
|
+
|
|
122
|
+
gate_name = CQASMConverter._map_gate(gate_name)
|
|
123
|
+
|
|
124
|
+
if not controls:
|
|
125
|
+
# working with 1 qubit gates
|
|
126
|
+
if gate_name.lower() in catalog:
|
|
127
|
+
for i in range(len(targets)):
|
|
128
|
+
gate_sequence.append([gate_name.lower(), [targets[i]], parameter])
|
|
129
|
+
else:
|
|
130
|
+
raise ConversionUnsupportedFeatureError(f"Unsupported 1-qubit gate {gate_name}")
|
|
131
|
+
else:
|
|
132
|
+
if gate_name not in _CQASM_2_QUBIT_GATES:
|
|
133
|
+
raise ConversionUnsupportedFeatureError(
|
|
134
|
+
f"Unsupported 2-qubit gate { gate_name }")
|
|
135
|
+
for target in targets:
|
|
136
|
+
gate_sequence.append([gate_name.lower(), [controls[0], target], parameter])
|
|
137
|
+
|
|
138
|
+
gate_sequence = [elem + [None] for elem in gate_sequence] # to be consistent with other frameworks, None is used for gate unitary
|
|
139
|
+
|
|
140
|
+
return gate_sequence
|
|
141
|
+
|
|
142
|
+
def _get_qubit_names(self, ast, n_qbits):
|
|
143
|
+
return [f'{q}[{i}]' if i >= 0 else q for (q, i) in self._qubit_list]
|
|
144
|
+
|
|
145
|
+
def convert(self, ast, use_postselection: bool = True) -> Processor:
|
|
146
|
+
r"""Convert a cQASM quantum program into a `Processor`.
|
|
147
|
+
|
|
148
|
+
:param ast: the AST of a cQASM program
|
|
149
|
+
:type ast: a Program object, as returned by the cQASM parser
|
|
150
|
+
:param use_postselection: when True, uses as many `postprocessed CNOT` as possible.
|
|
151
|
+
Otherwise, uses only `heralded CNOT`
|
|
152
|
+
|
|
153
|
+
:return: the converted processor
|
|
154
|
+
"""
|
|
155
|
+
if isinstance(ast, str):
|
|
156
|
+
ast = self._convert_from_string(ast)
|
|
157
|
+
|
|
158
|
+
get_logger().info(f"Convert cqasm.ast ({len(self._qubit_list)} qubits, {len(ast.block.statements)} operations) to processor",
|
|
159
|
+
channel.general)
|
|
160
|
+
self._collect_qubit_list(ast)
|
|
161
|
+
|
|
162
|
+
return super().convert(ast, use_postselection)
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def check_version(cls, source_string):
|
|
166
|
+
r"""Extracts the version number string from a cQASM program
|
|
167
|
+
|
|
168
|
+
:param source_string: the cQASM program stored in a string
|
|
169
|
+
:return: a tuple (major, minor) with the version number
|
|
170
|
+
"""
|
|
171
|
+
pattern = r"^\s*version ([0-9])(.[0-9])?\s*"
|
|
172
|
+
match = re.search(pattern, source_string, re.MULTILINE)
|
|
173
|
+
if match:
|
|
174
|
+
major = int(match.group(1))
|
|
175
|
+
minor = int(match.group(2)[1:]) if match.group(2) else 0
|
|
176
|
+
return major, minor
|
|
177
|
+
else:
|
|
178
|
+
raise ConversionSyntaxError(f"Missing version number")
|
|
179
|
+
|
|
180
|
+
def _convert_from_string(self, source: str) -> cqasm.semantic.Program:
|
|
181
|
+
r"""Convert a cQASM quantum program into a `Processor`.
|
|
182
|
+
|
|
183
|
+
:param source: a string containing the cQASM program to convert, or the path to a file storing itm
|
|
184
|
+
:return: the cQASM program
|
|
185
|
+
"""
|
|
186
|
+
# The string contains a single line, it might be a path
|
|
187
|
+
if not "\n" in source:
|
|
188
|
+
try:
|
|
189
|
+
get_logger().debug(f"Reading cQASM file content at {source}", channel.general)
|
|
190
|
+
with open(source) as source_file:
|
|
191
|
+
source = source_file.read()
|
|
192
|
+
except IOError:
|
|
193
|
+
# File not found, It might be a single line program instead
|
|
194
|
+
# of a name, so we can attempt to parse it.
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
major, minor = CQASMConverter.check_version(source)
|
|
198
|
+
if major == 3:
|
|
199
|
+
get_logger().debug("Parsing cQASM v3 description", channel.general)
|
|
200
|
+
ast = cqasm.Analyzer().analyze_string(source)
|
|
201
|
+
elif major == 1:
|
|
202
|
+
get_logger().debug("Converting cQASM v1 to v3", channel.general)
|
|
203
|
+
ast = self._v3_ast_from_v1_source(source.split('\n'))
|
|
204
|
+
else:
|
|
205
|
+
raise ConversionBadVersionError(f"Unsupported version {major}.{minor}")
|
|
206
|
+
|
|
207
|
+
if not isinstance(ast, cqasm.semantic.Program):
|
|
208
|
+
raise ConversionSyntaxError(f"cQASM parser error: { ast[0] }")
|
|
209
|
+
|
|
210
|
+
return ast
|
|
211
|
+
|
|
212
|
+
def _v3_ast_from_v1_source(self, lines):
|
|
213
|
+
r"""Converts a cQASM v1 quantum program into a cQASM v3 AST"""
|
|
214
|
+
|
|
215
|
+
# Parsing code from https://github.com/maxwell04-wq original submission,
|
|
216
|
+
# with just enough changes to make it parse the example on:
|
|
217
|
+
# https://www.quantum-inspire.com/kbase/cqasm/
|
|
218
|
+
|
|
219
|
+
# Create an empty Program object to store the v3 AST
|
|
220
|
+
ast = cqasm.semantic.Program(
|
|
221
|
+
api_version=cqasm.primitives.Version([3]),
|
|
222
|
+
block=cqasm.semantic.Block(
|
|
223
|
+
statements=cqasm.semantic.MultiStatement()),
|
|
224
|
+
variables=cqasm.semantic.MultiVariable()
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def _is_float(ins):
|
|
228
|
+
try:
|
|
229
|
+
float(ins)
|
|
230
|
+
return True
|
|
231
|
+
except ValueError:
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
# read QASM instructions line-by-line
|
|
235
|
+
for line in lines:
|
|
236
|
+
try:
|
|
237
|
+
line = line.strip()
|
|
238
|
+
if len(line) <= 0 or line[0] == '#':
|
|
239
|
+
#empty line or comment
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
instruction = line.split(" ")
|
|
243
|
+
if instruction[0] == "version":
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
if instruction[0] == "qubits":
|
|
247
|
+
if not instruction[1].isdigit():
|
|
248
|
+
raise ConversionSyntaxError(
|
|
249
|
+
"Qubit number is not an integer.")
|
|
250
|
+
typ = cqasm.types.QubitArray(size=int(instruction[1]))
|
|
251
|
+
ast.variables.append(
|
|
252
|
+
cqasm.semantic.Variable(name='q', typ=typ))
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
# Ignore anything with only one keyword:
|
|
256
|
+
if len(instruction) <= 1:
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
# Ignore classical bits
|
|
260
|
+
if line.find("b[") != -1:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# Ignore non-gate instructions
|
|
264
|
+
if instruction[0] in ['prep_z', 'measure']:
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
ins_qubits = []
|
|
268
|
+
theta = None
|
|
269
|
+
for remaining_token in instruction[1:]:
|
|
270
|
+
ins_params = remaining_token.split(",")
|
|
271
|
+
for param in ins_params:
|
|
272
|
+
if param.find(":") != -1:
|
|
273
|
+
raise ConversionSyntaxError(
|
|
274
|
+
"Unsuported token.")
|
|
275
|
+
start_indx = param.find("q[")
|
|
276
|
+
if start_indx != -1:
|
|
277
|
+
end_indx = param.find("]")
|
|
278
|
+
ins_qubits.append(param[start_indx+2:end_indx])
|
|
279
|
+
# Rotation gates
|
|
280
|
+
elif _is_float(param):
|
|
281
|
+
theta = float(param)
|
|
282
|
+
|
|
283
|
+
if len(ins_qubits) > 2:
|
|
284
|
+
raise ConversionUnsupportedFeatureError(
|
|
285
|
+
f"Gate { instruction[0] } has more than one control (n = { len(ins_params) })"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Check if valid gate
|
|
289
|
+
if len(ins_qubits) == 1:
|
|
290
|
+
if instruction[0] not in _CQASM_1_QUBIT_GATES:
|
|
291
|
+
raise ConversionUnsupportedFeatureError(
|
|
292
|
+
f"Unsupported 1-qubit gate { instruction[0] }")
|
|
293
|
+
|
|
294
|
+
elif len(ins_qubits) == 2:
|
|
295
|
+
if instruction[0] not in _CQASM_2_QUBIT_GATES:
|
|
296
|
+
raise ConversionUnsupportedFeatureError(
|
|
297
|
+
f"Unsupported 2-qubit gate { instruction[0] }")
|
|
298
|
+
|
|
299
|
+
operands = []
|
|
300
|
+
for qubit_index in ins_qubits:
|
|
301
|
+
index = cqasm.values.IndexRef(
|
|
302
|
+
variable=cqasm.semantic.Variable(name = 'q'))
|
|
303
|
+
index.indices = cqasm.values.MultiConstInt()
|
|
304
|
+
index.indices.append(cqasm.values.ConstInt(qubit_index))
|
|
305
|
+
operands.append(index)
|
|
306
|
+
if theta is not None:
|
|
307
|
+
angle = cqasm.values.ConstFloat(value=theta)
|
|
308
|
+
operands.append(angle)
|
|
309
|
+
|
|
310
|
+
statement = cqasm.semantic.GateInstruction(
|
|
311
|
+
gate=cqasm.semantic.Gate(f"{instruction[0]}"),
|
|
312
|
+
operands=operands) # operand=[qubits, parameters]
|
|
313
|
+
ast.block.statements.append(statement)
|
|
314
|
+
except ValueError:
|
|
315
|
+
print("An error in parsing the cQASM v1 file.")
|
|
316
|
+
return ast
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def _map_gate(gate_name: str) -> str:
|
|
320
|
+
# updates gate names to be consistent with Perceval catalog
|
|
321
|
+
if gate_name == 'X90':
|
|
322
|
+
return 'rx'
|
|
323
|
+
|
|
324
|
+
elif gate_name == 'mX90':
|
|
325
|
+
return 'rx'
|
|
326
|
+
elif gate_name == 'Y90':
|
|
327
|
+
return 'ry'
|
|
328
|
+
elif gate_name == 'mY90':
|
|
329
|
+
return 'ry'
|
|
330
|
+
else:
|
|
331
|
+
return gate_name
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def _update_parameter_rotation_gate(gate_name, parameter) -> float:
|
|
335
|
+
# Returns circuit parameter value for CQASM gates
|
|
336
|
+
if gate_name == 'X90':
|
|
337
|
+
return np.pi / 2
|
|
338
|
+
|
|
339
|
+
elif gate_name == 'mX90':
|
|
340
|
+
return - np.pi / 2
|
|
341
|
+
elif gate_name == 'Y90':
|
|
342
|
+
return np.pi / 2
|
|
343
|
+
elif gate_name == 'mY90':
|
|
344
|
+
return - np.pi / 2
|
|
345
|
+
else:
|
|
346
|
+
return parameter
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 Quandela
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
try:
|
|
23
|
+
from .myqlm_converter import MyQLMConverter
|
|
24
|
+
from .myqlm_helper import MyQLMHelper
|
|
25
|
+
from .qpu_handler import QuandelaQPUHandler
|
|
26
|
+
|
|
27
|
+
except ModuleNotFoundError:
|
|
28
|
+
from perceval_interop.utils import MissingDependency
|
|
29
|
+
extra_name = "myqlm"
|
|
30
|
+
MyQLMConverter = MissingDependency("MyQLMConverter", extra_name)
|
|
31
|
+
MyQLMHelper = MissingDependency("MyQLMHelper", extra_name)
|
|
32
|
+
QuandelaQPUHandler = MissingDependency("QuandelaQPUHandler", extra_name)
|