Skip to content

Commit 20f872a

Browse files
Add files via upload
1 parent b5a0560 commit 20f872a

1 file changed

Lines changed: 250 additions & 0 deletions

File tree

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
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

Comments
 (0)