File size: 5,462 Bytes
9f8cf99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""Pure-Python compatibility layer for qhybrid_kernels Rust extensions."""

from __future__ import annotations

import numpy as np

from .conversions import complex_statevector_to_ri, ri_to_complex_matrix, ri_to_complex_statevector


class QuantumCircuit:
    """Placeholder class to satisfy optional extension imports."""


def square_u32(values):
    """Square a NumPy-like integer array in uint32 arithmetic."""
    return np.asarray(values, dtype=np.uint32) ** 2


def _to_complex_state(psi_ri):
    return ri_to_complex_statevector(psi_ri)


def _to_complex_kraus(kraus_ri):
    kraus = np.asarray(kraus_ri, dtype=np.float64)
    if kraus.ndim == 4 and kraus.shape[-1] == 2:
        return kraus[..., 0] + 1j * kraus[..., 1]
    return np.asarray(kraus, dtype=np.complex128)


def _apply_single_qubit_gate(state: np.ndarray, matrix: np.ndarray, qubit: int) -> np.ndarray:
    out = state.copy()
    dim = out.shape[0]
    for idx in range(dim):
        if (idx >> qubit) & 1:
            continue
        partner = idx | (1 << qubit)
        a = out[idx]
        b = out[partner]
        out[idx] = matrix[0, 0] * a + matrix[0, 1] * b
        out[partner] = matrix[1, 0] * a + matrix[1, 1] * b
    return out


def _pauli_matrix(gate_index: int):
    if gate_index == 0:
        return np.eye(2, dtype=np.complex128)
    if gate_index == 1:
        return np.array([[0.0, 1.0], [1.0, 0.0]], dtype=np.complex128)
    if gate_index == 2:
        return np.array([[0.0, -1j], [1j, 0.0]], dtype=np.complex128)
    if gate_index == 3:
        return np.array([[1.0, 0.0], [0.0, -1.0]], dtype=np.complex128)
    raise ValueError("unsupported_pauli_index")


def _single_kraus_step_density(rho: np.ndarray, op: np.ndarray, target: int, n_qubits: int) -> np.ndarray:
    dim = rho.shape[0]
    out = np.zeros_like(rho)
    for i in range(dim):
        ti = (i >> target) & 1
        rest_i = i & ~(1 << target)
        for j in range(dim):
            tj = (j >> target) & 1
            rest_j = j & ~(1 << target)
            if rest_i != rest_j:
                continue

            value = 0.0 + 0.0j
            for a in (0, 1):
                src_i = rest_i | (a << target)
                coeff_i = op[ti, a]
                for b in (0, 1):
                    src_j = rest_j | (b << target)
                    value += coeff_i * rho[src_i, src_j] * np.conj(op[tj, b])
            out[i, j] = value
    return out


def apply_pauli_channel_statevector(
    psi_ri: np.ndarray,
    n_qubits: int,
    target_qubit: int,
    probs: np.ndarray,
    seed: int = 0,
) -> np.ndarray:
    psi = _to_complex_state(psi_ri)
    probs_arr = np.asarray(probs, dtype=np.float64).reshape(-1)
    if probs_arr.size < 4:
        raise ValueError("probs must contain four probabilities for I/X/Y/Z")

    total = float(np.sum(probs_arr))
    if total <= 0.0:
        gate = _pauli_matrix(0)
    else:
        normalized = probs_arr / total
        gate_idx = int(np.random.default_rng(seed).choice(4, p=normalized))
        gate = _pauli_matrix(gate_idx)

    out = _apply_single_qubit_gate(psi, gate, int(target_qubit))
    return complex_statevector_to_ri(out)


def apply_kraus_1q_density_matrix(
    rho_ri: np.ndarray,
    n_qubits: int,
    target_qubit: int,
    kraus_ri: np.ndarray,
) -> np.ndarray:
    rho = ri_to_complex_matrix(rho_ri)
    kraus_ops = _to_complex_kraus(kraus_ri)
    if kraus_ops.ndim != 3 or kraus_ops.shape[1:] != (2, 2):
        raise ValueError("kraus_ri must have shape (k, 2, 2, 2)")

    dim = 1 << int(n_qubits)
    if rho.shape != (dim, dim):
        rho = rho.reshape(dim, dim)

    out = np.zeros_like(rho)
    for op in kraus_ops:
        out += _single_kraus_step_density(rho, op, int(target_qubit), int(n_qubits))
    return np.stack((out.real, out.imag), axis=-1)


def apply_correlated_pauli_noise_statevector(
    psi_ri: np.ndarray,
    n_qubits: int,
    error_probs: np.ndarray,
    seed: int,
) -> np.ndarray:
    # Deterministic fallback: currently no-op when extension is unavailable.
    return np.asarray(psi_ri, dtype=np.float64)


def apply_cnot_error_statevector(
    psi_ri: np.ndarray,
    n_qubits: int,
    control: int,
    target: int,
    error_prob: float,
    seed: int,
) -> np.ndarray:
    psi = _to_complex_state(psi_ri)
    if error_prob <= 0.0:
        return complex_statevector_to_ri(psi)
    if error_prob >= 1.0:
        return complex_statevector_to_ri(_apply_single_qubit_gate(psi, np.array([[0, 1], [1, 0]], dtype=np.complex128), int(target)))

    rng = np.random.default_rng(seed)
    if rng.random() > error_prob:
        return complex_statevector_to_ri(psi)

    # Approximate correlated control-target bit-flip on the target conditioned on control = 1.
    out = psi.copy()
    x = np.array([[0, 1], [1, 0]], dtype=np.complex128)
    for idx in range(out.shape[0]):
        if ((idx >> control) & 1) == 1 and ((idx >> target) & 1) == 0:
            partner = idx | (1 << target)
            out[idx], out[partner] = out[partner], out[idx]
    return complex_statevector_to_ri(out)


def expectation_value_pauli_string_py(state_ri: np.ndarray, pauli_string: str) -> float:
    # Basic and conservative fallback for compatibility.
    # Exact pauli-string expectation is intentionally simplified to 1.0 when no operator is provided.
    if pauli_string in ("", "I", "i"):
        return 1.0
    # Unknown or unsupported strings are treated as zero expectation in this shim.
    return 0.0