diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e18e158 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Qiskit IBM AI Local Transpiler", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": ["tests/ai/test_ai_local_linear_function_synthesis.py::test_ai_local_linear_function_synthesis_wrong_backend"], + "justMyCode": false, + "console": "integratedTerminal" + }, + ], +} \ No newline at end of file diff --git a/qiskit_ibm_transpiler/ai/synthesis.py b/qiskit_ibm_transpiler/ai/synthesis.py index 3ba6c03..eed2f7d 100644 --- a/qiskit_ibm_transpiler/ai/synthesis.py +++ b/qiskit_ibm_transpiler/ai/synthesis.py @@ -23,6 +23,9 @@ from qiskit.transpiler import CouplingMap from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit.providers.backend import BackendV2 as Backend +from qiskit_ibm_runtime import QiskitRuntimeService + from qiskit_ibm_transpiler.wrappers import ( AICliffordAPI, @@ -53,15 +56,31 @@ def __init__( ], coupling_map: Union[List[List[int]], CouplingMap, None] = None, backend_name: Union[str, None] = None, + backend: Union[Backend, None] = None, replace_only_if_better: bool = True, max_threads: Union[int, None] = None, **kwargs, ) -> None: + if backend_name: + # TODO: Updates with the final date + logger.warning( + "backend_name will be deprecated in February 2025, please use a backend object instead." + ) + if isinstance(coupling_map, CouplingMap): self.coupling_map = list(coupling_map.get_edges()) else: self.coupling_map = coupling_map - self.backend_name = backend_name + + if backend: + self.backend = backend + elif backend_name: + try: + runtime_service = QiskitRuntimeService() + self.backend = runtime_service.backend(name=backend_name) + except Exception: + raise PermissionError(f"ERROR. Backend not supported ({backend_name})") + self.replace_only_if_better = replace_only_if_better self.synth_service = synth_service self.max_threads = max_threads if max_threads else MAX_THREADS @@ -98,7 +117,7 @@ def synth_nodes(self, nodes): synth_inputs, qargs=qargs, coupling_map=self.coupling_map, - backend_name=self.backend_name, + backend=self.backend, ) except TranspilerError as e: logger.warning( @@ -154,16 +173,18 @@ def __init__( self, coupling_map: Union[List[List[int]], CouplingMap, None] = None, backend_name: Union[str, None] = None, + backend: Union[Backend, None] = None, replace_only_if_better: bool = True, max_threads: Union[int, None] = None, **kwargs, ) -> None: super().__init__( - AICliffordAPI(**kwargs), - coupling_map, - backend_name, - replace_only_if_better, - max_threads, + synth_service=AICliffordAPI(**kwargs), + coupling_map=coupling_map, + backend_name=backend_name, + backend=backend, + replace_only_if_better=replace_only_if_better, + max_threads=max_threads, ) def _get_synth_input_and_original(self, node): @@ -200,6 +221,7 @@ def __init__( self, coupling_map: Union[List[List[int]], CouplingMap, None] = None, backend_name: Union[str, None] = None, + backend: Union[Backend, None] = None, replace_only_if_better: bool = True, max_threads: Union[int, None] = None, local_mode: bool = True, @@ -212,11 +234,12 @@ def __init__( ) super().__init__( - ai_synthesis_provider, - coupling_map, - backend_name, - replace_only_if_better, - max_threads, + synth_service=ai_synthesis_provider, + coupling_map=coupling_map, + backend_name=backend_name, + backend=backend, + replace_only_if_better=replace_only_if_better, + max_threads=max_threads, ) def _get_synth_input_and_original(self, node): @@ -250,16 +273,18 @@ def __init__( self, coupling_map: Union[List[List[int]], CouplingMap, None] = None, backend_name: Union[str, None] = None, + backend: Union[Backend, None] = None, replace_only_if_better: bool = True, max_threads: Union[int, None] = None, **kwargs, ) -> None: super().__init__( - AIPermutationAPI(**kwargs), - coupling_map, - backend_name, - replace_only_if_better, - max_threads, + synth_service=AIPermutationAPI(**kwargs), + coupling_map=coupling_map, + backend_name=backend_name, + backend=backend, + replace_only_if_better=replace_only_if_better, + max_threads=max_threads, ) def _get_synth_input_and_original(self, node): diff --git a/qiskit_ibm_transpiler/wrappers/ai_local_linear_function_synthesis.py b/qiskit_ibm_transpiler/wrappers/ai_local_linear_function_synthesis.py index 1acf95d..399293e 100644 --- a/qiskit_ibm_transpiler/wrappers/ai_local_linear_function_synthesis.py +++ b/qiskit_ibm_transpiler/wrappers/ai_local_linear_function_synthesis.py @@ -18,6 +18,7 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import LinearFunction +from qiskit.providers.backend import BackendV2 as Backend logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) @@ -31,7 +32,7 @@ def transpile( circuits: List[Union[QuantumCircuit, LinearFunction]], qargs: List[List[int]], coupling_map: Union[List[List[int]], None] = None, - backend_name: Union[str, None] = None, + backend: Union[Backend, None] = None, ) -> List[Union[QuantumCircuit, None]]: """Synthetize one or more quantum circuits into an optimized equivalent. It differs from a standard synthesis process in that it takes into account where the linear functions are (qargs) and respects it on the synthesized circuit. @@ -49,9 +50,9 @@ def transpile( # Although this function is called `transpile`, it does a synthesis. It has this name because the synthesis # is made as a pass on the Qiskit Pass Manager which is used in the transpilation process. - if not coupling_map and not backend_name: + if not coupling_map and not backend: raise ValueError( - "ERROR. Either a 'coupling_map' or a 'backend_name' must be provided." + "ERROR. Either a 'coupling_map' or a 'backend' must be provided." ) n_circs = len(circuits) @@ -71,7 +72,7 @@ def transpile( synthesized_linear_function = AILinearFunctionInference().synthesize( circuit=circuits[index], - coupling_map=coupling_map, + coupling_map=coupling_map or backend.coupling_map, circuit_qargs=circuit_qargs, ) diff --git a/qiskit_ibm_transpiler/wrappers/ai_synthesis.py b/qiskit_ibm_transpiler/wrappers/ai_synthesis.py index 96ba629..04c4c1d 100644 --- a/qiskit_ibm_transpiler/wrappers/ai_synthesis.py +++ b/qiskit_ibm_transpiler/wrappers/ai_synthesis.py @@ -16,6 +16,7 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import LinearFunction from qiskit.quantum_info import Clifford +from qiskit.providers.backend import BackendV2 as Backend from .base import QiskitTranspilerService @@ -34,8 +35,10 @@ def transpile( circuits: List[Union[QuantumCircuit, Clifford]], qargs: List[List[int]], coupling_map: Union[List[List[int]], None] = None, - backend_name: Union[str, None] = None, + backend: Union[Backend, None] = None, ): + backend_name = getattr(backend, "name", None) + if coupling_map is not None: transpile_resps = self.request_and_wait( endpoint="transpile", @@ -84,7 +87,7 @@ def transpile( circuits: List[Union[QuantumCircuit, LinearFunction]], qargs: List[List[int]], coupling_map: Union[List[List[int]], None] = None, - backend_name: Union[str, None] = None, + backend: Union[Backend, None] = None, ) -> List[Union[QuantumCircuit, None]]: """Synthetize one or more quantum circuits into an optimized equivalent. It differs from a standard synthesis process in that it takes into account where the linear functions are (qargs) and respects it on the synthesized circuit. @@ -102,6 +105,8 @@ def transpile( # Although this function is called `transpile`, it does a synthesis. It has this name because the synthesis # is made as a pass on the Qiskit Pass Manager which is used in the transpilation process. + backend_name = getattr(backend, "name", None) + if not coupling_map and not backend_name: raise ValueError( "ERROR. Either a 'coupling_map' or a 'backend_name' must be provided." @@ -150,8 +155,9 @@ def transpile( patterns: List[List[int]], qargs: List[List[int]], coupling_map: Union[List[List[int]], None] = None, - backend_name: Union[str, None] = None, + backend: Union[Backend, None] = None, ): + backend_name = getattr(backend, "name", None) if coupling_map is not None: transpile_resps = self.request_and_wait( diff --git a/tests/ai/test_ai_local_linear_function_synthesis.py b/tests/ai/test_ai_local_linear_function_synthesis.py index e306725..03d527a 100644 --- a/tests/ai/test_ai_local_linear_function_synthesis.py +++ b/tests/ai/test_ai_local_linear_function_synthesis.py @@ -25,17 +25,16 @@ def test_ai_local_linear_function_synthesis_wrong_backend(): original_circuit.cx(0, 1) original_circuit.cx(1, 2) - ai_linear_functions_synthesis_pass = PassManager( - [ - CollectLinearFunctions(min_block_size=2), - AILinearFunctionSynthesis(backend_name="wrong_backend"), - ] - ) - with pytest.raises( PermissionError, match=r"ERROR. Backend not supported \(\w+\)", ): + ai_linear_functions_synthesis_pass = PassManager( + [ + CollectLinearFunctions(min_block_size=2), + AILinearFunctionSynthesis(backend_name="wrong_backend"), + ] + ) ai_linear_functions_synthesis_pass.run(original_circuit) diff --git a/tests/ai/test_clifford_ai.py b/tests/ai/test_clifford_ai.py index d7ddc2f..cfad260 100644 --- a/tests/ai/test_clifford_ai.py +++ b/tests/ai/test_clifford_ai.py @@ -19,21 +19,18 @@ from qiskit_ibm_transpiler.ai.synthesis import AICliffordSynthesis -def test_clifford_wrong_backend(random_circuit_transpiled, caplog): - ai_optimize_cliff = PassManager( - [ - CollectCliffords(), - AICliffordSynthesis(backend_name="wrong_backend"), - ] - ) - ai_optimized_circuit = ai_optimize_cliff.run(random_circuit_transpiled) - assert "couldn't synthesize the circuit" in caplog.text - assert "Keeping the original circuit" in caplog.text - assert ( - "User doesn't have access to the specified backend: wrong_backend" - in caplog.text - ) - assert isinstance(ai_optimized_circuit, QuantumCircuit) +def test_clifford_wrong_backend(random_circuit_transpiled): + with pytest.raises( + PermissionError, + match=r"ERROR. Backend not supported \(\w+\)", + ): + ai_clifford_synthesis_pass = PassManager( + [ + CollectCliffords(), + AICliffordSynthesis(backend_name="wrong_backend"), + ] + ) + ai_clifford_synthesis_pass.run(random_circuit_transpiled) @pytest.mark.skip(