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.
@@ -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)