| """Public exports and compatibility shims for qhybrid_kernels.""" |
|
|
| import json |
| import math |
|
|
| import numpy as np |
|
|
| from .conversions import ( |
| complex_matrix_to_ri, |
| complex_statevector_to_ri, |
| kraus_1q_to_ri, |
| ri_to_complex_matrix, |
| ri_to_complex_statevector, |
| ) |
| from .noise import ( |
| apply_kraus_1q_density_matrix, |
| apply_pauli_channel_statevector, |
| apply_correlated_pauli_noise_statevector, |
| apply_cnot_error_statevector, |
| expectation_value_pauli_string_py, |
| ) |
| from .qiskit_adapter import ( |
| kraus_1q_from_qiskit, |
| apply_qiskit_kraus_1q_to_density_matrix, |
| ) |
| from .circuit import ( |
| GateConverter, |
| QHYBRID_PAYLOAD_VERSION, |
| QhybridCircuitPayload, |
| QhybridGatePayload, |
| get_supported_qiskit_gates, |
| qiskit_to_qhybrid_json, |
| register_gate_converter, |
| ) |
|
|
| try: |
| from .rust_kernels import ( |
| execute_quantum_circuit, |
| apply_correlated_pauli_noise_statevector as _apply_correlated_pauli_noise_statevector, |
| apply_cnot_error_statevector as _apply_cnot_error_statevector, |
| expectation_value_pauli_string_py as _expectation_value_pauli_string_py, |
| QuantumCircuit, |
| ) |
| apply_correlated_pauli_noise_statevector = _apply_correlated_pauli_noise_statevector |
| apply_cnot_error_statevector = _apply_cnot_error_statevector |
| expectation_value_pauli_string_py = _expectation_value_pauli_string_py |
| except ImportError: |
| QuantumCircuit = None |
|
|
| def _normalise_gate_name(gate_type) -> str: |
| if isinstance(gate_type, str): |
| return gate_type |
| if not isinstance(gate_type, dict) or len(gate_type) != 1: |
| raise TypeError("Invalid qhybrid gate payload") |
| return next(iter(gate_type.keys())) |
|
|
| def _normalise_gate_params(gate_type) -> list[float]: |
| if isinstance(gate_type, str): |
| return [] |
| if not isinstance(gate_type, dict) or len(gate_type) != 1: |
| raise TypeError("Invalid qhybrid gate payload") |
| params = next(iter(gate_type.values())) |
| if isinstance(params, (list, tuple)): |
| return [float(v) for v in params] |
| return [float(params)] |
|
|
| def _coerce_gate_matrix(gate_name: str, params: list[float]) -> np.ndarray: |
| if gate_name == "I": |
| return np.array([[1.0, 0.0], [0.0, 1.0]], dtype=np.complex128) |
| if gate_name == "X": |
| return np.array([[0.0, 1.0], [1.0, 0.0]], dtype=np.complex128) |
| if gate_name == "Y": |
| return np.array([[0.0, -1j], [1j, 0.0]], dtype=np.complex128) |
| if gate_name == "Z": |
| return np.array([[1.0, 0.0], [0.0, -1.0]], dtype=np.complex128) |
| if gate_name == "H": |
| inv_root2 = 1.0 / math.sqrt(2.0) |
| return np.array( |
| [[inv_root2, inv_root2], [inv_root2, -inv_root2]], |
| dtype=np.complex128, |
| ) |
| if gate_name == "S": |
| return np.array([[1.0, 0.0], [0.0, 1j]], dtype=np.complex128) |
| if gate_name == "Sdg": |
| return np.array([[1.0, 0.0], [0.0, -1j]], dtype=np.complex128) |
| if gate_name == "T": |
| return np.array([[1.0, 0.0], [0.0, np.exp(1j * np.pi / 4)]], dtype=np.complex128) |
| if gate_name == "Tdg": |
| return np.array( |
| [[1.0, 0.0], [0.0, np.exp(-1j * np.pi / 4)]], |
| dtype=np.complex128, |
| ) |
| if gate_name == "RX": |
| theta = float(params[0]) |
| c = math.cos(theta / 2.0) |
| s = math.sin(theta / 2.0) |
| return np.array([[c, -1j * s], [-1j * s, c]], dtype=np.complex128) |
| if gate_name == "RY": |
| theta = float(params[0]) |
| c = math.cos(theta / 2.0) |
| s = math.sin(theta / 2.0) |
| return np.array([[c, -s], [s, c]], dtype=np.complex128) |
| if gate_name in {"RZ", "P"}: |
| theta = float(params[0]) |
| return np.array( |
| [[np.exp(-1j * theta / 2.0), 0.0], [0.0, np.exp(1j * theta / 2.0)]], |
| dtype=np.complex128, |
| ) |
| if gate_name == "U3": |
| if len(params) != 3: |
| raise ValueError("U3 gate expects 3 parameters") |
| theta, phi, lam = params |
| c = math.cos(theta / 2.0) |
| s = math.sin(theta / 2.0) |
| return np.array( |
| [ |
| [c, -np.exp(1j * lam) * s], |
| [np.exp(1j * phi) * s, np.exp(1j * (phi + lam)) * c], |
| ], |
| dtype=np.complex128, |
| ) |
| raise ValueError(f"Unsupported qhybrid gate: {gate_name}") |
|
|
| def _apply_single_qubit(state: np.ndarray, matrix: np.ndarray, qubit: int) -> np.ndarray: |
| dim = state.shape[0] |
| for idx in range(dim): |
| if (idx >> qubit) & 1: |
| continue |
| pair_idx = idx | (1 << qubit) |
| a = state[idx] |
| b = state[pair_idx] |
| state[idx] = matrix[0, 0] * a + matrix[0, 1] * b |
| state[pair_idx] = matrix[1, 0] * a + matrix[1, 1] * b |
| return state |
|
|
| def _apply_cx(state: np.ndarray, control: int, target: int) -> np.ndarray: |
| for idx in range(state.shape[0]): |
| if ((idx >> control) & 1) == 1 and ((idx >> target) & 1) == 0: |
| partner = idx | (1 << target) |
| state[idx], state[partner] = state[partner], state[idx] |
| return state |
|
|
| def execute_quantum_circuit(circuit_json: str) -> np.ndarray: |
| payload = json.loads(circuit_json) |
| schema_version = payload.get("schema_version") |
| if schema_version is None: |
| raise ValueError("Missing circuit schema_version") |
| if schema_version != QHYBRID_PAYLOAD_VERSION: |
| raise ValueError(f"Unsupported circuit schema_version: {schema_version}") |
|
|
| n_qubits = int(payload["n_qubits"]) |
| dim = 1 << n_qubits |
| state = np.zeros(dim, dtype=np.complex128) |
| state[0] = 1.0 + 0.0j |
|
|
| for gate in payload.get("gates", []): |
| gate_name = _normalise_gate_name(gate["gate_type"]) |
| gate_params = _normalise_gate_params(gate["gate_type"]) |
| qubits = list(gate["qubits"]) |
|
|
| if gate_name in {"CX", "CNOT"}: |
| state = _apply_cx(state, qubits[0], qubits[1]) |
| elif gate_name == "CY": |
| for idx in range(state.shape[0]): |
| if ((idx >> qubits[0]) & 1) == 1 and ((idx >> qubits[1]) & 1) == 0: |
| partner = idx | (1 << qubits[1]) |
| y_matrix = _coerce_gate_matrix("Y", []) |
| a = state[idx] |
| b = state[partner] |
| state[idx] = y_matrix[0, 0] * a + y_matrix[0, 1] * b |
| state[partner] = y_matrix[1, 0] * a + y_matrix[1, 1] * b |
| elif gate_name == "CZ": |
| for idx in range(state.shape[0]): |
| if ((idx >> qubits[0]) & 1) == 1 and ((idx >> qubits[1]) & 1) == 1: |
| state[idx] = -state[idx] |
| |
| else: |
| matrix = _coerce_gate_matrix(gate_name, gate_params) |
| state = _apply_single_qubit(state, matrix, qubits[0]) |
|
|
| return complex_statevector_to_ri(state) |
|
|
| __all__ = [ |
| "complex_statevector_to_ri", |
| "ri_to_complex_statevector", |
| "complex_matrix_to_ri", |
| "ri_to_complex_matrix", |
| "kraus_1q_to_ri", |
| "apply_pauli_channel_statevector", |
| "apply_kraus_1q_density_matrix", |
| "apply_correlated_pauli_noise_statevector", |
| "apply_cnot_error_statevector", |
| "expectation_value_pauli_string_py", |
| "kraus_1q_from_qiskit", |
| "apply_qiskit_kraus_1q_to_density_matrix", |
| "GateConverter", |
| "QHYBRID_PAYLOAD_VERSION", |
| "QhybridCircuitPayload", |
| "QhybridGatePayload", |
| "get_supported_qiskit_gates", |
| "execute_quantum_circuit", |
| "register_gate_converter", |
| ] |
|
|
|
|
| def _patch_qiskit_kraus_compat() -> None: |
| """Patch qiskit Kraus constructor to accept NumPy Kraus stacks. |
| |
| Newer Qiskit versions are stricter about Kraus input shape and may reject |
| a 3-D NumPy array directly. The project tests still use this legacy input |
| form in some places, so we add a small compatibility shim at import time. |
| """ |
|
|
| try: |
| from qiskit.quantum_info import Kraus |
| except Exception: |
| return |
|
|
| if getattr(Kraus, "_qhybrid_kernels_numpy_compat", False): |
| return |
|
|
| _original_init = Kraus.__init__ |
|
|
| def _compatible_init(self, data, *args, **kwargs): |
| if ( |
| isinstance(data, np.ndarray) |
| and data.ndim == 3 |
| and data.shape[1:] == (2, 2) |
| ): |
| data = [np.asarray(data[idx], dtype=np.complex128) for idx in range(data.shape[0])] |
| return _original_init(self, data, *args, **kwargs) |
|
|
| Kraus.__init__ = _compatible_init |
| Kraus._qhybrid_kernels_numpy_compat = True |
|
|
|
|
| _patch_qiskit_kraus_compat() |
|
|
|
|