|
| 1 | + |
| 2 | +import numpy as np |
| 3 | +from bruun512_phase_cone_transport import ( |
| 4 | + N, C, S_SPECIAL, IDX, |
| 5 | + annihilate_common_mode, binomial_transport_inplace, |
| 6 | + bruun_transport_node_inplace, pack_residue_to_rfft |
| 7 | +) |
| 8 | + |
| 9 | + |
| 10 | +def split_pm(x): |
| 11 | + x = np.asarray(x, dtype=np.float64) |
| 12 | + return np.where(x >= 0.0, x, 0.0), np.where(x < 0.0, np.abs(x), 0.0) |
| 13 | + |
| 14 | + |
| 15 | +def add_routed(dstp, dstn, srcp, srcn, weight, flip=False): |
| 16 | + if flip: |
| 17 | + dstp += weight * srcn |
| 18 | + dstn += weight * srcp |
| 19 | + else: |
| 20 | + dstp += weight * srcp |
| 21 | + dstn += weight * srcn |
| 22 | + |
| 23 | + |
| 24 | +def unpack_rfft_to_residue(X): |
| 25 | + X = np.asarray(X, dtype=np.complex128) |
| 26 | + if X.shape[0] != 257: |
| 27 | + raise ValueError("expected rfft length 257") |
| 28 | + |
| 29 | + work = np.empty(512, dtype=np.float64) |
| 30 | + work[0] = 0.5 * (X[0].real + X[256].real) |
| 31 | + work[1] = 0.5 * (X[0].real - X[256].real) |
| 32 | + |
| 33 | + for m in range(1, 256): |
| 34 | + k = IDX[m] |
| 35 | + work[2*m] = X[k].real |
| 36 | + work[2*m + 1] = -X[k].imag |
| 37 | + |
| 38 | + return work |
| 39 | + |
| 40 | + |
| 41 | +def positive_inverse_binomial_inplace(wp, wn, left, right): |
| 42 | + up = wp[left].copy() |
| 43 | + un = wn[left].copy() |
| 44 | + vp = wp[right].copy() |
| 45 | + vn = wn[right].copy() |
| 46 | + |
| 47 | + ap = np.zeros_like(up) |
| 48 | + an = np.zeros_like(un) |
| 49 | + bp = np.zeros_like(up) |
| 50 | + bn = np.zeros_like(un) |
| 51 | + |
| 52 | + add_routed(ap, an, up, un, 0.5, False) |
| 53 | + add_routed(ap, an, vp, vn, 0.5, False) |
| 54 | + |
| 55 | + add_routed(bp, bn, up, un, 0.5, False) |
| 56 | + add_routed(bp, bn, vp, vn, 0.5, True) |
| 57 | + |
| 58 | + wp[left] = ap |
| 59 | + wn[left] = an |
| 60 | + wp[right] = bp |
| 61 | + wn[right] = bn |
| 62 | + |
| 63 | + |
| 64 | +def positive_inverse_bruun_node_inplace(wp, wn, p, q, c, s): |
| 65 | + Y0p = wp[p:p + q].copy() |
| 66 | + Y0n = wn[p:p + q].copy() |
| 67 | + Y1p = wp[p + q:p + 2*q].copy() |
| 68 | + Y1n = wn[p + q:p + 2*q].copy() |
| 69 | + Y2p = wp[p + 2*q:p + 3*q].copy() |
| 70 | + Y2n = wn[p + 2*q:p + 3*q].copy() |
| 71 | + Y3p = wp[p + 3*q:p + 4*q].copy() |
| 72 | + Y3n = wn[p + 3*q:p + 4*q].copy() |
| 73 | + |
| 74 | + A0p = np.zeros(q, dtype=np.float64) |
| 75 | + A0n = np.zeros(q, dtype=np.float64) |
| 76 | + Rp = np.zeros(q, dtype=np.float64) |
| 77 | + Rn = np.zeros(q, dtype=np.float64) |
| 78 | + Ip = np.zeros(q, dtype=np.float64) |
| 79 | + In = np.zeros(q, dtype=np.float64) |
| 80 | + A1p = np.zeros(q, dtype=np.float64) |
| 81 | + A1n = np.zeros(q, dtype=np.float64) |
| 82 | + |
| 83 | + add_routed(A0p, A0n, Y0p, Y0n, 0.5, False) |
| 84 | + add_routed(A0p, A0n, Y2p, Y2n, 0.5, False) |
| 85 | + |
| 86 | + add_routed(Rp, Rn, Y0p, Y0n, 0.5, False) |
| 87 | + add_routed(Rp, Rn, Y2p, Y2n, 0.5, True) |
| 88 | + |
| 89 | + add_routed(Ip, In, Y1p, Y1n, 0.5, False) |
| 90 | + add_routed(Ip, In, Y3p, Y3n, 0.5, False) |
| 91 | + |
| 92 | + add_routed(A1p, A1n, Y1p, Y1n, 0.5, False) |
| 93 | + add_routed(A1p, A1n, Y3p, Y3n, 0.5, True) |
| 94 | + |
| 95 | + B0p = np.zeros(q, dtype=np.float64) |
| 96 | + B0n = np.zeros(q, dtype=np.float64) |
| 97 | + B1p = np.zeros(q, dtype=np.float64) |
| 98 | + B1n = np.zeros(q, dtype=np.float64) |
| 99 | + |
| 100 | + add_routed(B0p, B0n, Rp, Rn, c, False) |
| 101 | + add_routed(B0p, B0n, Ip, In, s, False) |
| 102 | + |
| 103 | + add_routed(B1p, B1n, Rp, Rn, s, True) |
| 104 | + add_routed(B1p, B1n, Ip, In, c, False) |
| 105 | + |
| 106 | + wp[p:p + q] = A0p |
| 107 | + wn[p:p + q] = A0n |
| 108 | + wp[p + q:p + 2*q] = B0p |
| 109 | + wn[p + q:p + 2*q] = B0n |
| 110 | + wp[p + 2*q:p + 3*q] = A1p |
| 111 | + wn[p + 2*q:p + 3*q] = A1n |
| 112 | + wp[p + 3*q:p + 4*q] = B1p |
| 113 | + wn[p + 3*q:p + 4*q] = B1n |
| 114 | + |
| 115 | + |
| 116 | +def positive_inverse_residue_to_real(work, annihilate_stage=True, return_trace=False): |
| 117 | + work = np.asarray(work, dtype=np.float64) |
| 118 | + if work.shape[0] != 512: |
| 119 | + raise ValueError("expected residue length 512") |
| 120 | + |
| 121 | + wp, wn = split_pm(work) |
| 122 | + trace = [] |
| 123 | + |
| 124 | + def canonicalize(tag): |
| 125 | + if annihilate_stage: |
| 126 | + annihilate_common_mode(wp, wn) |
| 127 | + if return_trace: |
| 128 | + projected = wp - wn |
| 129 | + sheet_mass = float(np.sum(wp) + np.sum(wn)) |
| 130 | + projected_l1 = float(np.sum(np.abs(projected))) |
| 131 | + trace.append({ |
| 132 | + "tag": tag, |
| 133 | + "sheet_mass": sheet_mass, |
| 134 | + "projected_l1": projected_l1, |
| 135 | + "cancellation_ratio": sheet_mass / max(projected_l1, 1e-300), |
| 136 | + }) |
| 137 | + |
| 138 | + canonicalize("residue") |
| 139 | + |
| 140 | + for stage in range(7, 0, -1): |
| 141 | + q = (N // 4) >> stage |
| 142 | + |
| 143 | + for m in range(1, 2**stage): |
| 144 | + p = 4 * q * m |
| 145 | + c = C[m] |
| 146 | + s = S_SPECIAL[1] if m == 1 else C[m ^ 1] |
| 147 | + positive_inverse_bruun_node_inplace(wp, wn, p, q, c, s) |
| 148 | + |
| 149 | + prefix = N >> stage |
| 150 | + half = prefix >> 1 |
| 151 | + positive_inverse_binomial_inplace(wp, wn, slice(0, half), slice(half, prefix)) |
| 152 | + |
| 153 | + canonicalize(f"undo S{stage}") |
| 154 | + |
| 155 | + positive_inverse_binomial_inplace(wp, wn, slice(0, 256), slice(256, 512)) |
| 156 | + canonicalize("undo S0") |
| 157 | + |
| 158 | + x = wp - wn |
| 159 | + if return_trace: |
| 160 | + return x, wp, wn, trace |
| 161 | + |
| 162 | + return x |
| 163 | + |
| 164 | + |
| 165 | +def positive_irfft(X, annihilate_stage=True, return_trace=False): |
| 166 | + work = unpack_rfft_to_residue(X) |
| 167 | + return positive_inverse_residue_to_real( |
| 168 | + work, |
| 169 | + annihilate_stage=annihilate_stage, |
| 170 | + return_trace=return_trace, |
| 171 | + ) |
| 172 | + |
| 173 | + |
| 174 | +def phase_cone_stage_derived_rfft(input_data, stage_scale=0.5): |
| 175 | + x = np.asarray(input_data, dtype=np.float64) |
| 176 | + if x.shape[0] != 512: |
| 177 | + raise ValueError("expected length 512") |
| 178 | + |
| 179 | + xp, xn = split_pm(x) |
| 180 | + wp = np.zeros(512, dtype=np.float64) |
| 181 | + wn = np.zeros(512, dtype=np.float64) |
| 182 | + scale_exp = 0 |
| 183 | + |
| 184 | + wp[:256], wn[:256] = xp[:256] + xp[256:], xn[:256] + xn[256:] |
| 185 | + wp[256:], wn[256:] = xp[:256] + xn[256:], xn[:256] + xp[256:] |
| 186 | + |
| 187 | + annihilate_common_mode(wp, wn) |
| 188 | + wp *= stage_scale |
| 189 | + wn *= stage_scale |
| 190 | + scale_exp += 1 |
| 191 | + |
| 192 | + for stage in range(1, 8): |
| 193 | + prefix = N >> stage |
| 194 | + half = prefix >> 1 |
| 195 | + |
| 196 | + binomial_transport_inplace( |
| 197 | + wp, |
| 198 | + wn, |
| 199 | + slice(0, half), |
| 200 | + slice(half, prefix), |
| 201 | + slice(0, half), |
| 202 | + slice(half, prefix), |
| 203 | + ) |
| 204 | + |
| 205 | + q = (N // 4) >> stage |
| 206 | + for m in range(1, 2**stage): |
| 207 | + p = 4 * q * m |
| 208 | + c = C[m] |
| 209 | + s = S_SPECIAL[1] if m == 1 else C[m ^ 1] |
| 210 | + bruun_transport_node_inplace(wp, wn, p, q, c, s) |
| 211 | + |
| 212 | + annihilate_common_mode(wp, wn) |
| 213 | + wp *= stage_scale |
| 214 | + wn *= stage_scale |
| 215 | + scale_exp += 1 |
| 216 | + |
| 217 | + work = (wp - wn) * (stage_scale ** (-scale_exp)) |
| 218 | + return pack_residue_to_rfft(work) |
| 219 | + |
| 220 | + |
| 221 | +def run_roundtrip_tests(): |
| 222 | + cases = { |
| 223 | + "random_positive_42": np.random.default_rng(42).random(512), |
| 224 | + "normal_43": np.random.default_rng(43).normal(size=512), |
| 225 | + "normal_44": np.random.default_rng(44).normal(size=512), |
| 226 | + "ramp": np.arange(512, dtype=np.float64), |
| 227 | + "alternating": np.where(np.arange(512) % 2 == 0, 1.0, -1.0), |
| 228 | + "impulse_0": np.eye(512)[0], |
| 229 | + "ones": np.ones(512), |
| 230 | + "zeros": np.zeros(512), |
| 231 | + } |
| 232 | + |
| 233 | + rows = [] |
| 234 | + for name, x in cases.items(): |
| 235 | + X = phase_cone_stage_derived_rfft(x) |
| 236 | + xr, wp, wn, trace = positive_irfft(X, annihilate_stage=True, return_trace=True) |
| 237 | + rows.append({ |
| 238 | + "case": name, |
| 239 | + "positive_forward_inverse_max_abs": float(np.max(np.abs(xr - x))), |
| 240 | + "positive_forward_inverse_l2": float(np.linalg.norm(xr - x)), |
| 241 | + "final_sheet_mass": float(np.sum(wp) + np.sum(wn)), |
| 242 | + "final_cancellation_ratio": float((np.sum(wp) + np.sum(wn)) / max(np.sum(np.abs(wp - wn)), 1e-300)), |
| 243 | + }) |
| 244 | + |
| 245 | + return rows |
| 246 | + |
| 247 | + |
| 248 | +if __name__ == "__main__": |
| 249 | + for row in run_roundtrip_tests(): |
| 250 | + print(row) |
0 commit comments