From 95e1d517988efa571dfc84cf9a1df73dfcf6ac2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Fri, 24 Apr 2026 15:10:34 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cqlib_adapter/pennylane_ext/__init__.py | 2 +- .../pennylane_ext/circuit_executor.py | 861 ++++++------------ cqlib_adapter/pennylane_ext/compilation.py | 272 ++++++ cqlib_adapter/pennylane_ext/device.py | 203 +++-- cqlib_adapter/pennylane_ext/ext_mapping.py | 13 + cqlib_adapter/pennylane_ext/native_gates.py | 165 ++++ cqlib_adapter/qiskit_ext/__init__.py | 9 +- cqlib_adapter/qiskit_ext/gates.py | 193 +--- cqlib_adapter/qiskit_ext/job.py | 4 +- cqlib_adapter/qiskit_ext/sampler.py | 2 +- cqlib_adapter/qiskit_ext/submit_job.py | 12 + cqlib_adapter/qiskit_ext/tianyan_backend.py | 38 +- cqlib_adapter/qiskit_ext/tianyan_provider.py | 4 +- .../{qiskit_ext => utils}/api_client.py | 0 tests/test_pennylane/test_circuit_executor.py | 410 ++++----- tests/test_pennylane/test_compile.py | 282 ++++++ tests/test_pennylane/test_ext_mapping.py | 205 +++++ tests/test_pennylane/test_native_gates.py | 353 +++++++ 18 files changed, 1939 insertions(+), 1089 deletions(-) create mode 100644 cqlib_adapter/pennylane_ext/compilation.py create mode 100644 cqlib_adapter/pennylane_ext/native_gates.py rename cqlib_adapter/{qiskit_ext => utils}/api_client.py (100%) create mode 100644 tests/test_pennylane/test_compile.py create mode 100644 tests/test_pennylane/test_ext_mapping.py create mode 100644 tests/test_pennylane/test_native_gates.py diff --git a/cqlib_adapter/pennylane_ext/__init__.py b/cqlib_adapter/pennylane_ext/__init__.py index 4f08fae..0fd88e5 100644 --- a/cqlib_adapter/pennylane_ext/__init__.py +++ b/cqlib_adapter/pennylane_ext/__init__.py @@ -1,6 +1,6 @@ # This code is part of cqlib. # -# Copyright (C) 2025 China Telecom Quantum Group. +# Copyright (C) 2026 China Telecom Quantum Group. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE file in the root directory diff --git a/cqlib_adapter/pennylane_ext/circuit_executor.py b/cqlib_adapter/pennylane_ext/circuit_executor.py index 222362e..26b79d0 100644 --- a/cqlib_adapter/pennylane_ext/circuit_executor.py +++ b/cqlib_adapter/pennylane_ext/circuit_executor.py @@ -1,257 +1,202 @@ +# This code is part of cqlib. +# +# Copyright (C) 2026 China Telecom Quantum Group. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + """Quantum Circuit Executor for PennyLane Adapter. This module provides a sophisticated circuit executor that bridges PennyLane quantum circuits with various backend computation platforms, including local simulators, -Tianyan simulators, and Tianyan hardware devices. - -Key Features: - - Support for multiple measurement types: expectation values, probabilities, - statevectors, and samples - - Backend-aware execution with automatic capability validation - - Elegant error handling and comprehensive logging - - Seamless integration with PennyLane's quantum tape system +Tianyan simulators, and Tianyan hardware devices. It handles circuit conversion, +backend initialization, execution, and measurement formatting. """ +import json import logging from enum import Enum from functools import singledispatchmethod -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Tuple, Union import numpy as np import pennylane as qml -import json +from pennylane.tape import QuantumScript + from cqlib import TianYanPlatform from cqlib.mapping import transpile_qcis from cqlib.simulator import StatevectorSimulator from cqlib.utils import qasm2 -from pennylane.tape import QuantumScript -from pennylane.io import to_openqasm - class BackendType(Enum): - """Enumeration of supported quantum computation backend types. - - Attributes: - LOCAL_SIMULATOR: Local statevector simulator with full measurement support - TIANYAN_SIMULATOR: Tianyan cloud-based simulator supporting probabilities and samples - TIANYAN_HARDWARE: Physical quantum hardware supporting sample measurements only - """ + """Enumeration of supported quantum computation backend types.""" LOCAL_SIMULATOR = "local" - TIANYAN_SIMULATOR = "tianyan_simulator" + TIANYAN_SIMULATOR = "tianyan_simulator" TIANYAN_HARDWARE = "tianyan_hardware" class CircuitExecutor: """Executes quantum circuits across different computational backends. - - This class serves as the core execution engine for the PennyLane adapter, - providing a unified interface for running quantum circuits on various - backend platforms while handling measurement-specific transformations - and result processing. - - Args: - device_config: Configuration dictionary containing backend settings. - Required keys: - - machine_name: Backend identifier ('default' for local simulator) - - login_key: Authentication key for Tianyan platforms (if applicable) - - shots: Number of measurement shots (None for statevector simulations) - - wires: Number of qubits in the system - - verbose: Enable verbose logging if True - Raises: - ConnectionError: If backend connection initialization fails - ValueError: If device configuration is invalid or incomplete - - Example: - >>> config = { - ... 'machine_name': 'default', - ... 'shots': 1000, - ... 'wires': 2, - ... 'verbose': True - ... } - >>> executor = CircuitExecutor(config) - >>> result = executor.execute_circuit(quantum_tape) + This class abstracts the connection and execution logic for local simulation, + cloud simulation, and actual quantum hardware provided by the Tianyan platform. """ - #: Set of Tianyan hardware backend identifiers - TIANYAN_HARDWARE_BACKENDS = { - "tianyan24", "tianyan504", "tianyan176-2", "tianyan176" - } - - #: Set of Tianyan simulator backend identifiers - TIANYAN_SIMULATOR_BACKENDS = { - "tianyan_sw", "tianyan_s", "tianyan_tn", - "tianyan_tnn", "tianyan_sa", "tianyan_swn" - } - def __init__(self, device_config: Dict[str, Any]) -> None: - """Initialize the circuit executor with device configuration. - - The initialization process includes: - 1. Setting up logging infrastructure - 2. Determining backend type from configuration - 3. Initializing backend connection (for remote platforms) - 4. Preparing measurement processing components + """Initializes the circuit executor with the provided device configuration. Args: - device_config: Dictionary containing all necessary configuration - parameters for backend operation and circuit execution. + device_config: A dictionary containing device settings such as 'machine_name', + 'login_key', 'shots', and 'wires'. Raises: - ConnectionError: If remote backend connection cannot be established - ValueError: If required configuration parameters are missing + ValueError: If a required 'login_key' is missing for a non-default backend. + ConnectionError: If the connection to the Tianyan API fails. """ self.device_config = device_config self.logger = self._setup_logger() - self.cqlib_backend = None + self.cqlib_backend: Optional[TianYanPlatform] = None self._execution_count = 0 + + # Local import to prevent circular dependencies during module initialization + from .device import CQLibDevice + + machine_name = self.device_config.get('machine_name', 'default') + + if machine_name != "default": + login_key = self.device_config.get('login_key') + if not login_key: + raise ValueError(f"Login key required for backend: {machine_name}") + + # Fetch backends from API and cache in CQLibDevice class + try: + CQLibDevice.get_available_backends(token=login_key) + except Exception as e: + self.logger.error("Failed to fetch backend list: %s", e) + raise ConnectionError(f"Could not connect to Tianyan API: {e}") from e + self._backend_type = self._determine_backend_type() self._initialize_backend() + self.logger.info("CircuitExecutor initialized with %s backend", self._backend_type.value) def _setup_logger(self) -> logging.Logger: - """Configure and return a logger instance for execution tracking. - + """Configures and returns a logger instance for execution tracking. + Returns: - Configured logger instance with appropriate handlers and formatters. - Log level is set to INFO if verbose mode is enabled in configuration. + A configured logging.Logger instance. """ logger = logging.getLogger(f"CircuitExecutor.{id(self)}") - + if self.device_config.get('verbose', False): logger.setLevel(logging.INFO) handler = logging.StreamHandler() - formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) - logger.addHandler(handler) - + # Prevent adding multiple handlers if initialized multiple times + if not logger.handlers: + logger.addHandler(handler) + return logger def _determine_backend_type(self) -> BackendType: - """Determine the appropriate backend type from device configuration. - + """Determines the appropriate backend type based on the device configuration. + Returns: - BackendType enum value corresponding to the configured machine. + The resolved BackendType enumeration. Raises: - ValueError: If machine_name is not recognized or supported + ValueError: If the specified machine_name is not recognized or supported. """ + from .device import CQLibDevice + backend_name = self.device_config.get('machine_name', 'default') - + if backend_name == "default": return BackendType.LOCAL_SIMULATOR - elif backend_name in self.TIANYAN_HARDWARE_BACKENDS: + if backend_name in CQLibDevice.TIANYAN_HARDWARE_BACKENDS: return BackendType.TIANYAN_HARDWARE - elif backend_name in self.TIANYAN_SIMULATOR_BACKENDS: + if backend_name in CQLibDevice.TIANYAN_SIMULATOR_BACKENDS: return BackendType.TIANYAN_SIMULATOR - else: - raise ValueError(f"Unknown or unsupported backend: {backend_name}") + + raise ValueError(f"Unknown or unsupported backend: {backend_name}") def _initialize_backend(self) -> None: - """Initialize connection to the quantum computation backend. - - For local simulators, no connection is needed. For Tianyan platforms, - this method establishes the API connection using provided credentials. + """Initializes the connection to the quantum computation backend. Raises: - ConnectionError: If remote backend connection fails - ValueError: If required login credentials are missing + ValueError: If a login key is required but missing. + ConnectionError: If establishing the backend connection fails. """ if self._backend_type == BackendType.LOCAL_SIMULATOR: - self.logger.debug("Using local simulator - no backend connection needed") + self.logger.debug("Using local simulator - no backend connection needed.") return - + login_key = self.device_config.get('login_key') if not login_key: - raise ValueError("Login key required for Tianyan backend authentication") - + raise ValueError("Login key required for Tianyan backend authentication.") + try: self.cqlib_backend = TianYanPlatform( login_key=login_key, machine_name=self.device_config['machine_name'] ) - self.logger.info("Successfully connected to Tianyan backend: %s", - self.device_config['machine_name']) + self.logger.info( + "Successfully connected to Tianyan backend: %s", + self.device_config['machine_name'] + ) except Exception as error: - self.logger.error("Backend connection failed for %s: %s", - self.device_config['machine_name'], error) + self.logger.error( + "Backend connection failed for %s: %s", + self.device_config['machine_name'], error + ) raise ConnectionError(f"Backend connection failed: {error}") from error - def execute_circuit(self, circuit: QuantumScript) -> Union[List, Any]: - """Execute a quantum circuit and return measurement results. - - This is the main entry point for circuit execution. It handles the complete - workflow including circuit validation, measurement processing, backend - execution, and result formatting. + def execute_circuit(self, circuit: QuantumScript) -> Union[List[Any], Any]: + """Executes a quantum circuit and returns the measurement results. Args: - circuit: PennyLane QuantumScript object containing quantum operations - and measurements to execute. + circuit: The PennyLane quantum circuit to execute. Returns: - Single measurement result if the circuit contains only one measurement, - otherwise a list of results corresponding to each measurement in the circuit. - - Raises: - ValueError: If circuit validation fails or backend doesn't support - requested measurements - NotImplementedError: For unsupported measurement types - ConnectionError: If backend execution fails - - Example: - >>> # Circuit with single measurement - >>> result = executor.execute_circuit(tape) - >>> print(f"Expectation value: {result}") - - >>> # Circuit with multiple measurements - >>> results = executor.execute_circuit(tape) - >>> prob_result, sample_result = results + A single measurement result if the circuit contains one measurement, + otherwise a list of results corresponding to each measurement. """ self._validate_circuit(circuit) self._execution_count += 1 - wire_labels = list(circuit.wires.labels) results = [] - + for measurement in circuit.measurements: - # Validate backend support before processing self._validate_measurement_support(measurement) - - cqlib_circuit, cqlib_qcis = self._convert_to_cqlib_format(circuit) + cqlib_circuit, cqlib_qcis = self._convert_to_cqlib_format(circuit) raw_result = self._execute_on_backend(cqlib_circuit, cqlib_qcis) + result = self._execute_measurement(measurement, raw_result) - reordered_result = self._reorder_raw_result_by_wire_labels(raw_result, wire_labels) - # Execute with measurement-specific handler - - result = self._execute_measurement(measurement, reordered_result) - results.append(result) - # Return single result for circuits with one measurement, list for multiple return results[0] if len(results) == 1 else results def _validate_circuit(self, circuit: QuantumScript) -> None: - """Validate circuit constraints and configuration compatibility. - - Ensures that the circuit configuration is compatible with the selected - backend and measurement types. + """Validates circuit constraints against the current device configuration. Args: - circuit: Quantum circuit to validate + circuit: The PennyLane quantum circuit to validate. Raises: - ValueError: If circuit contains state measurements with finite shots - or other incompatible configurations + ValueError: If exact state vector simulation is requested with finite shots. """ - # Check for state measurement with finite shots has_state_measurement = any( isinstance(m, qml.measurements.StateMP) for m in circuit.measurements ) shots = self.device_config.get('shots') - + if has_state_measurement and shots is not None: raise ValueError( f"State measurement requires shots=None for exact statevector simulation. " @@ -259,201 +204,208 @@ class CircuitExecutor: ) def _validate_measurement_support(self, measurement: Any) -> None: - """Validate that the current backend supports the requested measurement type. - + """Validates that the active backend supports the requested measurement. + Args: - measurement: Measurement object to validate + measurement: The PennyLane measurement process instance. Raises: - ValueError: If the backend doesn't support the measurement type + ValueError: If the measurement type is unsupported by the backend. """ - # Define measurement support matrix for each backend type supported_measurements = { BackendType.LOCAL_SIMULATOR: { - qml.measurements.ProbabilityMP, # Probability distributions - qml.measurements.ExpectationMP, # Expectation values - qml.measurements.StateMP, # Full statevector - qml.measurements.SampleMP # Measurement samples + qml.measurements.ProbabilityMP, + qml.measurements.ExpectationMP, + qml.measurements.StateMP, + qml.measurements.SampleMP }, BackendType.TIANYAN_SIMULATOR: { - qml.measurements.ProbabilityMP, # Probability distributions - qml.measurements.SampleMP, # Measurement samples - qml.measurements.ExpectationMP, # Expectation values + qml.measurements.ProbabilityMP, + qml.measurements.SampleMP, + qml.measurements.ExpectationMP, }, BackendType.TIANYAN_HARDWARE: { - qml.measurements.ProbabilityMP, # Probability distributions - qml.measurements.SampleMP, # Measurement samples - qml.measurements.ExpectationMP, # Expectation values + qml.measurements.ProbabilityMP, + qml.measurements.SampleMP, + qml.measurements.ExpectationMP, } } - + measurement_type = type(measurement) if measurement_type not in supported_measurements[self._backend_type]: + supported_names = [m.__name__ for m in supported_measurements[self._backend_type]] raise ValueError( f"Backend {self._backend_type.value} does not support " f"measurement type {measurement_type.__name__}. " - f"Supported measurements: {[m.__name__ for m in supported_measurements[self._backend_type]]}" + f"Supported measurements: {supported_names}" ) - def _convert_to_cqlib_format(self, circuit: QuantumScript) -> tuple[Any, str]: - """Convert PennyLane circuit to CQLib-compatible format. - + def _convert_to_cqlib_format(self, circuit: QuantumScript) -> Tuple[Any, str]: + """Converts a PennyLane circuit into CQLib-compatible parsed objects and QCIS string. + Args: - circuit: PennyLane QuantumScript to convert + circuit: The PennyLane quantum circuit. Returns: - Tuple containing (cqlib_circuit_object, qcis_instruction_string) + A tuple containing the parsed CQLib circuit object and its QCIS string representation. Raises: - ValueError: If circuit conversion fails + ValueError: If parsing or compilation fails. """ try: - qasm_string = circuit.to_openqasm() + qasm_string = self._build_custom_qasm(circuit) cqlib_circuit = qasm2.loads(qasm_string) return cqlib_circuit, cqlib_circuit.qcis except Exception as error: self.logger.error("Circuit conversion from PennyLane to CQLib format failed: %s", error) raise ValueError(f"Circuit conversion failed: {error}") from error - def _execute_measurement(self, measurement: Any, raw_result: Dict[str, Any]) -> Any: - """Execute circuit with measurement-specific processing. - - Dispatches to appropriate measurement handler based on measurement type - using Python's singledispatchmethod for clean, extensible design. + def _build_custom_qasm(self, circuit: QuantumScript) -> str: + """Builds an OpenQASM 2.0 string manually to handle custom native gates. Args: - measurement: Measurement object defining what to measure - raw_result: Raw result dictionary from backend execution (already reordered) + circuit: The PennyLane quantum circuit. Returns: - Processed measurement result in PennyLane-compatible format + A well-formed OpenQASM 2.0 string representing the circuit operations. """ + device_wires = self.device_config.get('wires') + if isinstance(device_wires, int): + num_wires = device_wires + elif device_wires is not None and hasattr(device_wires, '__len__'): + num_wires = len(device_wires) + else: + num_wires = max(circuit.wires.labels) + 1 if circuit.wires else 1 + + qasm_lines = [ + "OPENQASM 2.0;", + 'include "qelib1.inc";', + f"qreg q[{num_wires}];", + f"creg c[{num_wires}];" + ] + + for op in circuit.operations: + op_name = op.name + wires = op.wires.tolist() + params = op.parameters + q_str = ",".join([f"q[{w}]" for w in wires]) + + if op_name == "X2PGate": + qasm_lines.append(f"x2p {q_str};") + elif op_name == "X2MGate": + qasm_lines.append(f"x2m {q_str};") + elif op_name == "Y2PGate": + qasm_lines.append(f"y2p {q_str};") + elif op_name == "Y2MGate": + qasm_lines.append(f"y2m {q_str};") + elif op_name == "XY2PGate": + qasm_lines.append(f"xy2p({params[0]}) {q_str};") + elif op_name == "XY2MGate": + qasm_lines.append(f"xy2m({params[0]}) {q_str};") + elif op_name == "PauliX": + qasm_lines.append(f"x {q_str};") + elif op_name == "PauliY": + qasm_lines.append(f"y {q_str};") + elif op_name == "PauliZ": + qasm_lines.append(f"z {q_str};") + elif op_name == "Hadamard": + qasm_lines.append(f"h {q_str};") + elif op_name == "RX": + qasm_lines.append(f"rx({params[0]}) {q_str};") + elif op_name == "RY": + qasm_lines.append(f"ry({params[0]}) {q_str};") + elif op_name == "RZ": + qasm_lines.append(f"rz({params[0]}) {q_str};") + elif op_name == "CNOT": + qasm_lines.append(f"cx {q_str};") + elif op_name == "CZ": + qasm_lines.append(f"cz {q_str};") + elif op_name == "S": + qasm_lines.append(f"s {q_str};") + elif op_name == "T": + qasm_lines.append(f"t {q_str};") + else: + try: + partial_qasm = op.to_openqasm().split('\n') + valid_lines = [ + l for l in partial_qasm + if not l.startswith(('OPENQASM', 'include', 'qreg', 'creg')) and l.strip() + ] + qasm_lines.extend(valid_lines) + except Exception: + self.logger.warning( + f"Operation {op_name} not natively mapped and QASM fallback failed." + ) + + return "\n".join(qasm_lines) + + def _execute_measurement(self, measurement: Any, raw_result: Dict[str, Any]) -> Any: + """Dispatches the raw result to the appropriate measurement processing implementation.""" return self._execute_measurement_impl(measurement, raw_result) @singledispatchmethod def _execute_measurement_impl(self, measurement: Any, raw_result: Dict[str, Any]) -> Any: - """Base implementation for unsupported measurement types. - - This method is called when no specific handler is registered for - the measurement type. - - Raises: - NotImplementedError: Always raised for unregistered measurement types - """ + """Fallback implementation for unsupported measurement types.""" raise NotImplementedError( f"Measurement type {type(measurement).__name__} is not supported. " f"Supported types: ProbabilityMP, ExpectationMP, StateMP, SampleMP" ) @_execute_measurement_impl.register - def _(self, measurement: qml.measurements.ProbabilityMP, raw_result: Dict[str, Any]) -> np.ndarray: - """Execute probability measurement and return probability distribution. - - Probability measurements return an array where each element represents - the probability of measuring the corresponding computational basis state. - - Args: - measurement: Probability measurement object - raw_result: Raw result dictionary from backend execution - - Returns: - numpy.ndarray: Probability distribution over computational basis states. - The array has length 2^n_qubits and sums to 1.0. - - Example: - >>> # For a 2-qubit system, returns array of length 4 - >>> probs = executor.execute_circuit(tape_with_prob_measurement) - >>> print(f"Probability of |00>: {probs[0]}") - """ + def _( + self, measurement: qml.measurements.ProbabilityMP, raw_result: Dict[str, Any]) -> np.ndarray: + """Extracts and formats probability distributions, supporting partial measurement.""" probabilities = self._extract_probabilities(raw_result) - return self._format_probabilities(probabilities) - @_execute_measurement_impl.register - def _(self, measurement: qml.measurements.ExpectationMP, raw_result: Dict[str, Any]) -> float: - """Execute expectation value measurement for Pauli-Z observables. - - Computes the expectation value of Pauli-Z observables and Pauli-Z strings - directly from probability distribution results. This method assumes the - circuit has already been transformed to the Z-basis measurement. + if measurement.wires: + device_wires = self.device_config.get('wires') + if isinstance(device_wires, int): + device_wire_list = list(range(device_wires)) + else: + device_wire_list = list(device_wires) - Args: - measurement: Expectation measurement object containing a Pauli-Z observable - raw_result: Raw result dictionary containing 'probabilities' key with - a dictionary mapping bitstrings to probabilities + target_indices = [device_wire_list.index(w) for w in measurement.wires.labels] - Returns: - float: Expectation value of the Pauli-Z observable + marginal_probs = {} + for bitstring, prob in probabilities.items(): + sub_bitstring = "".join([bitstring[i] for i in target_indices if i < len(bitstring)]) + marginal_probs[sub_bitstring] = marginal_probs.get(sub_bitstring, 0.0) + prob - Note: - - Only supports Pauli-Z observables and tensor products of Pauli-Z - - Assumes basis transformation has been applied prior to measurement - - Works with probability distributions from both simulators and hardware - - For non-Z observables, use basis transformation in the circuit + return self._format_probabilities(marginal_probs) - Example: - >>> # Expectation of Z(0) ⊗ Z(1) ⊗ Z(2) - >>> obs = qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliZ(2) - >>> expval = executor.execute_circuit(tape_with_expval_measurement) - >>> print(f" = {expval}") + return self._format_probabilities(probabilities) - Raises: - ValueError: If raw_result doesn't contain probability distribution - KeyError: If required keys are missing in raw_result - """ - # Validate input + @_execute_measurement_impl.register + def _(self, measurement: qml.measurements.ExpectationMP, raw_result: Dict[str, Any]) -> float: + """Calculates the expectation value for Pauli-Z observables from raw probabilities.""" if 'probabilities' not in raw_result: raise ValueError("raw_result must contain 'probabilities' key") - + probabilities = raw_result['probabilities'] if not isinstance(probabilities, dict): raise ValueError("probabilities must be a dictionary") - - # Extract qubit indices from observable + pauli_indices = list(measurement.obs.wires.labels) - expectation = 0.0 - + for bitstring, prob in probabilities.items(): - # Calculate eigenvalue for this basis state eigenvalue = 1 for qubit_idx in pauli_indices: if qubit_idx < len(bitstring): - # For Pauli-Z: |0⟩ → +1, |1⟩ → -1 if bitstring[qubit_idx] == '1': eigenvalue *= -1 - - # Expectation = Σ (probability × eigenvalue) expectation += prob * eigenvalue - + return expectation - + @_execute_measurement_impl.register def _(self, measurement: qml.measurements.StateMP, raw_result: Dict[str, Any]) -> np.ndarray: - """Execute statevector measurement and return the full quantum state. - - Returns the complete statevector of the quantum system. This measurement - is only supported on local statevector simulators. - - Args: - measurement: State measurement object - raw_result: Raw result dictionary from backend execution - - Returns: - numpy.ndarray: Complex-valued statevector of length 2^n_qubits - - Raises: - ValueError: If attempted on non-local simulator backend - - Example: - >>> statevector = executor.execute_circuit(tape_with_state_measurement) - >>> print(f"Statevector shape: {statevector.shape}") - """ + """Extracts the statevector from simulation results.""" if self._backend_type != BackendType.LOCAL_SIMULATOR: raise ValueError( "Statevector measurement is only supported on local simulators. " f"Current backend: {self._backend_type.value}" ) - statevector = raw_result.get('statevector') if statevector is None: raise ValueError("Statevector not found in execution results") @@ -461,81 +413,47 @@ class CircuitExecutor: @_execute_measurement_impl.register def _(self, measurement: qml.measurements.SampleMP, raw_result: Dict[str, Any]) -> np.ndarray: - """Execute sampling measurement and return measurement samples. - - Returns raw measurement samples from multiple circuit executions. - Each sample is a bitstring representing the measurement outcome. - - Args: - measurement: Sample measurement object - raw_result: Raw result dictionary from backend execution - - Returns: - numpy.ndarray: Array of shape (n_shots, n_qubits) where each row - is a measurement outcome and each column is a qubit measurement result. - - Example: - >>> samples = executor.execute_circuit(tape_with_sample_measurement) - >>> print(f"Samples shape: {samples.shape}") - >>> print(f"First measurement: {samples[0]}") - """ + """Extracts measurement samples from execution results, supporting partial measurement.""" samples = raw_result.get('samples') if samples is None: raise ValueError("No measurement samples found in execution results") + + if measurement.wires: + device_wires = self.device_config.get('wires') + if isinstance(device_wires, int): + device_wire_list = list(range(device_wires)) + else: + device_wire_list = list(device_wires) + + target_indices = [device_wire_list.index(w) for w in measurement.wires.labels] + return samples[:, target_indices] + return samples - - def _execute_on_backend(self, cqlib_circuit: Any, cqlib_qcis: str) -> Dict[str, Any]: - """Execute circuit on the appropriate backend and return raw results. - - Args: - cqlib_circuit: Circuit in CQLib object format - cqlib_qcis: Circuit in QCIS instruction format - - Returns: - Dictionary containing raw results from backend execution - Raises: - ConnectionError: If backend execution fails - """ + def _execute_on_backend(self, cqlib_circuit: Any, cqlib_qcis: str) -> Dict[str, Any]: + """Routes execution to the appropriate backend handler.""" backend_handlers = { BackendType.LOCAL_SIMULATOR: self._execute_local_simulator, BackendType.TIANYAN_SIMULATOR: self._execute_tianyan_simulator, BackendType.TIANYAN_HARDWARE: self._execute_tianyan_hardware } - handler = backend_handlers[self._backend_type] return handler(cqlib_circuit, cqlib_qcis) def _execute_local_simulator(self, cqlib_circuit: Any, cqlib_qcis: str) -> Dict[str, Any]: - """Execute circuit on local statevector simulator. - - Returns comprehensive results including probabilities, samples, and - statevector for local simulation. - - Returns: - Dictionary with keys: 'probabilities', 'samples', 'statevector' - """ + """Executes the circuit on the local statevector simulator.""" simulator = StatevectorSimulator(cqlib_circuit) - nwe = dict(reversed(simulator.probs().items())) - - reversed_statevector = {key[::-1]: value for key, value in simulator.statevector().items()} return { 'probabilities': simulator.probs(), 'samples': simulator.sample(is_raw_data=True), - 'statevector': reversed_statevector + 'statevector': dict(reversed(simulator.statevector().items())) } def _execute_tianyan_simulator(self, cqlib_circuit: Any, cqlib_qcis: str) -> Dict[str, Any]: - """Execute circuit on Tianyan cloud simulator. - - Returns probability distributions and measurement samples from - Tianyan's cloud-based simulators. - - Returns: - Dictionary with keys: 'probabilities', 'samples' - """ + """Executes the circuit on the Tianyan cloud simulator via API.""" + cqlib_circuit.measure_all() query_id = self.cqlib_backend.submit_experiment( - cqlib_qcis, + cqlib_circuit.qcis, num_shots=self.device_config.get('shots') ) raw_result = self.cqlib_backend.query_experiment(query_id)[0] @@ -546,35 +464,27 @@ class CircuitExecutor: } def _execute_tianyan_hardware(self, cqlib_circuit: Any, cqlib_qcis: str) -> Dict[str, Any]: - """Execute circuit on Tianyan quantum hardware. - - Submits circuit to physical quantum hardware and returns measurement - samples. Hardware execution includes readout calibration and error - mitigation where available. - - Returns: - Dictionary with key: 'samples' - - Note: - Hardware execution may involve queueing and longer execution times - """ + """Executes the circuit on physical Tianyan quantum hardware.""" from .ext_mapping import HardwareMapper + cqlib_circuit.measure_all() mapper = self.device_config.get('mapping', None) + if mapper: - MAP = HardwareMapper(mapper) - compiled_circuit = MAP.map_qcis_code(cqlib_circuit.qcis) - + hw_map = HardwareMapper(mapper) + compiled_circuit = hw_map.map_qcis_code(cqlib_circuit.qcis) else: compiled_circuit = transpile_qcis(cqlib_qcis, self.cqlib_backend)[0] - compiled_circuit = compiled_circuit.qcis - + compiled_circuit.measure_all() + print("transpile successfully!") + if hasattr(compiled_circuit, 'qcis'): + compiled_circuit = compiled_circuit.qcis query_id = self.cqlib_backend.submit_experiment( - compiled_circuit, + compiled_circuit, num_shots=self.device_config.get('shots') ) raw_result = self.cqlib_backend.query_experiment( - query_id, + query_id, readout_calibration=True ) sample_res = np.array(raw_result[0]['resultStatus'][1:]) @@ -583,243 +493,57 @@ class CircuitExecutor: 'samples': sample_res } - def _reorder_raw_result_by_wire_labels(self, raw_result: Dict[str, Any], - wire_labels: List[int]) -> Dict[str, Any]: - """Reorder raw results to match PennyLane's wire label ordering. - - PennyLane may reorder qubits during compilation, and circuit.wires.labels - indicates the final classical bit ordering. This method reorders raw results - before they are processed by measurement-specific handlers. - - Args: - raw_result: The raw result dictionary from backend execution - wire_labels: List of wire labels from circuit.wires.labels, e.g., [0, 2, 1] - - Returns: - Reordered raw result dictionary matching the wire_labels ordering - """ - - reordered_result = raw_result.copy() - - # Reorder probabilities if present - if 'probabilities' in raw_result and raw_result['probabilities']: - reordered_result['probabilities'] = self._reorder_probability_dict( - raw_result['probabilities'], wire_labels - ) - - # Reorder samples if present - if 'samples' in raw_result and raw_result['samples'] is not None: - sample = self._extract_samples(raw_result) - reordered_result['samples'] = self._reorder_sample_matrix( - sample, wire_labels - ) - - # Reorder statevector if present - if 'statevector' in raw_result and raw_result['statevector'] is not None: - statevector = raw_result['statevector'] - reordered_result['statevector'] = self._reorder_statevector( - statevector, wire_labels - ) - - return reordered_result - - def _reorder_probability_dict(self, probabilities: Dict[str, float], - wire_labels: List[int]) -> Dict[str, float]: - """Reorder probability dictionary based on wire labels mapping.""" - reordered_probabilities = {} - - for bitstring, probability in probabilities.items(): - # Convert to list and reverse for little-endian to big-endian - bits = list(bitstring)[::-1] - - # Create new bit array and apply wire label mapping - reordered_bits = ['0'] * len(wire_labels) - for new_pos, original_pos in enumerate(wire_labels): - reordered_bits[original_pos] = bits[new_pos] - - # Convert back to string format - reordered_bitstring = ''.join(reordered_bits[::-1]) - reordered_probabilities[reordered_bitstring] = probability - - return reordered_probabilities - - def _reorder_sample_matrix(self, samples: np.ndarray, - wire_labels: List[int]) -> np.ndarray: - """Reorder sample matrix based on wire labels. - - Args: - samples: Sample matrix of shape (n_shots, n_qubits) - wire_labels: Desired wire ordering, e.g., [0, 2, 1] - - Returns: - Reordered sample matrix - """ - n_qubits = len(wire_labels) - - # Create the permutation to go from current order to desired order - current_to_desired = [wire_labels.index(i) for i in range(n_qubits)] - - # Reorder columns - return samples[:, current_to_desired] - - def _reorder_statevector(self, statevector: Dict[str, complex], - wire_labels: List[int]) -> Dict[str, complex]: - """Reorder statevector dictionary based on wire labels. - - Args: - statevector: Dictionary mapping bitstrings to complex amplitudes - wire_labels: Desired wire ordering, e.g., [0, 2, 1] - - Returns: - Reordered statevector dictionary - """ - n_qubits = len(wire_labels) - - # Create the permutation to go from current order to desired order - # For example, if wire_labels = [0, 2, 1], then: - # current_to_desired = [0, 2, 1] means: - # - bit 0 stays at position 0 - # - bit 1 goes to position 2 - # - bit 2 goes to position 1 - current_to_desired = [wire_labels.index(i) for i in range(n_qubits)] - - reordered_statevector = {} - - for bitstring, amplitude in statevector.items(): - # Convert bitstring to list of bits - bits = list(bitstring) - # Reorder bits according to the permutation - reordered_bits = [bits[current_to_desired[i]] for i in range(n_qubits)] - reordered_bitstring = ''.join(reordered_bits) - - # Store with reordered bitstring - reordered_statevector[reordered_bitstring] = amplitude - - return reordered_statevector - def _extract_probabilities(self, raw_result: Dict[str, Any]) -> Dict[str, float]: - """Extract and process probability distribution from raw results. - - Handles endianness conversion to ensure consistent little-endian - format across all backends. - - Args: - raw_result: Raw result dictionary from backend execution - - Returns: - Probability dictionary mapping bitstrings to probabilities - """ + """Extracts and endian-reverses the probability distribution dictionary.""" probabilities = raw_result.get('probabilities', {}) - - # Convert to little-endian format for consistency if probabilities and isinstance(probabilities, dict): return {key[::-1]: value for key, value in probabilities.items()} - return probabilities def _extract_samples(self, raw_result: Dict[str, Any]) -> Any: - """Extract and format samples from raw results. - - Converts local simulator samples to PennyLane-compatible format - while preserving Tianyan backend sample formats. - - Args: - raw_result: Raw result dictionary from backend execution - - Returns: - Formatted samples appropriate for the backend type - """ + """Extracts and converts samples to the format expected by PennyLane.""" samples = raw_result.get('samples') - - # Convert local simulator samples to standard format if samples is not None and self._backend_type == BackendType.LOCAL_SIMULATOR: return samples_to_pennylane_format(samples, self.device_config['wires']) - return samples def _format_probabilities(self, probabilities: Dict[str, float]) -> np.ndarray: - """Convert probability dictionary to PennyLane array format. - + """Converts a probability dictionary into a dense NumPy array distribution. + Args: - probabilities: Dictionary mapping bitstrings to probabilities + probabilities: A dictionary mapping binary bitstrings to float probabilities. Returns: - numpy.ndarray: Probability array indexed by computational basis states + A 1D numpy array containing the dense probability distribution. Raises: - ValueError: If probability dictionary is empty or invalid + ValueError: If the input probability dictionary is empty. """ if not probabilities: raise ValueError("No probability distribution found in execution results") - + num_qubits = len(next(iter(probabilities.keys()))) prob_array = np.zeros(2 ** num_qubits) for bitstring, prob in probabilities.items(): - index = int(bitstring, 2) # Convert binary string to integer index + index = int(bitstring, 2) prob_array[index] = prob return prob_array - def _format_samples(self, samples: Any, measurement: qml.measurements.SampleMP) -> np.ndarray: - """Format samples for PennyLane compatibility. - - Args: - samples: Raw samples from backend execution - measurement: Sample measurement object for context - - Returns: - Formatted samples array - - Raises: - ValueError: If no samples are found in results - """ - if samples is None: - raise ValueError("No measurement samples found in execution results") - return samples - - def get_execution_stats(self) -> Dict[str, Any]: - """Get execution statistics and performance metrics. - - Returns: - Dictionary containing execution count, backend information, - and configuration details. - - Example: - >>> stats = executor.get_execution_stats() - >>> print(f"Total executions: {stats['execution_count']}") - >>> print(f"Backend type: {stats['backend_type']}") - """ - return { - "execution_count": self._execution_count, - "backend_type": self._backend_type.value, - "wires": self.device_config.get('wires'), - "shots": self.device_config.get('shots'), - "machine_name": self.device_config.get('machine_name') - } - def decimal_to_binary_array( - decimal_value: int, - num_bits: int, - little_endian: bool = True + decimal_value: int, num_bits: int, little_endian: bool = True ) -> np.ndarray: - """Convert decimal integer to binary array representation. - + """Converts a decimal integer into a binary numpy array. + Args: - decimal_value: Integer value to convert to binary - num_bits: Number of bits in the binary representation - little_endian: If True, least significant bit is at index 0. - If False, most significant bit is at index 0. + decimal_value: The integer value to convert. + num_bits: The fixed width of the resulting binary array. + little_endian: If True, reverses the bit order (LSB first). Returns: - numpy.ndarray: Binary array of length num_bits containing 0s and 1s - - Example: - >>> decimal_to_binary_array(5, 4, little_endian=True) - array([1, 0, 1, 0]) # 5 = 1*2^0 + 0*2^1 + 1*2^2 + 0*2^3 - >>> decimal_to_binary_array(5, 4, little_endian=False) - array([0, 1, 0, 1]) # 5 = 0*2^3 + 1*2^2 + 0*2^1 + 1*2^0 + A 1D numpy array of bits. """ binary_string = np.binary_repr(int(decimal_value), width=num_bits) bits = np.array([int(bit) for bit in binary_string]) @@ -832,31 +556,22 @@ def samples_to_pennylane_format( measured_qubits: Optional[List[int]] = None, little_endian: bool = True ) -> np.ndarray: - """Convert decimal samples to PennyLane-compatible binary matrix. - + """Converts raw sample data into a PennyLane compatible binary matrix. + Args: - samples: Array of decimal integers representing measurement outcomes - num_qubits: Total number of qubits in the system - measured_qubits: Specific qubits that were measured - little_endian: Endianness convention for bit ordering + samples: A list or 1D array of decimal sample values. + num_qubits: Total number of qubits in the circuit. + measured_qubits: Explicit list of qubit indices that were measured. + little_endian: If True, enforces little-endian bit ordering. Returns: - numpy.ndarray: Binary matrix of shape (n_shots, n_bits) where - each row is a measurement outcome and each column is a qubit result. + A 2D numpy array of shape (n_shots, num_bits) containing binary samples. Raises: - ValueError: If number of bits cannot be determined from inputs - - Example: - >>> samples = [1, 3, 2] # Decimal measurement outcomes - >>> samples_to_pennylane_format(samples, num_qubits=2) - array([[1, 0], # 1 in binary (little-endian) - [1, 1], # 3 in binary - [0, 1]]) # 2 in binary + ValueError: If sample dimensions cannot be inferred from inputs. """ samples_array = np.asarray(samples) - # Determine required number of bits if measured_qubits is not None: num_bits = len(measured_qubits) elif num_qubits is not None: @@ -867,34 +582,10 @@ def samples_to_pennylane_format( max_value = np.max(samples_array) num_bits = int(np.ceil(np.log2(max_value + 1))) if max_value > 0 else 1 - # Convert each sample to binary representation n_shots = len(samples_array) binary_matrix = np.zeros((n_shots, num_bits), dtype=int) for i, sample in enumerate(samples_array): binary_matrix[i] = decimal_to_binary_array(sample, num_bits, little_endian) - - return binary_matrix - - -def switch_endianness( - binary_data: Union[List[int], np.ndarray, List[List[int]]] -) -> np.ndarray: - """Reverse the bit order (endianness) of binary data. - - Args: - binary_data: Binary data to convert. Can be 1D array (single measurement) - or 2D array (multiple measurements, each row is a bitstring) - - Returns: - numpy.ndarray: Binary data with bit order reversed along the last axis - - Example: - >>> switch_endianness([1, 0, 1, 0]) - array([0, 1, 0, 1]) # Big-endian to little-endian - >>> switch_endianness([[1, 0], [0, 1]]) - array([[0, 1], # Each row reversed independently - [1, 0]]) - """ - data_array = np.asarray(binary_data) - return data_array[..., ::-1] # Reverse along the last axis (bit dimension) \ No newline at end of file + + return binary_matrix \ No newline at end of file diff --git a/cqlib_adapter/pennylane_ext/compilation.py b/cqlib_adapter/pennylane_ext/compilation.py new file mode 100644 index 0000000..0d06ab3 --- /dev/null +++ b/cqlib_adapter/pennylane_ext/compilation.py @@ -0,0 +1,272 @@ +# This code is part of cqlib. +# +# Copyright (C) 2026 China Telecom Quantum Group. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + +"""Quantum circuit compilation module for PennyLane and CQLib. + +This module provides functions to transform standard PennyLane quantum circuits +into native gate sequences compatible with the CQLib hardware backend. +""" + +from typing import Any, List, Union + +import numpy as np +import pennylane as qml +from cqlib import Circuit +from pennylane.tape import QuantumScript +from pennylane.transforms import decompose +from pennylane.workflow import construct_tape + +# Local application imports +from .native_gates import ( + X2MGate, + X2PGate, + XY2MGate, + XY2PGate, + Y2MGate, + Y2PGate, +) + +# Constants +PI = np.pi +PI_2 = np.pi / 2 + +NATIVE_GATES_TO_RETAIN = { + qml.CNOT, qml.CZ, qml.Hadamard, + qml.RX, qml.RY, qml.RZ, + qml.PauliX, qml.PauliY, qml.PauliZ, + qml.S, qml.T, + qml.Barrier, qml.Identity +} + +def compile_to_native_gates(qnode: Any, *params: Any) -> QuantumScript: + """Decomposes a QNode into a native gate set as a PennyLane QuantumScript. + + Args: + qnode: The PennyLane QNode to be compiled. + *params: Parameters required to execute the QNode. + + Returns: + A QuantumScript containing the compiled native PennyLane gates. + """ + # Construct the tape from the QNode with specific parameters + tape = construct_tape(qnode)(*params) + + # Decompose into a standard base gate set first + batch, _ = decompose( + tape, + gate_set=NATIVE_GATES_TO_RETAIN, + max_expansion=10 + ) + decomposed_tape: QuantumScript = batch[0] + + new_ops = [] + + for op in decomposed_tape.operations: + name = op.name + wires = op.wires + p = op.parameters + + # Mapping logic + if name == "Hadamard": + # Map H -> RZ(pi/2) - X2P - RZ(pi/2) + new_ops.append(qml.RZ(PI_2, wires=wires)) + new_ops.append(X2PGate(wires=wires)) + new_ops.append(qml.RZ(PI_2, wires=wires)) + + elif name == "CNOT": + # Map CNOT -> H(target) - CZ - H(target) + ctrl, target = wires[0], wires[1] + target_wires = [target] + # H on target + new_ops.append(qml.RZ(PI_2, wires=target_wires)) + new_ops.append(X2PGate(wires=target_wires)) + new_ops.append(qml.RZ(PI_2, wires=target_wires)) + # Native CZ + new_ops.append(qml.CZ(wires=[ctrl, target])) + # H on target again + new_ops.append(qml.RZ(PI_2, wires=target_wires)) + new_ops.append(X2PGate(wires=target_wires)) + new_ops.append(qml.RZ(PI_2, wires=target_wires)) + + elif name == "RX": + # Map RX(theta) -> RZ(-pi/2) - X2P - RZ(theta) - X2M - RZ(pi/2) + theta = p[0] + new_ops.append(qml.RZ(-PI_2, wires=wires)) + new_ops.append(X2PGate(wires=wires)) + new_ops.append(qml.RZ(theta, wires=wires)) + new_ops.append(X2MGate(wires=wires)) + new_ops.append(qml.RZ(PI_2, wires=wires)) + + elif name == "RY": + # Map RY(theta) -> X2P - RZ(theta) - X2M + theta = p[0] + new_ops.append(X2PGate(wires=wires)) + new_ops.append(qml.RZ(theta, wires=wires)) + new_ops.append(X2MGate(wires=wires)) + + elif name == "PauliX": + # Map X -> X2P - X2P + new_ops.append(X2PGate(wires=wires)) + new_ops.append(X2PGate(wires=wires)) + + elif name == "PauliY": + # Map Y -> Y2P - Y2P + new_ops.append(Y2PGate(wires=wires)) + new_ops.append(Y2PGate(wires=wires)) + + elif name == "PauliZ": + new_ops.append(qml.RZ(PI, wires=wires)) + + elif name == "S": + new_ops.append(qml.RZ(PI_2, wires=wires)) + + elif name == "T": + new_ops.append(qml.RZ(PI / 4, wires=wires)) + + elif name in ["RZ", "CZ", "Barrier", "Identity"]: + # Retain standard gates that are native + new_ops.append(op) + + elif name in [ + "X2PGate", "X2MGate", "Y2PGate", "Y2MGate", "XY2PGate", "XY2MGate" + ]: + # Retain custom native gates + new_ops.append(op) + + else: + print(f"Warning: Operation {name} not natively mapped. Kept.") + new_ops.append(op) + + return QuantumScript(ops=new_ops, measurements=decomposed_tape.measurements) + + +def compile_to_native_cqlib(qnode: Any, *params: Any) -> Circuit: + """Reconstructs a PennyLane circuit into a strict CQLib native circuit. + + Args: + qnode: The PennyLane QNode to be compiled. + *params: Parameters required to execute the QNode. + + Returns: + A cqlib.Circuit object containing strictly native instructions. + """ + # 1. Get decomposed Tape + raw_tape = construct_tape(qnode)(*params) + + batch, _ = decompose(raw_tape, gate_set=NATIVE_GATES_TO_RETAIN, max_expansion=10) + tape: QuantumScript = batch[0] + + # 2. Initialize CQLib circuit + cql_circ = Circuit(tape.num_wires) + + # 3. Explicit manual mapping to physical native gates + for op in tape.operations: + name = op.name + w = op.wires.tolist() + p = op.parameters + + if name == "Hadamard": + # H Q1 -> RZ Q1 PI, Y2P Q1 + cql_circ.rz(w[0], PI) + cql_circ.y2p(w[0]) + + elif name == "CNOT": + # CX Q0 Q1 -> Y2M Q1, CZ Q0 Q1, Y2P Q1 + cql_circ.y2m(w[1]) + cql_circ.cz(w[0], w[1]) + cql_circ.y2p(w[1]) + + elif name == "RX": + # RX Q1 theta -> RZ Q1 PI_2, X2P Q1, RZ Q1 theta, X2M Q1, RZ Q1 -PI_2 + theta = p[0] + cql_circ.rz(w[0], PI_2) + cql_circ.x2p(w[0]) + cql_circ.rz(w[0], theta) + cql_circ.x2m(w[0]) + cql_circ.rz(w[0], -PI_2) + + elif name == "RY": + # RY Q1 theta -> X2P Q1, RZ Q1 theta, X2M Q1 + theta = p[0] + cql_circ.x2p(w[0]) + cql_circ.rz(w[0], theta) + cql_circ.x2m(w[0]) + + elif name == "PauliX": + # X Q1 -> X2P Q1, X2P Q1 + cql_circ.x2p(w[0]) + cql_circ.x2p(w[0]) + + elif name == "PauliY": + # Y Q1 -> Y2P Q1, Y2P Q1 + cql_circ.y2p(w[0]) + cql_circ.y2p(w[0]) + + elif name == "PauliZ": + cql_circ.rz(w[0], PI) + + elif name == "S": + cql_circ.rz(w[0], PI_2) + + elif name == "T": + cql_circ.rz(w[0], PI / 4) + + elif name == "RZ": + cql_circ.rz(w[0], p[0]) + + elif name == "CZ": + cql_circ.cz(w[0], w[1]) + + elif name == "Barrier": + cql_circ.barrier(*w) + + elif name == "Identity": + cql_circ.i(w[0], 0) + + # Handling custom native gates + elif name == "X2PGate": + cql_circ.x2p(w[0]) + elif name == "X2MGate": + cql_circ.x2m(w[0]) + elif name == "Y2PGate": + cql_circ.y2p(w[0]) + elif name == "Y2MGate": + cql_circ.y2m(w[0]) + elif name == "XY2PGate": + cql_circ.xy2p(w[0], p[0]) + elif name == "XY2MGate": + cql_circ.xy2m(w[0], p[0]) + + else: + print(f"Warning: Operation {name} not mapped to native CQLib.") + + # 4. Final measurement processing + if tape.measurements: + cql_circ.measure_all() + + return cql_circ + + +if __name__ == "__main__": + # Example execution + + @qml.qnode(qml.device("default.qubit", wires=3)) + def circuit() -> Any: + """Example PennyLane QNode with standard gates.""" + qml.Hadamard(wires=0) + qml.Toffoli(wires=[0, 1, 2]) + return qml.state() + + cqlib_circuit = compile_to_native_cqlib(circuit) + print("Compiled QCIS Instructions:") + print(cqlib_circuit.qcis) \ No newline at end of file diff --git a/cqlib_adapter/pennylane_ext/device.py b/cqlib_adapter/pennylane_ext/device.py index 4ecb4af..ac60105 100644 --- a/cqlib_adapter/pennylane_ext/device.py +++ b/cqlib_adapter/pennylane_ext/device.py @@ -1,3 +1,16 @@ +# This code is part of cqlib. +# +# Copyright (C) 2026 China Telecom Quantum Group. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + """PennyLane device implementation using CQLib backend. This module provides a custom quantum device that interfaces between PennyLane @@ -12,6 +25,12 @@ from pennylane.devices import Device from pennylane.tape import QuantumScript, QuantumScriptOrBatch from .circuit_executor import CircuitExecutor +from .native_gates import ( + X2PGate, X2MGate, + Y2PGate, Y2MGate, + XY2PGate, XY2MGate +) +from ..utils.api_client import ApiClient class CQLibDevice(Device): @@ -28,34 +47,33 @@ class CQLibDevice(Device): SUPPORTED_OPERATIONS (set): Set of supported quantum operations. """ - # Backend configurations - TIANYAN_HARDWARE_BACKENDS = { - "tianyan24", - "tianyan504", - "tianyan176-2", - "tianyan176", - } - - TIANYAN_SIMULATOR_BACKENDS = { - "tianyan_sw", - "tianyan_s", - "tianyan_tn", - "tianyan_tnn", - "tianyan_sa", - "tianyan_swn", - } + # Backend configurations - dynamically populated from API + # Empty sets, populated on first fetch via get_available_backends() + TIANYAN_HARDWARE_BACKENDS: Set[str] = set() + TIANYAN_SIMULATOR_BACKENDS: Set[str] = set() # Supported operations + # Updated to include custom native gates (X2P, Y2P, Rxy, etc.) SUPPORTED_OPERATIONS = { + # Standard Gates "Hadamard", - "PauliX", + "PauliX", "PauliY", "PauliZ", "CNOT", "CZ", "RX", - "RY", + "RY", "RZ", + "S", + "T", + + "X2PGate", + "X2MGate", + "Y2PGate", + "Y2MGate", + "XY2PGate", + "XY2MGate", } # Device metadata @@ -72,7 +90,7 @@ class CQLibDevice(Device): verbose: bool = False, ) -> None: """Initialize the CQLib device. - + Args: wires: Number of qubits in the device. shots: Number of measurement shots. If None, uses analytic mode. @@ -80,12 +98,12 @@ class CQLibDevice(Device): login_key: Authentication key for cloud services. mapping: Qubit mapping configuration. verbose: Whether to enable verbose output. - + Raises: ValueError: If invalid configuration parameters are provided. """ super().__init__(wires=wires, shots=shots) - + device_config = { "wires": wires, "shots": shots, @@ -99,31 +117,19 @@ class CQLibDevice(Device): self.num_shots = shots self.circuit_executor = CircuitExecutor(device_config) - @property + @property def name(self) -> str: - """Return the device name. - - Returns: - String representing the device name. - """ + """Return the device name.""" return "Cqlib Quantum Device" @property def operations(self) -> Set[str]: - """Return the set of supported operations. - - Returns: - Set of supported operation names. - """ + """Return the set of supported operations.""" return self.SUPPORTED_OPERATIONS @property def backend_info(self) -> Dict[str, Any]: - """Return information about the current backend. - - Returns: - Dictionary containing backend configuration information. - """ + """Return information about the current backend.""" return { "backend_type": self.machine_name, "is_hardware": self.machine_name in self.TIANYAN_HARDWARE_BACKENDS, @@ -134,11 +140,7 @@ class CQLibDevice(Device): @classmethod def capabilities(cls) -> Dict[str, Any]: - """Return the device capabilities configuration. - - Returns: - Dictionary containing supported features and capabilities. - """ + """Return the device capabilities configuration.""" capabilities = super().capabilities().copy() capabilities.update( @@ -154,70 +156,90 @@ class CQLibDevice(Device): "jax": "default.qubit.jax", }, ) - return capabilities - def supports_operation(self, operation: Any) -> bool: - """Check if a specific quantum operation is supported. - + @classmethod + def fetch_available_backends(cls, token: str = None) -> Dict[str, List[str]]: + """Fetch available backends from TianYan API and cache them. + Args: - operation: Quantum operation to check. + token: API token. If None, uses CQLIB_TOKEN env var. Returns: - True if the operation is supported, False otherwise. + Dict with 'hardware' and 'simulator' keys containing lists of backend names. """ - supported_operations = { - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "S", - "T", - "RX", - "RY", - "RZ", - "CNOT", - "CZ", + if token is None: + token = os.environ.get("CQLIB_TOKEN", "") + + if not token: + raise ValueError("API token is required to fetch available backends") + + client = ApiClient(token=token) + backends = client.get_backends() + + hardware = set() + simulator = set() + + for backend in backends: + code = backend.get('code') + label = backend.get('labels') + if code: + if label == '1': + hardware.add(code) + else: + simulator.add(code) + + cls.TIANYAN_HARDWARE_BACKENDS = hardware + cls.TIANYAN_SIMULATOR_BACKENDS = simulator + + return { + 'hardware': sorted(hardware), + 'simulator': sorted(simulator) } - return getattr(operation, "name", None) in supported_operations - def execute( - self, - circuits: Union[QuantumScript, List[QuantumScript]], - execution_config: Any = None, - ) -> List[Any]: - """Execute quantum circuits on the device. - + @classmethod + def get_available_backends(cls, token: str = None, force_refresh: bool = False) -> Dict[str, List[str]]: + """Get available backends, fetching from API if not cached. + Args: - circuits: Single quantum circuit or list of circuits to execute. - execution_config: Execution configuration parameters. + token: API token. If None, uses CQLIB_TOKEN env var. + force_refresh: If True, force re-fetch from API. Returns: - List of execution results for each circuit. + Dict with 'hardware' and 'simulator' keys containing lists of backend names. + """ + if force_refresh or not cls.TIANYAN_HARDWARE_BACKENDS: + cls.fetch_available_backends(token) + + return { + 'hardware': sorted(cls.TIANYAN_HARDWARE_BACKENDS), + 'simulator': sorted(cls.TIANYAN_SIMULATOR_BACKENDS) + } + + def supports_operation(self, operation: Any) -> bool: + """Check if a specific quantum operation is supported. + + This method is critical for preventing PennyLane from decomposing + our native gates (like X2PGate) into standard gates. """ + return getattr(operation, "name", None) in self.SUPPORTED_OPERATIONS + + def execute( + self, + circuits: Union[QuantumScript, List[QuantumScript]], + execution_config: Any = None, + ) -> List[Any]: + """Execute quantum circuits on the device.""" if isinstance(circuits, QuantumScript): circuits = [circuits] - + return [self.circuit_executor.execute_circuit(circuit) for circuit in circuits] - - + def __repr__(self) -> str: - """Return string representation of the device. - - Returns: - String representation of the device. - """ return f"<{self.name} device (wires={self.wires}, shots={self.shots})>" - - def preprocess_transforms(self, execution_config: Any = None) -> Any: - """Define the preprocessing transformation pipeline. - - Args: - execution_config: Execution configuration parameters. - Returns: - TransformProgram: Preprocessing transformation program. - """ + def preprocess_transforms(self, execution_config: Any = None) -> Any: + """Define the preprocessing transformation pipeline.""" program = qml.transforms.core.TransformProgram() program.add_transform( qml.devices.preprocess.validate_device_wires, @@ -228,9 +250,12 @@ class CQLibDevice(Device): qml.devices.preprocess.validate_measurements, name=self.short_name, ) + + # IMPORTANT: stopping_condition uses self.supports_operation + # to preserve our custom native gates. program.add_transform( qml.devices.preprocess.decompose, stopping_condition=self.supports_operation, name=self.short_name, ) - return program \ No newline at end of file + return program diff --git a/cqlib_adapter/pennylane_ext/ext_mapping.py b/cqlib_adapter/pennylane_ext/ext_mapping.py index acf4eed..cb1574e 100644 --- a/cqlib_adapter/pennylane_ext/ext_mapping.py +++ b/cqlib_adapter/pennylane_ext/ext_mapping.py @@ -1,3 +1,16 @@ +# This code is part of cqlib. +# +# Copyright (C) 2026 China Telecom Quantum Group. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + from cqlib.circuits.instruction_data import InstructionData diff --git a/cqlib_adapter/pennylane_ext/native_gates.py b/cqlib_adapter/pennylane_ext/native_gates.py new file mode 100644 index 0000000..f2d18c1 --- /dev/null +++ b/cqlib_adapter/pennylane_ext/native_gates.py @@ -0,0 +1,165 @@ +# This code is part of cqlib. +# +# Copyright (C) 2026 China Telecom Quantum Group. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +import pennylane as qml +from pennylane.operation import Operation +from pennylane.typing import TensorLike +from pennylane.wires import WiresLike +import numpy as np + +def _stack_matrix(elements): + return qml.math.stack([ + qml.math.stack(elements[:2], axis=-1), + qml.math.stack(elements[2:], axis=-1) + ], axis=-2) + + +class X2PGate(Operation): + r"""Native X rotation by +pi/2 (Sqrt X).""" + num_wires = 1 + num_params = 0 + grad_method = None + + @staticmethod + def compute_matrix() -> TensorLike: + # 1/sqrt(2) * [[1, -i], [-i, 1]] + isqrt2 = 1 / np.sqrt(2) + return qml.math.array([ + [isqrt2, -1j * isqrt2], + [-1j * isqrt2, isqrt2] + ], dtype=complex) + + def adjoint(self): + return X2MGate(wires=self.wires) + +class X2MGate(Operation): + r"""Native X rotation by -pi/2 (Sqrt X dag).""" + num_wires = 1 + num_params = 0 + grad_method = None + + @staticmethod + def compute_matrix() -> TensorLike: + isqrt2 = 1 / np.sqrt(2) + return qml.math.array([ + [isqrt2, 1j * isqrt2], + [1j * isqrt2, isqrt2] + ], dtype=complex) + + def adjoint(self): + return X2PGate(wires=self.wires) + +class Y2PGate(Operation): + r"""Native Y rotation by +pi/2 (Sqrt Y).""" + num_wires = 1 + num_params = 0 + grad_method = None + + @staticmethod + def compute_matrix() -> TensorLike: + isqrt2 = 1 / np.sqrt(2) + return qml.math.array([ + [isqrt2, -1 * isqrt2], + [1 * isqrt2, isqrt2] + ], dtype=complex) + + def adjoint(self): + return Y2MGate(wires=self.wires) + +class Y2MGate(Operation): + r"""Native Y rotation by -pi/2 (Sqrt Y dag).""" + num_wires = 1 + num_params = 0 + grad_method = None + + @staticmethod + def compute_matrix() -> TensorLike: + isqrt2 = 1 / np.sqrt(2) + return qml.math.array([ + [isqrt2, 1 * isqrt2], + [-1 * isqrt2, isqrt2] + ], dtype=complex) + + def adjoint(self): + return Y2PGate(wires=self.wires) + + +class XY2PGate(Operation): + r""" + Rotation in XY plane by +pi/2. + Axis defined by phi. + """ + num_wires = 1 + num_params = 1 + ndim_params = (0,) + grad_method = "A" # 支持自动微分 + + def __init__(self, phi: TensorLike, wires: WiresLike, id=None): + super().__init__(phi, wires=wires, id=id) + + @staticmethod + def compute_matrix(phi) -> TensorLike: + if qml.math.get_interface(phi) == "tensorflow": + phi = qml.math.cast_like(phi, 1j) + + c = qml.math.cos(phi) + s = qml.math.sin(phi) + + one = qml.math.cast_like(qml.math.ones_like(phi), 1j) + neg_i = -1j * one + + # -i * e^{-i*phi} + term1 = neg_i * (c - 1j * s) + # -i * e^{i*phi} + term2 = neg_i * (c + 1j * s) + + isqrt2 = 1 / np.sqrt(2) + mat = _stack_matrix([one, term1, term2, one]) + return mat * isqrt2 + + def adjoint(self): + return XY2MGate(self.data[0], wires=self.wires) + +class XY2MGate(Operation): + r""" + Rotation in XY plane by -pi/2. + """ + num_wires = 1 + num_params = 1 + ndim_params = (0,) + grad_method = "A" + + def __init__(self, phi: TensorLike, wires: WiresLike, id=None): + super().__init__(phi, wires=wires, id=id) + + @staticmethod + def compute_matrix(phi) -> TensorLike: + if qml.math.get_interface(phi) == "tensorflow": + phi = qml.math.cast_like(phi, 1j) + + c = qml.math.cos(phi) + s = qml.math.sin(phi) + + one = qml.math.cast_like(qml.math.ones_like(phi), 1j) + pos_i = 1j * one + + # +i * e^{-i*phi} + term1 = pos_i * (c - 1j * s) + # +i * e^{i*phi} + term2 = pos_i * (c + 1j * s) + + isqrt2 = 1 / np.sqrt(2) + mat = _stack_matrix([one, term1, term2, one]) + return mat * isqrt2 + + def adjoint(self): + return XY2PGate(self.data[0], wires=self.wires) \ No newline at end of file diff --git a/cqlib_adapter/qiskit_ext/__init__.py b/cqlib_adapter/qiskit_ext/__init__.py index ffa2775..47b3d84 100644 --- a/cqlib_adapter/qiskit_ext/__init__.py +++ b/cqlib_adapter/qiskit_ext/__init__.py @@ -1,6 +1,6 @@ # This code is part of cqlib. # -# Copyright (C) 2025 China Telecom Quantum Group. +# Copyright (C) 2026 China Telecom Quantum Group. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE file in the root directory @@ -15,9 +15,8 @@ Qiskit extension module for TianYan quantum platform integration. """ from .adapter import to_cqlib -from .api_client import ApiClient -from .gates import X2PGate, X2MGate, Y2PGate, Y2MGate, XY2MGate, XY2PGate, RxyGate, \ - qcis_name_mapping, target_from_basis_gates +from ..utils.api_client import ApiClient +from .gates import X2PGate, X2MGate, Y2PGate, Y2MGate, XY2MGate, XY2PGate, RxyGate from .job import TianYanJob from .sampler import TianYanSampler from .tianyan_backend import TianYanBackend @@ -34,8 +33,6 @@ __all__ = [ 'XY2PGate', 'XY2MGate', 'RxyGate', - 'qcis_name_mapping', - 'target_from_basis_gates', "TianYanJob", "TianYanSampler", diff --git a/cqlib_adapter/qiskit_ext/gates.py b/cqlib_adapter/qiskit_ext/gates.py index 87f1477..2a309c9 100644 --- a/cqlib_adapter/qiskit_ext/gates.py +++ b/cqlib_adapter/qiskit_ext/gates.py @@ -1,6 +1,6 @@ # This code is part of cqlib. # -# Copyright (C) 2025 China Telecom Quantum Group. +# Copyright (C) 2026 China Telecom Quantum Group. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE file in the root directory @@ -29,8 +29,6 @@ from qiskit.circuit import Gate, QuantumCircuit, Parameter from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as SELib from qiskit.circuit.library import GlobalPhaseGate, RXGate, RYGate, \ HGate, XGate, YGate -from qiskit.circuit.parameterexpression import ParameterValueType -from qiskit.transpiler import Target class X2PGate(Gate): @@ -69,21 +67,9 @@ class X2PGate(Gate): Raises: ValueError: If copying cannot be avoided. """ - return np.asarray(X2P(), dtype=dtype, copy=copy) - - def inverse(self, annotated: bool = False): - r"""Return inverted X2P gate. - - Args: - annotated: when set to ``True``, this is typically used to return an - :class:`.AnnotatedOperation` with an inverse modifier set instead of a concrete - :class:`.Gate`. However, for this class this argument is ignored as this gate - is self-inverse. - - Returns: - X2MGate: inverse gate. - """ - return X2MGate() + if copy is False: + raise ValueError("unable to avoid copy while creating an array as requested") + return np.asarray(X2P(), dtype=dtype) class X2MGate(Gate): @@ -118,22 +104,13 @@ class X2MGate(Gate): Returns: np.ndarray: The matrix representation of the gate. - """ - return np.asarray(X2M(), dtype=dtype, copy=copy) - def inverse(self, annotated: bool = False): - r"""Return inverted X2M gate. - - Args: - annotated: when set to ``True``, this is typically used to return an - :class:`.AnnotatedOperation` with an inverse modifier set instead of a concrete - :class:`.Gate`. However, for this class this argument is ignored as this gate - is self-inverse. - - Returns: - X2PGate: inverse gate. + Raises: + ValueError: If copying cannot be avoided. """ - return X2PGate() + if copy is False: + raise ValueError("unable to avoid copy while creating an array as requested") + return np.asarray(X2M(), dtype=dtype) class Y2PGate(Gate): @@ -168,22 +145,13 @@ class Y2PGate(Gate): Returns: np.ndarray: The matrix representation of the gate. - """ - return np.asarray(Y2P(), dtype=dtype, copy=copy) - - def inverse(self, annotated: bool = False): - r"""Return inverted Y2P gate. - - Args: - annotated: when set to ``True``, this is typically used to return an - :class:`.AnnotatedOperation` with an inverse modifier set instead of a concrete - :class:`.Gate`. However, for this class this argument is ignored as this gate - is self-inverse. - Returns: - Y2MGate: inverse gate. + Raises: + ValueError: If copying cannot be avoided. """ - return Y2MGate() + if copy is False: + raise ValueError("unable to avoid copy while creating an array as requested") + return np.asarray(Y2P(), dtype=dtype) class Y2MGate(Gate): @@ -218,22 +186,13 @@ class Y2MGate(Gate): Returns: np.ndarray: The matrix representation of the gate. - """ - return np.asarray(Y2M(), dtype=dtype, copy=copy) - - def inverse(self, annotated: bool = False): - r"""Return inverted Y2M gate. - - Args: - annotated: when set to ``True``, this is typically used to return an - :class:`.AnnotatedOperation` with an inverse modifier set instead of a concrete - :class:`.Gate`. However, for this class this argument is ignored as this gate - is self-inverse. - Returns: - Y2PGate: inverse gate. + Raises: + ValueError: If copying cannot be avoided. """ - return Y2PGate() + if copy is False: + raise ValueError("unable to avoid copy while creating an array as requested") + return np.asarray(Y2M(), dtype=dtype) class XY2PGate(Gate): @@ -244,12 +203,12 @@ class XY2PGate(Gate): in the XY plane. """ - def __init__(self, theta: ParameterValueType, label: str = None): + def __init__(self, theta: float | Parameter, label: str = None): """ Initializes the XY2PGate. Args: - theta (ParameterValueType): The rotation angle. + theta (float|Parameter): The rotation angle. label (str, optional): A custom label for the gate. Defaults to None. """ super().__init__("xy2p", 1, [theta], label=label) @@ -272,23 +231,14 @@ class XY2PGate(Gate): copy: Whether to avoid copying the array. Returns: - np.ndarray: The matrix represe - """ - return np.asarray(XY2P(self.params[0]), dtype=dtype, copy=copy) - - def inverse(self, annotated: bool = False): - r"""Return inverted XY2PGate gate. - - Args: - annotated: when set to ``True``, this is typically used to return an - :class:`.AnnotatedOperation` with an inverse modifier set instead of a concrete - :class:`.Gate`. However, for this class this argument is ignored as this gate - is self-inverse. + np.ndarray: The matrix representation of the gate. - Returns: - XY2MGate: inverse gate. + Raises: + ValueError: If copying cannot be avoided. """ - return XY2MGate(self.params[0]) + if copy is False: + raise ValueError("unable to avoid copy while creating an array as requested") + return np.asarray(XY2P(self.params[0]), dtype=dtype) class XY2MGate(Gate): @@ -299,12 +249,12 @@ class XY2MGate(Gate): in the XY plane. """ - def __init__(self, theta: ParameterValueType, label: str = None): + def __init__(self, theta: float | Parameter, label: str = None): """ Initializes the XY2MGate. Args: - theta (ParameterValueType): The rotation angle. + theta (float | Parameter): The rotation angle. label (str, optional): A custom label for the gate. Defaults to None. """ super().__init__("xy2m", 1, [theta], label=label) @@ -328,22 +278,13 @@ class XY2MGate(Gate): Returns: np.ndarray: The matrix representation of the gate. - """ - return np.asarray(XY2M(self.params[0]), dtype=dtype, copy=copy) - - def inverse(self, annotated: bool = False): - r"""Return inverted XY2MGate gate. - - Args: - annotated: when set to ``True``, this is typically used to return an - :class:`.AnnotatedOperation` with an inverse modifier set instead of a concrete - :class:`.Gate`. However, for this class this argument is ignored as this gate - is self-inverse. - Returns: - XY2PGate: inverse gate. + Raises: + ValueError: If copying cannot be avoided. """ - return XY2PGate(self.params[0]) + if copy is False: + raise ValueError("unable to avoid copy while creating an array as requested") + return np.asarray(XY2M(self.params[0]), dtype=dtype) class RxyGate(Gate): @@ -354,9 +295,9 @@ class RxyGate(Gate): the Z-axis rotations before and after the X-axis π/2 rotations. Args: - phi (ParameterValueType): Rotation angle (in radians) for initial Z-axis rotation. + phi (float | Parameter): Rotation angle (in radians) for initial Z-axis rotation. Controls the phase offset in the composite rotation sequence. - theta (ParameterValueType): Rotation angle (in radians) for middle Z-axis rotation. + theta (float | Parameter): Rotation angle (in radians) for middle Z-axis rotation. Determines the main rotation magnitude between X-axis operations. label (str, optional): Optional text label for gate identification. Defaults to None. @@ -367,13 +308,13 @@ class RxyGate(Gate): >>> qc.append(RxyGate(math.pi/3, math.pi/4), [0]) """ - def __init__(self, phi: ParameterValueType, theta: ParameterValueType, label: str = None): + def __init__(self, phi: float | Parameter, theta: float | Parameter, label: str = None): """ Initializes the RxyGate. Args: - phi (ParameterValueType): The rotation angle. - theta (ParameterValueType): The rotation angle. + phi (float | Parameter): The rotation angle. + theta (float | Parameter): The rotation angle. label (str, optional): A custom label for the gate. Defaults to None. """ super().__init__("rxy", 1, [phi, theta], label=label) @@ -408,21 +349,9 @@ class RxyGate(Gate): Raises: ValueError: If copying cannot be avoided. """ - return np.asarray(RXY(self.params[0], self.params[1]), dtype=dtype, copy=copy) - - def inverse(self, annotated: bool = False): - r"""Return inverted RxyGate gate. - - Args: - annotated: when set to ``True``, this is typically used to return an - :class:`.AnnotatedOperation` with an inverse modifier set instead of a concrete - :class:`.Gate`. However, for this class this argument is ignored as this gate - is self-inverse. - - Returns: - RxyGate: inverse gate. - """ - return RxyGate(self.params[0], -self.params[1]) + if copy is False: + raise ValueError("unable to avoid copy while creating an array as requested") + return np.asarray(RXY(self.params[0], self.params[1]), dtype=dtype) x_qc = QuantumCircuit(1) @@ -507,40 +436,6 @@ qc.rx(-pi / 2, 0) qc.rz(p_ - pi / 2, 0) SELib.add_equivalence(RxyGate(p_, t_), qc) - -def qcis_name_mapping() -> dict: - """Return Qiskit Target name mappings for cqlib custom gates.""" - theta = Parameter("theta") - phi = Parameter("phi") - return { - "x2p": X2PGate(), - "x2m": X2MGate(), - "y2p": Y2PGate(), - "y2m": Y2MGate(), - "xy2p": XY2PGate(theta), - "xy2m": XY2MGate(theta), - "rxy": RxyGate(phi, theta), - } - - -def target_from_basis_gates( - basis_gates: list[str], - num_qubits: int | None = None, - coupling_map=None, -) -> Target: - """Build a Qiskit Target that includes cqlib custom gate names. - - Qiskit 2.4 rejects non-standard gate names in ``transpile(..., basis_gates=...)``. - Custom operations must be supplied through a ``Target`` instead. - """ - return Target.from_configuration( - basis_gates=basis_gates, - num_qubits=num_qubits, - coupling_map=coupling_map, - custom_name_mapping=qcis_name_mapping(), - ) - - __all__ = [ 'X2PGate', 'X2MGate', @@ -548,7 +443,5 @@ __all__ = [ 'Y2MGate', 'XY2PGate', 'XY2MGate', - 'RxyGate', - 'qcis_name_mapping', - 'target_from_basis_gates', + 'RxyGate' ] diff --git a/cqlib_adapter/qiskit_ext/job.py b/cqlib_adapter/qiskit_ext/job.py index 31fad98..1a7c3c6 100644 --- a/cqlib_adapter/qiskit_ext/job.py +++ b/cqlib_adapter/qiskit_ext/job.py @@ -1,6 +1,6 @@ # This code is part of cqlib. # -# Copyright (C) 2025 China Telecom Quantum Group. +# Copyright (C) 2026 China Telecom Quantum Group. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE file in the root directory @@ -26,7 +26,7 @@ from qiskit.providers import JobV1, JobStatus from qiskit.providers.backend import Backend from qiskit.result import Result -from .api_client import ApiClient +from ..utils.api_client import ApiClient class TianYanJob(JobV1): diff --git a/cqlib_adapter/qiskit_ext/sampler.py b/cqlib_adapter/qiskit_ext/sampler.py index 2ebe5e0..1e578e2 100644 --- a/cqlib_adapter/qiskit_ext/sampler.py +++ b/cqlib_adapter/qiskit_ext/sampler.py @@ -1,6 +1,6 @@ # This code is part of cqlib. # -# Copyright (C) 2025 China Telecom Quantum Group. +# Copyright (C) 2026 China Telecom Quantum Group. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE file in the root directory diff --git a/cqlib_adapter/qiskit_ext/submit_job.py b/cqlib_adapter/qiskit_ext/submit_job.py index 2903773..ab87d3f 100644 --- a/cqlib_adapter/qiskit_ext/submit_job.py +++ b/cqlib_adapter/qiskit_ext/submit_job.py @@ -1,3 +1,15 @@ +# This code is part of cqlib. +# +# Copyright (C) 2026 China Telecom Quantum Group. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + from qiskit_ibm_runtime import QiskitRuntimeService from qiskit import QuantumCircuit from qiskit_ibm_runtime import SamplerV2 as Sampler diff --git a/cqlib_adapter/qiskit_ext/tianyan_backend.py b/cqlib_adapter/qiskit_ext/tianyan_backend.py index c096aea..a8d6459 100644 --- a/cqlib_adapter/qiskit_ext/tianyan_backend.py +++ b/cqlib_adapter/qiskit_ext/tianyan_backend.py @@ -1,6 +1,6 @@ # This code is part of cqlib. # -# Copyright (C) 2025 China Telecom Quantum Group. +# Copyright (C) 2026 China Telecom Quantum Group. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE file in the root directory @@ -18,6 +18,7 @@ for both quantum computers and simulators on the TianYan platform. """ import json +import warnings from collections import namedtuple from datetime import datetime from enum import IntEnum @@ -25,12 +26,12 @@ from enum import IntEnum from qiskit.circuit import QuantumCircuit, Parameter, Measure, Barrier from qiskit.circuit.library import standard_gates from qiskit.circuit.library.standard_gates import CZGate, RZGate, HGate, \ - GlobalPhaseGate, CXGate, IGate + GlobalPhaseGate, CXGate from qiskit.providers import BackendV2 as Backend, Options, JobV1, QubitProperties from qiskit.transpiler import Target, InstructionProperties, generate_preset_pass_manager from .adapter import to_cqlib -from .api_client import ApiClient +from ..utils.api_client import ApiClient from .gates import X2PGate, X2MGate, Y2MGate, Y2PGate, XY2MGate, XY2PGate, RxyGate from .job import TianYanJob @@ -49,7 +50,6 @@ class BackendStatus(IntEnum): calibrating = 1 under_maintenance = 2 offline = 3 - updating = 4 class CqlibAdapterError(Exception): @@ -157,7 +157,6 @@ class BackendConfiguration: if backend_type == BackendType.quantum_computer: qpu = api_client.get_quantum_computer_config(backend_id) - n_qubits = max(int(q[1:]) for q in qpu['qubits']) + 1 qubits = [int(q[1:]) for q in qpu['qubits'] if q not in disabled_qubits] coupling_map = [] @@ -214,7 +213,7 @@ class BackendConfiguration: data = { 'backend_id': backend_id, 'backend_name': data['code'], - 'n_qubits': n_qubits, + 'n_qubits': data['bitWidth'], 'basis_gates': basis_gates, 'derivative_gates': derivative_gates, 'gates': gates, @@ -363,7 +362,6 @@ time_units = { 's': 1, 'ms': 1e-3, 'us': 1e-6, - 'μs': 1e-6, 'ns': 1e-9 } frequency_units = { @@ -397,7 +395,7 @@ class TianYanQuantumBackend(TianYanBackend): self.configuration.backend_name ) target = Target( - num_qubits=configuration.n_qubits, + # num_qubits=configuration.n_qubits, description=configuration.backend_name, qubit_properties=self._make_qubit_properties() ) @@ -427,10 +425,10 @@ class TianYanQuantumBackend(TianYanBackend): frequency_values = frequency['param_list'] frequency_unit = frequency_units.get(frequency['unit'].lower()) - if not (t1_qubits == t2_qubits == frequency_qubits): + if t1_qubits != t2_qubits != frequency_qubits: raise ValueError("t1/t2/frequency qubits are not the same") - qubit_properties = [ - QubitProperties() for _ in range(self.configuration.n_qubits) + qubit_properties: list[QubitProperties | None] = [ + None for _ in range(self.configuration.n_qubits) ] for i, q in enumerate(t1_qubits): qubit_properties[int(q[1:])] = QubitProperties( @@ -452,25 +450,16 @@ class TianYanQuantumBackend(TianYanBackend): error_qubits = gate_errors['qubit_used'] error_values = gate_errors['param_list'] error_unit = number_units[gate_errors['unit']] - supported_qubits = self._supported_operation_qubits() for i, q in enumerate(error_qubits): q0, q1 = coupler_map[q] q0, q1 = int(q0[1:]), int(q1[1:]) - if q0 not in supported_qubits or q1 not in supported_qubits: - continue p = InstructionProperties(error=error_values[i] * error_unit, duration=1e-8) cz_props[q0, q1] = p cz_props[q1, q0] = p if 'cz' in self.configuration.basis_gates: target.add_instruction(CZGate(), cz_props) - def _supported_operation_qubits(self): - """Returns physical qubits with both single-qubit gates and readout support.""" - single_qubits = self._machine_config['qubit']['singleQubit']['gate error']['qubit_used'] - readout_qubits = self._machine_config['readout']['readoutArray']['Readout Error']['qubit_used'] - return {int(q[1:]) for q in single_qubits} & {int(q[1:]) for q in readout_qubits} - def _update_single_gates(self, target: Target): """Updates the single-qubit gates in the target. @@ -498,8 +487,6 @@ class TianYanQuantumBackend(TianYanBackend): ) if 'rz' in self.configuration.basis_gates: target.add_instruction(RZGate(Parameter('theta')), rz_props) - if 'id' in self.configuration.basis_gates: - target.add_instruction(IGate(), single_props.copy()) if 'x2p' in self.configuration.basis_gates: target.add_instruction(X2PGate(), single_props.copy()) # HGate is very import. @@ -515,7 +502,7 @@ class TianYanQuantumBackend(TianYanBackend): if 'xy2m' in self.configuration.basis_gates: target.add_instruction(XY2MGate(Parameter('theta')), single_props.copy()) - target.add_instruction(GlobalPhaseGate(Parameter('phase'))) + target.add_instruction(GlobalPhaseGate(Parameter('phase')), {(): None}) def _update_measure_gate(self, target: Target): """Updates the measurement gate in the target. @@ -596,7 +583,6 @@ class TianYanSimulatorBackend(TianYanBackend): 'y2m': [Y2MGate(), q_props], 'xy2p': [XY2PGate(Parameter('theta')), q_props], 'xy2m': [XY2MGate(Parameter('theta')), q_props], - 'id': [standard_gates.IGate(), q_props], 'h': [standard_gates.HGate(), q_props], 'x': [standard_gates.XGate(), q_props], 'y': [standard_gates.YGate(), q_props], @@ -621,5 +607,5 @@ class TianYanSimulatorBackend(TianYanBackend): target.add_instruction(**ins_mapping_dict[gate]) elif gate == 'id': pass - # else: - # warnings.warn(f'{gate} is not supported in simulator backend.') + else: + warnings.warn(f'{gate} is not supported in simulator backend.') diff --git a/cqlib_adapter/qiskit_ext/tianyan_provider.py b/cqlib_adapter/qiskit_ext/tianyan_provider.py index 916a736..7a7911b 100644 --- a/cqlib_adapter/qiskit_ext/tianyan_provider.py +++ b/cqlib_adapter/qiskit_ext/tianyan_provider.py @@ -1,6 +1,6 @@ # This code is part of cqlib. # -# Copyright (C) 2025 China Telecom Quantum Group. +# Copyright (C) 2026 China Telecom Quantum Group. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE file in the root directory @@ -22,7 +22,7 @@ from pathlib import Path import dotenv -from .api_client import ApiClient +from ..utils.api_client import ApiClient from .tianyan_backend import TianYanBackend, BackendConfiguration, CqlibAdapterError, \ BackendStatus, TianYanQuantumBackend, TianYanSimulatorBackend diff --git a/cqlib_adapter/qiskit_ext/api_client.py b/cqlib_adapter/utils/api_client.py similarity index 100% rename from cqlib_adapter/qiskit_ext/api_client.py rename to cqlib_adapter/utils/api_client.py diff --git a/tests/test_pennylane/test_circuit_executor.py b/tests/test_pennylane/test_circuit_executor.py index 108c52b..599bc05 100644 --- a/tests/test_pennylane/test_circuit_executor.py +++ b/tests/test_pennylane/test_circuit_executor.py @@ -1,32 +1,71 @@ -""" -Comprehensive Test Suite for Quantum Circuit Executor. +# test_backends.py +# This code is part of cqlib. +# +# Copyright (C) 2025 China Telecom Quantum Group. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Comprehensive Test Suite for Quantum Circuit Executor. -This module provides extensive testing for the CircuitExecutor class, -covering all major functionality including backend initialization, -circuit execution, measurement processing, and error handling. +This module provides extensive unit testing for the CircuitExecutor class, +covering backend initialization, circuit execution, measurement processing, +result formatting, and error handling protocols. """ -import pytest +import logging +from unittest.mock import Mock, patch + import numpy as np import pennylane as qml -from unittest.mock import Mock, patch, MagicMock -import logging +import pytest from pennylane.tape import QuantumScript from cqlib_adapter.pennylane_ext.circuit_executor import ( - CircuitExecutor, - BackendType, + BackendType, + CircuitExecutor, decimal_to_binary_array, samples_to_pennylane_format, - switch_endianness ) +@pytest.fixture(autouse=True) +def mock_cqlib_device_backends(): + """Injects mocked backend lists into CQLibDevice for isolated testing. + + Overrides the remote API call dependency by manually populating + the supported hardware and simulator backend caches. Ensures the + original state is cleanly restored after test execution to prevent + state leakage between tests. + """ + from cqlib_adapter.pennylane_ext.device import CQLibDevice + + # Store the original state to guarantee test isolation + orig_hw = getattr(CQLibDevice, 'TIANYAN_HARDWARE_BACKENDS', []) + orig_sim = getattr(CQLibDevice, 'TIANYAN_SIMULATOR_BACKENDS', []) + + # Inject mock configuration for backend validation + CQLibDevice.TIANYAN_HARDWARE_BACKENDS = ['tianyan24', 'tianyan504'] + CQLibDevice.TIANYAN_SIMULATOR_BACKENDS = ['tianyan_sw', 'tianyan_s'] + + yield # Suspend execution to run the test + + # Restore the original state during teardown + CQLibDevice.TIANYAN_HARDWARE_BACKENDS = orig_hw + CQLibDevice.TIANYAN_SIMULATOR_BACKENDS = orig_sim + + class TestCircuitExecutorInitialization: - """Test suite for CircuitExecutor initialization and configuration.""" + """Test suite for CircuitExecutor initialization and configuration mapping.""" def test_local_simulator_initialization(self): - """Test initialization with local simulator backend.""" + """Tests if the executor initializes correctly with the default local simulator.""" + # Arrange config = { 'machine_name': 'default', 'shots': 1000, @@ -34,15 +73,20 @@ class TestCircuitExecutorInitialization: 'verbose': True } + # Act executor = CircuitExecutor(config) + # Assert assert executor._backend_type == BackendType.LOCAL_SIMULATOR assert executor.device_config == config assert executor.cqlib_backend is None assert executor._execution_count == 0 - - def test_tianyan_simulator_initialization(self): - """Test initialization with Tianyan simulator backend.""" + + @patch('cqlib_adapter.pennylane_ext.device.CQLibDevice.get_available_backends') + @patch('cqlib_adapter.pennylane_ext.circuit_executor.TianYanPlatform') + def test_tianyan_simulator_initialization(self, mock_platform, mock_get_backends): + """Tests initialization when a Tianyan simulator backend is requested.""" + # Arrange config = { 'machine_name': 'tianyan_s', 'login_key': 'test_key', @@ -50,21 +94,25 @@ class TestCircuitExecutorInitialization: 'wires': 5, 'verbose': False } + mock_instance = Mock() + mock_platform.return_value = mock_instance - with patch('cqlib_adapter.pennylane_ext.circuit_executor.TianYanPlatform') as mock_platform: - mock_instance = Mock() - mock_platform.return_value = mock_instance - - executor = CircuitExecutor(config) - - assert executor._backend_type == BackendType.TIANYAN_SIMULATOR - mock_platform.assert_called_once_with( - login_key='test_key', - machine_name='tianyan_s' - ) - - def test_tianyan_hardware_initialization(self): - """Test initialization with Tianyan hardware backend.""" + # Act + executor = CircuitExecutor(config) + + # Assert + assert executor._backend_type == BackendType.TIANYAN_SIMULATOR + mock_platform.assert_called_once_with( + login_key='test_key', + machine_name='tianyan_s' + ) + mock_get_backends.assert_called_once_with(token='test_key') + + @patch('cqlib_adapter.pennylane_ext.device.CQLibDevice.get_available_backends') + @patch('cqlib_adapter.pennylane_ext.circuit_executor.TianYanPlatform') + def test_tianyan_hardware_initialization(self, mock_platform, mock_get_backends): + """Tests initialization when a Tianyan hardware backend is requested.""" + # Arrange config = { 'machine_name': 'tianyan24', 'login_key': 'test_key', @@ -72,32 +120,32 @@ class TestCircuitExecutorInitialization: 'wires': 24, 'verbose': True } + mock_instance = Mock() + mock_platform.return_value = mock_instance - with patch('cqlib_adapter.pennylane_ext.circuit_executor.TianYanPlatform') as mock_platform: - mock_instance = Mock() - mock_platform.return_value = mock_instance - - executor = CircuitExecutor(config) - - assert executor._backend_type == BackendType.TIANYAN_HARDWARE - mock_platform.assert_called_once_with( - login_key='test_key', - machine_name='tianyan24' - ) + # Act + executor = CircuitExecutor(config) + + # Assert + assert executor._backend_type == BackendType.TIANYAN_HARDWARE + mock_platform.assert_called_once_with( + login_key='test_key', + machine_name='tianyan24' + ) def test_invalid_backend_initialization(self): - """Test initialization with invalid backend name.""" + """Tests that a ValueError is raised when an unsupported backend is provided.""" config = { 'machine_name': 'invalid_backend', 'shots': 1000, 'wires': 2 } - with pytest.raises(ValueError, match="Unknown or unsupported backend"): + with pytest.raises(ValueError, match="Login key required"): CircuitExecutor(config) def test_missing_login_key_for_tianyan(self): - """Test initialization without login key for Tianyan backends.""" + """Tests that a ValueError is raised when a remote backend lacks a login key.""" config = { 'machine_name': 'tianyan_s', 'shots': 1000, @@ -109,7 +157,7 @@ class TestCircuitExecutorInitialization: class TestBackendTypeDetermination: - """Test backend type determination logic.""" + """Test suite validating the mapping from machine names to BackendType enums.""" @pytest.mark.parametrize("machine_name,expected_type", [ ('default', BackendType.LOCAL_SIMULATOR), @@ -118,61 +166,58 @@ class TestBackendTypeDetermination: ('tianyan_sw', BackendType.TIANYAN_SIMULATOR), ('tianyan_s', BackendType.TIANYAN_SIMULATOR), ]) - def test_backend_type_mapping(self, machine_name, expected_type): - """Test all supported backend type mappings.""" + @patch('cqlib_adapter.pennylane_ext.device.CQLibDevice.get_available_backends') + @patch('cqlib_adapter.pennylane_ext.circuit_executor.TianYanPlatform') + def test_backend_type_mapping(self, mock_platform, mock_get_backends, machine_name, expected_type): + """Tests that all supported machine names map to their correct BackendType.""" config = {'machine_name': machine_name, 'wires': 2} if expected_type != BackendType.LOCAL_SIMULATOR: config['login_key'] = 'test_key' - with patch('cqlib_adapter.pennylane_ext.circuit_executor.TianYanPlatform'): - executor = CircuitExecutor(config) - else: - executor = CircuitExecutor(config) + executor = CircuitExecutor(config) assert executor._backend_type == expected_type class TestCircuitValidation: - """Test circuit validation functionality.""" + """Test suite for validating circuit configurations prior to execution.""" def test_state_measurement_with_finite_shots(self): - """Test validation fails for state measurement with finite shots.""" + """Ensures state vector measurements fail if finite shots are configured.""" + # Arrange config = { 'machine_name': 'default', 'shots': 1000, 'wires': 2 } - executor = CircuitExecutor(config) - - # Create a circuit with state measurement ops = [qml.Hadamard(0), qml.CNOT([0, 1])] measurements = [qml.state()] circuit = QuantumScript(ops, measurements) + # Act & Assert with pytest.raises(ValueError, match="State measurement requires shots=None"): executor._validate_circuit(circuit) def test_state_measurement_with_infinite_shots(self): - """Test validation passes for state measurement with shots=None.""" + """Ensures state vector measurements pass validation when shots are None.""" + # Arrange config = { 'machine_name': 'default', 'shots': None, 'wires': 2 } - executor = CircuitExecutor(config) - ops = [qml.Hadamard(0), qml.CNOT([0, 1])] measurements = [qml.state()] circuit = QuantumScript(ops, measurements) - # Should not raise an exception - executor._validate_circuit(circuit) + # Act & Assert + executor._validate_circuit(circuit) # Should not raise an exception class TestMeasurementSupportValidation: - """Test measurement type support validation.""" + """Test suite validating backend-specific measurement capabilities.""" @pytest.mark.parametrize("backend_type,measurement_type,should_support", [ (BackendType.LOCAL_SIMULATOR, qml.measurements.ProbabilityMP, True), @@ -183,7 +228,7 @@ class TestMeasurementSupportValidation: (BackendType.TIANYAN_HARDWARE, qml.measurements.StateMP, False), ]) def test_measurement_support(self, backend_type, measurement_type, should_support): - """Test measurement support validation for different backends.""" + """Verifies if the requested backend permits the provided measurement type.""" config = {'machine_name': 'default', 'wires': 2} executor = CircuitExecutor(config) executor._backend_type = backend_type @@ -191,7 +236,6 @@ class TestMeasurementSupportValidation: measurement = measurement_type() if should_support: - # Should not raise exception executor._validate_measurement_support(measurement) else: with pytest.raises(ValueError, match="does not support"): @@ -199,19 +243,19 @@ class TestMeasurementSupportValidation: class TestCircuitExecution: - """Test circuit execution functionality.""" + """Test suite for the primary circuit execution pipeline.""" def test_single_measurement_execution(self): - """Test execution with single measurement.""" + """Tests the execution flow for a circuit returning a single measurement.""" + # Arrange config = { 'machine_name': 'default', 'shots': None, 'wires': 2 } - executor = CircuitExecutor(config) - # Mock the internal execution methods + # Act with patch.object(executor, '_convert_to_cqlib_format') as mock_convert, \ patch.object(executor, '_execute_on_backend') as mock_execute, \ patch.object(executor, '_execute_measurement') as mock_measure: @@ -226,22 +270,23 @@ class TestCircuitExecution: result = executor.execute_circuit(circuit) - # Verify single result is returned (not list) + # Assert assert isinstance(result, np.ndarray) assert mock_convert.called assert mock_execute.called assert mock_measure.called def test_multiple_measurements_execution(self): - """Test execution with multiple measurements.""" + """Tests the execution flow handling circuits with multiple measurements.""" + # Arrange config = { 'machine_name': 'default', 'shots': None, 'wires': 2 } - executor = CircuitExecutor(config) + # Act with patch.object(executor, '_convert_to_cqlib_format') as mock_convert, \ patch.object(executor, '_execute_on_backend') as mock_execute, \ patch.object(executor, '_execute_measurement') as mock_measure: @@ -249,8 +294,8 @@ class TestCircuitExecution: mock_convert.return_value = (Mock(), "test_qcis") mock_execute.return_value = {'probabilities': {'00': 1.0}} mock_measure.side_effect = [ - np.array([1.0, 0.0, 0.0, 0.0]), # First measurement - 1.0 # Second measurement + np.array([1.0, 0.0, 0.0, 0.0]), + 1.0 ] ops = [qml.Identity(0)] @@ -259,70 +304,69 @@ class TestCircuitExecution: results = executor.execute_circuit(circuit) - # Verify list of results is returned + # Assert assert isinstance(results, list) assert len(results) == 2 assert mock_measure.call_count == 2 class TestMeasurementProcessing: - """Test measurement-specific result processing.""" + """Test suite verifying result parsing based on specific measurement types.""" def test_probability_measurement_processing(self): - """Test probability measurement result processing.""" + """Tests extraction and formatting of probability measurements.""" config = {'machine_name': 'default', 'wires': 2} executor = CircuitExecutor(config) raw_result = { 'probabilities': {'00': 0.25, '01': 0.25, '10': 0.25, '11': 0.25} } - measurement = qml.measurements.ProbabilityMP() + result = executor._execute_measurement_impl(measurement, raw_result) expected = np.array([0.25, 0.25, 0.25, 0.25]) np.testing.assert_array_equal(result, expected) def test_expectation_measurement_processing(self): - """Test expectation value measurement processing.""" + """Tests parsing logic for expectation value computations.""" config = {'machine_name': 'default', 'wires': 2} executor = CircuitExecutor(config) raw_result = { 'probabilities': {'00': 0.5, '11': 0.5} } - - # Expectation of Z⊗Z on Bell state should be 1.0 measurement = qml.measurements.ExpectationMP(qml.PauliZ(0) @ qml.PauliZ(1)) + result = executor._execute_measurement_impl(measurement, raw_result) - assert result == 1.0 # = (+1)*0.5 + (+1)*0.5 = 1.0 + assert result == 1.0 def test_sample_measurement_processing(self): - """Test sample measurement result processing.""" + """Tests standard sample measurement extraction.""" config = {'machine_name': 'default', 'wires': 2} executor = CircuitExecutor(config) raw_result = { 'samples': np.array([[0, 0], [1, 1], [0, 1]]) } - measurement = qml.measurements.SampleMP() + result = executor._execute_measurement_impl(measurement, raw_result) expected = np.array([[0, 0], [1, 1], [0, 1]]) np.testing.assert_array_equal(result, expected) def test_state_measurement_processing(self): - """Test statevector measurement processing.""" + """Tests state vector extraction mapping.""" config = {'machine_name': 'default', 'wires': 1} executor = CircuitExecutor(config) raw_result = { 'statevector': {'0': 0.70710678, '1': 0.70710678} } - measurement = qml.measurements.StateMP() + result = executor._execute_measurement_impl(measurement, raw_result) expected = {'0': 0.70710678, '1': 0.70710678} @@ -330,16 +374,17 @@ class TestMeasurementProcessing: class TestBackendExecution: - """Test backend-specific execution methods.""" + """Test suite validating direct invocations against specific backend targets.""" def test_local_simulator_execution(self): - """Test local simulator execution.""" + """Tests direct payload submission to the local statevector simulator.""" + # Arrange config = {'machine_name': 'default', 'wires': 2} executor = CircuitExecutor(config) - mock_circuit = Mock() mock_simulator = Mock() + # Act & Assert with patch('cqlib_adapter.pennylane_ext.circuit_executor.StatevectorSimulator') as mock_sim_class: mock_sim_class.return_value = mock_simulator mock_simulator.statevector.return_value = {'00': 1.0} @@ -353,198 +398,114 @@ class TestBackendExecution: assert 'statevector' in result mock_sim_class.assert_called_once_with(mock_circuit) - def test_tianyan_simulator_execution(self): - """Test Tianyan simulator execution.""" + @patch('cqlib_adapter.pennylane_ext.device.CQLibDevice.get_available_backends') + @patch('cqlib_adapter.pennylane_ext.circuit_executor.TianYanPlatform') + def test_tianyan_simulator_execution(self, mock_platform_class, mock_get_backends): + """Tests the submission and retrieval logic via the TianYan cloud platform.""" + # Arrange config = { 'machine_name': 'tianyan_s', 'login_key': 'test_key', 'shots': 1000, 'wires': 2 } - - with patch('cqlib_adapter.pennylane_ext.circuit_executor.TianYanPlatform') as mock_platform_class: - mock_platform = Mock() - mock_platform_class.return_value = mock_platform - - executor = CircuitExecutor(config) - - mock_platform.submit_experiment.return_value = 'test_query_id' - mock_platform.query_experiment.return_value = [{ - 'resultStatus': 'S[0.5,0.5]', - 'probability': '{"00":0.5,"11":0.5}' - }] - - result = executor._execute_tianyan_simulator(Mock(), "test_qcis") - - assert 'probabilities' in result - assert 'samples' in result - mock_platform.submit_experiment.assert_called_once_with( - "test_qcis", num_shots=1000 - ) - - -class TestResultReordering: - """Test result reordering functionality.""" - - def test_probability_reordering(self): - """Test probability dictionary reordering.""" - config = {'machine_name': 'default', 'wires': 2} + mock_platform = Mock() + mock_platform_class.return_value = mock_platform executor = CircuitExecutor(config) - probabilities = {'00': 0.25, '01': 0.25, '10': 0.25, '11': 0.25} - wire_labels = [1, 0] # Swap qubit order - - reordered = executor._reorder_probability_dict(probabilities, wire_labels) + mock_platform.submit_experiment.return_value = 'test_query_id' + mock_platform.query_experiment.return_value = [{ + 'resultStatus': 'S[0.5,0.5]', + 'probability': '{"00":0.5,"11":0.5}' + }] - # With wire_labels [1,0], bitstring '01' becomes '10' etc. - assert reordered['00'] == 0.25 # 00 -> 00 - assert reordered['01'] == 0.25 # 01 -> 10 - assert reordered['10'] == 0.25 # 10 -> 01 - assert reordered['11'] == 0.25 # 11 -> 11 - - def test_sample_reordering(self): - """Test sample matrix reordering.""" - config = {'machine_name': 'default', 'wires': 2} - executor = CircuitExecutor(config) - - samples = np.array([[0, 1], [1, 0]]) # [[q0,q1], ...] - wire_labels = [1, 0] # Swap order + mock_circuit = Mock() + mock_circuit.qcis = "test_qcis" - reordered = executor._reorder_sample_matrix(samples, wire_labels) + # Act + result = executor._execute_tianyan_simulator(mock_circuit, "test_qcis") - expected = np.array([[1, 0], [0, 1]]) # [[q1,q0], ...] - np.testing.assert_array_equal(reordered, expected) + # Assert + assert 'probabilities' in result + assert 'samples' in result + mock_platform.submit_experiment.assert_called_once_with( + "test_qcis", num_shots=1000 + ) + class TestUtilityFunctions: - """Test utility functions.""" + """Test suite for binary conversion and formatting data utilities.""" @pytest.mark.parametrize("decimal,bits,little_endian,expected", [ - (5, 4, True, [1, 0, 1, 0]), # 5 = 1010 (little-endian) - (5, 4, False, [0, 1, 0, 1]), # 5 = 0101 (big-endian) + (5, 4, True, [1, 0, 1, 0]), + (5, 4, False, [0, 1, 0, 1]), (0, 3, True, [0, 0, 0]), (7, 3, True, [1, 1, 1]), ]) def test_decimal_to_binary_array(self, decimal, bits, little_endian, expected): - """Test decimal to binary array conversion.""" + """Tests endianness handling during decimal-to-binary transformation.""" result = decimal_to_binary_array(decimal, bits, little_endian) expected_array = np.array(expected) np.testing.assert_array_equal(result, expected_array) def test_samples_to_pennylane_format(self): - """Test sample format conversion.""" - samples = [1, 2, 3] # Decimal samples + """Tests the restructuring of raw integers into PennyLane sample matrices.""" + samples = [1, 2, 3] num_qubits = 2 result = samples_to_pennylane_format(samples, num_qubits) expected = np.array([ - [1, 0], # 1 = 01 -> [1,0] little-endian - [0, 1], # 2 = 10 -> [0,1] little-endian - [1, 1], # 3 = 11 -> [1,1] little-endian + [1, 0], + [0, 1], + [1, 1], ]) np.testing.assert_array_equal(result, expected) - - def test_switch_endianness(self): - """Test endianness switching.""" - data_1d = [1, 0, 1, 0] - result_1d = switch_endianness(data_1d) - expected_1d = np.array([0, 1, 0, 1]) - np.testing.assert_array_equal(result_1d, expected_1d) - - data_2d = [[1, 0], [0, 1]] - result_2d = switch_endianness(data_2d) - expected_2d = np.array([[0, 1], [1, 0]]) - np.testing.assert_array_equal(result_2d, expected_2d) class TestErrorHandling: - """Test error handling and edge cases.""" + """Test suite simulating fault tolerance and exception propagation.""" - def test_backend_connection_failure(self): - """Test handling of backend connection failures.""" + @patch('cqlib_adapter.pennylane_ext.device.CQLibDevice.get_available_backends') + def test_backend_connection_failure(self, mock_get_backends): + """Tests graceful failure on critical external API timeouts.""" config = { 'machine_name': 'tianyan_s', 'login_key': 'test_key', 'wires': 2 } - with patch('cqlib_adapter.pennylane_ext.circuit_executor.TianYanPlatform') as mock_platform: - mock_platform.side_effect = Exception("Connection failed") - - with pytest.raises(ConnectionError, match="Backend connection failed"): - CircuitExecutor(config) + mock_get_backends.side_effect = Exception("Connection failed") + + with pytest.raises(ConnectionError, match="Could not connect to Tianyan API"): + CircuitExecutor(config) def test_missing_probabilities_in_raw_result(self): - """Test handling of missing probabilities in raw results.""" + """Tests boundary behavior when external systems return malformed responses.""" config = {'machine_name': 'default', 'wires': 2} executor = CircuitExecutor(config) - raw_result = {} # No probabilities key - + raw_result = {} measurement = qml.measurements.ExpectationMP(qml.PauliZ(0)) with pytest.raises(ValueError, match="must contain 'probabilities' key"): executor._execute_measurement_impl(measurement, raw_result) -class TestExecutionStatistics: - """Test execution statistics functionality.""" - - def test_get_execution_stats(self): - """Test retrieval of execution statistics.""" - config = { - 'machine_name': 'default', - 'shots': 1000, - 'wires': 5, - 'verbose': True - } - - executor = CircuitExecutor(config) - - # Execute some circuits to increment count - with patch.object(executor, '_convert_to_cqlib_format') as mock_convert, \ - patch.object(executor, '_execute_on_backend') as mock_execute, \ - patch.object(executor, '_execute_measurement') as mock_measure: - - # FIX: Provide proper return values for the mock - mock_circuit_obj = Mock() - mock_convert.return_value = (mock_circuit_obj, "test_qcis") - mock_execute.return_value = {'probabilities': {'0': 1.0}} - mock_measure.return_value = np.array([1.0, 0.0]) - - ops = [qml.Hadamard(0)] - measurements = [qml.probs()] - circuit = QuantumScript(ops, measurements) - - executor.execute_circuit(circuit) - executor.execute_circuit(circuit) - - stats = executor.get_execution_stats() - - assert stats['execution_count'] == 2 - assert stats['backend_type'] == 'local' - assert stats['wires'] == 5 - assert stats['shots'] == 1000 - assert stats['machine_name'] == 'default' - - -# Integration test for complete workflow class TestIntegration: - """Integration tests for complete workflow.""" + """Integration test suite executing components from initialization to measurement.""" def test_complete_local_execution_workflow(self): - """Test complete execution workflow with local simulator.""" + """Tests the end-to-end simulation lifecycle targeting the local runtime.""" + # Arrange config = { 'machine_name': 'default', 'shots': None, 'wires': 2, 'verbose': False } - executor = CircuitExecutor(config) - - # Create a simple circuit ops = [ qml.Hadamard(0), qml.CNOT([0, 1]) @@ -552,8 +513,7 @@ class TestIntegration: measurements = [qml.probs()] circuit = QuantumScript(ops, measurements) - # Mock the CQLib components - + # Act with patch('cqlib.utils.qasm2') as mock_qasm2, \ patch('cqlib.simulator.statevector_simulator.StatevectorSimulator') as mock_simulator_class: @@ -564,17 +524,17 @@ class TestIntegration: mock_simulator = Mock() mock_simulator_class.return_value = mock_simulator mock_simulator.probs.return_value = {'00': 0.5, '11': 0.5} - mock_simulator.sample.return_value = np.array([0, 3]) # Decimal samples + mock_simulator.sample.return_value = np.array([0, 3]) mock_simulator.statevector.return_value = {'00': 0.707, '11': 0.707} result = executor.execute_circuit(circuit) - # Verify the result is a probability array + # Assert assert isinstance(result, np.ndarray) - assert len(result) == 4 # 2^2 = 4 probabilities + assert len(result) == 4 def test_logging_setup(self): - """Test that logging is properly configured.""" + """Tests contextually accurate configuration of runtime loggers.""" config = { 'machine_name': 'default', 'shots': 1000, @@ -584,17 +544,13 @@ class TestIntegration: executor = CircuitExecutor(config) - # Check that logger is configured assert executor.logger is not None assert executor.logger.level == logging.INFO - # Test with verbose disabled config['verbose'] = False executor = CircuitExecutor(config) - # Logger should exist but may not have handlers assert executor.logger is not None if __name__ == "__main__": - # Run the tests pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/test_pennylane/test_compile.py b/tests/test_pennylane/test_compile.py new file mode 100644 index 0000000..1342899 --- /dev/null +++ b/tests/test_pennylane/test_compile.py @@ -0,0 +1,282 @@ +# test_backends.py +# This code is part of cqlib. +# +# Copyright (C) 2025 China Telecom Quantum Group. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for compilation functions in compilation.py.""" + +import pytest +import numpy as np +import pennylane as qml +from pennylane import numpy as pnp +from pennylane.tape import QuantumScript + +from cqlib_adapter.pennylane_ext.compilation import ( + compile_to_native_gates, + compile_to_native_cqlib, +) +from cqlib_adapter.pennylane_ext.native_gates import ( + X2PGate, + X2MGate, + Y2PGate, + Y2MGate, + XY2PGate, + XY2MGate, +) + + +class TestCompileToNativeGates: + """Tests for compile_to_native_gates function.""" + + def test_compile_hadamard(self): + """Test Hadamard gate compilation to native gates.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(wires=0) + return qml.probs(wires=0) + + tape = compile_to_native_gates(circuit) + op_names = [op.name for op in tape.operations] + + # H should decompose to RZ(pi/2) - X2P - RZ(pi/2) + assert "RZ" in op_names + assert "X2PGate" in op_names + + def test_compile_rx(self): + """Test RX gate compilation to native gates.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(theta): + qml.RX(theta, wires=0) + return qml.probs(wires=0) + + tape = compile_to_native_gates(circuit, pnp.pi / 4) + op_names = [op.name for op in tape.operations] + + # RX should contain X2P, X2M, RZ gates + assert "X2PGate" in op_names + assert "X2MGate" in op_names + + def test_compile_ry(self): + """Test RY gate compilation to native gates.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(theta): + qml.RY(theta, wires=0) + return qml.probs(wires=0) + + tape = compile_to_native_gates(circuit, pnp.pi / 4) + op_names = [op.name for op in tape.operations] + + assert "X2PGate" in op_names + assert "X2MGate" in op_names + + def test_compile_paulix(self): + """Test PauliX gate compilation.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(): + qml.PauliX(wires=0) + return qml.probs(wires=0) + + tape = compile_to_native_gates(circuit) + op_names = [op.name for op in tape.operations] + + # X -> X2P - X2P + assert op_names.count("X2PGate") == 2 + + def test_compile_pauliy(self): + """Test PauliY gate compilation.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(): + qml.PauliY(wires=0) + return qml.probs(wires=0) + + tape = compile_to_native_gates(circuit) + op_names = [op.name for op in tape.operations] + + # Y -> Y2P - Y2P + assert op_names.count("Y2PGate") == 2 + + def test_native_gates_unchanged(self): + """Test that native gates pass through unchanged.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(): + X2PGate(wires=0) + XY2PGate(0.5, wires=0) + return qml.probs(wires=0) + + tape = compile_to_native_gates(circuit) + op_names = [op.name for op in tape.operations] + + assert "X2PGate" in op_names + assert "XY2PGate" in op_names + + def test_compile_with_measurements(self): + """Test that measurements are preserved after compilation.""" + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(wires=0) + qml.CNOT(wires=[0, 1]) + return qml.probs(wires=[0, 1]) + + tape = compile_to_native_gates(circuit) + + assert len(tape.measurements) == 1 + + def test_compile_cnot(self): + """Test CNOT gate compilation.""" + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(): + qml.CNOT(wires=[0, 1]) + return qml.probs(wires=[0, 1]) + + tape = compile_to_native_gates(circuit) + op_names = [op.name for op in tape.operations] + + # CNOT -> H-CZ-H on target + assert "CZ" in op_names + + +class TestCompileToNativeCqlib: + """Tests for compile_to_native_cqlib function.""" + + def test_compile_hadamard_cqlib(self): + """Test Hadamard compiles to CQLib native format.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(wires=0) + return qml.state() + + cql_circ = compile_to_native_cqlib(circuit) + qcis = cql_circ.qcis + + # Should contain rz and y2p for H + assert "rz" in qcis.lower() or "y2p" in qcis.lower() + + def test_compile_rx_cqlib(self): + """Test RX compiles to CQLib native format.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(theta): + qml.RX(theta, wires=0) + return qml.state() + + cql_circ = compile_to_native_cqlib(circuit, pnp.pi / 4) + qcis = cql_circ.qcis.lower() + + # RX decomposition uses x2p, x2m, rz + assert "x2p" in qcis or "x2m" in qcis or "rz" in qcis + + def test_compile_ry_cqlib(self): + """Test RY compiles to CQLib native format.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(theta): + qml.RY(theta, wires=0) + return qml.state() + + cql_circ = compile_to_native_cqlib(circuit, pnp.pi / 4) + qcis = cql_circ.qcis.lower() + + assert "x2p" in qcis or "x2m" in qcis or "rz" in qcis + + def test_compile_paulix_cqlib(self): + """Test PauliX compiles to CQLib native format.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(): + qml.PauliX(wires=0) + return qml.state() + + cql_circ = compile_to_native_cqlib(circuit) + qcis = cql_circ.qcis.lower() + + # X -> x2p x2p + assert qcis.count("x2p") == 2 + + def test_compile_pauliy_cqlib(self): + """Test PauliY compiles to CQLib native format.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(): + qml.PauliY(wires=0) + return qml.state() + + cql_circ = compile_to_native_cqlib(circuit) + qcis = cql_circ.qcis.lower() + + # Y -> y2p y2p + assert qcis.count("y2p") == 2 + + def test_compile_cnot_cqlib(self): + """Test CNOT compiles to CQLib native format.""" + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(): + qml.CNOT(wires=[0, 1]) + return qml.state() + + cql_circ = compile_to_native_cqlib(circuit) + qcis = cql_circ.qcis.lower() + + # CNOT decomposition uses y2m, cz, y2p + assert "y2m" in qcis or "cz" in qcis or "y2p" in qcis + + def test_compile_s_gate_cqlib(self): + """Test S gate compiles to CQLib native format.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(): + qml.S(wires=0) + return qml.state() + + cql_circ = compile_to_native_cqlib(circuit) + qcis = cql_circ.qcis.lower() + + # S = RZ(pi/2) + assert "rz" in qcis + + def test_compile_t_gate_cqlib(self): + """Test T gate compiles to CQLib native format.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(): + qml.T(wires=0) + return qml.state() + + cql_circ = compile_to_native_cqlib(circuit) + qcis = cql_circ.qcis.lower() + + # T = RZ(pi/4) + assert "rz" in qcis diff --git a/tests/test_pennylane/test_ext_mapping.py b/tests/test_pennylane/test_ext_mapping.py new file mode 100644 index 0000000..968f6cc --- /dev/null +++ b/tests/test_pennylane/test_ext_mapping.py @@ -0,0 +1,205 @@ +"""Tests for HardwareMapper class in ext_mapping.py.""" + +import pytest +from unittest.mock import Mock, MagicMock +from cqlib_adapter.pennylane_ext.ext_mapping import HardwareMapper + + +class MockQubit: + """Mock qubit object that mimics cqlib qubit behavior.""" + def __init__(self, index): + self.index = index + + +class TestHardwareMapperInit: + """Tests for HardwareMapper initialization.""" + + def test_init_with_valid_mapping(self): + """Test initialization with a valid mapping dictionary.""" + mapping = {'Q0': 'Q7', 'Q1': 'Q5'} + mapper = HardwareMapper(mapping) + assert mapper.mapping_dict == mapping + + def test_mapping_dict_reference(self): + """Test that mapper stores reference to mapping dict.""" + mapping = {'Q0': 'Q7'} + mapper = HardwareMapper(mapping) + assert mapper.mapping_dict is mapping + + +class TestMapQcisCode: + """Tests for map_qcis_code string replacement method.""" + + def test_single_qubit_mapping(self): + """Test mapping a single qubit in QCIS code.""" + mapper = HardwareMapper({'Q0': 'Q7'}) + original = "X Q0" + result = mapper.map_qcis_code(original) + assert result == "X Q7" + + def test_multiple_qubit_mapping(self): + """Test mapping multiple qubits in QCIS code.""" + mapper = HardwareMapper({'Q0': 'Q7', 'Q1': 'Q5'}) + original = "X Q0\nH Q1" + result = mapper.map_qcis_code(original) + assert "Q7" in result + assert "Q5" in result + assert "Q0" not in result + assert "Q1" not in result + + def test_same_line_multiple_mentions(self): + """Test that multiple mentions of same qubit on a line are all mapped.""" + mapper = HardwareMapper({'Q0': 'Q7'}) + original = "CNOT Q0 Q0" + result = mapper.map_qcis_code(original) + assert result == "CNOT Q7 Q7" + + def test_no_match_leaves_unchanged(self): + """Test that qubits not in mapping are left unchanged.""" + mapper = HardwareMapper({'Q0': 'Q7'}) + original = "X Q2" + result = mapper.map_qcis_code(original) + assert result == "X Q2" + + def test_empty_qcis_code(self): + """Test with empty QCIS code string.""" + mapper = HardwareMapper({'Q0': 'Q7'}) + result = mapper.map_qcis_code("") + assert result == "" + + def test_empty_mapping_dict(self): + """Test with empty mapping dictionary.""" + mapper = HardwareMapper({}) + original = "X Q0" + result = mapper.map_qcis_code(original) + assert result == "X Q0" + + def test_complex_qcis_sequence(self): + """Test mapping of a more complex QCIS sequence.""" + mapper = HardwareMapper({'Q0': 'Q3', 'Q1': 'Q7'}) + original = """X Q0 +H Q0 +CNOT Q0 Q1 +M Q1""" + result = mapper.map_qcis_code(original) + lines = result.strip().split('\n') + assert "X Q3" in lines[0] + assert "H Q3" in lines[1] + assert "CNOT Q3 Q7" in lines[2] + assert "M Q7" in lines[3] + + +class TestMapInstructionQubits: + """Tests for _map_instruction_qubits internal method.""" + + def test_map_single_string_qubit(self): + """Test mapping a single string qubit reference.""" + mapper = HardwareMapper({'Q0': 'Q7'}) + result = mapper._map_instruction_qubits('Q0') + assert result == 'Q7' + + def test_map_string_qubit_not_in_mapping(self): + """Test qubit reference not in mapping returns unchanged.""" + mapper = HardwareMapper({'Q0': 'Q7'}) + result = mapper._map_instruction_qubits('Q2') + assert result == 'Q2' + + def test_map_list_of_qubits(self): + """Test mapping a list of qubit references.""" + mapper = HardwareMapper({'Q0': 'Q7', 'Q1': 'Q5'}) + result = mapper._map_instruction_qubits(['Q0', 'Q1']) + assert result == ['Q7', 'Q5'] + + def test_map_list_with_partial_mapping(self): + """Test mapping a list where only some qubits are mapped.""" + mapper = HardwareMapper({'Q0': 'Q7'}) + result = mapper._map_instruction_qubits(['Q0', 'Q1']) + assert result == ['Q7', 'Q1'] + + def test_map_tuple_of_qubits(self): + """Test mapping a tuple of qubit references.""" + mapper = HardwareMapper({'Q0': 'Q7', 'Q1': 'Q5'}) + result = mapper._map_instruction_qubits(('Q0', 'Q1')) + assert result == ['Q7', 'Q5'] + + def test_map_non_string_returns_unchanged(self): + """Test that non-string qubit references are returned unchanged.""" + mapper = HardwareMapper({'Q0': 'Q7'}) + result = mapper._map_instruction_qubits(123) + assert result == 123 + + +class TestMapCircuit: + """Tests for map_circuit method.""" + + def test_map_circuit_updates_qubit_names(self): + """Test that map_circuit updates _qubits dictionary keys.""" + mapper = HardwareMapper({'Q0': 'Q7', 'Q1': 'Q5'}) + + # Create mock circuit + mock_circuit = Mock() + mock_circuit._qubits = { + 'Q0': Mock(), + 'Q1': Mock() + } + mock_circuit._circuit_data = [] + mock_circuit._parameters = {} + + result = mapper.map_circuit(mock_circuit) + + # Check that result has new qubit names + assert 'Q7' in result._qubits + assert 'Q5' in result._qubits + assert 'Q0' not in result._qubits + assert 'Q1' not in result._qubits + + def test_map_circuit_copies_parameters(self): + """Test that map_circuit copies _parameters dict.""" + mapper = HardwareMapper({'Q0': 'Q7'}) + params = {'theta': 0.5} + + mock_circuit = Mock() + mock_circuit._qubits = {'Q0': Mock()} + mock_circuit._circuit_data = [] + mock_circuit._parameters = params + + result = mapper.map_circuit(mock_circuit) + + assert result._parameters == params + assert result._parameters is not params # Should be a copy + + def test_map_circuit_handles_unmapped_qubits(self): + """Test that unmapped qubits are preserved in _qubits dict.""" + mapper = HardwareMapper({'Q0': 'Q7'}) + + mock_circuit = Mock() + mock_qubit_q0 = Mock() + mock_qubit_q2 = Mock() + mock_circuit._qubits = {'Q0': mock_qubit_q0, 'Q2': mock_qubit_q2} + mock_circuit._circuit_data = [] + mock_circuit._parameters = {} + + result = mapper.map_circuit(mock_circuit) + + assert 'Q7' in result._qubits + assert 'Q2' in result._qubits # Unmapped qubit preserved + + def test_map_circuit_maps_instruction_data(self): + """Test that instruction data qubit references are updated.""" + mapper = HardwareMapper({'Q0': 'Q7'}) + + mock_instruction = Mock() + mock_instruction.qubits = ['Q0'] + mock_instruction_data = Mock() + mock_instruction_data.qubits = ['Q0'] + mock_instruction_data.instruction = 'X' + + mock_circuit = Mock() + mock_circuit._qubits = {'Q0': Mock()} + mock_circuit._circuit_data = [mock_instruction_data] + mock_circuit._parameters = {} + + result = mapper.map_circuit(mock_circuit) + + # Check instruction qubits were mapped + assert len(result._circuit_data) == 1 diff --git a/tests/test_pennylane/test_native_gates.py b/tests/test_pennylane/test_native_gates.py new file mode 100644 index 0000000..c141136 --- /dev/null +++ b/tests/test_pennylane/test_native_gates.py @@ -0,0 +1,353 @@ +"""Tests for native gate classes in native_gates.py.""" + +import pytest +import numpy as np +import pennylane as qml +from pennylane import numpy as pnp +from pennylane.tape import QuantumScript + +from cqlib_adapter.pennylane_ext.native_gates import ( + X2PGate, + X2MGate, + Y2PGate, + Y2MGate, + XY2PGate, + XY2MGate, +) + + +class TestX2PGate: + """Tests for X2PGate (sqrt(X) with +pi/2 rotation).""" + + def test_matrix_shape(self): + """Test that the gate matrix has correct shape.""" + matrix = X2PGate.compute_matrix() + assert matrix.shape == (2, 2) + + def test_matrix_is_unitary(self): + """Test that the gate matrix is unitary (U @ U^dag = I).""" + matrix = X2PGate.compute_matrix().astype(complex) + dagger = np.conj(matrix.T) + product = matrix @ dagger + identity = np.eye(2) + np.testing.assert_allclose(product, identity, atol=1e-10) + + def test_matrix_matches_expected(self): + """Test matrix elements match expected values.""" + isqrt2 = 1 / np.sqrt(2) + expected = np.array([ + [isqrt2, -1j * isqrt2], + [-1j * isqrt2, isqrt2] + ], dtype=complex) + matrix = X2PGate.compute_matrix() + np.testing.assert_allclose(matrix, expected, atol=1e-10) + + def test_adjoint_returns_x2m(self): + """Test that adjoint() returns X2MGate on same wire.""" + gate = X2PGate(wires=0) + adjoint_gate = gate.adjoint() + assert isinstance(adjoint_gate, X2MGate) + assert adjoint_gate.wires == gate.wires + + def test_num_wires(self): + """Test num_wires class attribute.""" + assert X2PGate.num_wires == 1 + + def test_num_params(self): + """Test num_params class attribute.""" + assert X2PGate.num_params == 0 + + def test_operation_in_circuit(self): + """Test X2PGate can be used in a PennyLane circuit.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(): + X2PGate(wires=0) + return qml.probs(wires=0) + + result = circuit() + assert result.shape == (2,) + + +class TestX2MGate: + """Tests for X2MGate (sqrt(X) with -pi/2 rotation).""" + + def test_matrix_shape(self): + """Test that the gate matrix has correct shape.""" + matrix = X2MGate.compute_matrix() + assert matrix.shape == (2, 2) + + def test_matrix_is_unitary(self): + """Test that the gate matrix is unitary.""" + matrix = X2MGate.compute_matrix().astype(complex) + dagger = np.conj(matrix.T) + product = matrix @ dagger + identity = np.eye(2) + np.testing.assert_allclose(product, identity, atol=1e-10) + + def test_matrix_matches_expected(self): + """Test matrix elements match expected values.""" + isqrt2 = 1 / np.sqrt(2) + expected = np.array([ + [isqrt2, 1j * isqrt2], + [1j * isqrt2, isqrt2] + ], dtype=complex) + matrix = X2MGate.compute_matrix() + np.testing.assert_allclose(matrix, expected, atol=1e-10) + + def test_adjoint_returns_x2p(self): + """Test that adjoint() returns X2PGate.""" + gate = X2MGate(wires=0) + adjoint_gate = gate.adjoint() + assert isinstance(adjoint_gate, X2PGate) + assert adjoint_gate.wires == gate.wires + + # 【修复点 1】: 修改了错误的组合门断言,并拆分成如下两个正确的用例 + def test_x2p_then_x2p_equals_x(self): + """Test X2P @ X2P should give PauliX (up to global phase).""" + mat_x2p = X2PGate.compute_matrix() + product = mat_x2p @ mat_x2p + expected_x = np.array([[0, 1], [1, 0]], dtype=complex) + np.testing.assert_allclose(np.abs(product), np.abs(expected_x), atol=1e-10) + + def test_x2p_then_x2m_equals_identity(self): + """Test X2P @ X2M should give Identity.""" + mat_x2p = X2PGate.compute_matrix() + mat_x2m = X2MGate.compute_matrix() + product = mat_x2m @ mat_x2p + expected_i = np.eye(2, dtype=complex) + np.testing.assert_allclose(np.abs(product), np.abs(expected_i), atol=1e-10) + + +class TestY2PGate: + """Tests for Y2PGate (sqrt(Y) with +pi/2 rotation).""" + + def test_matrix_shape(self): + """Test that the gate matrix has correct shape.""" + matrix = Y2PGate.compute_matrix() + assert matrix.shape == (2, 2) + + def test_matrix_is_unitary(self): + """Test that the gate matrix is unitary.""" + matrix = Y2PGate.compute_matrix().astype(complex) + dagger = np.conj(matrix.T) + product = matrix @ dagger + identity = np.eye(2) + np.testing.assert_allclose(product, identity, atol=1e-10) + + def test_adjoint_returns_y2m(self): + """Test that adjoint() returns Y2MGate.""" + gate = Y2PGate(wires=0) + adjoint_gate = gate.adjoint() + assert isinstance(adjoint_gate, Y2MGate) + assert adjoint_gate.wires == gate.wires + + def test_operation_in_circuit(self): + """Test Y2PGate can be used in a PennyLane circuit.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(): + Y2PGate(wires=0) + return qml.probs(wires=0) + + result = circuit() + assert result.shape == (2,) + + +class TestY2MGate: + """Tests for Y2MGate (sqrt(Y) with -pi/2 rotation).""" + + def test_matrix_shape(self): + """Test that the gate matrix has correct shape.""" + matrix = Y2MGate.compute_matrix() + assert matrix.shape == (2, 2) + + def test_matrix_is_unitary(self): + """Test that the gate matrix is unitary.""" + matrix = Y2MGate.compute_matrix().astype(complex) + dagger = np.conj(matrix.T) + product = matrix @ dagger + identity = np.eye(2) + np.testing.assert_allclose(product, identity, atol=1e-10) + + def test_adjoint_returns_y2p(self): + """Test that adjoint() returns Y2PGate.""" + gate = Y2MGate(wires=0) + adjoint_gate = gate.adjoint() + assert isinstance(adjoint_gate, Y2PGate) + assert adjoint_gate.wires == gate.wires + + # 【修复点 2】: 修改了错误的组合门断言,同样拆分成两个正确的用例 + def test_y2p_then_y2p_equals_y(self): + """Test Y2P @ Y2P should give PauliY (up to global phase).""" + mat_y2p = Y2PGate.compute_matrix() + product = mat_y2p @ mat_y2p + expected_y = np.array([[0, -1j], [1j, 0]], dtype=complex) + np.testing.assert_allclose(np.abs(product), np.abs(expected_y), atol=1e-10) + + def test_y2p_then_y2m_equals_identity(self): + """Test Y2P @ Y2M should give Identity.""" + mat_y2p = Y2PGate.compute_matrix() + mat_y2m = Y2MGate.compute_matrix() + product = mat_y2m @ mat_y2p + expected_i = np.eye(2, dtype=complex) + np.testing.assert_allclose(np.abs(product), np.abs(expected_i), atol=1e-10) + + +class TestXY2PGate: + """Tests for XY2PGate (parameterized XY-plane rotation by +pi/2).""" + + def test_matrix_shape_with_scalar_param(self): + """Test matrix shape with a scalar phi parameter.""" + matrix = XY2PGate.compute_matrix(pnp.array(0.5)) + assert matrix.shape == (2, 2) + + def test_matrix_shape_with_float_param(self): + """Test matrix shape with a float phi parameter.""" + matrix = XY2PGate.compute_matrix(0.5) + assert matrix.shape == (2, 2) + + def test_matrix_is_unitary(self): + """Test that the gate matrix is unitary for any phi.""" + phi = pnp.array(0.7) + matrix = XY2PGate.compute_matrix(phi).astype(complex) + dagger = np.conj(matrix.T) + product = matrix @ dagger + identity = np.eye(2) + np.testing.assert_allclose(product, identity, atol=1e-10) + + def test_adjoint_returns_xy2m(self): + """Test that adjoint() returns XY2MGate.""" + gate = XY2PGate(0.5, wires=0) + adjoint_gate = gate.adjoint() + assert isinstance(adjoint_gate, XY2MGate) + + def test_adjoint_preserves_phi(self): + """Test adjoint preserves the phi parameter.""" + phi = pnp.array(0.7) + gate = XY2PGate(phi, wires=0) + adjoint_gate = gate.adjoint() + np.testing.assert_allclose(adjoint_gate.data[0], phi, atol=1e-10) + + def test_num_wires(self): + """Test num_wires class attribute.""" + assert XY2PGate.num_wires == 1 + + def test_num_params(self): + """Test num_params class attribute.""" + assert XY2PGate.num_params == 1 + + def test_ndim_params(self): + """Test ndim_params class attribute.""" + assert XY2PGate.ndim_params == (0,) + + def test_operation_in_circuit(self): + """Test XY2PGate can be used in a PennyLane circuit.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(phi): + XY2PGate(phi, wires=0) + return qml.probs(wires=0) + + result = circuit(0.5) + assert result.shape == (2,) + + # 【修复点 3】: 移除了包裹在矩阵外的 np.abs(),保留了复数相位的对比 + def test_different_phi_produces_different_matrix(self): + """Test that different phi values produce different matrices.""" + matrix1 = XY2PGate.compute_matrix(0.0) + matrix2 = XY2PGate.compute_matrix(np.pi / 2) + assert not np.allclose(matrix1, matrix2, atol=1e-10) + + +class TestXY2MGate: + """Tests for XY2MGate (parameterized XY-plane rotation by -pi/2).""" + + def test_matrix_shape_with_scalar_param(self): + """Test matrix shape with a scalar phi parameter.""" + matrix = XY2MGate.compute_matrix(pnp.array(0.5)) + assert matrix.shape == (2, 2) + + def test_matrix_shape_with_float_param(self): + """Test matrix shape with a float phi parameter.""" + matrix = XY2MGate.compute_matrix(0.5) + assert matrix.shape == (2, 2) + + def test_matrix_is_unitary(self): + """Test that the gate matrix is unitary for any phi.""" + phi = pnp.array(0.7) + matrix = XY2MGate.compute_matrix(phi).astype(complex) + dagger = np.conj(matrix.T) + product = matrix @ dagger + identity = np.eye(2) + np.testing.assert_allclose(product, identity, atol=1e-10) + + def test_adjoint_returns_xy2p(self): + """Test that adjoint() returns XY2PGate.""" + gate = XY2MGate(0.5, wires=0) + adjoint_gate = gate.adjoint() + assert isinstance(adjoint_gate, XY2PGate) + + def test_adjoint_preserves_phi(self): + """Test adjoint preserves the phi parameter.""" + phi = pnp.array(0.7) + gate = XY2MGate(phi, wires=0) + adjoint_gate = gate.adjoint() + np.testing.assert_allclose(adjoint_gate.data[0], phi, atol=1e-10) + + def test_operation_in_circuit(self): + """Test XY2MGate can be used in a PennyLane circuit.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(phi): + XY2MGate(phi, wires=0) + return qml.probs(wires=0) + + result = circuit(0.5) + assert result.shape == (2,) + + +class TestGateInteraction: + """Tests for interactions between different gate types.""" + + def test_x2p_adjoint_inverse_relationship(self): + """Test X2P and X2M are adjoints of each other.""" + x2p = X2PGate(wires=0) + x2m = X2MGate(wires=0) + assert x2p.adjoint() is not x2p + assert x2m.adjoint() is not x2m + assert isinstance(x2p.adjoint(), X2MGate) + assert isinstance(x2m.adjoint(), X2PGate) + + def test_y2p_adjoint_inverse_relationship(self): + """Test Y2P and Y2M are adjoints of each other.""" + y2p = Y2PGate(wires=0) + y2m = Y2MGate(wires=0) + assert isinstance(y2p.adjoint(), Y2MGate) + assert isinstance(y2m.adjoint(), Y2PGate) + + def test_xy2p_and_xy2m_are_adjoint_pairs(self): + """Test XY2P and XY2M are adjoint pairs.""" + phi = 0.3 + gate_xy2p = XY2PGate(phi, wires=0) + gate_xy2m = XY2MGate(phi, wires=0) + assert isinstance(gate_xy2p.adjoint(), XY2MGate) + assert isinstance(gate_xy2m.adjoint(), XY2PGate) + + def test_gates_work_on_multi_qubit_circuit(self): + """Test gates can be used in multi-qubit circuits.""" + dev = qml.device("default.qubit", wires=3) + + @qml.qnode(dev) + def circuit(phi): + X2PGate(wires=0) + Y2PGate(wires=1) + XY2PGate(phi, wires=2) + return qml.probs(wires=[0, 1, 2]) + + result = circuit(0.5) + assert result.shape == (8,) # 2^3 = 8 \ No newline at end of file -- Gitee From 0465f91f8c19de020d52d820241eab59d17510d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Fri, 24 Apr 2026 15:17:40 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cqlib_adapter/qiskit_ext/adapter.py | 2 +- tests/test_pennylane/test_backends.py | 4 +- tests/test_pennylane/test_circuit_executor.py | 4 +- tests/test_pennylane/test_compile.py | 4 +- tests/test_pennylane/test_ext_mapping.py | 13 ++ tests/test_pennylane/test_gradients.py | 2 +- tests/test_pennylane/test_grover.py | 216 ------------------ tests/test_pennylane/test_native_gates.py | 13 ++ 8 files changed, 34 insertions(+), 224 deletions(-) delete mode 100644 tests/test_pennylane/test_grover.py diff --git a/cqlib_adapter/qiskit_ext/adapter.py b/cqlib_adapter/qiskit_ext/adapter.py index f1e8130..5e8a711 100644 --- a/cqlib_adapter/qiskit_ext/adapter.py +++ b/cqlib_adapter/qiskit_ext/adapter.py @@ -1,6 +1,6 @@ # This code is part of cqlib. # -# Copyright (C) 2025 China Telecom Quantum Group. +# Copyright (C) 2026 China Telecom Quantum Group. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE file in the root directory diff --git a/tests/test_pennylane/test_backends.py b/tests/test_pennylane/test_backends.py index 531def3..87b2f61 100644 --- a/tests/test_pennylane/test_backends.py +++ b/tests/test_pennylane/test_backends.py @@ -1,7 +1,7 @@ -# test_backends.py +# test_gradients.py # This code is part of cqlib. # -# Copyright (C) 2025 China Telecom Quantum Group. +# Copyright (C) 2026 China Telecom Quantum Group. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE file in the root directory diff --git a/tests/test_pennylane/test_circuit_executor.py b/tests/test_pennylane/test_circuit_executor.py index 599bc05..2dd3065 100644 --- a/tests/test_pennylane/test_circuit_executor.py +++ b/tests/test_pennylane/test_circuit_executor.py @@ -1,7 +1,7 @@ -# test_backends.py +# test_gradients.py # This code is part of cqlib. # -# Copyright (C) 2025 China Telecom Quantum Group. +# Copyright (C) 2026 China Telecom Quantum Group. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE file in the root directory diff --git a/tests/test_pennylane/test_compile.py b/tests/test_pennylane/test_compile.py index 1342899..ba857e3 100644 --- a/tests/test_pennylane/test_compile.py +++ b/tests/test_pennylane/test_compile.py @@ -1,7 +1,7 @@ -# test_backends.py +# test_gradients.py # This code is part of cqlib. # -# Copyright (C) 2025 China Telecom Quantum Group. +# Copyright (C) 2026 China Telecom Quantum Group. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE file in the root directory diff --git a/tests/test_pennylane/test_ext_mapping.py b/tests/test_pennylane/test_ext_mapping.py index 968f6cc..7b12df8 100644 --- a/tests/test_pennylane/test_ext_mapping.py +++ b/tests/test_pennylane/test_ext_mapping.py @@ -1,3 +1,16 @@ +# test_gradients.py +# This code is part of cqlib. +# +# Copyright (C) 2026 China Telecom Quantum Group. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + """Tests for HardwareMapper class in ext_mapping.py.""" import pytest diff --git a/tests/test_pennylane/test_gradients.py b/tests/test_pennylane/test_gradients.py index 2cf67a8..efb6ec1 100644 --- a/tests/test_pennylane/test_gradients.py +++ b/tests/test_pennylane/test_gradients.py @@ -1,7 +1,7 @@ # test_gradients.py # This code is part of cqlib. # -# Copyright (C) 2025 China Telecom Quantum Group. +# Copyright (C) 2026 China Telecom Quantum Group. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE file in the root directory diff --git a/tests/test_pennylane/test_grover.py b/tests/test_pennylane/test_grover.py deleted file mode 100644 index d3c42f1..0000000 --- a/tests/test_pennylane/test_grover.py +++ /dev/null @@ -1,216 +0,0 @@ -import pennylane as qml -from pennylane import numpy as np -import matplotlib.pyplot as plt - - -def create_grover_circuit(num_qubits, target_state, shots=1000): - """Create a generic Grover algorithm circuit for any number of qubits and target state. - - Args: - num_qubits: Number of qubits in the quantum circuit. - target_state: The target state to search for (decimal representation). - shots: Number of measurement shots. - - Returns: - A Grover circuit function that takes iterations as parameter. - """ - # dev = qml.device("default.qubit", wires=num_qubits, shots=shots) - dev = qml.device("cqlib.device", wires=num_qubits, shots=shots, cqlib_backend_name="default") - - def grover_oracle(): - """Oracle that marks the specified target state.""" - target_binary = format(target_state, f"0{num_qubits}b") - - # Apply X gates to qubits that should be 0 in the target state - for i, bit in enumerate(target_binary): - if bit == "0": - qml.PauliX(wires=i) - - # Apply multi-controlled Z gate to flip the phase - if num_qubits == 1: - qml.PauliZ(wires=0) - else: - qml.Hadamard(wires=num_qubits - 1) - qml.ctrl(qml.PauliZ, control=list(range(num_qubits - 1)))(wires=num_qubits - 1) - qml.Hadamard(wires=num_qubits - 1) - - # Undo the X gates - for i, bit in enumerate(target_binary): - if bit == "0": - qml.PauliX(wires=i) - - def diffusion_operator(): - """Diffusion operator for amplitude amplification.""" - # Apply Hadamard gates to all qubits - for wire in range(num_qubits): - qml.Hadamard(wires=wire) - - # Apply X gates to all qubits - for wire in range(num_qubits): - qml.PauliX(wires=wire) - - # Apply multi-controlled Z gate - if num_qubits == 1: - qml.PauliZ(wires=0) - else: - qml.Hadamard(wires=num_qubits - 1) - qml.ctrl(qml.PauliZ, control=list(range(num_qubits - 1)))(wires=num_qubits - 1) - qml.Hadamard(wires=num_qubits - 1) - - # Apply X gates again - for wire in range(num_qubits): - qml.PauliX(wires=wire) - - # Apply Hadamard gates again - for wire in range(num_qubits): - qml.Hadamard(wires=wire) - - @qml.qnode(dev) - def grover_circuit(iterations=1): - """Grover circuit with specified number of iterations. - - Args: - iterations: Number of Grover iterations to perform. - - Returns: - Probability distribution over all computational basis states. - """ - # Initialization: Create uniform superposition - for wire in range(num_qubits): - qml.Hadamard(wires=wire) - - # Grover iterations - for _ in range(iterations): - grover_oracle() # Mark target state - diffusion_operator() # Amplify amplitude - - return qml.probs(wires=range(num_qubits)) - - return grover_circuit - - -def calculate_optimal_iterations(num_qubits, num_solutions=1): - """Calculate optimal number of Grover iterations. - - Args: - num_qubits: Number of qubits in the quantum circuit. - num_solutions: Number of target solutions (default: 1). - - Returns: - Optimal number of Grover iterations. - """ - search_space_size = 2**num_qubits - optimal_iterations = int(np.floor((np.pi / 4) * np.sqrt(search_space_size / num_solutions))) - return optimal_iterations - - -def run_grover_search(num_qubits, target_state, shots=1000, max_iterations=None): - """Run complete Grover search with analysis. - - Args: - num_qubits: Number of qubits in the quantum circuit. - target_state: The target state to search for (decimal representation). - shots: Number of measurement shots. - max_iterations: Maximum number of iterations to test. - - Returns: - Tuple of (best_iteration, best_probability). - - Raises: - ValueError: If target_state is out of range for the given number of qubits. - """ - max_state = 2**num_qubits - 1 - if target_state > max_state: - raise ValueError( - f"Target state {target_state} is out of range for {num_qubits} " - f"qubits (max: {max_state})" - ) - - # Create the circuit - grover_circuit = create_grover_circuit(num_qubits, target_state, shots) - - # Calculate optimal iterations - optimal_iterations = calculate_optimal_iterations(num_qubits) - if max_iterations is not None: - optimal_iterations = min(optimal_iterations, max_iterations) - - print("=== Grover Search Configuration ===") - print(f"Number of qubits: {num_qubits}") - print(f"Search space size: {2**num_qubits}") - print(f"Target state: |{format(target_state, f'0{num_qubits}b')}⟩ (decimal: {target_state})") - print(f"Optimal iterations: {optimal_iterations}") - print(f"Random search probability: {1/(2**num_qubits):.6f}") - print() - - # Execute search with different iteration counts - results = [] - max_test_iterations = min(optimal_iterations + 3, 10) # Test up to 10 iterations max - - print("=== Search Results ===") - for iterations in range(max_test_iterations): - probabilities = grover_circuit(iterations) - target_probability = probabilities[target_state] - results.append(target_probability) - print(f"Iteration {iterations}: Target probability = {target_probability:.6f}") - - # Find best iteration - best_iteration = np.argmax(results) - best_probability = results[best_iteration] - - print(f"\n=== Summary ===") - print(f"Best iteration: {best_iteration}") - print(f"Best probability: {best_probability:.6f}") - print(f"Amplification factor: {best_probability / (1/(2**num_qubits)):.2f}x") - - # Visualize results - plt.figure(figsize=(10, 6)) - plt.plot(range(max_test_iterations), results, "bo-", linewidth=2, markersize=8) - plt.axhline( - y=1 / (2**num_qubits), - color="r", - linestyle="--", - label="Random search probability", - ) - plt.axvline(x=optimal_iterations, color="g", linestyle="--", label="Theoretical optimal") - plt.axvline(x=best_iteration, color="orange", linestyle=":", label="Actual best") - plt.xlabel("Iteration Count") - plt.ylabel("Probability of Finding Target") - plt.title(f'Grover Search: {num_qubits} qubits, target |{format(target_state, f"0{num_qubits}b")}⟩') - plt.legend() - plt.grid(True, alpha=0.3) - plt.show() - - # Show final probability distribution for best iteration - if best_iteration > 0: - final_probabilities = grover_circuit(best_iteration) - print(f"\n=== Final Probability Distribution (Iteration {best_iteration}) ===") - - # Show top 10 most probable states - sorted_indices = np.argsort(final_probabilities)[::-1][:10] - for i, index in enumerate(sorted_indices): - state_label = f"|{format(index, f'0{num_qubits}b')}⟩" - is_target = " ← TARGET" if index == target_state else "" - print(f"{i+1:2d}. {state_label}: {final_probabilities[index]:.6f}{is_target}") - - return best_iteration, best_probability - - -# ====== Usage Examples ====== - -if __name__ == "__main__": - # Example 1: 5 qubits, find state 9 (|01001⟩) - print("Example 1: 5 qubits, target state 9 (|01001⟩)") - run_grover_search(num_qubits=5, target_state=9, shots=5000) - - print("\n" + "=" * 60 + "\n") - - # Example 2: 3 qubits, find state 5 (|101⟩) - print("Example 2: 3 qubits, target state 5 (|101⟩)") - run_grover_search(num_qubits=3, target_state=5, shots=2000) - - print("\n" + "=" * 60 + "\n") - - # Example 3: 4 qubits, find state 12 (|1100⟩) - print("Example 3: 4 qubits, target state 12 (|1100⟩)") - run_grover_search(num_qubits=4, target_state=12, shots=3000) - diff --git a/tests/test_pennylane/test_native_gates.py b/tests/test_pennylane/test_native_gates.py index c141136..7109b12 100644 --- a/tests/test_pennylane/test_native_gates.py +++ b/tests/test_pennylane/test_native_gates.py @@ -1,3 +1,16 @@ +# test_gradients.py +# This code is part of cqlib. +# +# Copyright (C) 2026 China Telecom Quantum Group. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + """Tests for native gate classes in native_gates.py.""" import pytest -- Gitee From 26f80782c6380f7e01e2f5e52b8ccf4e3d8d0993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Thu, 30 Apr 2026 15:50:31 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pennylane_ext/circuit_executor.py | 222 +++++------------- tests/test_pennylane/test_backends.py | 1 + 2 files changed, 66 insertions(+), 157 deletions(-) diff --git a/cqlib_adapter/pennylane_ext/circuit_executor.py b/cqlib_adapter/pennylane_ext/circuit_executor.py index 26b79d0..d3a50d0 100644 --- a/cqlib_adapter/pennylane_ext/circuit_executor.py +++ b/cqlib_adapter/pennylane_ext/circuit_executor.py @@ -28,7 +28,7 @@ import numpy as np import pennylane as qml from pennylane.tape import QuantumScript -from cqlib import TianYanPlatform +from cqlib import TianYanPlatform, Circuit from cqlib.mapping import transpile_qcis from cqlib.simulator import StatevectorSimulator from cqlib.utils import qasm2 @@ -49,16 +49,7 @@ class CircuitExecutor: """ def __init__(self, device_config: Dict[str, Any]) -> None: - """Initializes the circuit executor with the provided device configuration. - - Args: - device_config: A dictionary containing device settings such as 'machine_name', - 'login_key', 'shots', and 'wires'. - - Raises: - ValueError: If a required 'login_key' is missing for a non-default backend. - ConnectionError: If the connection to the Tianyan API fails. - """ + """Initializes the circuit executor with the provided device configuration.""" self.device_config = device_config self.logger = self._setup_logger() self.cqlib_backend: Optional[TianYanPlatform] = None @@ -87,11 +78,7 @@ class CircuitExecutor: self.logger.info("CircuitExecutor initialized with %s backend", self._backend_type.value) def _setup_logger(self) -> logging.Logger: - """Configures and returns a logger instance for execution tracking. - - Returns: - A configured logging.Logger instance. - """ + """Configures and returns a logger instance for execution tracking.""" logger = logging.getLogger(f"CircuitExecutor.{id(self)}") if self.device_config.get('verbose', False): @@ -99,21 +86,13 @@ class CircuitExecutor: handler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) - # Prevent adding multiple handlers if initialized multiple times if not logger.handlers: logger.addHandler(handler) return logger def _determine_backend_type(self) -> BackendType: - """Determines the appropriate backend type based on the device configuration. - - Returns: - The resolved BackendType enumeration. - - Raises: - ValueError: If the specified machine_name is not recognized or supported. - """ + """Determines the appropriate backend type based on the device configuration.""" from .device import CQLibDevice backend_name = self.device_config.get('machine_name', 'default') @@ -128,12 +107,7 @@ class CircuitExecutor: raise ValueError(f"Unknown or unsupported backend: {backend_name}") def _initialize_backend(self) -> None: - """Initializes the connection to the quantum computation backend. - - Raises: - ValueError: If a login key is required but missing. - ConnectionError: If establishing the backend connection fails. - """ + """Initializes the connection to the quantum computation backend.""" if self._backend_type == BackendType.LOCAL_SIMULATOR: self.logger.debug("Using local simulator - no backend connection needed.") return @@ -159,15 +133,7 @@ class CircuitExecutor: raise ConnectionError(f"Backend connection failed: {error}") from error def execute_circuit(self, circuit: QuantumScript) -> Union[List[Any], Any]: - """Executes a quantum circuit and returns the measurement results. - - Args: - circuit: The PennyLane quantum circuit to execute. - - Returns: - A single measurement result if the circuit contains one measurement, - otherwise a list of results corresponding to each measurement. - """ + """Executes a quantum circuit and returns the measurement results.""" self._validate_circuit(circuit) self._execution_count += 1 results = [] @@ -184,14 +150,7 @@ class CircuitExecutor: return results[0] if len(results) == 1 else results def _validate_circuit(self, circuit: QuantumScript) -> None: - """Validates circuit constraints against the current device configuration. - - Args: - circuit: The PennyLane quantum circuit to validate. - - Raises: - ValueError: If exact state vector simulation is requested with finite shots. - """ + """Validates circuit constraints against the current device configuration.""" has_state_measurement = any( isinstance(m, qml.measurements.StateMP) for m in circuit.measurements ) @@ -204,14 +163,7 @@ class CircuitExecutor: ) def _validate_measurement_support(self, measurement: Any) -> None: - """Validates that the active backend supports the requested measurement. - - Args: - measurement: The PennyLane measurement process instance. - - Raises: - ValueError: If the measurement type is unsupported by the backend. - """ + """Validates that the active backend supports the requested measurement.""" supported_measurements = { BackendType.LOCAL_SIMULATOR: { qml.measurements.ProbabilityMP, @@ -241,34 +193,16 @@ class CircuitExecutor: ) def _convert_to_cqlib_format(self, circuit: QuantumScript) -> Tuple[Any, str]: - """Converts a PennyLane circuit into CQLib-compatible parsed objects and QCIS string. - - Args: - circuit: The PennyLane quantum circuit. - - Returns: - A tuple containing the parsed CQLib circuit object and its QCIS string representation. - - Raises: - ValueError: If parsing or compilation fails. - """ + """Converts a PennyLane circuit into CQLib-compatible parsed objects and QCIS string.""" try: - qasm_string = self._build_custom_qasm(circuit) - cqlib_circuit = qasm2.loads(qasm_string) + cqlib_circuit = self._build_cqlib_circuit(circuit) return cqlib_circuit, cqlib_circuit.qcis except Exception as error: self.logger.error("Circuit conversion from PennyLane to CQLib format failed: %s", error) raise ValueError(f"Circuit conversion failed: {error}") from error - def _build_custom_qasm(self, circuit: QuantumScript) -> str: - """Builds an OpenQASM 2.0 string manually to handle custom native gates. - - Args: - circuit: The PennyLane quantum circuit. - - Returns: - A well-formed OpenQASM 2.0 string representing the circuit operations. - """ + def _build_cqlib_circuit(self, circuit: QuantumScript) -> Circuit: + """Directly constructs a CQLib Circuit object from a PennyLane quantum circuit.""" device_wires = self.device_config.get('wires') if isinstance(device_wires, int): num_wires = device_wires @@ -277,67 +211,61 @@ class CircuitExecutor: else: num_wires = max(circuit.wires.labels) + 1 if circuit.wires else 1 - qasm_lines = [ - "OPENQASM 2.0;", - 'include "qelib1.inc";', - f"qreg q[{num_wires}];", - f"creg c[{num_wires}];" - ] + cqlib_cir = Circuit(num_wires) - for op in circuit.operations: + def map_operation(op, target_circuit: Circuit): op_name = op.name wires = op.wires.tolist() params = op.parameters - q_str = ",".join([f"q[{w}]" for w in wires]) if op_name == "X2PGate": - qasm_lines.append(f"x2p {q_str};") + target_circuit.x2p(wires[0]) elif op_name == "X2MGate": - qasm_lines.append(f"x2m {q_str};") + target_circuit.x2m(wires[0]) elif op_name == "Y2PGate": - qasm_lines.append(f"y2p {q_str};") + target_circuit.y2p(wires[0]) elif op_name == "Y2MGate": - qasm_lines.append(f"y2m {q_str};") + target_circuit.y2m(wires[0]) elif op_name == "XY2PGate": - qasm_lines.append(f"xy2p({params[0]}) {q_str};") + target_circuit.xy2p(wires[0], params[0]) elif op_name == "XY2MGate": - qasm_lines.append(f"xy2m({params[0]}) {q_str};") + target_circuit.xy2m(wires[0], params[0]) elif op_name == "PauliX": - qasm_lines.append(f"x {q_str};") + target_circuit.x(wires[0]) elif op_name == "PauliY": - qasm_lines.append(f"y {q_str};") + target_circuit.y(wires[0]) elif op_name == "PauliZ": - qasm_lines.append(f"z {q_str};") + target_circuit.z(wires[0]) elif op_name == "Hadamard": - qasm_lines.append(f"h {q_str};") + target_circuit.h(wires[0]) elif op_name == "RX": - qasm_lines.append(f"rx({params[0]}) {q_str};") + target_circuit.rx(wires[0], params[0]) elif op_name == "RY": - qasm_lines.append(f"ry({params[0]}) {q_str};") + target_circuit.ry(wires[0], params[0]) elif op_name == "RZ": - qasm_lines.append(f"rz({params[0]}) {q_str};") + target_circuit.rz(wires[0], params[0]) elif op_name == "CNOT": - qasm_lines.append(f"cx {q_str};") + target_circuit.cx(wires[0], wires[1]) elif op_name == "CZ": - qasm_lines.append(f"cz {q_str};") + target_circuit.cz(wires[0], wires[1]) elif op_name == "S": - qasm_lines.append(f"s {q_str};") + target_circuit.s(wires[0]) elif op_name == "T": - qasm_lines.append(f"t {q_str};") + target_circuit.t(wires[0]) else: try: - partial_qasm = op.to_openqasm().split('\n') - valid_lines = [ - l for l in partial_qasm - if not l.startswith(('OPENQASM', 'include', 'qreg', 'creg')) and l.strip() - ] - qasm_lines.extend(valid_lines) - except Exception: + decomposed_ops = op.decomposition() + for d_op in decomposed_ops: + map_operation(d_op, target_circuit) + except Exception as e: self.logger.warning( - f"Operation {op_name} not natively mapped and QASM fallback failed." + f"Operation {op_name} not natively mapped and decomposition failed: {e}" ) - return "\n".join(qasm_lines) + for op in circuit.operations: + map_operation(op, cqlib_cir) + + return cqlib_cir def _execute_measurement(self, measurement: Any, raw_result: Dict[str, Any]) -> Any: """Dispatches the raw result to the appropriate measurement processing implementation.""" @@ -378,12 +306,11 @@ class CircuitExecutor: @_execute_measurement_impl.register def _(self, measurement: qml.measurements.ExpectationMP, raw_result: Dict[str, Any]) -> float: """Calculates the expectation value for Pauli-Z observables from raw probabilities.""" - if 'probabilities' not in raw_result: - raise ValueError("raw_result must contain 'probabilities' key") - - probabilities = raw_result['probabilities'] - if not isinstance(probabilities, dict): - raise ValueError("probabilities must be a dictionary") + # Process probabilities via _extract_probabilities to handle endianness reversal. + probabilities = self._extract_probabilities(raw_result) + + if not probabilities or not isinstance(probabilities, dict): + raise ValueError("Execution results must contain a valid 'probabilities' dictionary") pauli_indices = list(measurement.obs.wires.labels) expectation = 0.0 @@ -409,12 +336,24 @@ class CircuitExecutor: statevector = raw_result.get('statevector') if statevector is None: raise ValueError("Statevector not found in execution results") - return statevector + + # Ensure the statevector is returned as a dense 1D Numpy array for PennyLane compatibility. + if isinstance(statevector, dict): + num_qubits = len(next(iter(statevector.keys()))) + dense_state = np.zeros(2 ** num_qubits, dtype=complex) + for bitstring, amplitude in statevector.items(): + # Reverse the dictionary keys to align with the expected endianness format. + index = int(bitstring[::-1], 2) + dense_state[index] = amplitude + return dense_state + + return np.array(statevector) @_execute_measurement_impl.register def _(self, measurement: qml.measurements.SampleMP, raw_result: Dict[str, Any]) -> np.ndarray: """Extracts measurement samples from execution results, supporting partial measurement.""" - samples = raw_result.get('samples') + # Extract samples, ensuring endianness reversal and format conversion are applied. + samples = self._extract_samples(raw_result) if samples is None: raise ValueError("No measurement samples found in execution results") @@ -446,7 +385,7 @@ class CircuitExecutor: return { 'probabilities': simulator.probs(), 'samples': simulator.sample(is_raw_data=True), - 'statevector': dict(reversed(simulator.statevector().items())) + 'statevector': simulator.statevector() } def _execute_tianyan_simulator(self, cqlib_circuit: Any, cqlib_qcis: str) -> Dict[str, Any]: @@ -503,22 +442,13 @@ class CircuitExecutor: def _extract_samples(self, raw_result: Dict[str, Any]) -> Any: """Extracts and converts samples to the format expected by PennyLane.""" samples = raw_result.get('samples') - if samples is not None and self._backend_type == BackendType.LOCAL_SIMULATOR: + # Uniformly convert sample formats for all backends to handle bit reversal via little_endian. + if samples is not None: return samples_to_pennylane_format(samples, self.device_config['wires']) return samples def _format_probabilities(self, probabilities: Dict[str, float]) -> np.ndarray: - """Converts a probability dictionary into a dense NumPy array distribution. - - Args: - probabilities: A dictionary mapping binary bitstrings to float probabilities. - - Returns: - A 1D numpy array containing the dense probability distribution. - - Raises: - ValueError: If the input probability dictionary is empty. - """ + """Converts a probability dictionary into a dense NumPy array distribution.""" if not probabilities: raise ValueError("No probability distribution found in execution results") @@ -535,16 +465,7 @@ class CircuitExecutor: def decimal_to_binary_array( decimal_value: int, num_bits: int, little_endian: bool = True ) -> np.ndarray: - """Converts a decimal integer into a binary numpy array. - - Args: - decimal_value: The integer value to convert. - num_bits: The fixed width of the resulting binary array. - little_endian: If True, reverses the bit order (LSB first). - - Returns: - A 1D numpy array of bits. - """ + """Converts a decimal integer into a binary numpy array.""" binary_string = np.binary_repr(int(decimal_value), width=num_bits) bits = np.array([int(bit) for bit in binary_string]) return bits[::-1] if little_endian else bits @@ -556,20 +477,7 @@ def samples_to_pennylane_format( measured_qubits: Optional[List[int]] = None, little_endian: bool = True ) -> np.ndarray: - """Converts raw sample data into a PennyLane compatible binary matrix. - - Args: - samples: A list or 1D array of decimal sample values. - num_qubits: Total number of qubits in the circuit. - measured_qubits: Explicit list of qubit indices that were measured. - little_endian: If True, enforces little-endian bit ordering. - - Returns: - A 2D numpy array of shape (n_shots, num_bits) containing binary samples. - - Raises: - ValueError: If sample dimensions cannot be inferred from inputs. - """ + """Converts raw sample data into a PennyLane compatible binary matrix.""" samples_array = np.asarray(samples) if measured_qubits is not None: diff --git a/tests/test_pennylane/test_backends.py b/tests/test_pennylane/test_backends.py index 87b2f61..8363801 100644 --- a/tests/test_pennylane/test_backends.py +++ b/tests/test_pennylane/test_backends.py @@ -19,6 +19,7 @@ from pennylane.devices import Device # Configuration parameters TOKEN = os.getenv("CQLIB_TOKEN", None) +TOKEN = "ZtQYpi6GVW24lrSOpauj16mRCAFrWN/3Et4xJjhn7dg=" SHOTS = 500 WIRES = 2 INITIAL_PARAMS = np.array([0.5, 0.8]) -- Gitee From 1a2049e4985b6af7808ce346a7ca998b6c508810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Wed, 6 May 2026 10:43:45 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_pennylane/test_backends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pennylane/test_backends.py b/tests/test_pennylane/test_backends.py index 8363801..2cda799 100644 --- a/tests/test_pennylane/test_backends.py +++ b/tests/test_pennylane/test_backends.py @@ -19,7 +19,7 @@ from pennylane.devices import Device # Configuration parameters TOKEN = os.getenv("CQLIB_TOKEN", None) -TOKEN = "ZtQYpi6GVW24lrSOpauj16mRCAFrWN/3Et4xJjhn7dg=" + SHOTS = 500 WIRES = 2 INITIAL_PARAMS = np.array([0.5, 0.8]) -- Gitee