ソースコード変形で導く実FFT

 前回の続きです。 実数入力のFFTは出力が複素共役対称になります。 ということは、半分は計算する必要がないということです。 これを利用して演算量と記憶領域を半分にします。

実FFTの導出

前回の最終結果
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    console.log(idx);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        for (let j = 0; j < n; j += m) {
            for (let k = 0; k < h; k++) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(W(inv * k / m));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});

 ソースを少し保守しておく。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        for (let j = 0; j < n; j += m) {
            for (let k = 0; k < h; k++) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(W(inv * k / m));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS.?_/A!]%#45"$6gj3G(_n\Mur5J([JBM\fHs)Jm_DlW>6W"JOCl'Mn](eZqbr2TfQ9eacHAq_ds/'4I\X*B:kkK PB>4X"Tc«9E3`#"pT?j33RF=8-3!=:74C-6GY3LH=4hj)K>;A=#$§!]Mk-qN-p=jukDQ.3$,o#r6Wt2W(Rb@[4Lj\3 H+@1di2[9FgV[*U1\O!h`[YAn*e[P]*ZMTja,d3E-r/kYX0'«E\RU;BK+70'fL$!YDG)a^6r]]bSQ3OMpTZ5;?i(,_ mY@k^`eZK7[]'@Hba§GmFb§pd]«c7.1,o(^Ig$,[AedX*A[;PG=§^70:\9ZO`8*(Z=TRdWqpF5lPG;([8l>67HhkHt E`O,pf``0D/c.3$Hn4Vc@O§O-Fk8AY;n*D1W-!@W14osNP[c?JC5igT3n]+X@gM-])B««#SM§fnogku.\pd$(SEQ_@ )UJ+@NqWJKo92=pd,-Q%a_3«f?"!U§)dW$^jjj3RrHH4-g,K`a«DP%B+$6@AKE~
GhSE`;/b/B§:WeDSBr>D!\'\IBkg8«C0d`/lSb`5qG«m99q94?ih43jg5>pEZf;1DKP?>I#2e6\^@/clN.to[Thr9* eBf:§«6@I_9L>!*.9#-IPbX):.D-,jT:Dr;Qa$0_Fs5F!,k%_A@,[A[\KXO6'Ho@:#SL/Pn0q!H$")H.+Hr1i$riOV SAPc4@h>VGCM`s*#ddU)09ZF_U)M!ndZ98Si>J/Uj6CiJ"rrUHD§]Mg[X4K0+/V9dK%3ClKZ^HZ2YfVnUKN'6hRp9' [*Wms]bZaYc(,6f_N;F?_"HAM1O/HdNb8c«XnF0smNNJ/I6[:5+7eJ7Ojc(",Fb!O+k.k/Hb"k5P8P]Ab(H7DDF:N« 7P/NpHXGk89/t§jC-P,EDRT9,*`R;ioI8^]WS!-$(R_:M5H]!?'>AGjm\VdNP-8WnP6Tn%#$F7K@bKnec`d+X9uh+; 6HG1FnZX7\MJ!fCZ_(T0P9LNV,ioYASfCUOCdI§%R_=Bt#8R,r-,eCDDWfOk8fL=«'g1`sp2N(nM9BSn;T)U0.(HiP ]DcqAFM^tD_FGI§`uuG?0Ns1J0g§«_7nH.K(#[O"\2:0RQ!..d"f?0s5D"-+\o(4("8"[j@ig5tSea"\Gq8%,!QGJ[ 79RP3,H9FCgEs«%;L5U/dT602F?>42_i"Y5hJu«gs.KBCce1+:2TCgiY'!s1=50-C/.rEqpFV9"B?O!1YH:[=e0S«1 4S(!'/B>!5;;9+81!@MtcP#Xlfn2c%\IClkIqCu?#f[4O@ITLPl;$e)e'!L1VL?%pDgbkcB"EIOh>"[1X2O4V'8iqu "[#fmGcVVKW3=3_p\1FYWGtG-m@8RGFXM8g#RX@KIue(TL9;W4ijhof/NPJne.nAr2N,8dQHi3aE"W"dJL26=lMs^f p0I^Cc>VTP^FUZQg>H8EgQB^47Xmr'?Q]tQ6Dc_7W1`%Zokk6$0$9RjZP9Ue#u§$VRY-I1I§NrP/kIoi@AJ#?CSN`j Ol!Ca[)7UFUPk`-W/_p9-cLp+Dk`4*,.5tf9«Hlc5JE3q6Qek`/§=##IIkMB"0MH%;\fZgMO90RYmnGG!JrXdB)~

 strictモードにして、FFTを先に計算するようにする。 "use strict";が効いているかどうかは、グローバルスコープでx=1;と書いてみれば分かる。 単なる文字列だからよく間違えるんだな、これが。

 DFTはコードとしては安定しているが、点数を増やすと計算に時間がかかる。 FFTはゴリゴリ書き換えているのでよくエラーが混ざり込む。 エラーの発生をDFTの計算完了まで待たされるのは時間の無駄なので、FFTを先に計算する。

回転因子をループの外へ出す

 まず、jkのループは機械的に入れ替えが可能だ。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        for (let k = 0; k < h; k++) {
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(W(inv * k / m));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSFEbmM«A§;9M#ME/,)[HaG.Bq_FgJ`§h?OT*Va"Ic\?^:YD#70Ps^Os.GEAkCu!?Edc?@.ugEEE3m«Tc?LuCFP>j #?qYj$FQ64_:q#\=s0)NJDCqY4dC5]/;t^1]6%@-UJ)#k;%_Bs-e.].NFtXH]aZ=s9gSqOW(C+,2n5F;QJdXZ0gi7] W1Sh)6h«*kW=X.p=8G«5g6@'fAY-YW[§S6(O.C^+bu_JUnnF3'XW)>5>Vu~
GhSE`D/\,^§H9tY32(c)GtF^XVQ"8k«SKQ/>.7(FlA#_KDW3Kj'*ffn:b)JAcAiPHKW0q6#2e6X^?;(D5dr:('IjoM lM;NW8f9%>8VjZ7Aa3orE\rn)KpN;-PB0ad§sf-30#a!/j9,'e/=\,S.l§/EKNNF@86Aa)k:E/MHtagX9OaGG:g$P6 %VII6J4T?:A*ZWQTWJ9DR1+«UeL7SaIT.j*cf!k(=+B+@^sORYD.?S_AoHL>_t6%f?o:gsTs>!Q-6J%j1s=5$]9d@9 /Mpa#X$rh+IHQ3g0I/.b\3=-s$s$]hnVk;deK.06?0%`mDatii66«iR3A$G3ACGQQK\@]#/(`-(R:4aDm-S>*)etAZ hstrq%oj[TKTj'Np*0^scaLN6!0u@VD$md^.e_lb=f1K@%r,A'KWgqt]@e,YaVVA]8HWmMK«-39=LS%rq`B71d_W@b +lo5QnZX7\MJ!fCZ@BEt-:K!68BNru4«.:DCdI§%R_=Bt#8R,r-,eCQgeOn5PW"VV.sTKpmGi3o§sn()V2-^j:b]Go \Ge9-3oN)Xi"L5§@i%n5)_Jqp!1p.(USOX6$?0^!>aj0:8hI'm!`O#s5D"-+\o(4(*r>R!Qq§9uVZi#ZhMNb)J?_FZ «YdF+8EtBT[L)?'P[5E*FK>«l\t)*RiC/>UD`FWn)sG§XF7r;'NA2]39ALBEeTPO:MhD2KI>aiafe?A#CMT.gFi@qq c[:`Qo)['Te2D;FS§P/7q,\?MKV4[)3WRKYo^-`lF#n5!D§T7?H0jR3HDWcXF,]#NmuJ=AE#-j/1YhBjA(BncWn3=. §kk1"ctR`o-K@RZrAT#8.QQiX«>g20FW32(kR(*as+"SpL9;W4ii/§PX]V^Ve5`HB«/\H=!1fSteKakh!_kEYecDBA V:bNLn,..-%r5A>Xps5.'g'`ed"ln)Ij?5#,\2RLM":GDe`>:h0!_"h0MCrM*Q#mED`Ft27QhK-8aHjT8r(#;=K§^q 1$rORlS29V+g5W(s7l8HX:,7LWG_pc>kH;@Sr^8H5[AIWUaT[nR[Vk'p(%Q(I7!~

すると、回転因子はjには依存しないから、jのループの中では不変定数になり、ループの外に出せる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        for (let k = 0; k < h; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSFDbmM«A§;9M#ME.QRet7je'afOg+F"G.g-ke,LhX!NbI^WVXbbn%ZB1XQFqTQ!-H#fcSF.iC§3Fr5§It(`b![a2 QRcZC;'1WU@h*G«io]0!O]J>I9^`,h(^KNG6(br5=X'Wo0VC«k9T=+/k@j.;W(FF0G"%J!!u,XjlL«4L0.!3qNRR09 U+TK«V':OGjbBSP[XmIe'l"oG3]cf^0«1Ob6W#?4T>1C#Loi!6H'GD8d)@`+(M;^=r_«_g]O/Y`T?PmERqhuOhZ4Wu E0^~
GhSE`D,8n?§H:NnEN8`KmRr(%RjLIico.amA"*T7*tT>;EJPcXWl/UE;?$I«NfuM@3fQaKg>(M*GL§G8'S^28Thr9* eBf:§«6@I_9L>!*.8rIRPbX):.(frg?TD2!/n*k@ls-W:8D[9`cGE5nE^c"K-pgl8#SL/Pn0q#n$")B,+Hr1i$riOV SAPaF-Ql«8D6W\+7%gX`I@1Lg(,[MCPCBK5'V«._3F;YiUB*l[8@umQL!@;Gd"girG/5)m3^RDE8=OuLDJ>E/]/MBK !\H72T[tAc>2?aI/.BYS"`A§ocX?/5Q"LMCYI5-^GEIgS^)qX3%se#DaD%§aOj1t`§8IF(]j/IrA;4ffEjV+Z*N1j4 \Duscn*«)D#q("*mfs-2S#q$]%k>1Eq@mnb«:%TMo1V$JT/='b%Bl:U]A?[Gad8"`8IKHUK«-26Z#0*mf/NW9U6D`Q +lnZAnZXOdMJ!fCZ@>'Q,"3R29$.=C4?]G,g4I7iB_E§k+§qSgQ4Vq0\@Ja1TsT@r>euV1C`b;X+Rq3.]+hLr'+-@q cE;V'§tSM+%cJW@n`paH@2LT6i«5aN^SRa#e-^//EEq5IU/!«*e,iQmefR1?]j95:o*G:a=QTWTl=g9/QT*oi3l'CA 2_U)V?mKkBDo[P)?:rQ«;;,`6Li16EApBDFcPqnDrsJVKnshiVBk%PXiC].7H*ri=`ZjA,n@h?=oPBnorO"LTR(?[H Ji0/E^g[?Q>d[;]Xj(=§Zht1=@RAF7:`Y)Z+FRHmd[Vpn:>8)!@D;F)E\c+-_EGor]B;\CDRbVg\iX[tAoT?-Yqtbc @Yo!I]_/@)g3k6RG`O`=A$):lIAj$t:S§"H?Z8.Gh(uXZ'2h!\Fj._Lotu!$CXgoe$EG)[/q1@]Lt/OXB57pe>\^X, GlJ+-p_^(74T2qForkeFT@JC%90T`!gC%N#3Y,C#+5u+680eL%`,.mSrm@0THSEKCc=E«=Gj8§_d!KDIAXAC§.cuoH 9oP;HGL*Njhm-hI,HjsUUc*7"2feoso.*r_$8>AM$_BBHgODF0BVXjikt"?B`§ok>=8;;%1Au^p~

これだけで回転因子の計算回数は激減するし、実用上は回転因子は表引きにすることが多いが、表引きも楽になる。

複素共役の相手を考えてループを分割

 実FFTは \(F(k)\) と \(F(N-k)\) が複素共役の関係になる。 \(k=0\) と \(k=N/2\) の時は実数になり、複素共役になる相手がいないことに注意。 この性質が色々なところで面倒を引き起こす。

 各ループではちびっ子FFTの点数はm/2、つまりh点なので、その半分のqについて複素共役対称になる。 しかし、k=0k=qの点は相手が実数になり、複素共役になる相手がいない。 そこで、kのループをk=0k=qとそれ以外に分ける。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        let k = 0;
        for (; k < 1; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        for (; k < q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        for (; k <= q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        for (; k < h; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhVPLheL"^(rbrlMK>1(>Nc"%W.i+(-Q)2ZGi@@IeM`u,[2[#-hhdoKS8N0$V1J=bq9JFc3L#1?gtTHt%4'h4f*F`; MDlPa>3VuUjVjoAK.'bt%l:mg!-dAZ]`X:/^4JmUM2]WE50GX12RUa(:1ZnZl_6d6=b],:*VZnf=tJ=«n6(K:=JRd` gr#JC,RqJ'=L)5d3]9H["!_S^PBmr*6sXR)a;f\*eS]3[Chuj2D2RBl^0=cF[8r_]>a/$K@PRS+§E\!1«Q?I)HWcVk g,VR§*>ttogum?`Wa:DTSE!?'=O?LqE9g«P!+§Mc+45#\g#cD(bB'$u$Ssc?H-0LC?5WH9R.sIE+*ru§j8~
GhVOdhf%7/%"?N05m6p)eVVldfJ(OoL1,9h*3`d847§XHDON0'C2Gu;jWs5Ts*^+_gN)WpmGT+!+E8Tg+2s;:.DrMB 3L$NTTpc0!Las>mO\:N%,%Kn*Q#RM#*JL+!^oQCn.O=A.Q(NsTO2uD-Y?>t@=J«fF5nRQjJM:YH$#fEOSar>Z%!.8C W_hd\.6§m;F9Wr«pBFi/+pKV6W68_mo=.)+Q1Umc;>LKpi-5ce63E67OnFEN#i43`156Z?L0tHgD5$M.UeQ:cma`pR ?iToS-dY3/4cE!$7e^7qE#(n'c38+Zjt5Q;+5«A!\?XbiBXs%+«o:oZkJr\I%n+^C[#]$$%YdgG,T,V]:5ihQ[k:Gd 4Ltj,6L]«T0(uQk6^DajN5bc!JVW+YX%-">[rSPOhD:GlamD,j:$uX'/oAl-:;_Jt1n!FZVYWjlDfEV,,Yp.2oB4Re phNB4I?d^OJVqjZ/]u4?)0Xm:4MN.Q§RnDU%C/*':q(:-iYm^QPfrMF#olNiT-[aEl8:,0+=UB2UDl;1gDNPpH(IY@ gG5J\ipt@Uk6n")^4K:.%9*0bKpOKQ\']:%2`*LmUpAD`i6pV`[§ujb>1B\Y]'csjK(uph^)50DW-«jJHifK-\puNX /$YfY0Lk90X"ki38_qVF1nf;]ddpt5p;3Grg.R6BI#qa[*QL!]"T«7rK§%§RD%$`u:8\8@[@,\p,3;(/h4eS/3bE,T /WE]+hQo)U4hj=6bmZM_C7X2'H83!VnJ;QH6;U«OD1MLLq@9g,3Sh'4[Y`GSHlCr1CoK4EI>n2]c]84ZYSU/Z1-a!j ;eAU*AmWg]#HB§WGpDN(gEhJS""a`Mg4=+gW[Son1`>],Q?Hciou=0#,.uK§kNibcKB4§T;ln).«Oc087hkPCEqSpc XA%;bl-Q5Y4332#7njX>hL1+_+-![ZY6W*EJ+(V/§MehEZ>r0thIFY!*_N=(GVZu48uD>R1;+`shE>C%J$\r8Z8qKk peBngaE.nVA"u`gWI7;W:.RIdMm/aT$[dG[7@KB«*5=IUrmaBSNPV,l$7;N1_dh7;:Ang;OP§eiI?ebCm-'Fo2KoZa O^>?JZtGn@%)o69?['cmHM?rJEb?dc977B@c2"Mjh0D;U1"HIJ$Rs5O7jd@]If0=cQ"a8~

 3つ目のループはループの形はしているが、実際にはk=qの1回しか実行されない。 k=0の場合を整理する。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        let k = 1;
        for (; k < q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        for (; k <= q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        for (; k < h; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS-T?#.;2'Rf^WSB'.3C;sWlF\iqM$O/d'+']JYZV@«2XYTV-G>iX([8R1d_EB"8*k4dnn7a]jO5R/H,RNUVY>Zu% SX)pD0q#[0]1E60l7JS>M?%!H*+*Hf/WioF^^q#r$Wn((*J!VbjJp.Ug*>KYQjm.kh(]$IcJ=-ekfmj?9psb1:Y[+N +TB8nCn1Go@o$YEm0]cYg6USrD3ZH![,i6(Z'rZ3§«2q;4Sm_^.]T#h_55*)O[cAi0;eNgeW.1[5r=pkOl§$)j%j,3 Lhub@_>irnHaE=E\LB>LOZ.§q%_«V«GXo!@:mZ`.B-KH_FscP41Vm@0ncO%6Z#T~
GhVOdD3*.*§H:Nn0rjYo]ktY§$%rTGK^N^i§oZ4dM%?OG9]?WP-;I.§[_§L«S[%3#c:aM[K9§E8PBpi4nN?D§G%"Pi *"%sh5Y_1`7§:«Va«X/VShfuI9KAQ_T#.rCi0A@`jq@BZb0IqsLZC2_2Cpsl/6?AmU^1t-5`i4EJA8C3POQ8F$o3ZR b!9MuaZ6H\F:KMFpDR7C+p)Trkf[MPZ`$?5Sb/af;0W9mkBQ`V7K\Z?O`l^a$16AR15>$hL0ul:'qqpSUeQ:]maNdP TDnW:-.#!-",pDO8e7P!E#+2h`W`O.2I""W2`«g/E,2j[d«6>:-pNb;09.2/mYIC!8Jg'e*"8N!M*K-?S=%O)BMtGo 1:dq§7[S>2?0l'd#;"gZ+05>@!Y;V[7us=Kcc)7QhCo[knb"5-FmnKN/oAH9c@YpI=CdT`CA!bphV\T@5osLgk/[@# pp?sjY"Kr!^r:QN--D*\)78)s5._q+*oN3G>@`#aV!bMlM.:5m§h^qtfQSkiITGrX?§bPbJL"L:Q!W*,hO];:gMH"M `-+g§359]XnaAKc$(o*ar!?;LSAJ3l7_«V#LF_ZaPZ3lN!A5sa99WOYWC«(U@-Z§.7hAJ"Xq)lN$2U4N1`q7DSea"\ q,+5m",mt@S$IhV7T-_e[S:EWV=a*kV4I1Tkk5_-$st8^F3?«2mm6u34D386g)*1D\CYR`?>X=uYt-hc*4I>bSsGee ?X':YWAeW\E6ccA?os,q0"!TTD6k):/H:J/0d5a,W"-W=+FUpUBhl,HVbNq.1EZI«Ha_@n_KCgPL[U9@h/Fs?CX;Jb %«5r]=@4UPB#pdKPdkOo\8VRJJaG.kQ@tQVDRJf\bkeW.f4?839E!^AqiEB\hf@O89aePLe_37IR"_7"LS§g."M(;F O;>">Zqa,N]FjV@5_G-63l'3K_U/«:RGG:Efr]C,c*JMA)u"`[\/uDD)_#U)W*r!"9U«BD7jXA§K,«,+14f>D"R[`Q \A(_qs2kK4.21pdAGFZ;\baS504YaUhdl]hR>9hWYN/V$gu$ZLd3B0jI«LJIGp8@rj:mX`\5WC)2a4\TF6:W5%'pY/ L'p!6*gt:!Sd;9m@P3L#/jr«e]8*Lb§(t[NF'62!7]P9cDsBL[ot-RO`__2JqXEL41MQFsm0To(hR_oA85[;f1c6BE lT=kQQJM~

 回転因子が1になるので乗算が省略できる。 k=qの場合も整理する。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        let k = 1;
        for (; k < q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        const w = W(inv * q / m);
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q];
            const odd = dst[j + q + h].mul(w);
            dst[j + q] = new Complex(even).add(odd);
            dst[j + q + h] = even.sub(odd);
        }
        for (k++; k < h; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS]d_.nTf§A@6WhJqpt>!kjb'neofT[6'rOQq]MENN)J9?Fqm2Pa3F@bNt)1AfptRCD4%0V4U,,«1+«n3!e10,Yn4 A5FV@`+nt^MLD-BOU§BM9F]l8Zqa_fD]«Z65$a)!ORo2mB3?'dp'#Z1nag*#J]2T4cn6biJkVJeX@,G/M%X#j1n.I/ mD3V]3liahY$.NZ%;Vr41kB6§k2JTM**\C@]jbl[RaLN5-rH2f.oXT7"MRAuA%,;_*Au§qJ.FOA#>t`n')"r'O]=j` iRckeDiXn?kVhl$§G*9_Z!-Hl)EHeJ;«'Z05Bc%MWH-^?*JWa4)^UM,[*`lWO#iMt*TFQ>_l'$HI«ttDFX?Lf_?U:g V]6R"9s/noRh/qmWFH"kY4$AZZJdI[p§hi3m_8~
GhVOdhf%7/%"?N05m2OrlI]kZggQS:%E%Tp$s;s§[)3r>@.S2[«bom8/LP7fr9pCkfo^aeD=73.Zqa$[U\I\#)44GN K2NIIPZ*§5E@!rnErg8eA:aZVar$#WNl%t+i);=4'`rQMbV/dP7H@80Xb,b?#)+>o3CSc§"MuuD96ce9X-E"1AXEjI ;hZt[8AS[7k;2%aH8jLAi@u`08)6F`/«:_a>=gt5iMEGl$ANJK.+I0'jB5PdLN$AM:W>*eaUkO>W\%b1NO4P7`86sA kPMSm>lkHlh8?1EB6=10"qRGa4P7HohOFa3G[oLW!]sGh'd^>ObNd,-Z?m#5I3N]4V-RW§EmTIX-iPOGE2GdNgU]N5 H*e6"K]$sZD«tbpLI''R0c'Y-"«MmTXr]k«I["=ii$shFQe\cbg#RV5i6p2(D\_§o6'2J^D,*«0iAGM?QfJ4cemCUh ^e[$Ql`-1S$rjTg.FFg$(ZE4-?^E^I/4(mZYsF]33OR$G"KnHN9:>Q('Ans@ppYdWbBaVY2NX.p\5S+Kj6IEcHo[3] ;6UM>K^5,:2kR:1EqgQui=/KNitPh:1u:^N);J*(GEU*@SH,,p9;QdUCY-4Bc-^[3r"6kGeb)@X5"!">gu`Yhe5HSh U\r9$e,mVq]iA§Z">-UDH'9S>O§C:LN7(Jr;cJ)M;2uG!`dH^3qBsYi[O?`S,)hL.+kip§+Km+JgOuI5#d,*\7F-OQ >$jk]1lteTMVt(5,J[mq)_ACLh9pPq\p«gi§13('(8o:TpOi,2ZD9J9[%+R];TZT@nb$Xj150k0hhUqrJneDGW7hod ;35l'/bX%0#]ArqE3!lsXPZ?Z'XhG-?FB=XV*C]97o:L$,*JNmD:].t@8_Nrd$Th1BHT-0MOqDA]6a@2A+`3SWXIN5 R@Qo%.dPAqMJh8qUTVH+TEYqsD:,@4##tq3Do3rB3mK71He7«F(r(9`)I85)3]§k4SNqJ@n.Frt'>].W%`hmWCq«DO dliCZpShu?VmGXe=#SkJflT;l3_(§k)hsl\ZK"'eb^/N7.!8ZqG?JoT!a@n/aJdL]AN`.MOL0eB6NXF`jo:\8Br,Ui BO`§aYc0AF2f^]k'Ofa"kP-h\k%RH5ED"%/peCEB3sIi['nM23%§ZB-cFlGa^GA4WojkoB0bZl)Q+9%b7;Bq\du7a1 [uBFBbs!t$p>`pVab-Gj/_.MnkqTRD«VD#§h6mVNk(8p+Mfd0ir:_.§rg21~

 if文が増えており、hが1の場合はそれより下のループを実行しないようにしている。 このif文がないとq=0.5について計算しようとしてしまい、おかしな結果になってしまう。

 回転因子を約分して、

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        let k = 1;
        for (; k < q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        const w = W(inv / 4);
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q];
            const odd = dst[j + q + h].mul(w);
            dst[j + q] = new Complex(even).add(odd);
            dst[j + q + h] = even.sub(odd);
        }
        for (k++; k < h; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhR4X:scVScnSPI#*TN,$Al`eH-$D-V0U0/RJD+8DJgU9`C7m0$CQ0N$FU;QL7/J">RF#pQ4(OWKsHm5o860Tb(8iq 2*h#F'FGTI'll16\[6@TcrVn%b/-hEb:cc?a@Rs\a9!hFYQdu`$@*adgD4H0RKF[%V3[t?!>VLHQUCq@.29d*Erc9« \MTi~
GhVOd;/ao;§:X)O3)r)nCR_F>+@I%P!CKBdO>b#ZR_U0YnWl.Oij@N«s#Xl68XqUP)rcuN>?]q?GJ«P6HnLB$JjZ5P §ZDs!JuFob§t+D$b>Y9%PUT*7fhhH$!do$kb$;g';q\/§n;oc0hL^IEMWK`>k7f)J,7"o_'_!SF(CAu$b93#4V@6Fi PFTMs*)/_pqH[Us7ms#H!d0n>GS\gXY*+65/.0_]j;d(VY0_]o:o%B*'gfO@*=ZHRUKt)BWNo@R3RT7(§pJ,WTC3;> FU>G%F:3^6Oj`W@(S$1§\TNJh]hM8[ef3>7§\,+"X_>8'>a.I-O_=QQB6Qa=bG:§4;@\FXlbjn+)@h^=Qo-K,H61mq =s6R#g+N*5a9.`IOq5N\LqpXZ\uU0SfoKU@!m[0j^%c3%WB0e§)OEAE>GpGolnk-klalpa")i.2X«>P#[e]LY*Ic_h Bih,BE_")^fG8§;N=9"ZY>6fBOU'O%EeQ'jk1"6POLU$S0B)K"ML@u-ink§i]r]eh["0K-_6n/s+(*>ZibL;h.:R'> 'V§OC]_HkL[);'G">=/A+33jb`mHge:Z7c=]h0bH>lcQ0N_8>8«3A[O>+k1Vq]gop]BbCFI"6MJ\BRC§eC+S[;197H e,dPp]gZ)B">-UBH(-.F%oR_bO4$eu@oRd^@>5!n`nYT.rHlok1=+V_Ok.O6§8lSnOB7p>FY".8"P55_VPe"§daunq @Hre'#k%LK!j§9'_H(I>0!,qbS)nQj§/0Y!JnbDC§(9(QP3-l:E7uR0-hH!2f«Z-].I:RB^3b=IW.r!AW]e02"`[Vj ?sco§E)dXhPs-5;rWoK"pkij5ou=0fr;hU'$u7j9§bgAs]3`8kPF«sg@le"*JC$h;HMU%tl>8WS:oETGp33Be/=eT# DRt+SN2d,L4$D69[mmF0)0@+!]a`kWdpZQ;h!6#"qtRi_e«j9Adp8.QmN.U:%0§S@Qi?Ac[[4%kj_D(kit*9[Io_uI lfB.eF1IpV(?R9-Fa«/%20!*-«HT,XYIhQN46M2M1bL$q>N>\sZD0/cR^k,m4*i08KH/s,_it%g5,d9SbT.j>1«`Y/ b08ebHXmV%42Ikn@P3KXDirK1mc>S@96Qfu*`4$PO_SOID*9-#Isan,%0^FELTP4W:sDdn-aXp0FjQ84U54).@c$+Z hZd1Ue,~
そうすると回転因子の値は \(e^{\pm j2\pi/2}\) になり、これは \(\pm j\) のことである。
"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        let k = 1;
        for (; k < q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        const w = new Complex(0, inv);
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q];
            const odd = dst[j + q + h].mul(w);
            dst[j + q] = new Complex(even).add(odd);
            dst[j + q + h] = even.sub(odd);
        }
        for (k++; k < h; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhR4X:scVScnSPI#*TN,$Al`eH-$D-V0U0/RJD+8DJgU9`C7m0$CQ0N$FU;QL7/J">RF#pQ4(OWKsHm5o860Tb(8iq 2*h$1'hgHd\[4)%o2«aHSF^e0E`+DE%)>mDQ,/DPb:cc?a@Rs\a9§@uYQdu`$@*adgK#I6h%^JW;p/)1J;R9_9;7![ 'FJ?P\H"HG2ba~
GhVOdD/\/g%/ui*5m6pJC(`HSDSomhG8Tc!@DM76BFjto'Ur^XZW^R.CcP'KjM-b=AQsTiXr[6+%(§btnfdgb'G["$ "c#G+,#D3f!`:BK,r,d(QD[Ei.oVHPZD[2?!r1h]Q+;R]Vm6]8k95+C]ep0N(!@=pXXHLU-4hH(.,_9[>pk#«/V?`k RkdfC:3$j-Yag>:ot/5pO,-%o"mRi\n1:S:>UB?I=;7@CaZ7*6U!S=b:o%B*PsW*+R#GJMP9F%f«*?0d§o!QeL`;fE kK@f$]*);NH@SkL8Eo)k$dQVNgq1@ECpS2mpVt'+!]O1"MO(lq7JV'GH#X7«nNc0B9Y/;WbJ=a)SbSsb_hmmlMoOjM c_J+G«X?$l[YoPbo7/-WjC>`m,4Nr`GP)4kqc\qY^g![F0>:Y'Wo!VuNCt5^K@=c]#>HQ@Y-*6%^`2§O/'§U*D8(CB K:hjZhGl"X*2j\`gS1dZP«nO5mu§\oj8l]bg`jq"'m87H,d*in9=Xns@c*«$pid4?Y,aE2D'0-dE2sr4a4GBopmTH4 MV*pq"d`:DYPu3\g>NEA_OiO$O§L9-dqBOmkAX)m?6p>_XoOe(8#:2W.bCD8/^SX«r=Qush=]0?5!cjq%.b/YolL4E QCl)QFau26Dd=,55tgM2]\Op!O§C:DN7(Jt;cJ)J@>5!nk1juNrC>671=+VoOk.O6§FT5"+Kos$G-ORt§6u[mR\#h5 8mb5dKW2Fs9eb]G#dBl;(0doK]=)%)DO/%H5thQ!$*"Fg*i-!K9:F=H_eHGSH8ElpHp'^G4,§*AhhUAbN,%"'W7ho$ :_7gS%JELr"?0«mh!!3Am>m>9Kl=N!«sO1qm>iZ+@9C^8Lq'?]g%?!/R+,(Q)Uf9#",'-IdF0I`=7=M(LO-#HBKn6u AEfV0FjmI=A5k_%)>%=+h[ZdRB$cO!p§][WVrPF!]!jiZNq%I3W%kEaUI14\mLb\-%0(tV0)^RIiWrV`n2jgu##[CW V«NF)bK:Jch`RQ"2od@`@=Fq[2\3§pP:%,N«IF?:`V5S8kM.i4AqoAFCYU"#G$a;s1E9G?;3+,.O«r144Pi«ge(W'_ >SM3*_^re(bQ=*e'6@X;0h+;@F%P_WMj-,DD[«%CZ=2J17"*o^WZ;fJoq;*CrG-Fl-pUEgEBSB*C497^60ien-QNLB MO9/Gk6(_\!CjUpKE~

 この時点ではevenoddとも実数で、odd側に虚数単位がかかって加減算されることで、共役な複素数が生まれる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        let k = 1;
        for (; k < q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = new Complex(dst[j + q].re, 0);
            const odd = new Complex(0, inv * dst[j + q + h].re);
            dst[j + q] = new Complex(even).add(odd);
            dst[j + q + h] = even.sub(odd);
        }
        for (k++; k < h; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhR;$bmo=Z§;9L7`>jg6"Ab1'$§OL7i6?LC>+u4)V'$]:]=V§eNT);PmU>7Ih(1o)a*nGXrY`!M>CD\qMH8la5tNVo /#MXfa$F$n,+,sO40\#/$rbMK\WXNi7sZUF/6$aA`K`kFLmrc#KrVs]dGRR«.%rPS1sJZjR@M(8Gr*E.8.p""X@\0« -5dCEc>=$(r?eW"P'%K,rF=Q^%fEOH5,(p346H9(9YRiK@9m_bi-u?_/oJ2\S.Y^lkliB.§VmsRp"CebM0Eq~
GhVOd>Aqt_§:Vs/(e$RR2Mk!T*J?*M+eV=q3aAHrnUN)klo!YN?75G-8W3/sR9d#`od)*L),%(Fcce=(hop='0J*YB KuuiWVZ$^m;?fa(§u$pQR«i5;*(=C6iTC@5b.0!C$%#HZk@2b)nq;=)(1§DT/$`>UKK)XJ86Aa)lRZ«hHuCTh9VS"3 :g$P6%VN#9K1P[K`n$K45YD:G1)]R4W_m"IIAp«_T#§N/hYj.pJ>5r:?"'!bqs6!S_t5«DYSsLH:tZ$#-@ZRc1s=4S 7mFTg>%i54;2Ij.iL:5#_N«Q`^iaJ"1V!%r_D;6P«u[$_]?!G:I2mNP%!hB4aD%§aO\Ns6§8Id2UKk]nnI1>qj[mdn >>Qi'=\%5c5*gK0§l/#§g`Id1cs-8/§QJJ;)!c]r9Z:=c]?XA]qg"/o)tS\«E1[A§-Mah4IaZ"Ng`S,g.AgsrI/FL> mHM9a;@#9Z0cG4'=`GsEf)(s.92agONhkGSbDR*X`tfpa6a;tsq*W-8,73o/%),tHc`jg.,[A0F*c)p:cU$Q-2KL«V 4!kYH>e[fIF@!:O(U/RZ0ZB)OGQuW1(Ja.46iC=='rC«uQPN)fE@iZlU2hj4F_CUa`7*3.fm#\H]b:hG0hCTo#0§4/ Lb%`j@trjO/7O3se?-A8'?Wfo4[2DM-iGKo4bGH+(CB6ZE_QE:d/PTAfe§?;`fLc"F]jf->FAbJ§e5§:>80e-b=nb9 V,>+Z416IQo*Y]Rc=)X+f%OD401InDhm+@O"LSB_"rIm!f;k(sFPCcg'M925eAp>81L2H"A,JN«J§J:?Z\YVE_$1'_ =3t>^=§!X>28Q\L4/Vrk;PES3fC\@DYHRj9Ke*XJ3>)quF=On/_A\W,/jj,0@UO`581T#_NSN§GFS;C\eLBlUVM$Lk G@!FSWu`//$uQ8*f]KY1X*PWT+Jc]$+2mD'YF+`PO1/WshAC"O..6«GUkO-qjW26cW://aQn/GH\R(V7]'0;8E,css )^K0«%nW6C8[DooDQ#/$fgX(]]?8AP6YqaRD`l4%p\!ZEL10]."(;JT?EX,crnH%Sf[nCLAo§JZ5./E//SA.)*]/4V KtdX4OX+Io.hjq_=1nmp*6]9GB/7\/4=Vpjl2>+XJ_cF;;_aWWY0TXQF8,/9l:3a+HVUPA8Yk§\FB@q(s$-ESmL084 WL(/.c=kHuQD_)r7«R%@OJ7PVBQ?;p^L*>iBZp~

 実部・虚部が簡単に得られ、複素共役になると分かっているのだから、結果を書き込む時に複素共役の複素数を作ればよい。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        let k = 1;
        for (; k < q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q].re;
            const odd = inv * dst[j + q + h].re;
            dst[j + q] = new Complex(even, odd);
            dst[j + q + h] = new Complex(even, -odd);
        }
        for (k++; k < h; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhRS.bmo=Z§;9L9`>l\q_4(X#85+70DoMn-)ZOK9!c!\_mT,8L§3"«I§+`M-ZV^jL3ch0+mlu\kkdsNn/S/,PPscTF /Y/dLp9I!jG@KSe_Pa!_Brc8V-W/ee,!V_YLGY!Ea:.(§WDlGp5ZC(5=GeUE.Qa9@c/Q«:§VA3[bCp§U%XrASo+'p7 l,NMhoQJ6LA'k!p*)%.§9/sE%I0b.g§no4T/iOk()qqC$bqEA9a[#JVn2In1IZe%§531;E^bLn/"ZXpJGLRHc2gbZ# 3flB~
GhVOdD/\/g%/ui*5m6pJb);T#fJ(P8%`«-H*3e6.BFjto"Ij§IZW^R.D)k0LpqMlQAQsUf=5;C`coMc\LZCg"«+'%n %!%bU?d@IFKSAnA:BcDcQ?_uh,%2lD-OSaDJ5=0qOU4!-1;2Tn(6E1YfmCQ3oJ0"H>Da!R%/ec$F8-tSp+\WH90X0T VXS/cOc7+:Cf\=borbRM_a@6uNk1hO=W5j>[ZYrI`,eH/A.7A`:oM3-aHNd28tfN@B4I=sC^=§d«>I@'7,#R«@Y\Rj q=«;7/qF4qQ7sfI#q`pEi*HtVHQ0JTD0ZWcc«,«p@-;8k;0!U90>«GkYG[K6^PJBoE>McE*§OA/M*§j;/@9%l2_Gn0 O*3gK§=+>R)ga§UhhrAo`c?Q\^dE%reHo4DCHf0/hKq"uU:Yh'le8OW>hYR3«l9>*1mqalHM«U-?K:lr:«kP1rT1q( put1J?'Uq«JVr#4/aDGA_Jd-§\[Wi/k$q§A"1#Sr;S-d3i#7KDR#,`:E\0k5J*6«8=0:H4"qi]u2la,gI(]Z6YN=30 a?+]Q_WB>6X+)dZEqfu"n2nkaiuDC@4?Ni>(#2Wp\!-ALm/WUk9;QXQ/(_FSc-^[3r=QtFh=XWl?D--V_gDN;3beb, QCmr4>/6N!ZS1m*TI$urDp!b(;+`L=..*fp8$s-EUj.u/c7R)dI0BMPZcp$+_UrN7Js.YlnnKXqN-NQsi4;3OoX)hA qFV[GSp^"o$-)lM!=gV>@§p\Y46§cqVV,2"NsJ$':6bk"KCni:Uc:LoS2Q(!`B853p$u?f$Mt[T§'p:BeZ]ie[3rIQ 6.?W+f[nY#\?7a+[5o=lrWoK"fRYJt«T)IjG=3S`Jm+1§6p7*`Y*ZB0UjAB$0c`!q5\Qr-*H`QMoubQB«2DXNrQ$6# KluP.9t0MP;>§l7TEj6LNbDEOK5-(8(\"1Z*Tn0d^$5§F`UAe`7^qB>%S^-:7o8%sI";§GrT16OXS!80G*I+`[lZ_a 2X#BK6§9BdW=G%ZPJqCX§mcut!%n0PoSa\_:pYPJ[Q9m4]df5§d(9KHNI1oI8-Vp%NRh$6V2L0>;)Ll9IrkOCjs[-? RUC2Hg`MenSnEL.]XIT§,33`;2E6J3r-VPo`6LYUA26p`BnH)]2/Je\HD6c]9-"m)6-K`IV)#Mu0S*C_@@GA74c_q_ %^PtXOM1PS:NEWf5YH5EE:cT§q;(Y4f(5t«3oknU§IW^+-3!cBo[nLC~

これで複素共役であることもはっきりする。

残りのループをまとめる

 残ったループをひとつにまとめたいので、ループ変数の範囲を変更する。 このとき、同時に計算する相手を複素共役にしないと意味がないので、計算するデータはd[q+k]ではなく、d[h-k]である。 そこで、とりあえずkを機械的にh-kで置き換えてしまう。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q].re;
            const odd = inv * dst[j + q + h].re;
            dst[j + q] = new Complex(even, odd);
            dst[j + q + h] = new Complex(even, -odd);
        }
        for (let k = 1; k < q; k++) {
            const w = W(inv * (h - k) / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + (h - k)];
                const odd = dst[j + (h - k) + h].mul(w);
                dst[j + (h - k)] = new Complex(even).add(odd);
                dst[j + (h - k) + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS]dgJ3Ad'R]@Nbi>kL«q5$3',$LXJVao7%kT=]()C,IeX6?JB._^b[X§QGa^JfTbUf=jH$=2_6I>T6SqgJBY'ktW p@B]_QqSQ([e;:@H@mrJc1*MR=]#I5J;[',O%fAKc6HUCB->AhXE#kKDE"=E?%Y-oFg7YfKFo!eNt(=RF`:BI@nX$R Al_#=lo>^HFATC%$NVn#C+cl-@SBme,i`oeD:phckfU7s-=JCFnn1k=MOqeO+BiXPO=%09H=L]bZiuj%6#3l62\!(% \%8N?b*/ou(7Y)!M_7n:ao«#\Hc\b43$ptWb§Ze>!%F*imOPtZ;T*Cp^Jc§]*s1TB=V.fc`kB=UFpqfH(%W%k:,,V# 2!op>X]1F+%#LRh.d81dNmN-s[2HU-n+R,^$hCJ~
GhT9$D/\/e§H;*)EA76?N%HmsY[OK!(.G\@"57TW1A!qB[§QMo9I.cC.,.=es1T^H8^rY74.(Od2Xg§ohK*m')WkVI TTD/EjP64j«/Ns39L+j(WA"9§;hcL%Vsn1#puG,7\U_@DZR`b§+o.P;)aD!$IP/`W71l8_6'1k1!Um@:PH_a*$o3BO b!9Q!aVe@#q%@qPh@6o>GQ^X];:cTbbG#3[«`5!;H/WM.SDi3A$T0mNH.j(c+`qMMC^SX\NPQO/g-TE'8=TBLm§_.S ^:sBi-dY--$77§]V:AWdg^cdCBEL2?>?o%?41.4'Y[]Op;0!Vd.)/J$4,8og,E^G1kp,MYK-o)-nKtf$1[Q'Mf§bT# URb4+KLJ\k`]b)^i$XKE;mF)*i3Bcjl2ala>;9+Y++5$=n\Z];:%">W/oAi`X5Q7%RS9qk;uNMGf=4+IO:)J$J,/sP r3WBF^.Q5\84@«](A3Rj§TllZ]t,@U*ahWu/[F`EW84m)E"0d(_SgX9H^5qq^J"@7Y1KnbJL!q+SSDsO?`ucRCU()m nom9aE..?Gk+?F`*56uFGRJTj3/:QhO*9K0$$Xl««)QSn$F"6L«*IndG0OgS**r«m+$j/^?(\Le1XnM(D8$-=>ueZW j_)«)(,/5ADTX.MBM%U8Rl$gdP1$BuPkMtdUY74(1H\2^k)Ns:SO!4e4SKqR0K,AQE3I]A]UI(2@\,7r\8G+W4WQ>d ^61sB00f:PJpVs!!8Af9Z.1nI[IKu6ZhuaHL^?@!:6bkJ#O*1«8TD=Y3GfG+P'Q$Flh.*s'_X81+.jW0WaRQTl9K«L K"*IIZ#AqP\?1\I«h598E.l^hCs*O+C?)d=DaGTT_a]YS_ulRaD@j%hUq4^qQqeK3+>cuRXu_^BH«"l)W`39NiQ`\b -(]CB=_X=BeLbJjl5mTVHZ1]MW\tc/NEOo->157,?.)#@-S[j15*BqF@/!J5mPKL+@«Zg0d_VK9R%E7M$0in;qS2;+ ^##q_[i(Zc3WZ@'9uG>N_jUj\$§««e^'T.§j:!QG=(PJlFU?$(#rtM@cuJ0WpeNGW§-o,PF,JC+[O@]qMpclg;*$'? jl2XM$q]bPlMXNt8p^Rj%d^6IINkj@bb*:§e_tKtf\@;SVmO!,j?Yjd=83^]i^lJ^nHsf1mu§)pFsQnoQ2;cL;S;9T E:3_$LRFS"e«Ma%`/O^e.WbLX'WNIc9lnWq7U[RS)>`"0cjtX):*)7rBW3U/Y.eDS5Il6:bp[F\Nk""9bX*=kL§_,U nViTd~

かっこを外して整理する。 回転因子も指数法則で展開しておく。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q].re;
            const odd = inv * dst[j + q + h].re;
            dst[j + q] = new Complex(even, odd);
            dst[j + q + h] = new Complex(even, -odd);
        }
        for (let k = 1; k < q; k++) {
            const w = W(inv * h / m).mul(W(-inv * k / m));
            for (let j = 0; j < n; j += m) {
                const even = dst[j + h - k];
                const odd = dst[j + m - k].mul(w);
                dst[j + h - k] = new Complex(even).add(odd);
                dst[j + m - k] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS.?d;FOi'Sc()MZ9tfKh8S>"\hN]I"EYN/6k/«K7gU25di5Xq[Q§K@@6B*+jKc[rD,`o_0^X""uD'Y6kM>rQV"E` 9N(;[_.rY[MF0/«c5a*>(N=atE'C2cf@D"YE`F;.-jXP2Z)8a5_gCTB@SK^o->^qqbQk?d*\raGBHBpeT5«d@?WT6l 1U"L8g`«@WZRbZCp+3!oLM;KF§3T1+]qpb3O«BJPQ-`133#oc:Pd`JtV6R('o362O9tjI^Q«§LrlB=(/\A+/h;>,Fo ]gWNmjqlS§fb4«fc6^D=nF+o7A$8O^O]H>[SX%3*I?=cXo\!13htmEdXJd!~
GhT9$D/\/e§H;*)EA7*;N%HmsY[OK!%`Pjb"4h«S1?:f2XK"Zg9I.cC.,.=es1T^H8^prW!u$d;f%iL0HgWnVL1OLA -tLU*\b$XKPp2;P'+e`#lH;E8PbEpR.=>>"?9+3\0§Ys9m"ulf8:I6"c4@+TT1cKC:kYb_§0u^%^s?/p9A%W[#+«7` AK\=Lj:!["Hjr2«pBFi-49B*Ukj+.WAPn(h4/klm]Po`'cLC7]N>`dr4RF*R'"V`+@LCRgN56F.3^RH18=TBHm%kSK 5(E=/:S«99'hheU8up8r\0$IdaTcY6[^q,>GA;EW@+T.ZU?jdY;1=p§G34mW=!FPRdRVk?#>M-gi_=G§B0%h.D9B1N 7_>A6#cjQ+NBL?gi$XKE;mjA+i,Q4)dhu-b9/38nO1pS/pi2YBVYBB\QaYk«2/p%"+VX4hW1UHd)E5_EEe.ff07X"^ T8n$LG:C$=\S)L0U42V\JhHpR=#Zqr!usn;8!]\T[Ds$c5!uZG0_§/_Sn/5umh.g$lXqV_+:46NY6Nb!gH,sk>HM_! r.E«A>i0q=q«+3FK^[k>IY§kp>f']I7YbrnL3r1pPZ3l>!B)Nq9:MC%WHk§'A*VqG,D`CtlW.X^6bbE^jr4l]««C/i 5Og>5J?_Fb«u*O,88«A*ZjH,`P[;U3g+.Z0DJp]Bn3qpdG8sQM"St@D\c53RM:!T%8(AC9eTBpbo7R.\k6=B§dkOeu .r1CEntWrE\E«MkY[>9h8$.^(ROm/ObCB2;Qk8oL«!D3+OM4#I'W:hoe$`,RR26q$hHd-Vi>gnPnbXP#2k3«Y*>fjE §GHVcp0B8%]35>]lKsrK[u_M^m?s\M/c9MP9fQZlf[u=BGi2N@%16.'«3$F[ksU%+_B@t(W]+tUYNkM1W`9jW\2)/p aX/K=PIIM=mpS%u>u_,OCIUS2592«H,UTGB`$4XTCsB52Y6iSYGbF)Y%djIoH\!g]hC$#5^\!1^+1@rnM+p7kJC_9. @2R@"9pEDIH§XsDAWMJ63Q4-7,j/;RTi«%[D]:^=T(S)NQYuUNEGQeiW^Q#-8i01.4Z]dn@oGooQ-P>u)X§!Eq*Ok8 6«C?>:*XT«SGT.Zg:§YK27';$nqE7hK$u(AKQ4'TpI;ub-@!§^RWCb9[%#tXIum§\8]Wo,$b*BjeZNH7TFXH$0>]9Z F.39pme9n)B"OqCN0tf#09;n9[nf>aH=W=U%Sb3^(«LG]HtkRX5Q:M=4D>-V;Oo@DI:-l@5Q++l]f/AM>jrqLTrtrC J,]PK$AZk~

回転因子を約分すると、

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q].re;
            const odd = inv * dst[j + q + h].re;
            dst[j + q] = new Complex(even, odd);
            dst[j + q + h] = new Complex(even, -odd);
        }
        for (let k = 1; k < q; k++) {
            const w = W(inv / 2).mul(W(-inv * k / m));
            for (let j = 0; j < n; j += m) {
                const even = dst[j + h - k];
                const odd = dst[j + m - k].mul(w);
                dst[j + h - k] = new Complex(even).add(odd);
                dst[j + m - k] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhRS,Ymu@>§;GD#iZF)T,";f98368D-4Io1+@H?)Ujt2>GP=?I39UhMS'1P3Y=_)s«mDV230h@sgiSL^eT$dQ)I^-e VN)[-\jHJVfsk72fAgpFWuA'1)WV$HP/>7nK(GQM(b;brBlG@L@f=Lg:2j(ao«KrM`5J^C%m=-#r/^,Pq+I2Zj)/Kb (QuXjmXk`I:VQH!:=:GWKVN7)K[Q.fq-I6#?2I2W~
GhT9$D/\/g%/ui*5m7'-22Tp!fPm54*JV2l0OQO=ZpR%l_9K=J=fj9R[[a-6r81#dZ7Wh6lX:!$-R/@_Hq9,%b_quW /i2q=\N>GJ§AI5)J2#4\:OXi-L*CZ2n9kN9A5!c,J:f)qF+.KtGtFR/(L0KaP@VA(#Z8m_O1dZ2^X?X3Z\,OT;h8fn ?`"^_(nAG4§pU/-27@gU#YN6:f+U>@_]$Kp;NHc]mV§,LiS,@[2)kqgdM5e+Ztnc>%+CaV_.odg6DbPiS+oJ>XVlT% 4T!t]cp6%m7O:0jjVJlk$Nfm,"4qm(SABh6[QQ4H@l7IfEZ7"l^5aGAVN=loLn#0!(KmXU1nhpG43BE#C!X"u>TWQ# g'KW'=ELoem§A'S04lIKMu>X;§§!*e-W%L[q,dfJn7:TJpY/Zu\R3ptZnX59L:\?\R:,p'qFLdmEW%^A-Q';ko'\[« FJ-g[9$YC@7;J8+p%!Ig\).-K^Ee*e@nV\"ZFj@>]59jl)N()QM/P.i/P1JBB5kP`f!)N4`(\%MC2=\nV]bK:=P+=5 H]Lh0\9rAnq1:#MYMg>tKK4LO_^h+0qd@-k>O(o3.5+X(fX+a1NGDk2k*9_BXul$3+HG`i/+*-«oo=!\cTtAt'ei+§ br=,R`X§,PO[fO+WL\«u-BsQhH9Oc3-i?!)]n.BGK*5\%3Mu3Xk^Se-hE;Ij7["B'«*GAbPF6j;@g3(f%>pmXAZ!mW dP1CU#B*7Sc`[pg[tGd1>§1T%?F4U=rT!\c+fNAZ1;Ni2ID%0\:3R/F9qkTB:L§:«SuGCl+63+Sq%9;=p*-O1"Eu8M \q1.`VCI1eWX*Lll'V*58;4p(9[Tn!I^=#a'[e,§j[_ehe#D\[!@pjDWsk!PLk,as,GjV§'=k/FG35JjeHtX39QF"7 OiUPcXfKaZ)g#C*eK39+(/X+boX=Q@-HFfX+E)UC9sK#F$.03lVaHX`mKl:*%"CTgQh9imeJ!8BgC.sI2??eZ)Wcj2 ^-RZ7')iN@C[A26HYBdTWjp(WC%Z-_7CnS§)R%6)/hV7RGf1;cGfoCI(M/79NF+9]7-jiZ0It6E=e§'#r.O§\V^I=/ F(«uI:hGSsIPWb1G^U*J*0Xm.*tS>Me/«/ETW\lo-\=1k>L!S9SJO=9B"1].7SF@Lq*M()m«_3nQHV83'r.?%Tkqci 6TmWiIsOa/1^.Qlk-1Va=3[q.5A4=8FjM9J4HEU;@kR«/%tgZX!!~

\(e^{\mp j\pi}\) は常に \(-1\) なので、

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(w);
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q].re;
            const odd = inv * dst[j + q + h].re;
            dst[j + q] = new Complex(even, odd);
            dst[j + q + h] = new Complex(even, -odd);
        }
        for (let k = 1; k < q; k++) {
            const w = W(-inv * k / m);
            for (let j = 0; j < n; j += m) {
                const even = dst[j + h - k];
                const odd = dst[j + m - k].mul(w);
                dst[j + h - k] = new Complex(even).sub(odd);
                dst[j + m - k] = even.add(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSFG_+Fea§;KZN'`TmBc*f-/,V:PQI0-)InPD.neo$YZg$L9;GBZrcRh_q_QTp§iS!cOa'(Ya.«(nDuUF`YYDm$sW F]GdA%eKKZ$i=[l[6%b#=`D#d37m]t[.8RJg,.Fn«%*VQFqd82?D=XG]?m2qCr'T,0s5e>gR9^]N;"iCa0;)79O,pK SB;JEhrBF3M5$BS`=Oh7PLYG:c+u@PFD)^$`A8btO6Qf5[8'hN@S\Z@BGpV5)7Cg:R1=GtDJSjdi`Md*[:[q[(YO8Z ;92-%FQURBL]§e8L\8ielnS;;+(M[9P5~
GhTQ,D/\/e§H;*)EA7*;N%Hmscs`lA(.G\@"59nC1?:f2XK"Zg9I.cC.,.=es5"th8^ptn!1SL,2Xg§ohK*m')O=sN TTD/EjO.6t«/Ns39L+j(W@pK#.74\-;Y[[#Hu^RW>enaPf`aUGOSh`,SKD015F5oeTa4LI+@uF)J@A[m9A%Wo#+7_8 AK\=Lj:'>mF:C?4mfluZ49?hjW9]@lAPn(h/#c%Y]Po`'cLC4\Kc1qb4REOB§A#8a0nnjD`aB6RD'?5YUeQ4_p=9X: T7?[f-dY--$77§]V:B2tg^cdC?ir?7>FiZa41.4'Y[]Op;0!Vd.)/G#4*/Kg:El]JdRVk;#>M.:i_=G§-e^@9XiloZ 7m!D6#cjQ*NBJ)'i$XKE;mAPSi3Bcjl3s]mV3n95pV)UBpjSRO-MQg1QaZ%AeSs-8-55am«'P*U2/\Kjj:§rW?U"IC T8n$L="1WrWFuaiU4.)8K$O8^(K[ER")LQ68!]\T[G)H"Y+aKBR.r.@c]WtVG7L7Robs,SOG$38«K6D)lQlgaX]@>K rk[b10!F,:rGtPp_cLdaY@380Qgb'Bd32btiKeeB8h*G5!B)Nq9'_qtY'KE'U[)7`,D`D3lWSX!6+mrejtdRu««\=Y Ic=RH!FpfNXt4%6QIO3m8JB5o.D;+DZkr]SeQtM.iB`§MG8sQM$1u!SF8qg;A"uU$O")a§X3§R/jp;-AlNTf*dkOee .qt7Co"Dd_\E?p!Y[>9h>HP6d)D>8QbQ%'aQk7d,«#OVCOM4#H'W:hodl(§QR:dST?Cn2k@3=P(nbEcl)YPAT%Kpu( M#2c>qkbutS%8lCeEN>17nAWVp.Wl72L`"*-5«a>Cue,T4OC(p"p$5JWiHPfl%FTl66i>jWY;qH=7t^L9%=e"mS%Xu ZAHY§kABM0],Pm!(Y§Q:RMJ`]T+8=*,UTGB`#rt'CsB52WsR/_GbAQ1%djWkpC§,e]MEkH?e#g9^"\Qb-T2!J!a=]S L7png2%fg+k%j=d$%$LLbZ3GZ$d§31+^lbLBBV3mm(u(JWu3_Vs";)(0,6]UKW3O>MpRlkV§Joe6C]i,)rj4-Y+G(J ET=d>kq3iSI5O6`"Buf)iT)fT3=jhH[)(W'§Pj$[MC!i§jBjbalkoB.,kj`pV0IE/i7*1X#DAM6e«Ma%`/N2:.WbJR §um7a9lkMm`_.Sh\b+7mV0@Fh8KK^"BW23KX7'omc^«U'1S1§.,;+3QZV\\IiP4hD7;GB~

乗算を省略でき、加減算が入れ替わる。 ループをまとめるために変数名を変更しておいて、

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w0 = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const z0 = dst[j + k];
                const z1 = dst[j + k + h].mul(w0);
                dst[j + k] = new Complex(z0).add(z1);
                dst[j + k + h] = z0.sub(z1);
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q].re;
            const odd = inv * dst[j + q + h].re;
            dst[j + q] = new Complex(even, odd);
            dst[j + q + h] = new Complex(even, -odd);
        }
        for (let k = 1; k < q; k++) {
            const w1 = W(-inv * k / m);
            for (let j = 0; j < n; j += m) {
                const z2 = dst[j + h - k];
                const z3 = dst[j + m - k].mul(w1);
                dst[j + h - k] = new Complex(z2).sub(z3);
                dst[j + m - k] = z2.add(z3);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhT9_d;":f'Sc()MK9YJF\rC=fRbg@'+B%=HI6G!>r?M"l^#qBh_D;!g;IlFJfKpA5KOFC(kR/tS[]B8G(_$?MK^,X ]§HSTj;\qnHW'4)#pam/F0Jp`4q§8j;9m@«="NS7Y4EZqPa/JlJ/dF6_(-#=C9mR-Cf*NYfE5j]?Qjb*R2Hjm3CO\8 n06\ncNqr!`sd2KX?!R6-hhu@SoG1W7JV`fCmT\Wbi«rhiO`5R=d>0(4HK.#0Z.$§GBrnFZ8^2OB0*(Q-FdtF./.5] #rkBHoYk%W]*Wd]6%c_:##qj1>=$WEJ8iP,a865oD_L§mGVJtIeBXZB8.XJF9+MP_FO.ZLT«$uNhnBQP5;k3ZlpV$2 PtDF2X*!=e(mYs2enXjJ'NHugYA#cSPd629g*4BU8Tm7Mf;e.tf_>bB=Z$!~
GhT9$>Ar7S'Roe[3§(%k`jTVDfJ$9!LLOJB!p_`e)>[/HX"$s]'8]=,b'$bZ^[I8q;%aUf(BKA/SaUsh`_WC=?K;N8 R;lTZh§1m"n0Lj6KP?`so!]fOBHfqR?Y4gi`ak2«3'§h.k!p2Kn#5j9"ZOlQ,hZu^;Oh@Wa'P@TO(n.rZX`CeVZOaM ^.^>G0t;i.§pU/=%C`on#g1:%lNs2[@DcQ+e0Q?24.7O,G^%1t9o_]7oSliWh6YSS`p(dgA.;K!+k%BHS+o\$eE[$\ r8pYeJMf;JJ^qUNHMV««HU0nhPIS_OE«W1K92?MN>3f.Ph.4fh:NkmN'_CT?R?Ji_1[C0UDJ1'/[ItLkFC\L]+YO$3 c%4G8=S0repJg7g(Gh6-C4CfQj(SCh'«L§ggpR:@GqsE7Vh\50ZX5W'ZunSNJpT]pR:(e^ook\JoD3/r[TL!;o#Fu4 FJR)D,j/V>d>1u#?N)5J0)AAq[j9:IPbcrAQWG,@aqb+D6aIF^nQA>a@0uGl$9mgVB=P="`(\%]'"3_-=!7"@=I:]2 %N^sUF8P+ncPrS[^4nNC!-A§tLR0\*§§EW"pXC4bUqBMS^heo0Bg_JA1p95e%,I+3OuQ'>mrd\!`*Hc>00i?(/RY[N "5ct.(b9\LMEE*H8^5B/@m[f8V:SWtm?Kn+fLlKJI$%g]?§tu^§).%1bIf2HC*28bi2FYIIB=l«Lj7-oi:EC+[AW=a KichZp>e=(JRHd,O^PqW4\DjJgXT.X2U-^Er.knPEN1!4+)KhB8uefd[\rMce1e?ahHjKe8YM17qtU+%+Ukc7d"1-f %8l$0Eo0lTi3$5dGO"R_%(D=UL$b:CY=e_1k?7k"§2\QH6o;\YIY34e-e57+SZ0Ro';:Tk3pM^[a.7PRTc2II%Ps;R '[0Ic;Vn9KhIEV!eYoJM_AW`Zjfb!D@Z4Jp?tLTq)fA-R2][6bO%m"aqoun-IFC5?d'656G@hn[NY?J:=E?"EK+]M= Ru%L'(?W^Ob§p]L\TuK$ahM*Hlo!DuSh*P$5ACBi541lf0QVQjl=n(7#DN^/QXt>5R+BYp;tJXU25R=§-^N:c[;9Zh s'IAjSs6ri15X#BPtXV\Pt`WY.HdLHf!=ON2PGo.V3D;Mj[:E@M83:e\ZH2f0;%cNlR96-eh5:ik;9lE$T>2ZLW>)E /«fns`kB21;'D!1Jf7F(lhsKDUh9>s§=7Yd8/"+$Y$gXEX*«%hLd«%!0gY*r7jsY7H$TkJUH6ZrT\§PLU$O]-ZJ!0G rV[O1l?8l!3Xn+GOPBcsqjo]mGuJh+"j\D/kaG«Bmr.82YYP~

えいやっとまとめる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w0 = W(inv * k / m);
            const w1 = W(-inv * k / m);
            for (let j = 0; j < n; j += m) {
                const z0 = dst[j + k];
                const z1 = dst[j + k + h].mul(w0);
                const z2 = dst[j + h - k];
                const z3 = dst[j + m - k].mul(w1);
                dst[j + k] = new Complex(z0).add(z1);
                dst[j + k + h] = z0.sub(z1);
                dst[j + h - k] = new Complex(z2).sub(z3);
                dst[j + m - k] = z2.add(z3);
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q].re;
            const odd = inv * dst[j + q + h].re;
            dst[j + q] = new Complex(even, odd);
            dst[j + q + h] = new Complex(even, -odd);
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS^O_/=ii§;KX9`Pg6RALo>p8C«*fM(bMr%ugf$(oaf?6"t%\pC«j=`Fu':YX?;%chq=§f*1N1L(0YjnGo^"?MZ7% Qu/Xk7>Z]FBYr3*Da5SW0E`(Nr+N«!ZXaM]).p+G:Vre\J/§gE«N801,1?s@PIO[p[^m^Y*b*sQpc0ZN$r8mkh;@-$ ,#*j8!+]7-OP%S>A)j;R+/IR/JZtM§QS>+BoZX5hGdCqkfs%MJHAZ5C=EVB/=pc0LB\ctXkD#"/8L21nSs/qj)1Jj- e@T#uYi;[_nkR?Em§$9H)Whjog3%I1:Tp§7'kMbh(;8FceaTrpNc,UdZn4XI_!Sk`3caN@e`$7%a5Jq@#%/§HMM[mY 2anL#bTaL;,2Sn.CnDRBpH#S(3q6aOGd>uup2Vh/?p§_']Sp!KQM:_O,/XnIIfZUq#dj~
GhSuqgMYb8§:N/3%"pb0=s>3585Y%[MP.)0!)4-B%e:/:%7;bc[E57=/gZnds#m^3g[_)SC'iOW3+pOl3@gK[)1tV[ gllel#!jk(aV+\@P/a[§-]].§aH=@6);pc8RAU+H[S3k"Onn§SC@>F-7_5QElSV+gEX#k2Af:N«`4LgGCJ?GoBI«Ia P*2'G's*_:@s)_"e\qVe;N=j\GUs.>()*Jr)=l#§4=S1[1DX_G'_8aeX?u-BQ*Vo7B/V)PQmrQ5ggJjS2e'@!kNC$p n,3C?e?jm#$'/fr`?]YUT+iVlj4iQ#$@BoY/ML\:>u>X+3Qo041L$cEXX>mEH.6KX,mU!fg5%PQCUh`W_.l\N$nRhN XbF"CThXrROaCua83.L_F!Mf2lT[PKl@.!T3oHPZ6T]\N-*jNKZJhX^H>*fCVpUS4G%eIZ[.Kg=9/«fhYk==nDt2;= 0)L`hM\/?%l4J0F8)\UAs1#ch=mFs:MEPS)>#TKY6/XO5gCY+VAEXjC+9/M%o«rt"XUQOb'(4:X4*Z)U!cO!NR3MC` EX=\6Df.er§7t>gXPc%)j2X0mKR%P+k=`=_cCgB,>]kRb('E!Rk*9XeRHA1o2uZ?WX6KJV/RKXg!o856K%d5U1!7gi 9+>?hd,*ke85PM=Q*8Aao#[,§T1WfNr§MW#JtLtZ"Cpk?9V!ufs!CIr7j;[gSHStkb(=o@.rtuA`.6e;RRO'Nb=mUk dNESl4*O3AkLW8/?c!jSXjc%U«\u^+s)br9§4CN«'[Rpqhk3RkVu§BMD4so`2\+\N?)^HT*96e`q]eKGc6>R[Jd3!C I6l§)=#Ig"eHC*]m2-@88B§I>9VkfNI^aGH!/^/'E_Jk\^f5W4J/'H^?§$LD69u9YOojA§NSMR>]mZhO[@ot6,SrJ1 Lg3[2\sg9edH#FI*EIu3AjH%(lKuP-[am.5O#-%G8pNp3XGit3§!C\Ceg=(g9onOI2/q/DfpLm«V4uU«5VER[XJ$C+ RZ8ERS/kS@=B2a]I]Cq_]NFEMC[g«d\FOrZAj\)nbF`IfNFoGpI(C«m«),:'s-suaqlh*3_9Cl;\u,^5AK31A>SGSn K[Z.Wd%kidA.@_EHIB«*:RhOQS2mgFhQ>B)h.loO«jL"b-*,Wi«j.e2e([U$H!Q0`d]>BSlEtDDNB\in*^p4P.`OpL YM(M(p\IFE]#%6P4F()6Z«uhO;D29ibE*$.DrX)4Fn70EX%0Reos(FJ9S)P2>1'FY^Yf§[$Crg!«+c;;+8OVt5ek`Z ZSb#R>js:T^5fbBIm3q113)@~

回転因子は符号が逆なだけなので、w0w1は複素共役だということが分かる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w0 = W(inv * k / m);
            const w1 = new Complex(w0.re, -w0.im);
            for (let j = 0; j < n; j += m) {
                const z0 = dst[j + k];
                const z1 = dst[j + k + h].mul(w0);
                const z2 = dst[j + h - k];
                const z3 = dst[j + m - k].mul(w1);
                dst[j + k] = new Complex(z0).add(z1);
                dst[j + k + h] = z0.sub(z1);
                dst[j + h - k] = new Complex(z2).sub(z3);
                dst[j + m - k] = z2.add(z3);
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q].re;
            const odd = inv * dst[j + q + h].re;
            dst[j + q] = new Complex(even, odd);
            dst[j + q + h] = new Complex(even, -odd);
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhR"pb79+X§4Q>@`Ea6P'c§,C5gmT(0/.BS>aS\."BksC+$]4Y(raNHjt,Z=b`rfM('[_,^.X;WF#=«:P>Q17%C0fd RT]m9bo%rujQ#[e=Z]i?Q+BCMUF7H73"e1m>-=)I9oCVLDope6§]k\cIdDnu/a>4`l1+eFd2hAX3gBKX\o:p?!.pTM Q!o8GeibU=0d2*fi$k5sMYmBmF-3q+]-7j+3qaW@YXuq~
GhSup>Ar7S'Roe[3§(%k`g/*dfJ$9!LLOJB!p_`e)>[/HX"$s]'8^`Tb'%%R^[I8Y,jOAs(BXt\SaUshIOJ«!-%f)2 4HrKg!HBDPa`@K§?=\WbXi_K«aGIdc"lQ:dZ76!I>aaL='8sqUeD1AWXUGP.2B.Mfo`Due-qj=ich8eL)9jiOBVl,J ?BBcT20`opR6"Pip#.VR.7\1s6C,YDM[`Q+RIJYT49`XOT*dG9$@+ND2Bh_=lGXa@^R11l9T=eND_Z+g$aEtnq>'[- f3n^i26C:f_D>AFZH7_I,%mCTokeg\B6VWa/[/_BXsgj*>ZRK[R@R!pc9GjbWcZ=`9+F"V`eZjKCUhfY_«FX"N@`ce 2*tCSco1WpaMT-3UmR)="VOV5%A-s7%6g,"*Ul7SM46WpF4o,nh=Auh]s\.5_dpgk41«s@3dT$ijsA\if_WW1n*o(g Y(r+%M[;crb_QJbP6cO$5Mb+WZ3YFq`«!=3CoBM*+Rg8+h3«aL1@ZqfKR_VCES!=jecq9m#nLdTI6:hg^gjdX;\U2R 0§cNlmG4DD6lC@S*rH"2r47P_Y`+XjEX`Q/+*CIaW=H/]@R3'(]J7YMdi;'m`ZIcW2-h«2jX\8TTpm]#:ffM.7er28 W'uXW])/L$#\m8Lnto)/T2"RpV3DJ,?R"hO6ock4N4I7hp§@JWh\JZM7"it88Vg-A/=9L;R:F*4"pCP;(6tIT1C[Y^ j"ukdK0;Mmh]TaD%0ml\?>jJXBP_#Q*/"VALuMA_TKegX;3b*U%p_rB6E+F3I6:hCX%M=jj6u738.^N^LsV:B§I$9Q cuIr%9lSuH_7eQ_g%Yng=P/2(oI@oMGQ§!nO.o[8M+C+'7W0!FihX0/:moTWBHgDR5Z_g`+g)mALT4«X3qc6f;6cP1 e`:S9aa9OO?6@_F6X2\TWrH(;4§ad$_jhlf235(BE:ieFF7ANShLDP'Dr^1eFa+bR_1HN?R*N(1m%['(Wbh§0j$!#C qb%«cBkYDuR:5QT*G+F#2=5C4«ibVNZ_r02=`EEofZ]X>RP1;\bZ`9_[de§?Y%RV%Ih!BcIG@P'g['3$)Qj0$AK35m >VcJlL«IL/fVWhljNNTmI«ff?[P[LDI:+\0,4%5[#2b_VSu)Y6TAbh/9«CnB"asSkJV`k*?Rd§bgi)l>1j97KA\nR) (JQHG\p[:ClB-)8h!".RNFfl,cgA1?p%Mh7=a(-dWb]r2D_p4mrV*[)g251k(VEFDUgpRthqu+D0gY~
複素共役になるデータを省略する

 ここで、もし結果の後半を省略したらどうなるかを考える。 結果の後半を出力しないということは、今計算しているDFTの前段のちびっ子FFT達も結果の後半を出力してくれない、ということだ。 つまり、入力がそもそも半分しかない。 ここから全ての計算を行い、後半のデータを省略して出力することになる。 状況をはっきりさせるため、実際にデータの有無をコメントで書くとこうなる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w0 = W(inv * k / m);
            const w1 = new Complex(w0.re, -w0.im);
            for (let j = 0; j < n; j += m) {
                const z0 = dst[j + k]; // exists
                const z1 = dst[j + k + h].mul(w0); // exists
                const z2 = dst[j + h - k]; // doesn't exist
                const z3 = dst[j + m - k].mul(w1); // doesn't exist
                dst[j + k] = new Complex(z0).add(z1); // needed
                dst[j + k + h] = z0.sub(z1); // not needed
                dst[j + h - k] = new Complex(z2).sub(z3); // needed
                dst[j + m - k] = z2.add(z3); // not needed
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q].re;  // exists
            const odd = inv * dst[j + q + h].re; // exists
            dst[j + q] = new Complex(even, odd); // needed
            dst[j + q + h] = new Complex(even, -odd); // not needed
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSFI9l>S;§;BkK@WmIr'-NcD1_N]_S=mKO7>`+>2V;k_6;!NTf>`[j"G:Ir%2/-!):(*6;t*kn(Hm>M6CPcWrK§"B (!1_e$#DN'2EU%B']]C4n080e/a0=,'#pW*`(,DpGbO6N!MAa?%$§sB?c8[\OrIq%O'j%`LCXE>M$VEmA8p-sb5=HY /leYQKcDiH-A:lV,s#2YQ\f@I"_Dff*B@:KmNHrHVm7t§d^#.«m*SAtY1^X5Ra;AL#hdHJeb1W:-4F#L4tTf@§'MnO ,*rsRGpSq^od%MZOTVgi!'V3X1EA1.`B69BMM)P[N')%_lduN5k§VC8-IWNb§Oq(q`5*4aHk)_?e"HW+Z\.gslseec rTdNbUW4`UHGfd.`'ocHkU8ba-Ei]T:99Js§En-D"sRi.16\I6Xp3ZK^7nl=r]tU/9t$c^9fAh§fm%i^\!=]ElOP!V JdD~
GhSEaD/\/e§H;*)EA.TUN,?OWZ!jT"%`Pjb"4h«S1A!qBXK"Zg9I)*M.,/b#s1T^,acoKPY(20`n"/_!GgmVN«QUnu "V9_I+$j.g;"@jVS/5«Ns/Ii/,-n'7FThIR9NHjtXXE^E^1@oPOa9ehF=2]-P/W"s`I7qu)O)QGP%RQ>gXaG"(MKZQ AR:IM_%"Vd^b`AVh!3iQ-n.[B5_L\L)\uXL4YkumVg/W(X+)\Mj='(l=(jo[m-R:q:4I@Zeh@tpbjR6§iX«"sX7PrI ]«C#nf$ndP:'o81rf^%t6kS,*`/S]!)q]EgpU%_r[«>!\D«A/#lt2gu1!_I_cDn7pX@j$aTsYKiR;9=\Q=8@5]]rY, $X*pHda>M6LE[7]P``ftq_]!.>O1\eYQg^mf*Q[DaM^_XSoWN6IbO;>(;@6rm__nc$+=PC5AB@>DjZl%M.,^'s87G? [WL5'.ChE9d2I.c#3CaK§(Z6D]dIA'@S;S9ApLhXG2%Va4VW5l'45;0>ET9lDW'Qtm,?=rOtm?)Wj-tWhLNCQ19jal C]Z6Jmf)§@^NW'`Hjb#j)92Gk%'O(ZDkcb9*i\$Y8S+_nJ=RHKV)?MWBl6Uc1u2kq,k4iXIF+:`;""bI]EUYbFau3h O*jAJ@Z8Z\Wlfu;P1l2)bCjnK8oKJ_g'N7fC9Jf'nKPf:%lpNd2l-IZ;$8O?e/4a0BWK:-H*§U'8U/C.i-rqFdW«Kg d9"aU%HREEM4i15O^L>+27D3>\#QQDh7WM7#STO$Mc0dEIkZ;a.]Jr@gRP^+;T2o;\@f@LQ,5`)n§P/mJniqrW7hnI UEJe.>4)W:#Th25GYkkZepiAj(:Ig)]uol#Q1%PjUSs;b,*JOX_=[k1S5c+9"B,Oj'-TmqHKmuflKp\%6WL-4m«^1j W0efMeThPOH-]!OGDKui8%kc)eG7Fh/,7A`(YWK(YHf%K]FR#lr>B]!@q.I,o)"7=*DftmC-%!Y@lJHi#[,lH_tV=\ p/dPpVH"*§_SL\?[[3Q@gm!52'%Ks22qPH%WGK68RorXAI!#LY29=V%pnX-%(KCI«YCUP9.E`mMM-?DS40##?;9hf( ]enXjm"mKgkI@I.MFK98Z"P!CW1\n,nf7:«4YJMXWbg"o%GNu4Q#CEfj,4P@G[BX§CGp-=RZ""B2FjPR3,gl1cmb>T d0XInE"§0QeDrCb@GnM!'$:4HP+W#:HuXB5e_]§%WGBWfCg>lNrSaTEY_oY*-PlL+0'6k?YW"«W#l[ckGM=9(=r)[F ndHo()9fc9oK0c.I§JD+-:g97`DXP>R5.ZE[@?jafh;[V4H(§fB!h@)n))5^[g=8/N99BP]DX§P'5](T-Kpg)?7%5O 1%4Yis)eG*rr~

 入力は4つに分けた真ん中より前の1/4と、最後の1/4、つまり、h-km-kと書かれている部分が存在せず、出力は後半、つまりh+km-kと書かれた部分を出力しなくてよい。 実際にえいやっと書き換えるとこうなる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w0 = W(inv * k / m);
            // const w1 = new Complex(w0.re, -w0.im);
            for (let j = 0; j < n; j += m) {
                const z0 = dst[j + k];
                const z1 = dst[j + k + h].mul(w0);
                const z2 = new Complex(z0).conj(); // dst[j + h - k];
                const z3 = new Complex(z1).conj(); // dst[j + m - k].mul(w1);
                dst[j + k] = new Complex(z0).add(z1);
                dst[j + k + h] = null;
                dst[j + h - k] = new Complex(z2).sub(z3);
                dst[j + m - k] = null;
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q].re;
            const odd = inv * dst[j + q + h].re;
            dst[j + q] = new Complex(even, odd);
            dst[j + q + h] = null;
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    return raw.map((v, i) => v ?? new Complex(raw[n - i]).conj());
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSFK?#Q2d'Sc)J.unp31Hkl9lQX`XWOB2nRjP8.G#:[A%F1UMdphL89`=EA`fNFe/WGu3RIe)S]KR0F3X'Tc)TSN2 mhhp#R)_DRn2X2a@_.,?""6AO(!DPe#UAaPPKs=OJ*/8m,9"SdAXj15U')fNQC$J%?:.(51upKcq_9Cb«m+O:)27#P 12h\SR:Fuj*RMjIEki!R5I!4"Q>A1JPek>X2«!6DJ;C=«DPsYG82DAC'l]5C*"Rg'Gf2o"lVX>ADuj_nj*hXjocAa@ OD*Ld4:H9bN6udU>%DJ[bm'Op7M=*aaDfduWn4SZr!jn)_r2;s*:sOQKuMZEg/XG3!s'2s(^Tjl:M-MoL^W§C3->59 iQOo[1"N5pJDD+`JluXn;TdZSU#7PH«+D,D«J_`uLW7X!hO«T8B)+_Xnkt3>)UP.H0-$`VDHhL6]>IP:)QN`'Vft"p 1[:5hWAJWs«BWB6YP7='=4do:m6]To'ig`/oqg@0/sXhuMUST2fD#HU?@D7=Wp[0(*-[1Og«!*1gHUsr*qDEQ]R§§( «XVK#Pgt*T35\gP#9>m)+__K1U#'3^:A%r!mH?1=hE§^Z%§qqU5W«bkioZ8>J$NE8gCj7d_ZnK.=$+cVG+-['/l!b6 G)_QU48+Ni\8OBrb2X%3TeIOc>Cp`6Hkmk!PO`998ER+V,??gkbCc!Z9\EbbWldsl3M#pUKWHl$#*VieK!Ve«B+2=Z prGq2C,8KRBM"%jXXJ[jAc«?_:,H_u`A,2W-«:(:_7J3A?\DI7eYWZP%5>W,5"N*(Zt>/V1/b9r6:b@L=1JAm"od5: "FMX1AZ#et=:(KL:Ir.(E$Kf(k92!ojn7Ah99L)1(6rg663!5aX«P?62DcOd9e6%?lgsFL=6+$,Z§"/l35YLk@!,aL ~
GhSEaD/\/e§H88.5o9\n7^cbffJ$9!K48§>!UDWp)>YL1«`PkE-5,E2PjQ!MJ)Bcb§g^D3CESba]B?_KqMZb$8PpoD (5YV%L=R*#"Nrf/k=$/D5HB`p#k§)rHA:?X:f.rIm«t,SYW\!9O2Tq/nZ«')JJS[q'ZW!DV.!N$TCZl_d_.q93«q?? ,k;!Q'O@F.'X:=qCMrrWCF#_bF@.+sHsO)29(F8@«r.]OT^WAE6:DANQ?@mBK_FbWk2HfpL1E0*7YpsXd`0%0gGP1e :Z$Tf-I>SQ,B?CY7ZO*Wnh?*o_'Z=5o«-HVGIaUSG-§[u.CY_glEct*2T@j@OZBJj0!d#f;@,3M!b\[`\[%0+NQLd` 7D#3^LqJbVc0b#He*]>s@X/(,R«]`.Z4;ZAAhOKiM@3Mb0#5#ka5,4,b_;Z^«u68j7H,]@Rp=JA1Tbpo"$]sVqf+«g SF5YK_@=3«NUd]8+a*(fhnR(@4dgIn9#_)c9#HU5=?c>T[Jn@o*A36U`gUgW:)ChiIDk(Q`-t>S(Qeid,_j§aFMH7: 0HLLt«1la,3«\hrM`«g]cV@?I$^j8hGOP#"]uAFX4#BIZBEE*"kREu^GO]4$*_F`E`j1d#[aS]b1o?[!c*V0hVE7ib OSEi_5:Xk>TQ^l`?6E!j3«ToT4^/MV(aAT2«LI5['"d_39H\PVoO]YNSa\#"p'/Jr>Xf9GRq!(Y6i,[V7JMhP_5JWt HW_-*X/X;q@4=V^c4,\*:R`6(0;[tk'Hb1$#+scR_01^E0Wm!FAZR«9]_F@C§r],7§IAGe+FUpQadd7«S2Q(a`EWW: G^C_e"a9up*o90hW51$:C/==%Jtso1\0#f+4Qn,:O>h",ru0«:FA$1kV;i)e^nHk1E@lr6,X!0-lNJh"M«Zhr'*gc7 !D"^:nlG5E*BW/XZ;aKFpIm8I;+K\fneQQBSd(D17^\_«?9JCrs(-,pL2)WQBY1gdb%C-B2jn;E/hTmfg§AWf1Y\t] ZbtRub2Xt5f'/ckU)X7dCk/O9ReY^/S,cmL4TOA_`aYIeP(,,AGRSq5@5EVfL)8nRN]+g^:(MF%@L#9«.?/%(Nj!ft 0NT:H^iJr`«6*`P^Ef92ng*g0c#Hj#Y:g.C]p%_DVY3?!`un#F7eNLLld@BZ(s?K_A`71a'n3rA;Otj*F`htpVW2Ep «cSo-e5%$f«K«)dl^NWTGS,SEVj6$FdE#kP=0oWY)P!tD`4DYE!QV«*7Hu3@C$;]/-[aM)h4=L2YR\@5,O>D*M9?@l p.i)aa75FKdttWXH^g,Klp,[-$$csUs3S§D"YRYt]u7CL%3.9qNRtJXisZ]cG.H8"lg%;VTQ8AP0k"KH,:>jC5@X«" =@CDS["'71CR1h7VFe)m-OFWB[3ujLD4\R-5=9Ml#IUh(23.GUTcBOBD_36ihN25mAFa8sBp3G24NIYqf)*ft[#«G- NqJIJI_uCJQG>%1;IBCLBR[Sn7-]3#Y0*2Q«[KY(76/6"rrETl5L'~

 出力側は複素共役になるものをnullにしてしまい、入力側はデータが存在しない部分は存在する部分から複素共役の性質を使ってでっち上げる。 Complexクラスにconjメッソドが増えている。 これはconjugate、つまり複素共役の意味で、自身を複素共役の値に変更してthisを返す。 conjは自身を変更してしまうので、z0z1は複素共役を取る前に複製している。

 z3dst[j + k + h]の共役複素数にw1、つまりw0の共役複素数を乗じたものだが、

\begin{align*} (a+bj)(c+dj) = ac - bd + j(ad + bc) \eol (a-bj)(c-dj) = ac - bd - j(ad + bc) \end{align*}
なので、結局、dst[j + k + h] * w0の複素共役に等しい。 つまり、z1の複素共役になっている。

 最終的に求めた結果も後半部分がない。 このままでは検証ができないので、expandFFTResultで後半部分を復元している。

入出力の実数化

 今まで、入力はComplexを想定していたが、これを実数入力に変更する。 あとで分かりやすいように一時変数を導入してから、

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w0 = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const z0 = dst[j + k];
                const z1 = dst[j + k + h].mul(w0);
                const z2 = new Complex(z0).conj(); // dst[j + h - k];
                const z3 = new Complex(z1).conj(); // dst[j + m - k].mul(w1);
                const y0 = z0.add(z1);
                const y1 = z2.sub(z3);
                dst[j + k] = y0;
                dst[j + k + h] = null;
                dst[j + h - k] = y1;
                dst[j + m - k] = null;
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q].re;
            const odd = inv * dst[j + q + h].re;
            const y = new Complex(even, odd);
            dst[j + q] = y;
            dst[j + q + h] = null;
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    return raw.map((v, i) => v ?? new Complex(raw[n - i]).conj());
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
FFT(src).then(async fft => {
    const dft = await DFT(src);
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS]e9lGVK§A@Zck.$X]>h"361a-Sm05/d$j1teo.P+Rb$B>)!Z_.RS`jU5%%f>D5HZ'/@"njdMpL)p'%'BtrU_"Ru 1Z>12=MSbH6B[m0@3tV$"_R#gXhjNYiuO4887@-PQ@i`M>:bNV7`j]0,"Ws@LjLG2l41W4Lic-s/_FHM6«]0Z5n;4\ oGIj?,j4;BTYRe7A6585Og2nQmU2H5ON!UH"@^F89e"Wf8c"0,fZY?Fh(tPL7h!;4p0Y^6aG5Y(P3XH$XblI:S#«e= ^oH'h=@Ocue]SS*/7t-#,e'"3jZ0s>;75nAam3V0YD,B`«:ZM4q-rI49X8k?Nd>(R%0aMRg]1k9Q-_§jYC!74;uI-t gBsCp_RcloC?R/_D9/F-'O1Pp(?b>K5H@\[(@2.Ng;«4s\(p5a`P.D;qT!«c«DHNrlS5VDZ]VSmA5]-^^>CTk*VuM2 [B§X«]7]eZ9MH8n~
GhSuqD/\/e§H;*)5oBJa`jR)'fJ$9!K48§>!p`l«)>YL1«`PkE3YH!sPhuiJpYJc37(mj02?SePhVE6scgc5o)§G:\ \]R=5`nlNQ$5k7S9!V'q`)j\i#mV§*0_/dCi0/.\A«IoPQ/7?"§*XVKg%aO>nhW_tEs«3=§IDo2ccJssZAJCn=9b`- 2asCfN+SU8§9g`7K$OcC5@R§X.R;UP!Dcc3MQMOR'oC_n3#RLLf,§1@§;m88/5p65cD)OVDq##mO@fnGh?.Wc1UTS+ r-\G19:J0Kc-Bir+qf(uBS61j85D-_(#.f(S=H;B[(su;KS"4`3A3Q*,L7tccb9uO.H37k/ih=4)COAafAoPN-F;Ff «QCk?:a76LQF;d$+[-0E9m`:De=$./4F;W$F.*R.G7DC],As\k5kRA'6bDQQY9G>TDGjL%T4s#§C@ed^ZlaUR.f$gD +BlZug`Xl+!iXR4hS4b_#«$G"j?iQ>["`:95«m;`nc§.9"3n.Pg03p+dlFa=T1E§dYnM3S=[eo.$/t?-4>+f7BFdF> Y/h7t%N[`=§IFN?`2:W5qa-GBLGo`nh,AFjR^hFEN8a)i]HlOf4rdA?'D"8s0e]cf2D(4em95aW7Hn!+ldcXL+:`SY #OV«ooWB=\CEf1!9`S«[MC#TZ+[]m@j:MPgAlmiR$06`'hHck_Q2VWM?BR!r_[(:#3+h$^T`7`Pq*uooN@#f2?s/"] ;ji%Z@SS>Q)\f*G@A_OEkplDW4+BU@k19U;S!"f!CLah1?F4U=rSe%oV4]+O)F-DM?`-N;l)s0U/ef(BiPFR)%9«h] !r1S9K-F-E?3Jf=?jhRb/Z>r+jX+)rRp_4'Csj=k,mrOLU_a,?0>.C?1^klp§.s§,9"Rj8Lro9A"(O5sTRjla'cfEe gNr§69Z9m]=>qu]AAJL+a3L0@S[\!/?B`pr"hgo@E«"tTSlog[f9PL2PMP*GVqDKMojRQd$ID1^E\30@3YYe5F'DA§ W1.u!ZAU§f:@op#3TW*f0ZUaE0Z!b)nV]$BHk7UO-tQ*D%\\(9T9]RVl$EM99a5)e,"F$T=Z,:([*ZGMoO:(f'uSGd Vq`U9""iO-E\8YZh0^")q$:`Yj8«p38I_kuYZ8sN%J)e[!-#GRp?naM5u;_uX^#a11me^T^E#?QVPNSP8\]H+W83%i %\GE/G4jQ_SM(KtaiC9H](«%qD#CO-AI/-^o"9bnc2«"i\c@YjH"G?A3J$_KN/Sdf]kq«i.ZY4B%V+/`=S6E,h?(!U *dMgOKis6lVU.M_qDSK0=ShlX6*t>-\$R#mSaSG)):$H'§XV!@mZg+[4-:)@$r?#C«)#)!kqW:?NP$-iV^g«UrqMbM FCP;+hD6."IB;>IIUpunXK§Q=;WJE#F]$HRmjlmmVTf~

入力データを変更する。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = new Complex(even).add(odd);
            dst[j + h] = even.sub(odd);
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w0 = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const z0 = dst[j + k];
                const z1 = dst[j + k + h].mul(w0);
                const z2 = new Complex(z0).conj(); // dst[j + h - k];
                const z3 = new Complex(z1).conj(); // dst[j + m - k].mul(w1);
                const y0 = z0.add(z1);
                const y1 = z2.sub(z3);
                dst[j + k] = y0;
                dst[j + k + h] = null;
                dst[j + h - k] = y1;
                dst[j + m - k] = null;
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q].re;
            const odd = inv * dst[j + q + h].re;
            const y = new Complex(even, odd);
            dst[j + q] = y;
            dst[j + q + h] = null;
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    return raw.map((v, i) => v ?? new Complex(raw[n - i]).conj());
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS.=>>)mg§;B$=/*:igDSoNE!jZ]4XY5PS^+CP^2PlNo.7r7]"ol2umY[__c:c4ek;tII9d*8pUZj4$p.q,Dg$.9B UHu%eTo1IKHBbS54^'QofL8§:V=1JilL79MM#A#%KD>]CYZQFA"'h7kXlVU"Q5X4/c]%$V^ER1N§\LOeI@LOTF-skA [:4d1$0.k^CRhYlq@\«c=nfd7gsSrp5t9'957Y'B5>f2;BbrfA8^"WIUdj«$3et$56*Z>W3d-Q;64:^SP_\RWKY7>R O4O:(.sa*C5W-.NLmbiHF']c3,Ou1]q[rAS__#c:lW-QBbE/c,3Mc=BVUi§gZ]Y7t%«RRu~
GhSuqD/\/e§H;*)EA7*;N,?O/Z!jT"#0""Z"kINm1A!qAXK"ZiF«o"k.).2"J)?B_,jHss0a!7PhK.!`TB#GUR-#9= 9W$U3;g/;4>kjI0!]EUgHh-nnAnWAY$I9C$:Wd4>$91(%\`H«sO«:F-o7B'gV37o\'Ih«@n09U0i27jW7I4\MrBt%\ gm:T);?kaWS-+i§Zf5"-^§_g,M"QGIP"io)K@EDG4EE>Tb4+ZhNLCiH4Dl+]/Mq8+aUk@3l'#+@V2%b4:c@itfVoa: 5(31\(=5n,1CRH39[B>IE\@\h@1u_Qc*!aZL=4NTH`nb2A[e)6r:;W6@Z5U§Q-=4h[@)0Z5mqFZ\:[u§KoQS"3oi^W U0UT*KNS\"F-#C;l"iq0kF-O%^AbTf^)§Me%dA1oan7H-YE#i5T«k4ioYH`b':sB(XNo/)iha'[fkT6%]g`b':j!>K kr#Z:AE4)odsq;k*kr`[]FDumkWq§B[(2NoK\o2tq=^CjWK1mJp.EdK[9F^UY+aO*R.qjmf«PPSEt§ZGEXgb*8Alc9 Wlr5t`u>TbR*BH(]S"6Ngtf`Ws8.hTJ8\j§2_`4S;g8c#borJ"iK:^Be!T6shR4sNU^kYmd(Be30m)^g5nZ\1kXQZh !c]$Djs:;uW$];c54CG;LiJ3q0$ck9T%'pU.21D?-+t=_['fi]_HoTki+7RgDD\6Y2ou"O9t_2P`^bpu**$-«=§YMS a«Lllf+!Y$Vbs>0/*ePQj9KTn0^#o9Yb/fSgU6QURj]2EUq_/'8-kqL«=YB,b1EVUdWAi3=r0dGo?,%mRql"Q]F45t rZ)[s%!rS[*Q§O/_R?0#hNiq)>98Bu\maXe(b/\@SgeqbinLk§!(_^7ZD.t>#?1JA2QSm;5%WqO#95Q43[p_sDY5pU gmY4B«*-GrRH,Q)"$9ST[jjK.WRlF?Xq"+C«B[##J)§0;S"skFMkq`N@n/'X[^'=F%c)Sh,oq-=lDRr=WDh(G(G7?[ @«!'r§GT7lAMqOACR-`ReS%]*[G,/i93o11;nFWaFJXms"0b6K"J1o4!325-ngGE3Po;'ReMnt[5^fg,^et$%]nHKZ f"JOU]>_q!=G+da1$iitBXg']2qsPI@`1/Ul.MZu6T;G];\Q8p#jN#;N]U#>M#WBEGpGuH*Vsgl(L%]f@8k?(hS[Uo \qccEeufVU+L$Y]YKRS2L\nrIbLE_._[>;o2`us'p5EJZ7hOH[qE6"@Cm,]CB,7^n^/o-WYG«(Wa'?gsUlGkO+D*lV V"/c%/=J#bQAiuK9glsLaf\#"]=Z_EHHKofD?4^aYrC^DW$MB0O'bJPgD!#TpAd^uhUVW\rE/YmLLJ((cU§E2ZZ*ok rrURMP"Y~

DFT関数の入力は複素数なので、複素数に変換して渡す。 FFT関数では入力の入れ替え操作の時に複製が作成されているので、今のところ問題なく動く。 続けてdstを実数のまま使い続けるように変更する。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = src[v]);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = even + odd;
            dst[j + h] = even - odd;
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w0 = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const z0 = new Complex(dst[j + k], dst[j + h - k]);
                const z1 = new Complex(dst[j + k + h], dst[j + m - k]).mul(w0);
                const z2 = new Complex(z0).conj(); // dst[j + h - k];
                const z3 = new Complex(z1).conj(); // dst[j + m - k].mul(w1);
                const y0 = z0.add(z1);
                const y1 = z2.sub(z3);
                dst[j + k] = y0.re;
                dst[j + k + h] = y1.im;
                dst[j + h - k] = y1.re;
                dst[j + m - k] = y0.im;
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q];
            const odd = inv * dst[j + q + h];
            const y = new Complex(even, odd);
            dst[j + q] = y.re;
            dst[j + q + h] = y.im;
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS]g9lldX§A@7.lqn*,K2X.g+`6$!1q-^NSa!u.FK!$`§s*b8G§YM%]t4na`lN#>_§@+H5!?TE«jN[§DHqMqQMR/s +P`3%!R!e^Ej:pLJ,bLFZ@r`SDW(\V4:]+Z!5I>^,V^tb]fJ_9J)r2j"j'c#5+G,§7@-T55D7>kdm(\h[Phs«Ji5:? rl%E@36QI«-![Mg6^6Mn§8h!K#`qZu48?H`T9K§8"O4CVa$f7^M§"ZEdLZX7M/u3B1@]@o7;«_onUJ52qA*kWa^Kn- 'ZLElR#tB-p,-#B^Mg@AQnmO«'#UO61%5DJlR8dnB[eNYp=,_[lPW$Zg^0a3@58oq:dg[=5\F$nXDl`%YnC`4Muc21 #Y#_3!tK2@Qb_fM9XqCqrR@E4^s^,M)n0lCpmi#\§tZ«U.6k8E.IJD^>p"i+W\%u*+qeP?QBla`G3j\Cg"h4)?'t3! 8W/8hgn'Wkk§-aD«UA:3[.,S!F*K#A-)'El2,C7ZLA«/Z^:h]1$p?B6_g't#ARr@:ZZq*R>*RV!))[]?e)"(0G^foe «pV4!r9gc`Bq\^W§QlVW!;/(47?X$Hg§p@*bb2«IBeJ_a-7=_t?tri@kD7WHlf^CQf1«f%M6Dgt$s_OG?(fYaL+Bt@ 9$kZ+eF_R6_"n]:G@2O7!h!2H)J_@[2/P$kbSOi]+a/6b/7\Oup.cIIUY=pH*)thf4S:P[i4]$N`n?%*fB5t"#l5HX MBlh;\>q]l2M;LYam(§#)pRBTaJ>8"n^VM!e1KJ3*SH403P>G;W3:[#`2_N*3RWE"ZERlFMW!:Fm6=pqj44[$?4NrA NnUj4§a!h!F%QTKGs2^_9%=ji_Qmj1FXA[fL6`M*2`pU/UC9«=bL0?aO§H@?Al!1;TnRT]_KRq*Fk/`b!?7ZDC[N`B Otc7EjSfZFrZTKe:55$=Vi7B/f9miA+"PEYAH~
GhSEbD/\/e§H88.5oBJa7^cd§fJ$9!K48§>!UDct)>YL1«`P;5-5,G(`G^J1HgaKtU*M3k%4#t=GL%;%]-C)pZau)e =VX'#B)WZNf-7;@0$$(,ZCq§pc=.@44X4=[#^hN>WXD,dDO',KI2pY,R(E[!gF=U$l?Nd\:1khGK«cc;jOkl>pY%CH "h:PL.PGHIP:Nr/';"0h_8[!m'9mOa%7D§rlNY=-_G?HddZld\$r;@(Pi*\F=^S,9d§ikaCmb4T(p$#$(0,ek[#$Hc $r-?bl%cHj!];er§Z^Rtm.AC§§#+k$:#I?VkDC6b>Bj%RmT#$oEYH!b5MM;:;ojmsM4E+1+'GJ284']tho[f:Q_)«n Fa61?BI6[dM:9p3Yp\O`E$"4Vk7Vp2^V7ou(o/l«D#@R(E8ucOWtVGJkL*C"mt3*0PJ\`NReiF2\?%PP),:!Je>OSJ 2'#(hG_mC%-+sHHj«§mO!T3TehH)/:(o^%8h«%"K§7Oc#f:+j*(*U§mfHR471L$IW=9Ggt`o9XVpkC§GT.=rs;JXW# KVi`X*q=4=iUMX.GW:^T;6UFQ7;hSNh>M[ndG\«8,hHQ2?YAFWRB:\\Cec%>5H)T-`dF8_(b2Y'Y$?§0RY0q_§HX(- G3DH\q§K%P+j:««R#=Z=]=d;WAcT^Yh+_0%'J,Z?H(?:D>:I7RU9qE.)5t4q;%A?nm()/Dq;4L«]:oQ@«+m4B6F=e? !3\F>]7d(:Ts1[sOIhIMV2s§I)H?0X%>C2,!l/tS[a;1Jh$UeGP_BL8rt^7oV4bdF)F-DMh]PS5kuMii]POO3#PEb, ^\$LTjpJF%k6CiI5-GSqK>%N>J`$h;8RKhR5H'0Z:^-H@Vf=W-Z4opDNA2.blbglS.-+7TaFU^/V5Q.!$1%lW+=«9e $-(1u+G%«6'j\HQPb)@eQ2s#`L./o]K(E.qNA"2B`N_BTfl2EU(NjQQ%t!,2^§BkRoo>Nt'(hWq_iDoBT>DBX@T(\6 *g[)mNs!N62:#6c>f_ZW/I,erONh$Q?KN9)5iq$@)T/§]O«J^*RDS2t,'LWB!T%h«>]i.)jPjV[eg/D=+c9o$;*h1K ]ZHNNaO>,XMtm(+)H(RfPVmi9`Q!c"kQSrCnem.YPXV.?dqXaB`:BdjZ:7Do3R>_kB,0U+YR[7p8">q+H5«[n%K$\` iKL*\^T2q*-Q75W>02:1'*k#lh,[Z9d;T=(^c§$'«86AXYah5)14«n$8a7X5r«dQlbt$M-+WPg=3LQY9m@kEe9R"Qs IV:*aGeXPQ"T§/:AXul/#^56J$l7TC@*t\,EX(->GI)2/olZ«S:q=US!6-#7oD8+-8ZDS3"\N1iIQ>'O§80[dI!BK6 F_s,:8Jl9/8oI§foRnR[A3$L§%NA§_a)16(*104jIa:>4%]g@DHalY";nFFAhW@?8[(a'_q228Nd3«CPo§*Moc-qoo @>sRQLW%B`]7«T;)8%+DaO:#!1U#t.k#P§)W#cl/'j?tbNc7RVOIj`D[%,=`E4;p:«[GneH=4.EGE^X`WK08.r(`=D :^38L/,;fdrg2R+(;Hj02§Eo!i^ajU$%@:)FBhW:~

 今まで複素数を出力していたところに実部を、nullを出力していたところに虚部を出力する。 出力結果も仕様が変わるので、expandFFTResultで対応する。 out[k]が実部ならば、虚部はout[n-k]にある。 k[0]k[n/2]は実数で、対応する虚部はない。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = src[v]);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = even + odd;
            dst[j + h] = even - odd;
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const w0 = W(inv * k / m);
            for (let j = 0; j < n; j += m) {
                const z0 = new Complex(dst[j + k], dst[j + h - k]);
                const z1 = new Complex(dst[j + k + h], dst[j + m - k]).mul(w0);
                // const z2 = new Complex(z0).conj(); // dst[j + h - k];
                // const z3 = new Complex(z1).conj(); // dst[j + m - k].mul(w1);
                const y0 = new Complex(z0).add(z1);
                const y1 = z0.sub(z1).conj();
                dst[j + k] = y0.re;
                dst[j + k + h] = y1.im;
                dst[j + h - k] = y1.re;
                dst[j + m - k] = y0.im;
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q];
            const odd = inv * dst[j + q + h];
            const y = new Complex(even, odd);
            dst[j + q] = y.re;
            dst[j + q + h] = y.im;
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS^O_2a+$'LhbD`EecDC+G2s+XSIup][u[/f\7SW"``M*9tl)6:OYh]!.*i9lJ@ipZ5s(il,-/!$1ba_cHFca§aY[ [V@'gWt1q"H%\fh@#8!=\--bOhFQ0`F!He.)=L:J4I!UV;!iQ6MZ"m=ht\4r\VI/F+aa^qpPr15**.7gEor)m5N^32 T§q1P:HK2dR*?UT?:s3T`'_[\C@idaSpK-=c.TNR/BB?JXjD-d\kf%6,GU:C'RhX0+7s#;lHe_K\qR"W6Lur(«;bX\ 9VZ1k4_L\`M.RSFWtVS7e)1SX3kfk~
GhSuq>Ar7S'Roe[+H/IFZ)u)"2@NU7U"G[)^q'tJL->a[W_.A%8"pu[nu^8ghr.!:-.1E")+\«0I/M9LkO8aOX-02T 'J'mG@"L9ONs*Wdg>k[«MVkFmJd«G)qE.(5.B'0PNfK^PDqU8mpcIL§_Y?lZKttkX!K)2TU,4DooOFK[;L1/]URZt* .M@#X$]8h`5=[8gc\>YQfEOnr")Q%'RVs)[)^IWl@ATlH$4>*e/6([QA6E9«?0Kl)M2O0WN6)tb]gLk)§2sLogSUD6 I/)iF5CeKWeAs(I)6#jQjC'%gZVB6-2S27fcXi[\fY/8§UtfQ7X,h/\`?jZcIq6j)[*PgON`89'P/%rNFJUgh7B>uo T[?N5§Y`D=D0''g§'W_dOlqUE@D;_foV+%9\8]FU)N5V>gg@b1)La5CVs%D7§hc94Em^DLH,E;/mBos$mmi)Sl(P(! gsR_0-nk)t6Do;1nJ8hZah/-5)';-Y_N]=JSp8b!s694uQUM$gc1`uj94F5OiiiVr0;\T18-2Es_qtoL=R.gm%tmk2 d%bCe!Ba[3%Zf;e%T.HVR6YujDsmY:_c#H/jJ*gD'Y[>i9/PHEioqFGP$i5Ib=^pO%Z:L)«AUbGC"3VkpK@iaGd9Zn /'V.G])jdl§LX/$!q>c3XbfIT*dYGu9JGfYG32Mf5jf'FmA5p«gn+Z7-§QbW7c8>(@hO/TptlcSGf§8§;*g#!1+a§= PF4eT;Mm00)\s«_/i"=uV40:Un%p(LRpQ96IZF+uY2-h"dA!jjnT,Nn(^#X-\H2§Jn7cqcp'nITNQg90JB+]0R2dW) pcWLV$,E53!'1ZRJKX1b>4/N«qc;m./":93]Ql=Ah,>1gC$Tp_2un4D2feCN).«)#!%=G,6otB`@fop,6$;YAPc7cI Tc§IPME9OWQ*o,,oe`\Mc@pl]\E8ano:;#XO^Ag3Nq_lN9!E=/EBWb1#DXDQ",.N2ne-]b>/uh@*`\R4+8?Uh^=@c§ lk«1P(]h4^*gh)4ji*«ml"§12Bq(D)D*#cuSUK%'cHj::.*a>=h«Zcp2B0Fsn^fdHRHQ1"S6bi0BNFHklku^Lm«#7R FBZ8N"o+9G)ARUdVVY(kdF?gL8`c5W4i`ns3.JkJ3ste5[NX#moLpd`A_\IM:!#eHO:8*t/iea"6Tlk/7tKkS;@kno [6=N!/i-8#7cEisCi;gigk@ipr1C?t;Lsbq0HN5^d;e@2ebIFB[0M1En/f$U@WPmBVrdkPCQ*nZK56A/*iha9Fk`iL _FgP%a'@mHF>*HS/YC]:;!u\gK3aqG?Ng%tgB-YGMR9#uAaP;$LXY9]§[cU^_RnZZV(%/abi![E>@,lq2Cos[Gm5M[ !r:]^P0aUi`"NI=fM".A+a;QFMWL#C^DS+.[2L$$F8)IJV\I«§os!/l]:%[6bS7hkrX1ojAcXA]XOPJM^;bWZni+U. _`0.[)QQe=_nU[$_,L(a~
\(y_1 = z_2 - z_3\) は \(\overline{z_0} - \overline{z_1} = \overline{z_0-z_1}\) のことなので、z2z3は不要。
複素演算を実数演算に展開

 回転因子も含め、複素演算を実数演算に展開する。 とりあえずデータを読み込む側。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = src[v]);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = even + odd;
            dst[j + h] = even - odd;
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            // const w0 = W(inv * k / m);
            const wr = Math.cos(2 * Math.PI * inv * k / m);
            const wi = Math.sin(2 * Math.PI * inv * k / m);
            for (let j = 0; j < n; j += m) {
                // const z0 = new Complex(dst[j + k], dst[j + h - k]);
                const re0 = dst[j + k];
                const im0 = dst[j + h - k];
                // const z1 = new Complex(dst[j + k + h], dst[j + m - k]).mul(w0);
                const re = dst[j + k + h];
                const im = dst[j + m - k];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                const y0 = new Complex(re0 + re1, im0 + im1);
                const y1 = new Complex(re0 - re1, im0 - im1);
                dst[j + k] = y0.re;
                dst[j + k + h] = -y1.im;
                dst[j + h - k] = y1.re;
                dst[j + m - k] = y0.im;
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q];
            const odd = inv * dst[j + q + h];
            const y = new Complex(even, odd);
            dst[j + q] = y.re;
            dst[j + q + h] = y.im;
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS^PbAMqd§4Q?hMHN9]`(mqC6m«dSMUY(k/h2!@Fcd«23!lJDd1lHK*CBhuE;EsPK?h/OB5B[@aN^!pY/.td:;QO0 S`IYCD'o1F7fl_l7jEk=)n>Zn,nO\*f*%>YO$q,Z(:_W@D%qVB$rT'XJ@W0@i#NWWP$6;a-7PH5E_hE@0L^3jU$5«' "e3bc9:iC*Z28[TF8RF@k/`iA%sPP\;9N`l_SnsG3eXBl4fYK>b6!?%X«k0cY1mE>lph1+\;eA7A+42KP4TV\E-SX] hX?!Q3u6=j0V+!K.R%;§j+Q+537/>QB$A6,B3A%E7k+.dkt7tFm#oarhh[71«RIgW9\X8V7q/SZ)sOBQaQXDg8[;F* Gj""sOoKMKenEGjO=%)+a73!fl=oOYrVNC51"DMbLH_\hL74dCX,Mnl;u%LJ.E*=Z41@1>o*UP$ptuY'=^/21?§hi% 0Q"ij)c;f":!W$q=?0"'kL>?@iI1#%;KC"?i@rB(,A0hS[(7'=@3"LAYi09[pC0HiD,p~
GhSurgMYe)§:NH>+;TBu]IR-*d@uHV'L>q>!Lfma?0§h(1lB52\CP5nbMIjLpWS_`6qNW6ZjYMiF3^9nT§F6k19X>? b,XB.2>:`o?"[Tbo8KdB+q:P:l]csW*\?l-7c74LW\-§IE2rhj+':p\[%(tEh,Zg=;9uS!i]JGo§sO^«,u:uV4"/E/ cQGn%«d?t=7]qT[,Ws%Y_i5bP->;Lk%C2)Wl\7h5iMVQYkm3l4_:'JF"Z[EFg,I/'FCXL%M:8)VaVlMj;«>80>P«p= W@«6ZI.t0)TVAm?`+\,/FoJ%t#>mlb3cUA«igQAW;gQ«RcW-fnG+H1F5FS#-.;§H-RF=4]1Q.D0+fGb-)mq"/+u6FX O;EP*H9mgY/:)!#HQk[FB1#0F(Tb(AqRh,_O>'k*oXfe;`YnM@-Wl"^V.g[X%BA';O5%«^f$CY\[9E=2[V=W[2JU8* bmpI"^#0@kP$W]]ifCk2^pes(lpFC:N?A:r[Wt[jZ`oAelcgG%M\*=>[R3bP9AfRkK/#§A$-PM4Z*.:SpphaG.ldgg K@#9Oj8fbr+!*(,#*6_iQRLJk]HXh/YODJ$V§8\:EQQO;%U\D["duRgd«nM,/:XlBb8T]KbG%;Vcs2=f1,Od5]d!YN *8I7V7H=il>sOjF,dSKG">J#G^9TS)R%^jgbF!mZV,pug$1(,QDl+fI9n*YW(F7/^0S2«/)I*2"aT§h"Jg8O)Nt«Hq ?s,`r«LJ9Z'1OC/.i+AI;b#co\k[kBD6Tjuo.(s9/]eL2CZi0);$8d1ILF#[;F#40*?ZGc:SrKRCTqYmY^TY;4C2?( EFA[]hrA2o"rM/@J.«!K@#4]7Q?skB^6*Qqi1ZSLD][$;Fpo,)>W41j!6Cq6Z,>AW!Clnd5k$«9D#aj^#%[HokMDbd V7«gcBiI3qR:2PoZTPe)*o!5sO/B.*!S=Dm>i3§7(E2Z2nnX?p«WHsqoQf#S2nn«^WUHmGn8)\/=a.\Wmbj1?b42]> G,9bQ4Q,e#i:*U'6s2UC«:Yj?A+(?2>+§/f"3QgC3V$AF§_54o.AH9d7%TN3VC"+j->.XH%;CY/jp«,/kN"[u*U/1M $K"YTRnBt?"tc4Cm7C9uN8pmtCXekLm6=Zu.SW'!Fa?_mW^5*j6lT11l(;?*g-dbmHTo^0_)\+D!a\R:7r§+4?=;-V O:qr5aVc0`K=BZ^gWFm$XHaFrO07[Jm3t_MRN=W>kq/LEfa9\R«3?U#3.(hUEG:>P7snseH2^$H,Dc-i`/Q%gaqe^$ RoDr`'u:GSjKQiEeYAMj8;M^;!RWbalWp`oVodRr"WXsCD3MmJgE;CSs#2IoUnU;^+*X;1BP;hbHJJA4fY+m8Y/ao\ K2()%(N.g=E)gr:46T_T]P9k_.@Lao9km,8E82.f_ngn"]_V@q;]S$^i4qu`F_>D9X`T-`VEiSV='89$?fbkN>_K4S *k_'IH'1u`jATa_rl(I'gr0G]+]%§k`[7k[!tqX5eT.[WAQ1OrlO#fI!_#)ElEB%g"L[G8olPCYdo?dA;«_MrZZiP5 WG*saGkX9_M)tr/$E_7I=8Um6T3D5+)6S8)bEt_-'qXC9(Z(p/+9~

y1は複素共役をdstの虚部への出力時に符号を操作することで行っていることに注意。

 さらにデータを書き込む側も展開する。 ここで先ほど導入した一時変数y0y1yもお役御免になる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = src[v]);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = even + odd;
            dst[j + h] = even - odd;
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const wr = Math.cos(2 * Math.PI * inv * k / m);
            const wi = Math.sin(2 * Math.PI * inv * k / m);
            for (let j = 0; j < n; j += m) {
                const re0 = dst[j + k];
                const im0 = dst[j + h - k];
                const re = dst[j + k + h];
                const im = dst[j + m - k];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                // const y0 = new Complex(re0 + re1, im0 + im1);
                // const y1 = new Complex(re0 - re1, im0 - im1);
                dst[j + k] = re0 + re1;
                dst[j + k + h] = -(im0 - im1);
                dst[j + h - k] = re0 - re1;
                dst[j + m - k] = im0 + im1;
            }
        }
        for (let j = 0; j < n; j += m) {
            const even = dst[j + q];
            const odd = inv * dst[j + q + h];
            // const y = new Complex(even, odd);
            dst[j + q] = even;
            dst[j + q + h] = odd;
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS]f9lGVK§A@Zck(kXjX=!lOR[>odl(LZZ9mUK!0Sj_L//:uS)uk7t5aN!eC«C*'Gjio3RG"-)JDUg,_#P;VE=\6> b`YZqYh1EiQ'$[_oYjR7hZ9^"0Es-jn7+cGnKX;k[1,7sFV2T#YCdNi8.Zhs§Ss-T7U09YpA76R;V8SeIC1Lo(\)nZ 4'j]N_.§'\c-g1LNtQeif_jk,§QaI§jb]HtFZ!7hjTF2=LLC=C!ZNAmZ(9U7^1g4ifY^§jI]e;VH_.$OaFi$T"/kf) 35.=lQCd,e6qX0[-c7F:§G95.$'R5U=6tWp]jA3SgO$EKB%:Iq@«Ph*L.A!c«O.J8?0XL!.^#RM\V*Ti0AuAG)%(W> C_,'b;,Sbp-OGThOY§2iH-,1QG1fLKB5T@4=tk4Fl-*_5-UT\okVig*«l%Vmgg#o.k2Y1LQj'WD%r3\V:>1«K-*j_: eV2FpbZJ.%^#AhG*NX:8[jDpZNH5J$\3g])]10§E_(JQL"1!3eFH]/b3Vghd/«)c`3ds8mNE/j_LGTq0=luLd3?+c? U)_UOTsFVK.@DV2JIK:h.8dpfl4#^T^P0n%(]~
GhSurgMZ%0§:Ml+§4Rb?fM^c1/W/(jMacA7"hJQuUtCI«k\;Di`C-F?M=PNBJ+!e;8^u!V;$2XpcC$K+1VAZ5AlZrD .173==`?[XQ§$^3"^1"W(«YO8N.HiR*V/lT^o(SG.KnFO1;Dpt(=V^lD\L-71+X\V/fgTE?'$HDbEDdjB:G.QNG"I` #ZF/I05/2C%Ik6fBDAR=n\`r]XW4R=LfhBo=_nXuAAUu8nIuD'_9f.%_8rsc;,«l+4'deb"rBY4RRqkIr!m6N?`9Na dHAt;?b6@XC?IuS/Ne)LT5='PNp4q«Y§g)iOJ=b#Sher>26!8)AhL.1j36p*ZhmAWX@g+KWoQPSC4ZS`>*DSoRCGu4 b?dTKTZ0Ob*OO'SOWUCj8-^qPXKMBqZ?fB810TY$G6Q(t1[^8uY_JdnYuUE>P_k_H2S#p@c_BQE)Y]H'Bg3(cWaNGb #LS94nO@dqY«b5Ba.L,`\%T-36:rj7§;;gm4=h!eC8:fNHV_g/Jo**JW9Ma8[ltBX#%fN"iepJ3b7__=%=)(?1"[P# VG*.#4«+EI]HmV#o=6GM\-ohmGPCRps1Y6Fn/_§@%qGlOBB0MiX`s;O6(XU>[i3EslYXgS6(6/*LZUWSLX;S:"@tZ1 Tc[:q9>Fh"RTDm:*c;%HjTAjM(,=%[gJh`N_:,Y>Dor4V;+Y](,je9T#-^$3cJ^I/3O]ng_1.RHI8PY§.ZBE$+l6^3 5Z^mP?,BN0.#])\Pc%MNj3bG56hqn\:rrGQ§8W.n$er)^>F?lHRhn;E;*l_)\:Y9Q§'1ZtIG!-'VD_PklMquEDm24* rSd5(Z',.r[N@b]L1`1c>slO.W9aG]li)]qC=^.F1^i%n«"sD%>Ql@§4H=2=bWY3!Zl]8^cQ(=W!9I=8gb.g?\MLK) f1nO>a[O0«VMRBMWHZp7Za3o[pZfoF@cmj#?KSc91M:O0])l@Ra3'=S]_3A;2q,JoQHYpqB1b\/G6$,j5HP^'llc§O WYa-udsS4+:TGoacYT*u%F1cXS4ZMXKJpM;Vl*:ZSUVt\43NatBP2'G1R%#:la"6\U$aO12t2i'1Uh=`a+/aX6Q%`Y cn>oE4ia3',ps=5T/klD*:\2VDg0(1/LC06I+:tc@d7gp*gWg=QicQ!,N82E@Fj@a`AaDm+0X4,Rc'J%bg4D["?hBQ ,bhK9D1lZPRDq-o2§#C1][Oq.q*32L#kNH+N7WhaK/D?,*NmYYp)%7i*52$t`l'/X=?52§$IW9«D8"8KS54i,d"Jq> 3H>e(VY_"BB'+p7[l[«Z_tKQr`.7lGPuT;J§akD0,7KU>06oEk?XW6:?hN#s\:_W/omf\6F>$@P*;K4g%^*!.eIAOu 6$7k3ZGCL-/0'F$)kVA%F8J!a%mAS:dSu7)?WQMo;7f*gYs@$i«EW*#?cR«P>==HG96b;\o/*:-g`b%CrI]Nb\onH0 6_;-KLeE-a>9l$gW8l!Q$#1C.[6tP;5VWb:qO9DE7BLl$45I#;GK,MO:jNsqm;72pXP:YMrqM0g4a`6CO@h%rqUGF5 iqnJF?A«5JGaYd«XhkbLqA8oT>iP~

 すると、最後のループのevenは読んで書いているだけなので処理を削除できる。 コメントも整理しておく。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = src[v]);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = even + odd;
            dst[j + h] = even - odd;
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const wr = Math.cos(2 * Math.PI * inv * k / m);
            const wi = Math.sin(2 * Math.PI * inv * k / m);
            for (let j = 0; j < n; j += m) {
                const re0 = dst[j + k];
                const im0 = dst[j + h - k];
                const re = dst[j + k + h];
                const im = dst[j + m - k];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + h] = -(im0 - im1);
                dst[j + h - k] = re0 - re1;
                dst[j + m - k] = im0 + im1;
            }
        }
        for (let j = 0; j < n; j += m) {
            const odd = inv * dst[j + q + h];
            dst[j + q + h] = odd;
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSFFd7V;1(kqGQ'^)c+':+H3UCIXJT"mG@KsV_00$0F1bosg5IqgiVe:E«1\bOH1Inn-!I!3?/Ojj=CAVnIiBIg/j Ct395YuodoNNX2+h.$pIa*8OHq'p$H(1'QV]YXdV%h«Hn8Jcm7g-f0jmr;]nG_/_h^#CG2@+>:%c>5H)2=jG%OE,%t ,«?>q5%c!f1#UH0pBKltFjiOUao6S=:#(MqiFk@XQ=m,(e1R(SZ5[P*`if«qpA=SujZKi.C9]*V'XZ]5L+T%8":>+U [b«6)nCN?*j`(§laON.§+aXjDmOlCTA"cJd+b^)hj:u,T#m)qW+(c*O%`QQ'56)9+UNZ~
GhSEa;/b2I§:XAW+H/H[Z)-(R«X`!WU"GZ>^q(7RL-?HoW_.A%8"pu[ZCM^/n%0enM3@(u23>oAIJ;RfNd"!UBjEGk aca\b)G\AN(9uqS+If+L,PbE31D%s/8%q_(HqGaO;NPB2M]7+U$%:n=]-t4T;(:aMklJb;6K0Pr.e65pmkT§CRW0Ld 9HD«NUa78;A2,Goa(N;L+2/mHja`=Yb_f-4@Y=.p;dKWbVe#9*a$N3)6D!q*H4/Q_C#\+\-Y'H:8ScE,(;_E4:tmi@ T2.\!m0D!7HRIg;nD$*RZH8jeThdRdj4iph49[A3Pec.VE«YG^F§d^51]-@§rGQ5ll`AF6;GL1VNKMR*CN?+"'>HDM 0sG==e-O0tLUOb>p]p5'l"K7;eMgiBNa«]:N0^XA?%R--O8Gtj=JO3-lP.$nA/_8)]0\F=BAW-(Xnp-hg5i!q($s:H §8gYHDd696$::Y>I/!fX*PH_?;K4FC."j§@W-T.tBDpGO4'In(PdT.R9UD>VqF§+;LkCYC0/«tg8HOCnA@:!(:`b:% 9Tja=%O!sk§-7jFbRRKlLq"_>GOOu!]qoE3/Qn1-aV;Haa_NZf*0TQVgjA#«1fcKrgCB,FBh`YcRVU@l9_V]rO9A[Z +-kpq^g2J[QUH'!('3cB`Rg\M$p+l@Fd5kOo^'ek1kFF"d[P;$m;7=[Yh(-LTGbS;],5nRGD-!%=[nHO(`a?EjG7N$ \=1V];P#U0@GS.qGe1t04A%P8,i;*R$9l3.K)M5+Tj0[m[BZI17JU.%Vj%7?H`-rD5kq^uX*oV;F+a;#$K:O;)Nt9b §§6/%-Nt?O+$^'_#8!\Z6-HslH/Io"Dm?;(_o\ZVfUcM>dF[bHF9%1DC#lMmFF_bb!)Pd66otB@YlXZKd§Ar[0bmdH cje)cboGKVb$+)QH5KX_[tR0U\22Ar++-3C\`_]GJN(1#/ok:m]+SP5R"Z:@qa%;F*7§J,$6_Qn«O8[$FJR9\[@m=' XNP436$.OLVUpBkBC=[t91Zm-T_JWaje^@L>R]=*`b'sKkA'n8J_.:#"A8uNh\#X«@-DPGIH@^gq@'MegckZP>+I33 m6=C;2qW;kII,'Q6nb,os4J.fAfdOX"8]sF89:5IBD`"n7k8YV)'l=?CDql«IloPrFu59/q,§?ge0JN`U"\2«Nk7h- JH(C)FXCf?*QA1'(*+'dW5fPVelI_%nE`RK=DXi«oJt=fF2g5e*=N7W4.I5IWn49pDD_t^a'Ij§HB)=8Yi5/\FWs[M 0+P?GPo§Z]V*3YB$CH#TdXlq4og\E[8mbWA\;erMhR#1'bNK![)),:p_bmCZC'eT§Ek"C2A.e`qK]_"H"YsY@RQJJX U5#53"IDg]c/hYsX0HNR]D%c;N+*[$*`b@J2pn7dM>l\q^XK(\h`S]MMNtdC[P*b-[J\9(gi1*1PU5T/T§AN+\_YPL ~

 この段階で、後処理や結果検証のためのDFTにはComplexクラスが残っているが、FFT関数自体はComplexクラスは不要になっている。

正変換専用にする

 この状態でinv1にすれば一応逆変換はできるが、入力、つまり周波数領域の値が実数に限るということなる。 フィルタのインパルス応答など、応用がないわけでもないが、この仕様ではあまり使い道がないので、思い切って正変換専用にする。 つまり、inv-1固定にする。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = src[v]);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = even + odd;
            dst[j + h] = even - odd;
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const wr = Math.cos(2 * Math.PI * k / m);
            const wi = -Math.sin(2 * Math.PI * k / m);
            for (let j = 0; j < n; j += m) {
                const re0 = dst[j + k];
                const im0 = dst[j + h - k];
                const re = dst[j + k + h];
                const im = dst[j + m - k];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + h] = -(im0 - im1);
                dst[j + h - k] = re0 - re1;
                dst[j + m - k] = im0 + im1;
            }
        }
        for (let j = 0; j < n; j += m) {
            const odd = -dst[j + q + h];
            dst[j + q + h] = odd;
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSFGheU(_§BE]*«uaD(dl«mlk+^5=§G@dp=J(«YV6)Z)?*HmJA,*h-g/>§\ap^,Yp:d0rQ+/I9iP9^=QLCsa^AbqK Q73#-.YO.fBBpg^5iO^rJG7gfCB:O_122=OC*P:^>J.sMRlJtM.RX%o?Q,;Wcea%]\§e@JToup#3C'V4-Gl%)gEP5R $FKo1cpsG,W.+_*E/)1!rOe]cV0:!MEK.mn§r44pQ%6Q)%'0d!$Sl5EP$onOq'erom1Rj\q'PM69§'*e.6cTqkq1*4 Ta5l37=@"sZ=LA]X":k3mhI=qa=j>?Z0HiLj7jn(-ch5I1W(Q8X\JDS-]*/FVq^c.8m!a(ZPnh/X-`96T%2]oG«tD% «aePQ@\@:(>C,9tdte>gY7%BoT)(uPN2Y8§#Lt'V;Z~
GhSEa;/b2I§:XAW+H/H[Z)-.T2@O`WU"GZ>5e@b(L-?HoW_.A%8"pu[ZCM^/n%Br7M3=fJ1!n)%IJ;Rf4!%0Ee#HrS PQd-/F>Y5r8jnj*Jo($§$ft_q`kXu«*r.];n0sa+$:.F«bcgi!`UkN2fSB!V$k#Xn9+NGr',%-M=d!.`CnJ"3m2>!e _GlZA$X,6)72Bs]1OL*Yr9u:(.gKl=,*m-VM[dOa9R%F'Sq=e"2)He'0_2;\e6?V«c4_#WC,.%j9\Mkr2^"Ub9't,C I6R]#XXddRm@B*__DCZYfd76iQp06B-F_YCGmmsB.>,=c3Yf$Vb[pIdc0UKGqu;j?X3rW,.QL@Xej3>3XiZ9S9`-6u 75(B@d+V$§*OO'SOrpL+UP#*f«knY1]R!GB10V3[HNhM#1[_E+Y_HLWf.$P-as9M!p:3$CB5Vt-`r0^2mAE7F$?e6H 6lUHk)s§BZ"R4DX+9."4-Z@jqP["V>$;n9X=?_P>NW.]*/M2kdg0,Ime"p]d4qY"'jre6pX%ZQ[-§-nNAA-B+clRk; 9ThKM**2QR6NfM`1L$UC(ii2cDVs:LHUBOG>I#BdO=>RJP1O6U4iYr2\ark8do]XQDd*tbdY-aD1tSE]K^N(g5S@I[ ISu:]!-\HX^*:E%YR+^(CVK«i@b>+nYKDNMVe9)F/ecm§Ujc+fCR:hQ2oqrH;.^0Tf-JZ#ScUoUq7Y;d.eBek_?VQ* +/Mb\_MWp*FjZ3U:Dok1?F-;FX_8W6enl>J§)j+sG2«;:@87«ZafXHtNhWb'nTu*!PQH_hkQ(F/Z9$h7_-ib«@;.Ar n9B#.Fb?.arZ9oe$,E2i!ST?bK-BIt)Xa.[§j(LeG'MnVrLp#ifR]`jR-SXrEWGYi2Km`"d_f`_!%=EVU)a6qBL%"_ FRG)d/B+I;:IB/"0ul2Wl'ApFm[L*F#?s/'J/c$hFAH:/i)t:!EoT6o@(O(Y(WCg>$R>Km+cVsCd#4oAO1L$FfXkHu >9(t*>QXR;a/U$1:ccm[9l!§$B3:L4R"D+t;,d,.a-g-ka*q%X@L-oHY4)ho$DF',R«.^n'Eo.QHK/rr«DDjF9q«.Q §nB'Im+Kc>cdpnoBkDd2:$DJc9X>>«hZpa`^GjS#HM]gRdgJ(9"mjL"G$qQ'4qoa0s']K(8m6ngiM_K"a$@_HiBVq$ ojZC>Wo+AFjt_^-89D§/3]Z]sh;MUG5\fqkX§t?gX5u#TGD5(H3G,tEqiG`t;hQ3«Z0.s_]Kj4Lg2gE/5^6?QD«S"$ 2hJemoP6o.2**>J2q""i.:'aT!K=bmE0@3tVgpP`)8,hAA.e`qKP$MT11>dcK75>u__+cE96L4!f.KLoVQ-3T_)§8« MIfV1=DdHm^jS$Ap\j!!d)Q-91I+L-Q$QMKIfB«$n=*ptjarg*Mq'7=_ggPE+7d-l~

回転因子の三角関数の中に現れる符号は整理して外に出してある。

符号の調整

 最後のループに付いている負号をなくす方向で符号を調整する。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = src[v]);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = even + odd;
            dst[j + h] = even - odd;
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const wr = Math.cos(2 * Math.PI * k / m);
            const wi = -Math.sin(2 * Math.PI * k / m);
            for (let j = 0; j < n; j += m) {
                const re0 = dst[j + k];
                const im0 = -dst[j + h - k];
                const re = dst[j + k + h];
                const im = -dst[j + m - k];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + h] = im0 - im1;
                dst[j + h - k] = re0 - re1;
                dst[j + m - k] = -(im0 + im1);
            }
        }
        for (let j = 0; j < n; j += m) {
            const odd = dst[j + q + h];
            dst[j + q + h] = odd;
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhRk8;/=$§§BE]*.IOGnc"8kgK2*g'quC5Fie+V#`AOWl.6«4@I2>gtCi,Z.42UH9q;3Ji§tj0LLs:I[O«h%_IoQiF 8u4uk$5SQ'Yghn?rW;T?VU,n:gqX]f0T@g"K«g'P6+146dR'3'1nPtaQ"Gjur2en.hW9F_\Sn-RQ41")%JLb0caW\" #7m(E(46@-_uW4BI6>;2,DfUJ6m"§B'c1!-iOXqSLO/*\R,V!!k§-Zt^-atJCP5En%«h4Fm^M,WTXI.AD9**#lOm9c N3UY`W@e#2.HkB=Dj>LC?=]IRU\jn]O«1XNh(ug\$kO_ik7YNLAaC^h"l]T$1r#(nGAEK§+8k22Q((UR§,tb?gf.HC ;§NH?ku?S$/B342_@o%A8r2@S8s[?*;>^D9L"tefBWL0*"?%`VaXcgG$nCJ#"?_OqCroqaaWo)M)qKU>n.5>F'km~
GhSEaD/\/e§H;*)5oBJa7@%ZnfJ$9!LLOJB!UVcr)>_0'«`PkE3YLOFPjP:9J);8«8^ptW)G"E1SpTmMa1J+f]IFdJ A\fTO%q]?VM+I2BODS«7U§:\.(l7Ct+^\jb4p@-M+iqclq(4h9R/Z%=q_5aJ@3OlMjT*8F7\V^j9Cd.qm8J0`RSb]Q 9GP[^TEQQ!A@WgF_e75R+20$Lja[e/`:@ci@Y=.p'41NmVdT!§L-e«=6Cr=RH40,oGrEY[b_BXcPLbik*J@.7Tse_` 5+fRdg's^KG0XVt_DCBQaX.PYQp09C[;YIBH1o;6;Z_«L`YF`mb[pHmSCsB@pAQF\«i')6Q#`T#C5`:jYpNPp1@Z9H =$;Ib6B.):*OO'Si?,S9UP#*f«mQ0,*5*ao)(ikfpBmE0HJDes?k#>eA#JNX-kegjg?5b*5Dtu^C"1I0d=*§LWaNlY JnRNTGb_pIf+:"9j0.A!gW«T?+XIp,LdIIa§'H=4[FTi«]r,ER6!9*J`cU=pV,%5qJ_3bdp`bp;P/Qj66e33i#3A-a as;8Jjq,>g%2§)K>rtI51$"MQh7pT%kDl:Q[q§'Z,'3N?-a1X>H]7+qo§'Yado]Y«D`]KcUbU-%Bs(0O0\pFd5iQ:- IT2W:!-\Ta^*:GaYm=a(CVFd>@b@BhYKDNMVIO8qU^?+W+hcDLXZ,+ioGiOf8Xr-UJt9?4Fb4N;qR`I-T;XG)C?tBr P6'§fq)B]`AV@q3@DSVDn^)NFDUVF2#ko$rrtYkM$Z^bAD=og,MSeMpY@3Yl$7P>8!r)j`e*a@L27CU4B'hX0a."#A #o#Q#*]BJj?r1qFm0Oh"OE:X8pJ,@JVmF(S«gr^ENFlT(L(Qj]GFMtCON2>mLV5§uN7J)]ic,Pn!`4b3aQnH'SOn[; \9@^d81lAMZ;%EuQ*.%*5s;V[3#7%6BG#L11M:Oa7k>NrU2>jgI8fe*BsP)Iqu?ck7WCPsE='PqZ0i!%Y7I4F^p\0E 4QV2!bq?M6H]VP§?r[_(aH]9F"?c6(36tbfa+@:[@L-n)Wpi[V$DrQIlY[dBVAMU@">>;n-$TN`G!W%A\Db-f7TO#] f@@P%-*2:C2(mY1o15uh4KI=,,A8k/BF]oHTjp2[FcAipi/s%=St6IEc-7%$_kn;-m'I9l%/@S?=f%8siGgih@«#h: jOJ4.^tO+Fl§AhBLW+QB5k>WEk%1%bh-T:-\o"ELX;\U;a§^R7*$6uW2[f8(NEqceXJ>TcL=]9GQAT#8WG+!*f=+D" [VHWIGg)«dF;(GDNpfd?W_u5ZBNO\p[H2*pQZ0oo)>c(I9\!MYaBY=Qp!G!Ws*H#m0+GWhR%WS2Z'D7rk;:1)F(EMF 9i7t]D7b.6lDokQ)4VIED«:.qJ$(0rq`fS*cT]'eJ4UJ+Fl.%B+b+>'l]\sBJsQe%e\ODd^]+;'gr]-~

これは虚部の符号を逆にして扱うことを意味するから、expandFFTResultでの出力の展開時も符号を調整する必要がある。 実部は前半、虚部は後半をそのまま出力した状態と考えるとこの状態になる。

 すると、最後のループのoddも読んで書くだけになるので処理を削除でき、そうするとループの中が空になるので、なんと、ループを丸ごと削除できる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = src[v]);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = even + odd;
            dst[j + h] = even - odd;
        }
        if (h < 2)
            continue;
        for (let k = 1; k < q; k++) {
            const wr = Math.cos(2 * Math.PI * k / m);
            const wi = -Math.sin(2 * Math.PI * k / m);
            for (let j = 0; j < n; j += m) {
                const re0 = dst[j + k];
                const im0 = -dst[j + h - k];
                const re = dst[j + k + h];
                const im = -dst[j + m - k];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + h] = im0 - im1;
                dst[j + h - k] = re0 - re1;
                dst[j + m - k] = -(im0 + im1);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhR4X:scVWd'6Eo#*L;?%Lri§`Kb_j$>H>^aX*ANGm\G,+WA«gadNRe>RF#pQ4(OWKsK.tM\J!g;G._0'PnKmXFZUq 66:s2[T1BP)5)mu5]V«[EJ[#28d\C38o[EZ:mF?j§8/pbUSpPF""FLi6)];n"%"?#-YAYm"G\skKEc3?!SqUhOHQ6§ Wha@R"TV1^2:q~
GhSEagMYb8§:N/3/;-2«/Wl/(Zm`dN7F9USJYk#6(O7]fL*9XWb2cGIQ]g]cs5'0WONWRK7"A!HkKK>5B8.W%\mhIu -'..MS«7Y?@`]!D[RHTSo$9@0+JL6EI$kTAPYBZ)+K%I?fDNQ"qL]UNUZ=m6V53Fb#8GFA`*$§>jQ@`EBC'.cj$J9a 3gN3+91H)*C.WZ1=3HRF^*P2hJ;lt@MQLH0626QkY6)[A1:W'KTcs^bchg$L6?h#)VDuKHR*7="%9_IOg;IXh\n:O) n[gIQ"«)$Y+d3P1Hd,'I4A$7_j:Y\S(0BQGQuYt/gfTOd.!M)[qU7:-Iu`4^9'3e3qTPu#6m^>"#q'dg/sKr,[jEa4 (>XQ\+^+Ha0)$7`^muJd7BTa@F8L"Oi[A«DSBB_>E,P#^s7?_8oVn:*MV^iY%FW:F+mnm-[(K_GhDe7,@FV6-*Gl§/ Q«s+$77AjQFm@0-^VemL>P93T(F+`§Qo:#*7hKsrJ%«:i«>e/.MZ`*`mCB>p"It$§aI)`Re'O4§n97=h2Ymj9K@#QK Ef\*]KZb;Yi3HIp.>j,tQfR«§h«hs9'Fhc§/+O@A_sNu,-M-UZR4l[q(Cm%VWlB@sBlP8tBs(0OD46+Ah?X.F!'Bh* Z?u\_$ZP\P§RKu^!)_VCNTUj?KYbC*;@8ka>Bs-oOADbG>,TjSh@"W>_Ug%^?j§ck?"Ue>)j:3'/>G]bN/e6.3FOWb S#.7IP\5qO3§`CUPV+$rO4%k\anO2S6Liq7_-onFd%m5h``Q*jUAQm+;b@)04]DE2+DKQc]7\)2'u?nUL2_EZKdD;I L[X83P6)/]%\Np?!sm>hT]P%pC@?8PG;r4diP*\jm(b«Mkj*+":§p.sZ#YX`*FQ5q!#3`\d1ZMVBL.(heFYXFQK$=3 \fnV_fgM4[:M\c^§#4U=69(9]4#8)M+j[j$^lR,'rgC#GV=mX:GIP]:8au[$_§\R!M"BAS66aa.eK/Hc-OT§-]!V8@ Ztm:Y+K7P%X6RdP;g32@SYP-)(u@]bn12!P;pXCg'89"D2kIkN6lB>2H/@=cI>Z84nTn$n`d)V'YH>J;6«cU?$Mb?! 4kNk:g\«ojRe§mGYds:O30DrubYJ%+,_5oM«HTX§)'-a*%LsjuD,H@,/Y(Y!2M59«l>UG",]pQ[GIVW§F9gigleYYM 92.FS[%§a8F*u+Mh^jVF2`PUJ`i1§[keht?k4/TP6KP@+XTJ3o/87C7brQL8ae6+a)7PH+_bZ1BcSE(`XU8k*5MQld VNao4,)8*(T«*Y5OjNO«5c.4OZUD882;ANbG[qiqo«tBle])Xc4a;-s/!a'"iO"embO!>'ZTnHcNBRO@WXU:TY1+t. qlAu%QJXl]>:t/cZ:$j1%K-)Fis1Ed~

符号の調整もバカにできない。

 このループをスキップするためのif文は分かりやすくqで書き換えると

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = src[v]);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = even + odd;
            dst[j + h] = even - odd;
        }
        if (q < 1)
            continue;
        for (let k = 1; k < q; k++) {
            const wr = Math.cos(2 * Math.PI * k / m);
            const wi = -Math.sin(2 * Math.PI * k / m);
            for (let j = 0; j < n; j += m) {
                const re0 = dst[j + k];
                const im0 = -dst[j + h - k];
                const re = dst[j + k + h];
                const im = -dst[j + m - k];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + h] = im0 - im1;
                dst[j + h - k] = re0 - re1;
                dst[j + m - k] = -(im0 + im1);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhR4X:scU(cnSPI#*TN,$Al`eH-$D-V0U0/RJD+8DJgU9`C7n;$IrAg80Fb=,W\Ra>9P4l_,__e/2^3h=!9dS`>YnX Q:i%:b"Pa\aAB*«0[f%17M70R$5mTQ(n-te=-AtQRYidF/\ahB,:Ble+WG*STf%q;;()QFg=dYS!«ANE.pN~
GhSEaD/\/e§H;*)5oBJa`g1?EfJ$9!LLOJB!UWo=)>YL1«`PkE3YLOFPjP9nJ+"C,9%8dJeNb=HpY9i145LuPk#KMp W«s6:Gq^Jt8k)G6P§0_6$e8Ta`^*!gN-q$*p_6t5Kd8$Qjk§M1A,6l1pL/HE_EuZCRK5i?,>fAF-2BFEp*i3`RSb]R 9GOP>TEQ]EA@3[,_l)V9O4Y)0$G_BOiq_02\01^J7$PFJ.6'GBYu$X::h5!Jfg«/Kc_`[SfpKu=)%Dkg5hP.1lFPQb kMm([q>kE2=\U4h,%L:9QTU4:qW§8nh\K;Sae*TL$f§ERY+XQ]/uqdsbhb?lmsd**«SD*:.470LNKIU(f§T/%-=1KY @URSXBHnd\*no^>iube;USFAq«j-fIG4_L_jg:D4h[=C@lu@OJ$u>X;S2s+-6C%>fj$$Q«r%'EQCXm3uBKqF_«Uuoe +PP7ZhC(§A-%o$CH2f8)S_14Y.>8a-1nlGjY^D[4BDo=*>?VdSPdNV]9\7K?I3N)/`.#kjB9`U[,_j§aEjmc=:`b:e >`jAL%KS[u§IFN;_@9@e%##la[r60a]qK-/2-H§Ka;eOLj«^44O7@ThmTD5'NFdh>G/N«)>1:?U(«3]]e%HLu,X,8m #As8«E#7Hp$R2""70's4'-$'F_[cWn«@/D:R@F"!9d$§:l*Uhsh0«keEtH§2E=OQ+G1oBN"lBpa2an«B;VC9tXiDe_ #"5f>a"\/T-PY,N((«eO%tdC:2@\3KE).YQYCYW9,R5aG:2eT`dFc,,4cC"G!@8R4J3OT%j6ka>@bZiLeh.§L]XktE 5gdLDEO/4:#"8dDJ`$P38RKhbmnC06-JLUuV:fhE(`%SY(fBpJ[g=A]GoU$Kloc4oN#hsUcN;K-!3Kp#?6«.m4D.Q$ .PBCji@I.-d[CV.;aON4@cpj§CK"[QUCum9FY!N:VJG,#ce)2JN`\a,R8,N6p0@uE#L(>Iq>DGu*DU.$o@b$KCsRb, BPKOTEp«/+[+RBOf62s9:d(PNf28'9mbe/)8]-H@]\C->YM^-h4bL(r%"j.L4G,MBC3[GTGr(BO-o.R«lTpj§_9K#E hqB?J2p0$%flfo8g4qEfr'.iq;FQN53$(/c37.cuC>8hum;B«L9F+IM7GGMKaJ>fI_70^*6.6ID]_!§g]gn+rk9-N[ 0kF*rQGgZWli:MTKFh/-i]tb-1#H5Wg`^0,_#BQE,Nsp9li]!4AnDfU)lPW!ppVGAkmD#C2#I«WGO)Oe+:i`%pDAsm r#_'J:+os#>gd5kD§$fq)2`F53«7;E6«!21ijsJEG,B#?;=7l#PBS86ac;atJ,_g,I(TH38KB=0nS3Qsr7pktF1;r_ PR3ED0s'?k=§pPbCB~

という条件になるが、この条件を満たす場合はすぐ下のループがそもそも実行されないことが分かる。 ということで、このif文も削除できる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = src[v]);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = even + odd;
            dst[j + h] = even - odd;
        }
        // if (q < 1)
        //  continue;
        for (let k = 1; k < q; k++) {
            const wr = Math.cos(2 * Math.PI * k / m);
            const wi = -Math.sin(2 * Math.PI * k / m);
            for (let j = 0; j < n; j += m) {
                const re0 = dst[j + k];
                const im0 = -dst[j + h - k];
                const re = dst[j + k + h];
                const im = -dst[j + m - k];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + h] = im0 - im1;
                dst[j + h - k] = re0 - re1;
                dst[j + m - k] = -(im0 + im1);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhQFuYmS?%'\r9p2n$_0lQAMDA1BPE%W:9§dZi«t6CUpdn_]=09CR[[pPq>liuHH]5$`bh-KiIp>mZb/'"6?u=gei7 -UKp$4;>dV:8"PG=@6Dk63LR.C^`Hu1oRuRcltmkNn,.9.pK§\1-u3nNr?'BHJ)Z;e/a.IZtS7nKK2ZP3hl9H;qElB \'F*D\ZKI1Rp^fhGM-i'C(TD«1@bJI"?rO~
GhSEaD/\/e§H;*)5oBJa`Kjs«fJ$9!LLOJB!UVcr)>YL1«`PkE3YLOFPjP9nJ);8R,jNA5C*8Y_Hg\H)*Uf"8o=AD, OsQ1N4IhON/Rlg06+04M8+M^%B-«orO*m?-plnJ'V')[$(,u00')I4sm%`WB7\upH[0IDjN5Sju«TN«ehe2:d1ugoQ R6$X§8P;OUa_FClN_diX+20$Lja`=Yb_iN6EC'A^.B`fl;_iTPA"b(O+j^$P]nL@+R8%[UZS@kZdMfF$Ks6E[e90$S kMpbnq>iG§?:-%c4SP;*/pW[,o[tP!^+HM0F"l^I'"E9GY+XQ]Y3Jg3bl0V7s*lfeFk12+.46U«`^i',CN?+"'>HDM 0sKjge-O0jLN^5Sp]p6Rk\0.:eMj[=O'Wf;N0[E4]*.«:*qra^YXY7bWDiFQP%g"Am0dXScdMB1Xa9"Am>"Q4$?ick 6sFuV)]'7§K$^JMJ,Kjq-Z@[n«*YA)$=V(>Wh)W6=o[UGN`K^"AM"B/e$\A)4qY"'iZMajZV3_VP/,s4\_lU.-kAX# -HQ?JLCbN1M1EWUiJ§aA7U2,2]=3"agep!(L9/HlA.GeanqtZ#a(2n0%ph4TNE(u6G/N«)>16`Gk1VWse%HLu,QUs0 #A*]4E#7Hp,nXI670's4k8V_jL)gKq\k4DbH>_k>RQs%SdMj3)]CFaTk9,.@Ooeeum'M\P(Oe`uhj.jG7"s#c>>J"] %#Ce§a"cZn,8A]«$O3GcLW6*2Rmlmr[§9#`2aD=aOolD4VmeB]'XfE>NT-1#5]I;L^`NiNG5(uDZ#)raCKDZA?=.27 T]kA3\CmYXK;§J35k#§2ZnH+'G«4J?Sa@H+dX4#m1^cB!N%"P6gOp5j,"\jUp7;aJ=q:UjB7]7QJ5W`s^CP'k^,%2[ _;hrT/f?Sn(H$«PKitt!FKs,kK,oXKCN$=jmr;«Rh0+i*i6J\C/;W)N$ITO0b«OHbVUW=/;sr^L3QZL_,br9I*-$EH HjKRoYaS]Hk«tJ=?C*SM$LQPB2[HEZe6"g'o@+eNA!JKf3JMai=83NCJuKQ*p@Mcid#AjG3`(R_J!i7)=J$cnHFt;0 \J\YoRe§[«YdmUP2UJmTZI:uM$/khq«Ll]b7`Y$]K#NSt=OcB^#]'18c!YXD3\9!n#"9ls?s\,r3=`WQDT6gEWVN9W P+",N5t5pCarK:iM2s=B)Yh;Y+fabFJXY`l!e()gViOjZqbrffTH@dZ'>3;i@9UZfT[rU*@*>m8Y$_OP'_iK6\gm"Y nkeCd_=>Vsp`9b/=:8Z^">quR.oF4/:kD@§!1:r`U/hB9FnahI@B3L"\VJ^1hT_X)4+#i_q§=_ln7E(U«NO]0XSn:Q A'8f:iKajXV(>KaY^q«(!V>#g;#~

コメントを削除して、残りの符号も調整し、自然な演算で計算できるようにする。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    idx.forEach((v, i) => dst[i] = src[v]);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[j];
            const odd = dst[j + h];
            dst[j] = even + odd;
            dst[j + h] = even - odd;
        }
        for (let k = 1; k < q; k++) {
            const wr = Math.cos(2 * Math.PI * k / m);
            const wi = Math.sin(2 * Math.PI * k / m);
            for (let j = 0; j < n; j += m) {
                const re0 = dst[j + k];
                const im0 = dst[j + h - k];
                const re = dst[j + k + h];
                const im = dst[j + m - k];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + h] = im1 - im0;
                dst[j + h - k] = re0 - re1;
                dst[j + m - k] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhRk8:N(hM§B4*eMK]34>?FZJ@)C$]LF,1Vep§nHM(>l"^c"pIGOI(2/0Nq`(@lf6n',5OJ?RdqPm^§MX:YTfQbP"« E`$6kF+s#Tdjg:§`npj;0.O$+,8^nPC_2*:$64=*GqF,DnCu')+;7jip^jn0(1F?qW@i1g9(E§7.B\c\[DJ]uj%P03 T`NT-'n-MLhY5RZ29\^/Kk9lFnOBDj%PldS3uTS?.UJ=8«"h%\=>lp0/[f)JUiS[K5*R[96gPpkf;=@0ipQtEGPqXb -Ss$;Fq_4`%C7GOOI\.7p90fFmD6/kbuc[§c"7q$@`cYgDjY;hS`CJ;ZOW=DH%QoC]GkV1dq>jgNPkG,'«B0OD2«2A #HXk,GIi8[s-j8mgd?6t\YWTqFMaWXdGcb5[rF;)U§V"ekWJ~
GhSEaD/\/e§H;*)5oBJa`Kjs:fJ$9!LLOJB!UVcr)>YL1«`PkE3YLOFPjP9nJ);8«8^ptN)Fu.FSpTmMa1J+gZ76_@ A\fV%$l3ATM+I2BOBl1'U§:\.(l7D/+^Yuf4p@-M+ir?#q$fQpR/Z#gLG6H-@3MW=4$ZXr8«ej?8Uesk6d!\D1Wflg W?R`HZs_s`Ct=8Pph[J1_fl6[*3c,m=MAQQC_2§+_Fm\p'F^SL=K';,aKrTWDkUC9(K6j?@SG?,BapKf6J04hC:e*; oC9FGr;[aNV^@T?SiQI6RllX>qJ6g(^9+Q[ZT%!;'%hOgY+XQ^/uqdsbjHIWq`gLT.d]$mPS4d.@jIG#27ZPLM;+K3 (g+-YC4p-J6egBPrdd.X3nh,RFj96qd>Pl+d:+SSQf-ZOj8S«PC\Xm!3^rL)R«Zj§+d^IL=Z«ugX'T"!qdEI§Jt8>; U+O(«NUi6=60Qgb^OCsIZa9'e8hNREKeR'ZQ;8^iXT*«^8'«*.Z5,rhWUJK9:B[bMfLqY-/«dl4`:.tFm^IKOb(J'* 8b.DV+b7UK,2f_0fc^\u+d_9`md8gp[hK:c+WKtc=i0Kf]HYPT.:01!K+eYg@p*6SmBB]Sehl!U\Mh^KFK^AuM2)iB 6!:o%g]b358VH,4§\p$fq:;g,@>E^5.ME/XNh(HZ-BM#XoL=VCfm%Ga9+\f]E=OQ+G1oC9$,.SHDi5feU;H,mXiG'J #"6r$j-9%t§IRiY"bY7B6fV%T91/M=4A\TJ)^%,CaUsE;e(Yck$C_jHL/C8dTX]U]i"#rb]6f$DY\ci`CKCO!?=.27 T]kA3\Ci,-K;§J35k#§2Ub?DlpH%§USaASKdVLm]1^cB#N%"P6hZaaE4H:@@loc4oZlT2]cO/§5!2\=ugchZC\E«2L ;@Uf>E8#O§«V/\";6btK@S-C,U`N#Pea;?q!,eZ,i§Sh6m#JKXC>>6§dAX1EeR$V.?2US,fje]*A`dFUha*%X)«0F0 '9E4tYJ(§Z@cn`8m@2QZkC`.gp4«YfD>§PdIPX85__+f]o>KhM4R:oF.p9]H0t[mo_Je?I9T;!_hn6NsD3Q"=G]OO# ?XYnbp3$:m§+8%ta;V-Qd0na#n'§Dk_HV!:NI#Q7XQTJg?qYR8LX!SNqs8\971At+=]qSa(?§si94;h7Xln6EXi§lk AigiVTFQlS9861cTT61Fi>i§C#G%7J*hm8AC8bl2>'5V_plmeQ>n\#]Yc[Gt«S+1D(C+]DbOP5\c(:G/"mf#fdXlq8 ohP#.M7s60;§^o*I`:YuRaMWRE0t§5i]pCIW.7"k)-kHlZLPY!_Ib'?11>d'H+fE?@:q".-$'(1`CNk,iFoiMFIJ,> @N"8K«]bRc2kQSMq«lSdkd-§RH8)=-V/DLa§%!E_roP3g\DUr8@W`)pq"Gjr253S~

 これが一般的な実FFTのコードになる。 あとはループを少し展開したり、基数を変更したり、k=m/8の場合は回転因子が \((1+j)/\sqrt 2\) になるのでもう少し複素乗算が減らせたり、回転因子の対称性からさらにループの長さを半分にして回転因子の計算回数(表引ならテーブル参照回数と容量)を半分にする、といった最適化が可能だが、この状態でもそこそこ速いので、今回はとりあえずここまでにして、その先へ進むことにする。

データ並び替えの位置の変更

 このコードでは関数が呼ばれてすぐにデータを添字のビット逆順で並び替えて(スクランブルして)dstに保持し、あとはdstの内容を複製することなく処理を進めている。 これを、入力された順序のまま計算し、出力の直前でスクランブルする(スクランブルを解除する)ことを考える。

 これは、たとえば畳み込みの計算を実行するときなどによく使われるテクニックである。 畳み込み(高速巡回畳み込み)演算は入力をFFTし、あらかじめDFTしておいた畳み込み対象関数を乗じ(スペクトルごとに単純に乗算するだけだが、複素乗算であることには留意)、逆FFTすることで得られる。 もし、入力側でスクランブルしていると、逆FFTでは最後にスクランブルを解除することになる。 つまり、スクランブル→FFT→窓関数のFFT結果を乗算→逆FFT→スクランブル解除の手順になる。

 このスクランブル位置を出力側に移すと、逆FFTでは先にスクランブルすることになるから、FFT→スクランブル解除→窓関数のFFT結果を乗算→スクランブル→逆FFTの順になる。 もし、窓関数の乗算をスクランブルしたまま実行すれば、スクランブル処理を丸々省くことができ、FFT→スクランブルしたまま窓関数のFFT結果を乗算→逆FFT、と実行できる。

スクランブル位置の変更

 まず、スクランブルを後ろへ持っていき、入力は複製だけしておく。 当然のように演算位置がずれてしまうから、とりあえずdstの添え字に全部idxによる添え字変換を書いてしまう。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    const dst = Array.from(src);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n; j += m) {
            const even = dst[idx[j]];
            const odd = dst[idx[j + h]];
            dst[idx[j]] = even + odd;
            dst[idx[j + h]] = even - odd;
        }
        for (let k = 1; k < q; k++) {
            const wr = Math.cos(2 * Math.PI * k / m);
            const wi = Math.sin(2 * Math.PI * k / m);
            for (let j = 0; j < n; j += m) {
                const re0 = dst[idx[j + k]];
                const im0 = dst[idx[j + h - k]];
                const re = dst[idx[j + k + h]];
                const im = dst[idx[j + m - k]];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[idx[j + k]] = re0 + re1;
                dst[idx[j + k + h]] = im1 - im0;
                dst[idx[j + h - k]] = re0 - re1;
                dst[idx[j + m - k]] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx.map(i => dst[i]);
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSFJ9lJ`N§;KZL'dHI3`Y?jYUbu-4cq$;fWZ$1M5$3§d(YEl9(e@Wrh_VV)DeqqKA?G6Ek2"WF?LN7OmUS'ihge)3 "Qjb2p2=sTl[rFg4I0r9^TdEHA]+Nu^`tfB4FtPlh;[=ee\g;U#\,KZH36EI\7>(YM/5toX'"d'O>§2=]IPq%KQtf# :9!0rP%i4%-,@E"I(k7Gpu^Nai]@>[5#50d$I[KK3Xeg3oe§cVJnOBTZ_§Xb*5E.+..Vc?bcf]%r«cSfAKf$08jGU6 mMid],I("-fAToP6J'/c6Qr\T[m8s,_dPS.8Rik2ZbdF\M%C?i"jpN3"TqUZODAbg2Y(gSP4Sque0§HAMSlBs,GUFN bA;R0!JnWIMdpb$)Ir=LcoMZa%=h3t+P/kRQPHS]b5+h3iC?7=\d"H7C«d_H,>ke6;«Q-^D@mn93)§97#KNO\i_j`6 ^CA3?_7BM:_hW$§qH>8,WXhB.%JO+2f?cH8It$G.(:u,6YEJRX«Ys452C`ij>O,n;EDbjBX@bRT=BVrg,:Ug%:m%b= +#qN6[.RY^_J\4-/`*1-(N9/3)N8=AaRl/pIuF[n9-6n9;9['2hM-)Ki10d4)HW8:/39^)§ROHr*F-§FBEkasC1g8_ ;I-b9=(Ej^T)=U%.p)%BeSln+YK2mmY(X18«U3os,#9W_0>lu,§)c6WSOqC*pBeg%K9A'RR6A6]m6fei_i%fQ(,JSF ^BXE.*68Zps0DT3QWP^sd2IH=^'«?W~
GhSEaD/\/e§H;*)5oBJa`g1?E=>«cLLLOJB!UEc;)>YL1«`PkE3dPf=@QQ%:hfm*WM39921Q]]?4nmb%Nd!UJGuJ]\ eYS)eGq^Jt-BDn=jCDJZ=]k^OA/n#0`5pOJJPXDRVTd0(YDZV)i\>l'r-]F>eR;V0opPp#Gt+MbX=Ed>OmQ(rd[F$q K:+'dMisBrglU'LL^]o1)a@e):auGajq08TL;aAUVbTYA9:h«a1DgKG3=§()(TK'/CZh+D1RO0D%$T,«M`V2KrgDKC l1fZ(mV>%;R[Te(;^*u?rb.LNED3!A@cQ5§L-5>cpS>U]X*+[.2KRDrE\i§@bhdh[cC2,d/5o;S5dc`i)/F7=/JG«F Gr;g>-WCJqU5-1f-i='jG'(*4am/,ohi;"WVOWsTpq6[j`ZFkU-Wn8^Q0T-t$gY`,T(YTicHjr.Z«$p?(%$cRU#u8d .YtP*OuFj?H4?R35MlDEhbJ"@LK#H>5pSd`H!!5k;.ojjgO!+h§X7Ee^mM9m?2>h%2%]X6cTg#Q?qFi§]-.\([0G_Y BZb,]Ls7)na(pjUA$N7*'HTV=PIp_$pOD't2#)g,aH'jA?g$JbR;\/VHq>A^0K>%lKDDsn@R42H6CAV3ZiJRg('5aF «p0d§q$M(Bi8tE11EVMTG537=bT_c^qYR-^-tk@to/]SgGD>`jL"'qO\*5mY?]hjK('H_d,0PQG62(RLIp85t'@!qF gj4@bA3_to5)tIj3ME[[=YM704;8j5gGH"hKmO0KIffh/,rn5QG'Y+)lq;.lrXq[,V190e'u1msRWA/]JCoS!=+=Ei R>f8GZs[(@"*hJm_43emJD]C)PQ2,l§tf?C(D%j?6-Hr?a'km+_[_$1L=Df;-56hPE""aEB"§'dJ0L#5*.j@_0M2iU P%T>Z#='aWOJb@%9«LOuJTg'?FoFpnU`KLfL4Nu4PbK#)2nj*TA\M2Vraf'kSYO4«E"ecMCG01K91p,(EAMZMrNDI? GML?`lJj"f*DU-'/qN4[A*)h);E>;G9l!$Nc+«\)V^>Q-Rn7S^DOXd#IXuo«mQ+/S`sk3gr+(D/D.Wc-%j$k"s%uJ+ 7Fo9Yp*'era'n«i-h/_^$DoH69F7EsFCR1f«?]i(hIYQf?nq69ms5ta614RZ?Ndj'BmG+u5ps;5qB^is«NuD1_«4*/ 3)«ms3M5X7_S.62h/Bqq1:V3iGC/PaUqJtfBclIl§H9YL-EUu"*lg§C/37C+U)@=!YkG«T%)rNKLII3«f9[I\`\e^\ EhGM(Y_;:gElO48Ngqo,hM:6HbFI39M2,HB]o%g"_GEZQ%b)1/o@47c:EW75m(4etJ]@G-Jo>pLTIQ]p:ht:pgkmqb .je3d§#MfSg_0/XpGN-2«rVhaBLWrU:3]P,gp=+[_p2);9DX>*!E2n>+iAKt^s'S?=[orWeP=uG+4"Quq#/NGl*HFh 7po678l"L8Ms_*-^H§5sOc(^JnQ[+sq"HFnir«3~

元々のスクランブル処理はdstを作ってからforEachで一生懸命処理しているが、書き換え後の最後でやっているように、idxmapすれば並び替えのついでに複製まで一発でやってくれることに気づいた。 ソースコードレビューって大事だね。

 結果が正しいことを確認できたら、このidxの変換を削除していく。

jの書き換え

 まずjのループだが、元々のループでは0からmごとにnまで変化している。 これはビット逆順に直すと0からn/mまで動くことになり、元々のループの最中に最上位ビットが変化するから、ビット逆順では増分は1である。 なおかつ、回転因子のくくり出しで分かったように、jは演算内容には関係なく純粋に演算位置だけを指定している。 したがって、演算順序は気にする必要がなく、関係する場所をすべて列挙できれば問題ない。 よって、jはループ範囲と増分を書き換えることで簡単にidxの外に出せる。

 また、mhqといった変数の加算は、たとえばmが2だったならビット逆順にすればn/4になるので、単純にn/2/mのように書き換えればidxの外に出せる。 試しに初段のループについて実施してみるとこうなる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    const dst = Array.from(src);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n/m; j++) {
            const even = dst[j];
            const odd = dst[j + n/2/h];
            dst[j] = even + odd;
            dst[j + n/2/h] = even - odd;
        }
        for (let k = 1; k < q; k++) {
            const wr = Math.cos(2 * Math.PI * k / m);
            const wi = Math.sin(2 * Math.PI * k / m);
            for (let j = 0; j < n; j += m) {
                const re0 = dst[idx[j + k]];
                const im0 = dst[idx[j + h - k]];
                const re = dst[idx[j + k + h]];
                const im = dst[idx[j + m - k]];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[idx[j + k]] = re0 + re1;
                dst[idx[j + k + h]] = im1 - im0;
                dst[idx[j + h - k]] = re0 - re1;
                dst[idx[j + m - k]] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx.map(i => dst[i]);
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhRS,b79+X'L_\E`@Na/TXZ\QR]u>jg_gl^63+EHLIPeOGAeq6«2>Gt:$15Fa7E\Wer)+^I=!$\j\#f6mP:`(9]C"N >%J;inXcF75pjb^1%r,nJ]eKI-R72sh§'!i^q,78;Y$-DA-])e;20sG_*0/mKT]_?«i8>J:T«gS§LnH'#_=-W_*J-r FY$I«Ijc>bP-u^j«"J.]"(S>8S#VVIHj§XPW2HKP3jHNc?/tB«5dtQW]nJZ+e«RNT]§Ftsj«V=R(YS#)K)~
GhS-YD/\/e§H88.5oBJa`g1EGfJ$9!LLOJB!UWo=)>_/\«`PkE3YLOHPjP:9J+"CLajc7/#tQ?5SpQKAa1IPVUol\V «%296$*M@\QE_'V,jV2K,P>-/EtH_$,N#X0>U«Ho.ETD-`LT!XK`8pfD3WhVW)RlLA3b.I,§nAu(§>8Y[i%D4c+§jC VN+YRdZTeuZBu5dj1XEk9«8Fl"^iPmo4$O[\0/F`.OnfsPT9,-:oMK%..XOf\8YV5?B§q/=$s9Ja_7X4*l98=:j6\5 5+d«!ed\:KAk+U+§NMq%Elk(%U!G?2Za]0NH1o:eVZj6,Ft4tbRB-2U34!ghg§%DcH(B)GFioONW§53H`gU.JbJ\0j ?M?"^$+3)Tm@[bQ1])glD!8ZUc0F!§bL)6CN72euIWO/sE6$eb1T]!eC?uYJO§YDB:YS2nk@`B8Z«$qf'^`q,Th«%; .YtP*RPuiKH39ki5«euO:BrfD67O,`§«3o**`Yuk5$@pQmX\DM$tV#7(t0dP9:naW"0nJ«q*h-2,>$h^QE3EZ#3A-a `YEd;jq,>q23DjI:>HIMqa`,Us7#a@hg:4bhiJ$;KhUEE6=,erHCmlCjH0VZdob.fD`93WUbs`_Bjj[C0\pFd5iQ:- Iaj[e!I#"-S.j"l/->«],sp`*K89H7Y#5Uqbg!dp'?E[2H=oRu38SY$!lA"a.A-iqVl.VAi'PeuK?4-]%Pb:ehkq[Y KYk=>U?a21%3DA3/5kr3/VOnR!J9-dm%>%gH6F6)p2*r%:]@pVEpZRSc^9f`!,euY1"oIMn9`eX;Lh%r!OO6L$A-b5 ]RD%`-BT72a:3=7*2u(KRtcXc3!KaK]E2_HRkP7?:dpPW+RhM\"!%SrC>(-%SO"-N#3d#doHc`a3BW[DR(hY$-4E?D -P+S2SDVQ>*mX!Cp-§J`b.Xnfa(hNFQ_pFj%LW'>_gMj$1Hc)G/8)edQ>!X[D/2i2iXm?IX-mRXh>qj0_r(r*jF(tS @«t4*§§\s'F2H3?ki),+r6§],6BjX-LZ`!6Va§!$«k3sO6[17*>%n#`@.+0,4oT+§2mdg4_$87in1Iqd>6W4TqTr/# p5\M0I§iN[@S`-RgXu4?BSHSXTsblng"«`PIb7+F!J5fG«G.2Td=+%k@«#g1(3S@oWO.CgD7'Xr,7N(1d3A§p>/F^^ @K#p,d-\5I@4G1$3Sd!oXE+k9RM02n6oZD45%*?-cm^SH0oJ1P_o7#p*p-3`UB:/0//^`4m`#q«dP+§%N8$TLiJV4u ZA^E':«e9U)Vk`ma9,*Y#('SL59aMl?@A)Z8aYkM/(AMoPi.]f§b[M@;sP%ZDKj-iTs%T'\VJ_!gkRHY*lTS)cg])q OR(`[J'_gMhpB38Ng5§LCd5@WDY]cS\JA(B.8TTH4Mnl%?^ddN~

 大丈夫そうなので、後半のループについても同じように書き換える。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    const dst = Array.from(src);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n/m; j++) {
            const even = dst[j];
            const odd = dst[j + n/2/h];
            dst[j] = even + odd;
            dst[j + n/2/h] = even - odd;
        }
        for (let k = 1; k < q; k++) {
            const wr = Math.cos(2 * Math.PI * k / m);
            const wi = Math.sin(2 * Math.PI * k / m);
            for (let j = 0; j < n/m; j++) {
                const re0 = dst[j + idx[k]];
                const im0 = dst[j + idx[h - k]];
                const re = dst[j + idx[k + h]];
                const im = dst[j + idx[m - k]];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + idx[k]] = re0 + re1;
                dst[j + idx[k + h]] = im1 - im0;
                dst[j + idx[h - k]] = re0 - re1;
                dst[j + idx[m - k]] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx.map(i => dst[i]);
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhRk70l+Z\§;BlVMAr+b9'eg:i!):I3YXVs$Q4QU?mV'HP!`GGs*O4oaEDE'cD!+Bn(rChZ4-NWqB:+e«ZbHK#Q(V, S??$pZIH,7^.6b13W.*oo+Rt/'mpqP_!@[DQD1b6)A`"dgC$am=Flp=5*^4>/+n^UG>tKg5kL1?E(4-'ipdGG%^nY= K6?/(`oJlF7pGBAnS«n:>k\§:"I?%u(ViB>PP:E--=kK`Q.sW^Wid$GN)«M3d?GE1_-jOOWIkRc§l6tg\SK@)9c`+J Kou«?$IE?Ms!LQfSE(M>«JsLj\D_%En%G(§M.3"§DZ[*=Tn'd(:`L8+0C\DFV]A_]T\#hHam_38n+f1t§GG2r$,$($ !cS§jr§\\l5>Tr@Z2~
GhS-YD/\/e§H;*)5oBJa`jT[gfJ$9!LLOJB!UWo=)>_/\«`P;53YLOHPjP:9J+"CHb*5«K1XJbQ4nm_NO%5=P>pZ;0 -'.->+-C[t,0:3YejZ!^nuk)M5XZ-AHlE[mQ§2S85n9J;meJ#_ktfOXL@Km)91bIC"r,)iM6a@caKJ%Hc'!_1N!]St l:;[:/JE.p(/r(kHl?o#nMNT]#Abf_Uf\QO)h^GDo#alnAHGYW-rf7MjEXhZKQ(*VNa/+RBLla§)@]h35ha-mC:gA7 oBD$Kh#qJ-FsKfhce7iP9ch;/IBeWgDj*9RijmiC!]sHM$Hd5JWrD?sSt0A_rD2RgcFV6"j?t:n)tBK0-9'(§RC+A[ H5>n"C*B!eco1V9s1k07*CR§4BDV:Ip7a66B^*[s*1Rsogg*(\)Leho]BFYk;_NRU(=MQAWSqEjqU''3O+Tn:akh+N D«VdR«)iK*;Q'Nhn0bjhfX)$c\/7IF6FV5B4cAN7rO;Yo>*T],[Fen_Qcs>q,%(ku?C$C#1A*4j%0,b:.VRD+4UB:F 8Y5EjnDWo+#^B#iG)F.ba«c+VI/E^-"%@i\aFa=V`pc#9YK1t%O2k2i`;JJ0XD4iGSJDU]Rc]TDhO*G5\oWN)M2*,J ;-CU6g^UbR-\?Wr";'[P#L"BH#eb(\V%bbS?$T^§O?6Behk\a)h?eK:_H/!8i!=p*:RPenQ)(G\irc.uEi-1W:O/6; $L8UgTqA§sn0hRTTmRr".?sT:§F?9)#H`7!$eV\jDp'n`p=«f;X(Zg%`e*"Bn9BA:fnOs1^PCuC$8CZkJG'+Q6'-6n /bc+kj9DBIkO?pU7447/QXHu3hpj(C*Io>oN?$pmo@nQE__o"*;HsmbDua'Q".XLba'fP3#g9(W^()(?jPdfUgmR$= h3g'#>^L@qL@§lcAsS`=[J57QPbV_HfU`-PSNb[U.LT$«i1C§A]FRa\9c"Mn+R+u_c6WQUHuA^,f0ZlgdH!oqS#.o@ "(G?-#+W7eECANDRn+on,;o6S;K@MABr3HJV\r@lFjJCMH%(?d[%Q@=0^l]t/#«E^K=ren-9cSN=t5:CQPFTD[>!:V $[Je'e\E#f_ef§3g%G@hf]E,Z"fMBe'V,0Wi+)4p3M%«>+rT]F6;JpjfQ0%?3?IoECOr)Vm>spMgJGgRn4us4RA=E( +§78eFA2\.DT;hog+]V1(-uk0K/tPu"F_U%kf$cW^G1:WDn,«!lr90Rbm62OXn*4NEL/[,9Z[]Mn;-4XFa0WQZe/2l \«tn'2!m«\W.9;_V+/hVHP6^79/Y=e127K1KQ""NcSE(>W\j`aC"/h^660IR1@T/>rJ6-h!o_6X§QIX?ee0k,$H9NN GD*]M.;Ca$D0L3J>\b%sT_>U9P'^#Zhnpk=oE?k$%(Y6nr«Ic@n/DL(Wa)O8Du2QjF3AJ[k*cZaV+Ic^jUqLh%OQ_W Z2~
kの書き換え
kjとは逆に1ずつ増加しているので、ビット逆順にすると最上位ビットが変化することになり、ループ上限がnになる。 元々の最大値はqだったから、増分はn/qである。 これでq回繰り返せばnになる。 元々の初期値は0ではなく1なので、ビット逆順にした後も0は除外する必要がある。

 とりあえず、大文字のKでループを書き換え、それをビット逆順にしたものを新たにkとして問題がないか確認してみる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    const dst = Array.from(src);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n/m; j++) {
            const even = dst[j];
            const odd = dst[j + n/2/h];
            dst[j] = even + odd;
            dst[j + n/2/h] = even - odd;
        }
        for (let K = n/q; K < n; K += n/q) {
            const k = idx[K];
            const wr = Math.cos(2 * Math.PI * k / m);
            const wi = Math.sin(2 * Math.PI * k / m);
            for (let j = 0; j < n/m; j++) {
                const re0 = dst[j + idx[k]];
                const im0 = dst[j + idx[h - k]];
                const re = dst[j + idx[k + h]];
                const im = dst[j + idx[m - k]];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + idx[k]] = re0 + re1;
                dst[j + idx[k + h]] = im1 - im0;
                dst[j + idx[h - k]] = re0 - re1;
                dst[j + idx[m - k]] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx.map(i => dst[i]);
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhR;#9+->J§;BlXMR0[--3Y,mJZBn=B[JG;aApWh\`CH;M#Ma2«`mj§k02jI_tZEcdC:u_i§l\],LRBk8.Qt?XpDAe b1«D*iG,%uYr:nW"N]uB=bnMi@mY.-in/U12p6JIW@D=p'8(naT#WV§SjeJ#YkY'sVT««k@#c]KO$OG^67UFVbre1n K@'MjZ/2'E?8!LdFf!N0+76XBcFGF_@b:J@b$Qcldi[0^U:O%r~
GhSEaD/\/e§H;*)5oBJa`jT[gfJ$9!LLOJB+mV0?)>_0'«`PkD-5,E4PjP:9J+"CL8^rZ^bs3J@pY9i§45LuPVH(`0 W«s6;WBVPG,sru!8`ID(!qg2kE=gMB,@;f$4p>"f+j§,1q6`3QM#P3§>nGhlTMTaDAjC@)O>%-dP5Lh_]9FD>Asc4F ;hZ\SBYhe*=j;fEn6'hm8§PP,!jI4aGokQg/dOZk§Z)CN$4]j].+I«#'NnLN>\M_aP;,g^1*iogN/#Jn+`-^*FgkuC oC9Grh#LVnFs«[oOg3A*QTU'JqW§:@H^e«'-Z«gJ>2]6l[t8Xe\Z@uS1G@$.0E*2;«i')6ST:G,C5r"`YpL:@10HF\ 07"DiKN]OeFjMc8RJjsFmchN.c+:-_01RZ(;GN.Tpaooq_]«r§c`dVGb-=n;/l7Ck4RR5iT:^DRb8U§;;m[VNKdunl XruHFCYg`ukp;!XJ,/DSGWW3V(K(T#J7sTfo:UJq@;Gh.lT88c+dBCi@nbLu/U\gO%E)Sqj_'kRN[04k/$dis2(/u% ;m+1D2?t«FYDIqQD!$I#3!;R1gUjq=s4gL/pkGlk:Ljg9;t>f>j$PYoP6^7E:1IF8^>6"QON+X]VM-`V^iZK.!/ZIG qE\?"!8I`oF%IYKYR§%fNS$"A(d.G6«LH\1A8^OHRQs%TdVBNEg@:3P;\6Ye;%>/`4Dc=dr"jsf*K6!qEep-CqB6G" -PY,`(0tjl2L(J+BqX;d3smEc!sJK]fg@3sH«W:$mG"?@*kf))1[Rmic^9ee+!.Vh0\T@Ln9rqZ§qE82!OO6L!eR3u BXmM*,4EpKAHjgD;q.!"4i^[@IQ."*Iftj';(W$§jmmG[oPd895alJC)C'c%\HX[t^sciAG6/.\D$o5p;[B]nl3>T- §calQeDB+SJ-?bE](9=X\[`T#p/$;79UihMT!;-E;/6V§a-bHXac\_]«_ajcLPK`mM§%YJ4ue+D5GOlA;MQ6`a`@Z4 q!9=+FZm\da:"`_f0Pq)DrCaLBQu7bpYqm3«q%-r]`9til0HStDeSgVJdnB\Y^"hfegOoad"e40WHT.?SSr\7:[K=2 SJ)r«#44rh4$ouWCSt+[7/:g=a9W(Nk%0f-=p0W>A:hd_acok$qkDLWL:,9@gD_?0%Z;*RQWOQ,q3(Cu3p?:+T.m[T -]RP§HJ1?6+3fWsF(.3Y$[fc+,.:6`To*">W>R>NR@B:"LPuSm+1#J\h7+D"n?-]LC(W@T>/P91=JRBO0qFb_0f?NU 'T`Qe!=\\1o"IW2HGFeUiKDpZ(G"!S1DJf24V@CVTrI\rEjrYnp"7eGKA=@Z/,(.o^BS+5kn@«*(ioTFr\s]7i>ie0 l«*;_=n?^ta+h>mR'DaI9R*AZjU(p7"1j/;#l~

 続いてループ内部でjと同様に書き換えられる部分を書き換える。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    const dst = Array.from(src);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n/m; j++) {
            const even = dst[j];
            const odd = dst[j + n/2/h];
            dst[j] = even + odd;
            dst[j + n/2/h] = even - odd;
        }
        for (let K = n/q; K < n; K += n/q) {
            const k = idx[K];
            const wr = Math.cos(2 * Math.PI * k / m);
            const wi = Math.sin(2 * Math.PI * k / m);
            for (let j = 0; j < n/m; j++) {
                const re0 = dst[j + K];
                const im0 = dst[j + idx[h - k]];
                const re = dst[j + K + n/2/h];
                const im = dst[j + idx[m - k]];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + K] = re0 + re1;
                dst[j + K + n/2/h] = im1 - im0;
                dst[j + idx[h - k]] = re0 - re1;
                dst[j + idx[m - k]] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx.map(i => dst[i]);
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS-TcYLBO'YaHGI)fu§>K[M/.eG"-)!nJroH7.JW_"@j\?UO409*«8dV9o;(\iI@-!>ulmIb%VJ@UhSf]XD[@"4>n iO-UEq%[RCN:B!HbpmJ-%F,q$Hu)J:eQ`:b-LUB6U@*O;@[qbGIB=§#G6SfPifqS*1(r(jAfX55bIOQW9^o/BlO"§J qn52SGrfD1JDYE45`=Zs.p=,b,M\W[)hMfb']_.WD§FWs3KU-`0E1")7t*o117@(%#YKL0Ge\V6kLQ>XX8C(``nZYb 3g8G'YT!fZXHsA1hiFX_(LOZgGFb5,Mh*";GJ"cGc«uGNXp"VNlthLI#KD)§*r~
GhS-YD/\/e§H;*)EA7*;N,@5XY[OK!%`Pjb6e6B^1A$3-XK"Zi9I.d2Ms[>0J+"CH8^psB",URFh/dednW#G"MbHC% Vp`nhmK@]enoL3IfHk7ujaS)O5S6(tpc8HRU`qHi"XbZnY2r4XlmM5f^#1gCd[[Gk*$NT@n09U0i7B728)e§1rC!?K _U"F1V#mf-2$Y#JZbiT#g]9tml?^Shaf[t8Q0=Ee1359I.e)]rKc1qb12[KC'UlH$A-gYToYE#QdNVJ)OZ)m#fqB:4 55t§Z9q[("B,HlSR%HUsi*Hu!-QBRQD0?(Yc;8ah@-;8k+h«T§'q^5C(LtOEmmN1oW/[lO%[p5S`#[nW0!o7ne1NC8 *pnOt+j=-D2YK[h(sY;1AZelHk4N«ZHnQ*`*TD!o_=]$jYE#9%T«b.roZkWI-[a\sH*r.a'eZ]7cW8MDkV]@Qa/Xbo %BjdM,\*03Sc83+TD3Tho«q7««DQ§!Qo=]E8*)\dJ%NF)=mBL$Wrm@Qk'$'C_DN#hO\h\Ad'Y,"ra!?P>HVC]!/j5T %l>82I(TZ;O4haSa?+]'LYKGMGQ6JSEqfi>n-_q[FUG«[\iU,F3BV8iZQ$6§l>>!ZWRS(^kusCU-+[uR§-R!O6U,81 (T;P/@4jicLRJ4LgrM(VJ2'E(?Xt«I95[hfPR6H(P[9]@«f+;KStb]h"::)2bd:fE].]8K'+@W(3HA_65DCp+^i8,n ;X.!-V*7IpOdEiSp'aUl"p#3T3++6pG$EfVmFsJj*h?T*`§aA_SZ*`u/j«TK§_ksapq8m^6«(jO6Ih+S$OXbZTZ4]! $[GP$/7X'An"L"S*!C88nS[d(mXRV:"I0tu«QiQsf[23d;+Y9BaC7E.2Qd`1D8gQ\M§Y0Rb:bK^kl#`n_]BVK1O1AV [CI/Kj-ZQcA,sslNqo?5XK;7S@E@^[rYNr\«%#Z-U6otGfO>K"mniF!gZZbh$Q:5N)L*fjD@lROT*rSt@q9N1nT«^L .NB:V?WY8hentNb]K=8UpdSPP#HaTkPO6_uNE5fprOnk/d\6+sCE:d%:JFcS2XmZ:h%gbY4;3OMUN*Ld*B[sGf*8T2 LmsF@\^Q8qnYrS-eEhH:_J'(k3#«mc7Ihq;k*W6J\N§t>'d1«,+tKCXV;AJ,7,m4A§Xa@+*+d8P`jO'+gH!O**#FS= h51pqhBhVs/^Up^KrMuAk/c#h«$-J«KHYP[aIV,3@6lP'bN.4m"_:L`G0EQ0mZ^d.q`l],d/T3*;UFR>b_1FePimDY f0lb2a2q(G§R06\RfB?DH>o]_#«[r)`jTo:,f§]@Fn/?WArbhU:)8#,!a7?@_VB>lZp"rdE"BTBs#!EPp:/ht*N[-H Sf]`]KXW=(=F\$aK0;5:=\"gI.t(!jIs:irj§[1@-^DnB§a4Xg`dckGE+,Cp~

 問題はここからである。 実は、ビット逆順の補数の計算は、1の補数は簡単にできるが、2の補数は簡単ではない。 1の補数は全ビットの1と0を反転することになるので、ビット逆順にしても1と0の反転である。 これはn-k-1のような演算に相当し、DCT4のようにオフセットのついた演算ではうまくいくことが多い。 n-kのような演算は2の補数であり、これは簡単には求められない。

 データの並び替えをしていなければ1の補数と2の補数はひとつずれているだけなので、ループをまたいでインデックスを渡せばなんとかなるが、並び替えてしまっているのでそれもできない。 Split-Radix FFTのように二重ループにすれば求められそうな気がするが、定かではない。

 そこで、とりあえずiという変数を導入して、h-kidxを引いて求めてしまう。 ikの近くで空いていたのと、imaginary、つまり虚数にも通じるのでこの名前にしてみた。 これさえ求めればn-kh+h-kなので、idx[n-k]idx[h+h-k]idx[h]+in/2/h+iと計算できる。 コード中ではちょっと変数の順序が違うけど。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    const dst = Array.from(src);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n/m; j++) {
            const even = dst[j];
            const odd = dst[j + n/2/h];
            dst[j] = even + odd;
            dst[j + n/2/h] = even - odd;
        }
        for (let K = n/q; K < n; K += n/q) {
            const k = idx[K];
            const i = idx[h - k];
            const wr = Math.cos(2 * Math.PI * k / m);
            const wi = Math.sin(2 * Math.PI * k / m);
            for (let j = 0; j < n/m; j++) {
                const re0 = dst[j + K];
                const im0 = dst[j + i];
                const re = dst[j + K + n/2/h];
                const im = dst[j + i + n/2/h];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + K] = re0 + re1;
                dst[j + K + n/2/h] = im1 - im0;
                dst[j + i] = re0 - re1;
                dst[j + i + n/2/h] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx.map(i => dst[i]);
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS.?:JZU.§B4*cMK`mK,1)6Bd"G=2e!PCLep(kA)n*7LA2.hXhqa]Pm$«.4$/'n.]q%)«,ROBKF;«Usn?tZH?tVZG MW:1$.p6Pef2"2$VK7-1B\'F:0mbo[f\d6#G`=/^L#u+"5I`DN[*Y[:::/f+CcB\F-[M\r3R-J,-_deo0*upV«S;Lu R+W+1qW77=U)X3`Hr),=«CZ8'W7p6+U^L\dW3efe+@N[kS#§6#K^VXC4[FTKoERCU>gCpTC!!«Hdhp\eS"C`B/;%cU 8§pA\bD+2%f8mJ!LaPt+SsmuGMW>U,Q,WRi)DCutD=Er(.AToV>jIXS0$;%3_L>Df#1=ddlNC,feI$P'los^Dd8'3% i_f-lH$ca«IX=!L!GY?f#Q~
GhSEaD/\/e§H88.5oBJa`jT[gfJ$9!LLOJB+mV$;)>_0'«`PkD-5,E4PjP:9J);8«8^ptN)+Z%ESpTmbGGN?^.QJqX +[S'X01]?i:l/G?3%qR§rEa%T7.eF=QO«keJubo5jQ*««7\"]k^LQ+aM!e5=.(\(cTK$;?0Fc0fr0V-oPb5$hQqK,3 Kdn`h_9"Q;08R^==``Un_uun«Y^m4.*nMqq#X9dNV5JSG%SqR§eDq_d=]]9'B\1NGA"«P;OK«%L)CiLS[)l+nFj9(3 _`(r6$+/4K9L^«PA*4@TGn«F:fd3eLRpV@lDp1?rE+?;YT\O*2X,l;bb:2T8Iq6j)[*,OD:38h)§UuXhjAK2/bJrUS c_=W?.jX]rg?"pq§);H'K?,q9d_«Gc]eDu;/]R@j!p>:ZG?6.oK9,-H/p\>4`(Q]q\Z5DMU?6$HqUoT:pObr#r)e.S D«NiqR$[FP;Q%;-q('+KfV/a§KcE?7_R=_d50:%:rmT+T/W11o=roAq)s4RmOg#%!0BMbg8pj,A_;@-'m`ZTV#GESJ j:R0"§!,=1_IGcR;\U2RTuc'gg\\bS-lRJ#f§R§kK@^ud7:u'EROuT*o6\*SXK^Q!=r^(C=r>`*Qkj)@1I?W1_[_*u /RJSI'§6_(aZOA@LiP"hbTV]H]mWm-'Ju4^H!;VWO/DQhUpLWE'D)]dY=Onq#_GV%bVK3PV!A#U';caiB!Zq[7*5gi Fc45cgSbQ§V/[EPGJTk;M;J«L@6kCMMQP`q%86T,."Wg"0(M\+e#P8?jN«7ulYF^§oJ:k:cB/6;cqaZ/#sNtakmA?s #?/\KN+S?k96V2[,lGG4=5`Q@@HW[OngLO"§?f>EXcaaNkQ4:rbu%sRcm[M]AJ]S4#'#P_G4jOD(VLo/ES/1GGU/?I :to2nDa'6i.o!hIj(P++![§.$!]PDE7^YXd24PRp>5tKemTHi=MLZ9k7r]_oEG`A%PY«S!'%8qd)Z.fLR.@$87eVua /q.9%\__/f,8$ra,l0§SMCH'V=u_ZaGVOl\$.el/]Rm,di7q%d«m[-)0I\4MYm-N6^t0:+7KJF$hJ\-_Cb+-aT#>b7 Dg5RS\_!>6kufS5bjKO6fPrNU0J*H'9/J,Ap@)m14?r38)$.Q\]W>""Mr6QW3+6gfi6866\l*5O_qK:Il^`$g%Z;*R GAITQG/a:o%aaUfB'efB4UC_:eVBjIT4guYR1m19=lKfa33dPY+'$No:X*i5cWnuTA(%«+r@pr7d*91Z"Fl;r;niU- 6uCj09.\MUY*goe5?JgeX9tm.*)R,l>C56O@=A-$.cHQE_`:briDqAD-+bDg/P§h+1X^F?_!5-TAf9)>«a3*>«VSQ[ '2"nU:;r=\[m,>8cQ6EO_5,AA^,e0e`kHBdl\ubi.E[«6H=*9§hnNu`U1s~

 iと回転因子の計算に残っているkidx[K]で書き換える。 iidx[h - idx[K]]と二重にidxを引いてる点に注意。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    const dst = Array.from(src);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n/m; j++) {
            const even = dst[j];
            const odd = dst[j + n/2/h];
            dst[j] = even + odd;
            dst[j + n/2/h] = even - odd;
        }
        for (let K = n/q; K < n; K += n/q) {
            // const k = idx[K];
            const i = idx[h - idx[K]];
            const wr = Math.cos(2 * Math.PI * idx[K] / m);
            const wi = Math.sin(2 * Math.PI * idx[K] / m);
            for (let j = 0; j < n/m; j++) {
                const re0 = dst[j + K];
                const im0 = dst[j + i];
                const re = dst[j + K + n/2/h];
                const im = dst[j + i + n/2/h];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + K] = re0 + re1;
                dst[j + K + n/2/h] = im1 - im0;
                dst[j + i] = re0 - re1;
                dst[j + i + n/2/h] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx.map(i => dst[i]);
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhRk6bmM«A§;9L9`>kq`VK``Rd$#S@nDLEnOL2u/9sKPRM!sc?or!LGpQ1pko8s=u"E5aFDus(B6%H)O.=q,mKTWg^ I(jcG+o8_Cn>aN!V0N+)O;NY«61L.7@L?*n«ZRElMf4GWBnMB"1;66:`jaaGfBj§#199G5OCbs[]911B1WH*EX;7Te E#eaU9"l`+eIRNp.*aDa=_HODA[QYlXt`S§s1qoQKD^aA:U'"\UVZFE$-§FOCd1oF*nb%?U-0@9%mL(§oS*_~
GhSEaD/\/e§H;*)5oBJa`jT[gfJ$9!LLOJB+mV0?)>_0'«`PkD-5,E4PjP9nJ+"CLajc64bs3J@GPBni*DND2a[rH= §ZVfp+`Seq-#uT:]Z:0IB9dHZd3BlC-NlU:=§gEcd8TkK3:_n)^q«qah5[,EFM28d;,9`M#%^ATcEgg!O§e$[-)de0 Po$gcjGL§$6tH(/YjirIJlDc`JIHfF2^g9lIT.L`:ZE-/T)OhM_1=#?0%%1J3iaW«R/UV%efYh:Q/=p\jCdbHB§iLQ ]«C#j>%gs#;2WFsrQtcZU52"D^l«3;/+Vb\RAU@oYLXD`]?"RVBcMD«%XIRdOg?P),3tI,*DifT+,r.gd$i(N«I*!@ kU+dlafNDI@\9n9bdjVRcSb")^:kT#0M"2U`o=H1j+\=n:X>[«T?=DgH5ENu9-/;fC?fHgi3HCrG+/-BTM[""1`\tg G`«[)-,"uYkq@Pb%W=iqn$80E%TkQCD=aEJPi5420@tET/4+#c\]:)?RG7R+_ASHKPfqB§nJt4)Iog0e2.t8WKVi": #944;4C/[-O4h_d;/ceo,.Dc7n,BkoW:17!Oe9]/?Dlj!RDt"q3[YH^mo«k#@D:R%3%V1H@8mW0>(Gn`#QYJI47n0V Hi\U2@-3VCk$\pRQQ*2@%PW.kmA_q8+N`E_gGAqZ1o/_MUlFCTVl)e-7A:bk§9rMY/G!3S\oobg@`T^)2X6§cUmt7@ \%Lr$a_SeTe2Roe]3n,P`GY`a5::ek`KT)C%9*/4."XrB0(M\_e§sNojO/h(lRRoPoJ:k:cB/7^TV$bB8,*.=Ue>C+ mnG"pd@4\O0AS*=JabhC=Fn/TqlN*"O@N/8#Ckk+EgcRpN%?YsX;OWh0XS\+j.a_!§"U5HMHt]$iC2AY%1[_E5Yi-t 6Q]!r2E,%#O=De=!PK#=*/=StcAW#[E\-,\.FCZqVa_mgF/5,F§T_U2RVTE§fq,P'LlIekZJcg$2j=9>^>)GmE_>Pm =$DYnREHu'o',i#_O[Yu(Y2$'OhK]=B§>q"(ITq1K/ep@@RkCT?4>L_rbMKJ]Rm.rptUlq/SN;l4MPQK(XM-!0()?d guGSbD-«JQ_idf4Ok?(^,;Fg:.«§+X;R*WVZQ8N4\-lgM!.XUG8eiq0_f08*iQ0«gX*1^FBbeP=B!q$.2\:a3bCS8\ $+5*$qKEC1m>Y]DMm_AlHFiB)kS,tU'4^=GG"rhJK#:«!S_cXEF4_-^=NiRF1j;;*§^Wb>aUl%'2kJs2§@:kaYRT)# I]^O4:$IQ"eGP6(dHt7**ah§aY\iKMO+>L?oi/dS[«jo7.,ebd_BW7sp§_%`om1`)h6Z61TMYp(LXQpmK.Qp=KF+JN E1LHoouHsek`^e2G>P8KV/M@\§$[3\r2)/X+[qgg8CqD^s1SLd^n]?~

 これで小文字のkがなくなるので、大文字のKを小文字のkに書き直す。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    const dst = Array.from(src);
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        const q = h / 2;
        for (let j = 0; j < n/m; j++) {
            const even = dst[j];
            const odd = dst[j + n/2/h];
            dst[j] = even + odd;
            dst[j + n/2/h] = even - odd;
        }
        for (let k = n/q; k < n; k += n/q) {
            const i = idx[h - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] / m);
            const wi = Math.sin(2 * Math.PI * idx[k] / m);
            for (let j = 0; j < n/m; j++) {
                const re0 = dst[j + k];
                const im0 = dst[j + i];
                const re = dst[j + k + n/2/h];
                const im = dst[j + i + n/2/h];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + n/2/h] = im1 - im0;
                dst[j + i] = re0 - re1;
                dst[j + i + n/2/h] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx.map(i => dst[i]);
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSFH5Au0.§B4BkMAqbhY"=qdU6V)i«N§fND@o5=6oZeg[HTnUrU)i/!/AE`bMiVQpR[ufU'TuJp1eRL`%3Ep,1u06 RDSAdVDYP§XOkcsXjS)7:VaLr?#TF%5dD[d'"E_p4dSF-b-J5RIjXeam4§:gQb8%i;9«-3/FGclKes=!5'?K8K=$fQ #([4GqqT+]2ur;LIX#(i[A?W-*t#R!D-^DEQ(BU:DJu*fYd/,]TU_Qhj__'\nm![_7hol?@A=/W._1p7QmCbM8]5j8 Io"M#\o[;fm'/PVEPX3-`.F*@b^(e)#DfkH83BN(j$YZpFq1El3Bin:X682[T;UT'Fa%f^0§00+`)XN:*^/XtC=KV# `Vo'bDT"-0Nhc«gg@,-/BQ]§NIoGB:[LsRP8O%s'Y*\bDJqrNk0"§8sDQ"L2B7]t6(EXTC~
GhSEaD/\/e§H;*)5oBJa`Kk«FfJ$9!LLOJB+mV$;)>YL1«`PkD`Y.B*PjP:9J);8«8^uMM)/,l9SpTmba.p_g.N'[H _*^6WB-aWJ.![F9S/5«NJ$1U3,'n:NQPk?]_.6OOEq3aib>0[`h`9OWM!ehN8@mJ65]NBX0Fc0fr0V-oPb!bFQjYTH Keb;p_8m'g0+#to=``Va`rr4YY^m'_*nMr$#_+VgV5JPF##B^s«9,1_=]_OhB\1P90i!3V,cd??L«T*0D;l5:>r2«P nBI;;!Y)PY-DM3F3Tgo2Gn«F:`?qa9>MP;VDp1?jY[]PnT\O-3X,l;bmM*#)Q-«BrG*u@PM+X^$ifE5j>Udk7D+n:/ /D:7b6F,gJD=.`1^mui6Lg73rk4Ncg_B«Es(sCeDS/Ze0?(G@45*f]jkZo;E06uF;ZJIghRb)9'cW8N/+#L!*a1?n: 6a(aP]VO/@3q^Oc^V\aI=`tr7(B]I[Qo5JtB+_T'J%C'$AJ%75MZ`*`j4LJ0"K[1,9:>Q#'Ans@nF))oDY4pT#GET1 A;>qm§!,=1^gjXR;\U2R0$4(0gP«Q4.2mS$f§R§kLY"P37HX+pRP2e.nU%mQXK^Q)=tE3SRVUAWS*"+iF+aOMLE64t =hXsm'tCdu_XXi:+Nd3gFY%!Xd+_W56:')*.rWQ5o$N]YlBKkNGClE=n.ge#3.`:$2RAEn+mCJlk_(C?S=-H#':psG @GP«GoJUHD7qnItnm/Jt:uXE!E*/8*Zsp)FFbZ`@>84o[(Dh@oU?U2G$ZbXPj7Njn)u:HO*hFS_^n@;"g(Si«6.-K) geYMC:Dh>+K9u!YC.['/@BRE'a/?%(!m%.8Q:§;oFhLCn33«Q2`O.sj§O«*r"5PU!4ELBcgu49!IJ2VgmDh:NlsTo> E@q1?b4rQs[«fIf§)BMsgdi)4X\kjt@fa)rUI)"Ic>be(W:F,7G_C0«(]@0SFf9H7>G3Z%7^Bp§(B6IZ?/M3VO/,X\ l[3F@fa5"JjP§«qGJ-PEE4asR[$$X+38U]UDa$*lXhDEX%,XddK'e9Z`'npe2YE/eWjtMlpEQ;BDrF6iiPdM#2Q[>$ L0$)%+#pl,d0dIj*p07M6^N!02o/-6n@[n+2=DfnbI^fcki'k$/+7b0V?f(=HeJlV4PqqSof8W?BU!G[-(pZ+m'I9k "9a4ZZ:ZDpK§c>%!/i'«J@oWo";Sp1X?E_q]ibg0Y\4UCXWX:EY+>R2$$:MM#2=B4#.b]fC8b@#!6t$gF*tSF0#$78 c*p`SUP`CEWG+!*dhbc+ghRY/5A!Ip\p;nii$Xf(]bFn=W""-#`B6lZg/4^B#>:geVZQ,`UXfJ'f§!l,=Pk>cR4pqO ,pS_S@94ko-*ni?D,*s/=`:p!E9c2;@N!Z:«a3*>«VSQ['1uWiDWi4pDh%[O?Te6#!L#!7l3c«o«IWipf,HO.JsQe% e@n§_J,]S_#LrD~

 idxの表引きは残ってしまっているわけだが、実用上はどうするかというと、この変換はDFT点数が決まれば決まってしまうものなので、回転因子も含めて全部テーブルにしてしまう。 三角関数も順序よく計算できるわけではないので加法定理のような公式も使えない。 テーブルにするのが一番楽である。

h・qの整理

 続いてhqmで置き換える。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    const dst = Array.from(src);
    for (let m = 2; m <= n; m *= 2) {
        // const h = m / 2;
        // const q = h / 2;
        for (let j = 0; j < n/m; j++) {
            const even = dst[j];
            const odd = dst[j + n/m];
            dst[j] = even + odd;
            dst[j + n/m] = even - odd;
        }
        for (let k = 4*n/m; k < n; k += 4*n/m) {
            const i = idx[m/2 - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] / m);
            const wi = Math.sin(2 * Math.PI * idx[k] / m);
            for (let j = 0; j < n/m; j++) {
                const re0 = dst[j + k];
                const im0 = dst[j + i];
                const re = dst[j + k + n/m];
                const im = dst[j + i + n/m];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + n/m] = im1 - im0;
                dst[j + i] = re0 - re1;
                dst[j + i + n/m] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx.map(i => dst[i]);
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhRk8a_l_b§;KY%ME*#nKLJaBM+"))83Kr;TsohF"@P6!7PX-?«I+>!m19\d;^=2Sq«"«gG++$_(u7]'+PNV?Ct^,[ @Je1«")U$!«pu5QjAYie"+9Zo[g+>N?,EoZS)\«8)Q§«Y!S^)ZjJ+g;6R#:P/=+)Jp$c2.pn/]\«?]@)5]l)>USH6I EcqS:*G"u)\?i6>BnsDV6*gP40O,1;2$S]oj]BQd`BMncXKS]1R\K$D5Ys!AWiRknc*8f;Usg1m;§P5OLZsOB3mcJ$ \I,VsefgrLnms"§U-.'U\6-ghWe%)jk!o%":o%gqZ`[,-g,_$Y*b`h^2.0?_rK=b1WQTnG«4;P_>9NaQ3E3>tBdYW[ qW$rH=#pb6SkZDCdOqo'r)P6§!WVEC5)Oo)oC§N[PDM%O[_3aT@PlA(NfK-gWFs?k-dHqWR];AMH;g@pi@EX+fq0Z; Uf$$5>Na1;>Z(5TH«a'X>hF9c=iH0+`)QFim#fDRL08%9U#"^Q#Xd«G"mr>ZDc]5[«J7BiaB\s,X16M'Gr"g]+*NZQ -N~
GhSEaD3*F0%/ui*+U%Zb-*d§nD§A^JLLT!I"kIPCh9Z[`Z+@AKlctep^Eg??o%ur0Z7Wi!>Cb9gOaK\`'LeG6X+HP0 bXG@RpG3F^1+5R«8X/>9?iFB.*(9G9aQFFl?d?hEQfR.lfr_dROFGO2UEA?XIU:IiQ3"pH7Y8Q3$M%,]*YWuWQ9ck6 9bimq:300JaC[k/MU*1[Ij-'b'qjm9J7h`=@OpE@N()\TS=IK«585Sd§;mhFe1%b?f@_#'IPq>0OgIQFh#VB`,IKlp r4N'lF-f,oc)tSV,#WT4DhIpq8=sbW3k",ZH$76F;Z]$^'+2m!jJ)K)NpKF_49#mQXcb«B.QL@Z)CPDZY/s"U-;NTA dIV#fe-O1%UMXL:`!>6`cp?e$eM477B.r`f:@g4>ee$UjipQ(7WrJb0gI6§Z13V]U@oh1XB3!ut`r'R/mCPZZ8q*p) Lc4`02ol?;"7"AXJ%gKC$X7aR$6]X(9#J$H?TcH'p§=RM4Y?4)g01)0dk.n1T1E§dZ(#_.Z§JFC,_UY+jP]8[BFdEC ?BRf@*;8lH8L-Ea9:I.*m#.%^h7p](qi7Dg=(F[I,]e4$P8@aNS\t8\4L0BR2\m.ZgSVqfBj;§5>%lLh>^)2>!-,82 h^QkZcs§c§CEf1)9`S«;KZ>WOaO!(N781O\fp+L-!r47@\!k`+nP7MR§AB8q#"K58g+pkF?Qi]$(g`;Ei68ESN^^ek Kls/KWOH#GI?[3X?bZ2o'MA91!)>%bJlZ^T4>nbXlTFp6gaLKHPteI-=HM>[*\VGnD«PX-)cLo@Q§@p;q?^,JJkmMF [:!\Olh/V«+LCD=)rO##J-TUnS\e$DpU/«BM'L'§kMa6m,AS%"0WaBOP?dS-Z.B19Xd=@iZ)'U6MXn8FrN«c8Md"NU MT\l9['/'+7§S?:G,§;lCASSj%P67dC9nLu[iAjB`oj+0-/HE8G+f2M;:Q«=/gkYfp!-#Jf§`ESk/9Q`c(OVV)`8A« l0n\C2#TX#a.A80X6>VX9WYU">>OlL>§VtME7TJZ-=C]?M=(+-oQ3uV"?W0u0B_)9[R9!nU5§MI9[hCh,ICLtL[$It 8q-rHi+bg'O>:t7gT"(._\DtnFLA$@§9S]4Q_P,J-oNQmm;s§ZNhXq026X^JmWEiG83St:gWV!,g/C!#"WmI§;KugQ 2e._PNkPo§i%JD98P+"'4IlAS3§:B;WYl@I)TT(*fQb(a%Ham.2"3:e6AOr1s3:bU^%$^1]J[$#Sb?fi>:k^"`0`XF Xb_T_GWlJ\l?G/5M`ikU`E[iCZmWOF!WQZ$[%PUSc31nZoYi*e)PX\0eJ3odTc9k$o+.-V*E"oI!N%E#fJ`/A#ZtFW gbS#TReoNQU4lmH§=:V$0Gc_QXiA!omYTI1)hg§G4fl)4(TQt_+2jC-_mPRLeo-'98tTDe$3?KN.s:`+4t(nonEZ§O #q:b!LMC1§iP0jCMZ_BD~

すると、やたらとn/mという項が目につくようになる。 mhqはなんとなく逆数っぽいものになりそうである。 とりあえず、n/mMと置いて書き換える。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    const dst = Array.from(src);
    for (let m = 2; m <= n; m *= 2) {
        let M = n / m;
        for (let j = 0; j < M; j++) {
            const even = dst[j];
            const odd = dst[j + M];
            dst[j] = even + odd;
            dst[j + M] = even - odd;
        }
        for (let k = 4*M; k < n; k += 4*M) {
            const i = idx[n / (2*M) - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] * M / n);
            const wi = Math.sin(2 * Math.PI * idx[k] * M / n);
            for (let j = 0; j < M; j++) {
                const re0 = dst[j + k];
                const im0 = dst[j + i];
                const re = dst[j + k + M];
                const im = dst[j + i + M];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + M] = im1 - im0;
                dst[j + i] = re0 - re1;
                dst[j + i + M] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx.map(i => dst[i]);
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS^Q4)]a*§BFNoMAljSJY9"\80kc7l«pSY[tl1/U,Wj^)Z#8iI.uJ`!:rgX>!b.@n,E(NJa\6!>hlqa?o1#un§$bB aUn.f1+Yg?@C(5:EYZmXk72HaREQ%pn6*Ym;S1D*YHkM\]Frk0$B2I(J.r=A_eHL_8[F9B,\'+tD#9rM5V`K^l/C;W SfPCTNJAaM3?CpGOEQKh`^l_-3'oSK'$X@YfcDs4NA5+AMBMnW-'FYdp)(/6Zp5Bqof4^`,\W`C;*=>/Xt"Ps8oK[P Xt*el`iKMW,8fl:2/jV§0gYHJ/1[)a.;«7X/cEE;>Qe5dk^/fQ0'^n8#t@SZV3'cMi\1nLgi1*2?='4C«LM7jbXP;q D41iXX6HagdEA*T1Zpe]baUZ9E/E#KBE-'p[no`C7a:t9^ps4-*SrhUZ(n^>dVDX\"KB\'hQ?*i\m4i4=;.F?k3,«W 7@?rG"'n*YQ\lU#U:*YFAs"OF`,ll#S:iCt%Ipm$-1Bb)>n%1h0,8UJqOg*D@=$c%]BiK"U5m%#*eDZ_E6?rOdh.Bo JqhgdZhESX"i#>9!r~
GhS]i=`«=Y§:XAW+H/LGYpCK*MdjBQ@aR*Q!``G].JCRh\lsqo1""tFYam?Ch`F2'§p5Kd%I?^GI.PV%+\-'L'q>tL 0)Tb(RuspIrDI/T$*ubab297QK8?*:q)TbibB[7P.66-oFsG9pT\hR§i!$TdQ^;g#@6eh#§O5uHK)CiZLR=^AVOs\m VXhn]V`?-VAA'64_.V#P57nNm$G^teca0if0gYRb,>m*V-g1!S%>BrbKHfXf«=§!GAN%Q=RgmJS-DaRO[ue§j$T25= q>'[-f5:XI27nrIM+H:\P=b0G!IZ_[CYooZjN/Ha:C>«J[4«XaR>^A%33tK]CqYulWT#P:/3/h!eOF_W\L(D+19ha] =$;IR6+)e*3bX_(_EW#§Sdls2XHn«`3IAIVF./0OD["Dq4moIJY^2q"Z!HF8ARn;lLTITOca)\ENV^V-g5gkU:§1dV +PP18?9]t((/cO[q""U#S_::V.7?4I8;(l%YBWb>f)!]OGI5G>B^>;H3ScBqogoL++LYdOIOV%_+UR)a2;,TV%GHH] R$Zk7%u2/fhNAQoMhRQCn)T=ggto`Zqtl\R_09EaIO§uZSWc!C*hIuZP!#sSD`8FHf%WlV!GP48AQgM2!_dfu§0S6_ 6U+2d=+Ima2ch892DtS(\JHkSL`o+^:_aQG9kIF4QO2`J.D8S-XTS"p4U9pb§nGqa6)8@0g$XQib5d/NVAsWRZ[Rd" bR§uGNQ'9!W"gAM@I08>GG%dlG«g+POs@>CX66csC#A*apLo>-QH3`cFiK#LGOTj(NS0§8L;,HR9,h6?1g:)RBIQ1W AcI?5lH1"QpSsbX(t0sc(U]SF)M\k/G6Oq_§[6LLh.`+urLc:27ulhgOSAKelU;3§JtSDsb.n\"2)^//h!XR+fE!+s /J-_qA^]0a9>MAg0W,FrYJidGYp$lqOr:?j'!E=dACdf@7Arqe:KC]Xa0u:t1c;0NO;$f?;T%4m;,pJo%aV1Ie^n\u ?L1EOElS:nj2N*.[tlf6?i6P[@7_LoUZsLF?sN['45@Kof>dN3k\X*?"c8'L]rLhHK8-Pn$Yt@Dj;`$2B:,5]k^_9Q N%=(\H45A1=[^OW-S_0H(66«?Se'ds7O«KmY@)+4LjlAb>e6§XXG>bW_s_'44:nn*f(0pgdt>§eD):R1BK^%5-LT;$ a=l"$?HVI-%ailUhED8$8HS?+1alTlU,4eX59INp!.qt1;8jj+^!gp_J]3cA,cd@qN2G;_XR4VWDit*C=O[WSP"D@> []D^EJSso\`;\TVEC4§@Eo\oX§C$%dF2D2q$X`E$(9TB4TScq>W@9H3X)Zc37[+84+1%aGh7TdZn:ON]«k7*5C',Q4 Tnm3iB!O8n?tn],PX?I!§ATN1D1@=ooorsQ§PoAIJ\i$WbR+N`Uj%K`2'oZ#6qhfcXId*NbFnR5g«+92_CqK§;>LEZ .mpJsrqMaBg%_6D-=s@@JDP\Frm:/d)`§2--mB_RI«gDDrWA(ApYc~

 iと回転因子を求めている部分にもmが出てくるので、これも置き換えてある。 m=n/Mである。

 これでループの中からmがなくなるので、ループもMで書き換える。 このループはm*M=n、つまりM=n/mとなるように書き換えればいいので、初期値がn/2、最終値はn/nだから1で、ループごとに2倍になっていたものはループごとに半分になる。 減少方向になるのでループ終了条件の不等号を書き換える必要がある(よく忘れる)。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    const dst = Array.from(src);
    for (let M = n / 2; M >= 1; M /= 2) {
        for (let j = 0; j < M; j++) {
            const even = dst[j];
            const odd = dst[j + M];
            dst[j] = even + odd;
            dst[j + M] = even - odd;
        }
        for (let k = 4*M; k < n; k += 4*M) {
            const i = idx[n / (2*M) - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] * M / n);
            const wi = Math.sin(2 * Math.PI * idx[k] * M / n);
            for (let j = 0; j < M; j++) {
                const re0 = dst[j + k];
                const im0 = dst[j + i];
                const re = dst[j + k + M];
                const im = dst[j + i + M];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + M] = im1 - im0;
                dst[j + i] = re0 - re1;
                dst[j + i + M] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx.map(i => dst[i]);
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhPlP9+QV.§;9NN'lsL.pdin%WPOKODVg0S_S\m:Qr4l[J)2bCV+Gm:mC0D8`s?k3q%fLc#'#U'lW§u1WH*§q'op=u fBM)2RpeFgiW\R]=Oig2ZA3'9pIJ`UYi*Dp?)ignSp`=ejfReV$G$iTXg:3mFP@5«Jcu9VXbKoD"q;/[5nZa;V_dVo nmG_jTS+GA"WG5PFSP`."psu9/#S«Qi1'^9hhbYHOpD@#k>UZ«]!S:XP/=-kZX=ksCJFO~
GhS-YD/\Gm%/ui*5m7'-2mJhdgG#-N*=ggb*3a7sg>P3u_9Q]:(U\\uCcP(6aF>9\agV,1h?@T[L^tSbUdushX\')" /4:narWM41$`1p#M";f"jI]Gp;8c3>I$jt`$8/A'K9I\S4)s6lqpFs-6$+c46F,_$6§P7:_B^O§pa§`1#sT«P0[J*> (!=0s"c!gDjrdQJf%fm00p#?W2TS4Va#/baU6")ddSi73a/;-9.WQ5\Zn'WYZtnjH(oKS9P1«606g"+[[d5fV[q=Xk n[gI1!#fV8%§4Gt3p2P^Gn«F2`?qb^2I""U2mtjqY^JB5+h>j§«hIC"h+Pu[2_q:2dUcH.#8L'F`#jsr>UdkS2C>VR (@?bn+j;u\S,:)%E''+si43«>c0NuV(Bt1Y`E«§@EHB#«G?6H%#+s§C;m[%qcJ(MNF'5--H-PY3l=[/257@lVj2\`_ =@99:_n9M=(cq?On+ZU#EopXu.C§tB+\\:/j`;3GepkA[?tBpZ=_@tsl-ch0+#7(gZ'XtHZeYAB,f7;2CpnX!b6(k` ,s5%GnF?#a147;XG)1`[+=,`,]R0GJSrpin=MW6:`RcQOJSBLjVK7!§r8)-=/'>=A((8QX(>?.BV?NFVZk+MQW'W]s b?'M3_JNUHk'0i.kiG9M*K(ato_):qA#SYER5\hHq.N=fF@^2f4§/RKp]dH4>7c,77b7+E§p35CFJ=k,bY6NI4-=Ji \8JP3HL%uYP;H#9\IS«A;gaK+LH-?Y'O/8Q1JI2JU'R@(\B%V1NB_FM?rKC0I!iS4;7*U"nN4;J)WLs(;q«Y@=J@TU \R%dX2;G?B8^D0>0Z`RTcKc]9\§Lr48@?(l#>Tifc#4Z4rWjX-U0«T§YBq+gLB,:L,P27,.FoWN$T\g-:=\k4e[hjf lM.kCmX`a«g/)J8%A@§"rFn@Yc`ob5KsmZ)BM"ca;0>a:%pR§SlJJ0MH_)difK]',YsOQ+3,oP.L/:5rP"qLpApb=s n7=bb0Xra60m+1sR9MW_CNqRe[sfk*!aj@)/%GGCOTjIYi\FQaSF5^5U?,XY^%S\nhDmc,DI+3QRNl6%+Z5DsgP_`u 63bE[9Uhn.o$]eD+scr=QW554§KHRi"rBcV9jT^ELt-,Mf/AP(D^_8>/fFju8i§St2bnihUMb!`;'dd«jNqa8F:U0q '4^1CFq-.«:cAqiX§;JW0D«mPQfI>"NBL@M$JHT.U]oSaCJX0^Y/b§b2M#§k*Bt«2_A8/(c$trr«T"§C!IHjbQ\G/9 §_7'-P«5?j1X0B#)KpiZ=u%JB>T4Bb)CkH(%o,8W(!@`o\D#(gbee9a\7=k/0-LgFrUJM.m)>_]0:GkF;Ko8*kK?X1 Isj"\Obn,!,BYWbq!W\T(iT'~

 2*M4*Mという項も見えるので、D(double)とQ(quadruple)で置き換える。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    const dst = Array.from(src);
    for (let M = n / 2; M >= 1; M /= 2) {
        const D = 2 * M;
        const Q = 4 * M;
        for (let j = 0; j < M; j++) {
            const even = dst[j];
            const odd = dst[j + M];
            dst[j] = even + odd;
            dst[j + M] = even - odd;
        }
        for (let k = Q; k < n; k += Q) {
            const i = idx[n / D - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] * M / n);
            const wi = Math.sin(2 * Math.PI * idx[k] * M / n);
            for (let j = 0; j < M; j++) {
                const re0 = dst[j + k];
                const im0 = dst[j + i];
                const re = dst[j + k + M];
                const im = dst[j + i + M];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + M] = im1 - im0;
                dst[j + i] = re0 - re1;
                dst[j + i + M] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx.map(i => dst[i]);
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
FFT(src).then(async fft => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(fft);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS.>?#,'H'Sc)T(%7u9Q?JNK):"@1.I9BgYe/nKpqbH3J`o;?fRMdMS64N4G9A4mk«0>oN^A9nX/e)pI;^52dWR9" Pj^Bn.OD@U5.drBB)$O7OX\t,aD$?QcoTu99!-sO,-ZYsBt1?8LPQWW96^IXmmF2#^'rs«'LNs*DF7;4F%!iA+f«,f 0Dsp%ISI69J@7NhULn>HL^BgN-mU7`nZGUg8!s§IoMXi/ro^s^fR>MB$,J0V6fn.-lc7u)%NFGW:?EJc.*]bJ_Q`im d>HFRrj=O«F$6;Xp>.(:30jk4Ss§-O_4Kh-%[j6$qUD=#ddC\/WReJ,`*AlTl.!21'QN4#CjS%H\!=VUrX0UgS:(~
GhS-YD/\/e§H;*)5oBJag6QImh(Vf$L14;?!V«`N)>_/\«`Pl/-@.Blb'$\X^[I2Y,jN?_%4%*^cTT=\j5§*n>*YVB Z4N_"H4pom=XZ_1OZ5#B5lOo_*(9G;aHm3a:VgUBAE;gor)T>47"8V45)AW«:e80$0q^L%8=0Nk8Uee1IDScL1X%X/ ;hZt[BYhe*=Y>M$nQBqn8§PP,!jFs+niX'XC_2'V:6_p@.6'H'Thpl(:o%BjG+i2':W>B-acNT9dMamULTk?^WP(/§ T9%lXkm#L2ZSNJaLm4Gpg>kO6d2/:^hV7sc]XVJ+;Zb]TZPA_Zb[pH]SCtHkm!L%!=s[HdQ#`T#C5dD4YpM8«R5g2J =jNh5:a77W%bg'ZYWpdc,9;$3=«7#0p#q)q_J(_?^GiC-iJtON(Kc;cjk)$9"EN\nT(fdscd/?JAW?Q>.bYDc6I#;1 XruHF/)Ds5ko)md^ARpk]Ud]L$RnM§J1-("o:UKV_UeS.m/nqu§B[$q?qaYG/q§o!)Mm4§B:n;2ZQp.\?'27mf:Y53 9t2HEfGm"ur'W;bbia/*Jn#oK\USl(nF§M/K@aX*IO8!«SW\2-)BYa=JikckG8eX3o509%Ji'?Y2D):M!N%q1"@ue! ck":i$ue#Tjr9I«(Q/:+g^9ZV`8I?$YK@jh9kJ%pU^?+W+hcDLXW?ji4U'e+''7Jl^sb3Ibkk#ZQ%RHj6Okh,;*XJ: _?kp=K14BrY;IAtE>$>eYc!TBCEPSfa8no«eaqHJ[I9"tG]r*k:R*lP4Duns§A>IVYLcidKhnY:27bs>AFB_ql9oq] _§Wg«C;D$\O#i\ii6_`Mh/h(Dn,p^E_2c!c^,H,7USsK2a.",>lN'9@bRlu>:l7-Ol%71oVn[@*J[_s[>6NU@@0§N1 "re7;79^0I>pIKhhk,;2NE:o4Y023*7spj$`o\8\kZ@iY6@[Bt\>2`.«*e#=_qt!bqR7j,*_]3WT2.V"/GE'^-SFE. 1i);ENegDD1VO2Jpo5B^§6t!hH$9shR9MW`CNqRe[sfk*§miE(/%GH>U'9>lnhO7qSBgGjU?,X[^%S\nhBhQ:\I3"s C6!s]WS!>2kYhQ['H.\")-]gZjemNg76p>Q0«7Lh+ug'§(+bK)XLr«E)Qh=0XDO;QDn:H[//eZJDDO8?CXsU#T\Jdi =:$K6Jol§Jh8>36`0`RD08P66fO9GO«=5bi`L`O\@]mEhf`OjIJGh2q>0r8@p§ojJk]pBc9qYPV@j)UZ6C1BBk!)P! .e!N1!5egVf$aL«$1#^rarSg,)2kaf%6Hoh/t!A]mN7«i9db;"#?K/\`A5LtXL«-cZLDW@mO3L7gC-HP5Q:M=^:8E5 #*UN;lI+8]%f3HhY7G4r1/[IaUsA'>rr!AW%tQP~

 これで大体完成である。

前処理・後処理を関数の外へ出す

 FFT関数の冒頭でsrcdstにコピーしているが、呼び出し元がsrcの内容を必要としない場合、これは単なる無駄である。 必要なら呼び出し側でコピーしてもらえばよいではないか。 また、FFT関数の最後でスクランブルを解除して返しているが、元々の趣旨を考えたら、スクランブルを解除しないで返した方がいい、ということになる。 そこでこれらをえいやっと削除したのがこれ。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(dst) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    for (let M = n / 2; M >= 1; M /= 2) {
        const D = 2 * M;
        const Q = 4 * M;
        for (let j = 0; j < M; j++) {
            const even = dst[j];
            const odd = dst[j + M];
            dst[j] = even + odd;
            dst[j + M] = even - odd;
        }
        for (let k = Q; k < n; k += Q) {
            const i = idx[n / D - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] * M / n);
            const wi = Math.sin(2 * Math.PI * idx[k] * M / n);
            for (let j = 0; j < M; j++) {
                const re0 = dst[j + k];
                const im0 = dst[j + i];
                const re = dst[j + k + M];
                const im = dst[j + i + M];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + M] = im1 - im0;
                dst[j + i] = re0 - re1;
                dst[j + i + M] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
let fft = Array.from(src);
FFT(fft).then(async idx => {
    const dft = await DFT(src.map(v => new Complex(v)));
    fft = expandFFTResult(idx.map(i => fft[i]));
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhRk8gMYb"%#46B'L]+"fiIG2PUoJc@U4/`@)/4:'uQ]uE\5(h"'IXk7GiNhCbJGI@CaI9r5ns`kp1iDqBES(^qV6' aSOScOKVCDFCjBJ[U?L;6dTEI'8U*-Y«B[f«GlX_A@3BNi2me9l"5p8Z@`f6P]_]@«b_m6Mqc5FHe-6qHcR[RT)'U« !Dt?«j9e*E\S5Vi#6p1S6fVK>§0h]?$851?r*b>[C^XMu?om§M-/\kBmR-W`s02Sno2«01>VRLr_?4U@fK]F`h%-k- rO.DRW4TH«5)\2s9Ad§FSQ?S/W>s,:_n[,u`qX[;JXAXI-8=Yrm4%KXFTCl«h>iRj"?«tQ\YUOd[snq>W.§Nn>p.NG O\lc:,!9P5$4[^HLA0NRDC§#3"i%$_>rlTZQ9(5Q35,f>+Gf'9E(E1mVkL[20§#cka5UkeEa>!tS9o0=akL(36«Gdj PN«@l\[WiH%UHS2):m(ZGGN%'URD!dJ+"[+-,pFZIeV«7N9sf^]GY>Y1jDsu.#clsa(i.7eW+K+Db§Q(4@Z2Sc"'Vj %lStf[#+4"E[U@250:0>lieJ#:,u[qIV(7-?9b`saVOHPB#moqoM/t6a?+fo"M$G%/lY;!n-b_l.Rs~
GhS-Y>Ar7S'Roe[+H/IFZ)-0j2@S-!TidVS^q;6lL-?HoW_-787%t*IZCM^/mlQ«SKt97GJOD9>GJ6G9.numP_+C>c M"ENkOJJ,'(:b\^Y-Ln(=_8XAoZ5+?$%\/s\l=h4I@]8Q,EM)Mbhn-E]ag=)b9[7JdtC,'=;9iJ4PQ_1=D:78">aVb F%$fk=(F$5,/\'«N?=8oPR>3\1?*RWZa_.UkcH7\_kT1RK;$YRK2?)aTfk`SG++A0#*U35c"§\RILg=[T;fH1b!-u_ qpKUp7EnS««$F)@qWO)tED0_Eid,OL/+V\\D9iLCZW:9r\]\6BT>m;4F3NK"11t1PAMt\Z`hAZ[@+«fh_H8-kA+H@, d1'b8,dS!7jZ^"8fZk]^SD:>pea(i#(PO«9C«V0G?us4+G?«+pL7a@./6[aRlYiD>\ZYO:?LtHlrT"[[LI%V+I]+cQ 2Y;__apWM%XtsGaILg?e2X`s>+U8R2W6.4RSn6DT^U\ccKo2P)jW8i]A8^WF$#:[W;_C«#D;[ropq%a9NEq2-+>i2K 4CN::i:/Ucn«hI*(PRW@BQF.Bmc!§MM?^9+K)@s4n2_k6aYZ#rdlD\_5qa18''§Aj`ITkCKf(H`B`Y(m/HeUlXO%Kt ncdr(@%2(jA3Lb0S8p2>bTV]SqRa§c;UArljJstcgpkl"\b+Ia'G!AH§V4-QPE%l)+FHiA*4ceCTliC(^(Lrtp@sHk !-_HE3G_.,$§/$ZMdC*«@dtj@dSR-a*JN]a"2TF]p/%Tfh?N=n]r8\E.X^q'Q"T6P]bFRWXMD*-!PD`GOX32bUb7F> jp%J*=o/fG"2X_;6,W_9j8901>/?u'@P%c@F7^77Gl>,Y7K@]Rf5`k@We[j*!§-Mk7#q>,@L_n>T[RlG=«2U§F^Y6a )rJ?5iE;p#Dh§__T/NJ-3%2S6Bb+5RQV=9;Wf(§/fK*>=s7UuQF(O>*r2Fl]kP1I''5);s(c?C[@««r(``Jjk0C,S@ cLlu2«sN`:\V295KrZB04h§6\i"\r!Jud?./`8N§kHUcDoNp\p/a(";[>-p«o(R7?;b90TRR>U;Z"mms9*Fr$1aoST bW:9I48nqo(WmR#s5?D7C9#.gE!!l8a99Z/VAIW@2dpR:f^sn%W5E(u^rra29YG!8aUPhp:55%=X«rgABVM!OBMOMS ':V#Pn--FUfJo.K.X§(pB"=:ID`mI#Yps.2b`W]?ps)J#:2e1NMAeu:iU3=o1o1Rl6M2FTaqVL?3']iDYd]>d!sZ1T AA6o@8ZP^SB[hR[L(5urL>uZrRNUp0RZV[IM/@(YD/N>i§[NoBMqisQ)65O;>3ol2g9QPn0(1§45b(CVhg>_)Ddg#L oEFYK$o*80ofITln_,!fZu«*obqH#f=KPr^6mAET[QL.MHPZPV+2e,J%f~

 ビット逆順テーブルさえなんとかすれば、すぐにでもC言語に移植できそうな姿をしている。 これはインプレース演算(in-place、その場で)と呼ばれる形で、入力データの記憶領域に結果を上書きして返すことになる。 入力データは破壊されるがメモリ容量は半分で済む。 メモリアクセスの範囲も半分以下になるため、現代のCPUにおいてはメモリ容量よりもキャッシュに対する性能向上効果が期待できる。

 ちなみに、ビット逆順テーブルは再帰でさらっと書くことが多い。

void bitReverse(int *buf, int n, int step, int i)
{
    if (n < 2) {
        *buf = i;
        return;
    }
    n /= 2;
    bitReverse(buf, n, step * 2, i);
    bitReverse(buf + step, n, step * 2, i + n);
}

が、この記事を書いていて、C++ならstd::vectorで割とさらっと書けることに気づいた。

void bitReverse(std::vector<std::size_t> &v, std::size_t n)
{
    v.push_back(0);
    while (v.size() < n) {
        std::vector<std::size_t> u;
        for (auto i: v) {
            u.push_back(i);
            u.push_back(i + v.size());
        }
        v.swap(u);
    }
}

size()関数を使うので、要素型をstd::size_tにしておかないと、符号が混ざってるとかなんとか怒られることがあるのがちょっとアレだけど。 std::vectorも何度も確保して捨てて、を繰り返すけど、事前にテーブルを生成する程度なら十分に実用になる。

実逆FFT

 元々の趣旨を考えると、逆FFTもできないと意味がない。 逆FFTは演算の順序を逆にすればいいだけである。 と、FFT (高速フーリエ・コサイン・サイン変換) の概略と設計法に書いてあって、このサイトを初めて読んだ当時(20年くらい前)は途方に暮れたものである。

 具体的には以下の書き換えを行う。

 実際に逆FFTをinvFFTという名前の関数にして超えいやっっっっっと実装するとこうなる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(dst) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    for (let M = n / 2; M >= 1; M /= 2) {
        const D = 2 * M;
        const Q = 4 * M;
        for (let j = 0; j < M; j++) {
            const even = dst[j];
            const odd = dst[j + M];
            dst[j] = even + odd;
            dst[j + M] = even - odd;
        }
        for (let k = Q; k < n; k += Q) {
            const i = idx[n / D - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] * M / n);
            const wi = Math.sin(2 * Math.PI * idx[k] * M / n);
            for (let j = 0; j < M; j++) {
                const re0 = dst[j + k];
                const im0 = dst[j + i];
                const re = dst[j + k + M];
                const im = dst[j + i + M];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + M] = im1 - im0;
                dst[j + i] = re0 - re1;
                dst[j + i + M] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx;
}
async function invFFT(dst) {
    await STATUS("invFFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    for (let M = 1; M < n; M *= 2) {
        const D = 2 * M;
        const Q = 4 * M;
        for (let j = 0; j < M; j++) {
            const even = dst[j];
            const odd = dst[j + M];
            dst[j] = (even + odd) / 2;
            dst[j + M] = (even - odd) / 2;
        }
        for (let k = Q; k < n; k += Q) {
            const i = idx[n / D - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] * M / n);
            const wi = Math.sin(2 * Math.PI * idx[k] * M / n);
            for (let j = 0; j < M; j++) {
                const a = dst[j + k]; // = re0 + re1;
                const b = dst[j + k + M]; // = im1 - im0;
                const c = dst[j + i]; // = re0 - re1;
                const d = dst[j + i + M]; // = im0 + im1;
                const re0 = (a + c) / 2;
                const im0 = (d - b) / 2;
                const re1 = (a - c) / 2;
                const im1 = (b + d) / 2;
                dst[j + k] = re0;
                dst[j + i] = im0;
                // re1 * wr = re * wr * wr - im * wr * wi;
                // re1 * wi = re * wr * wi - im * wi * wi;
                // im1 * wr = re * wr * wi + im * wr * wr;
                // im1 * wi = re * wi * wi + im * wr * wi;
                const re = re1 * wr + im1 * wi;
                const im = im1 * wr - re1 * wi;
                dst[j + k + M] = re;
                dst[j + i + M] = im;
            }
        }
    }
    await STATUS("invFFT: done");
    return idx;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
let raw = Array.from(src);
FFT(raw).then(async idx => {
    const dft = await DFT(src.map(v => new Complex(v)));
    const fft = expandFFTResult(idx.map(i => raw[i]));
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    await invFFT(raw);
    LOG(raw.map((v, i) => v - src[i]).reduce((sum, v) => sum + v * v, 0));
    STATUS("finished");
});
GhSE`>uR4X'Re«2\Du$^#mRH4(3SWdl?§nkenUg4R1=eeXF\=k6DC%KpYNLiJOG;P%t*_,kM37PD[$=-SXt"c^ng9§ $jPXecdIGR0"jrs'!AG,R@e'qr9XL`§t[>]$,U%1*E[I./[UUbNF3sH?+;)2]DKeKhfim4gHU«4$i.$YKf§>$ONWDq C,cRg`A5\'_KjD%/KrH]/QtWjLTt/j9L[$RiHg'/1Q[>Hd8/fE)MDudFIuYFk8,P.G9I2).c.])+2eI>E]$7ZmGJEW l*c'OLLj5b%eL**LTh:VB?P7PL8.J\>5AhJI0X\'BSK%+XQrn"4QfZKXQAYclne]Oh_4AabA[iaGmIeMa«\7r#B8;q ;tMrSf)T*i:1K$3>6f)H68L=c[5enoRkU#(H%kh]4«$6AZq(`iFL.t.`@4`^V3dh3n4D)B$$E§k8/ER8(%Qk«mr(t_ bcOlp2Kn$:!#H:)a4H"!:((i8BBd-EVMK§g:"4)kH8VRuqs3Y?=5P63diV#9?JChmNI/N2Ph3-\CAj,+d)SBu4+S6G «p[N!W.5P'mC*(rb3LOa,K*\sq\k$.TTJm?[S+,Z;A\W:qGlmVMG:*Q;aid5npBb(W.%]3m%RNe70?PsG«k[*n)dfU ]b/6//;d?f]V?S1@?U+#/L`t0g'"Od:7Tk[+X6_gnK$6EP.BjW'8\B7>j3L4(Jj4§`Y*JmcE_39i8T(6i!A]§p4r§b R.g'JY§2WaHrCM\C,m8«.!V.*mPD16)HIutOS'§5_Vt*AL6"BW§ep0IAme()ehQ=6UU)«RZBAu,$//78Cc#§;Am!`N 9;Y4^)Jp:42A$V?BO[FKU_"K5[51-0elBFN$I.G+S«f«ki(G%QKm_!__k;PD^f6QcEmR§tU*(XH7Ug1M9_N\W0]%Bc g5grCo2jP51A9-"@1_SuNuRe%V=P#b[1g`SU^Y_2YJl\%m)1=.lD0=;,ub5b9EN^do#gN$2TL8SmRf1lrnQOmV=Oum /1«;«ikE\-"$f>jN3M*'JWM(HEhBccXRPck-M_#J~
GhSurD/\/e§H88.5oBJaR?h:e>;=W#L14;?!qWiO)>YL1«^i`u-FRIK`HG/C^[I19(*uh«Yo600nt(2g4H^5#,(hZ6 ,VLnb'nb`g=d2KWLbp951g>aS+6ArDM=*Otoo-[>j$f@^A02NMNS*ii^@QqN(uH':«m\t,",cB§7§r#faY2n)Zf6?! )5RXm.AJ*3BZA.?)39SUj3qu-:FlcT"^o4nip"=afHC),99BIB-fa]dG)_Y)N+1U"WZfl/eX;>:HIaAu9Zj,$miZJh (M\.gkNDG\?^hAOe6YS6(:!S5cSg%ca7Rn/2LT)%j>Cn!:E§T)lE6f9B$SRi2m_D^m%;A3/ltbZ3TWXU«!SgPF%qBE 1@YkuLTQC+KPD`?%CFC9dm*'A'CC``ZMOFjmHd:#MUTe7^N8UD\§ZbE*nKg8GHITB==f*db9egbqAC/Wn).MTS+]cn UF(A$Fq?g`9AV)AklmkXro^JGn@§'\9h6n"%R_N52@oDG,c[VHhqZ[CMdTa:>Kl?MZaW+iTFK,]MC2bgjj^TSn97GF k,Jm!#GEYSOCo8_.6N§'#s9'2;\\Xd;38Ee§,6+Z;%.\«`oLbfJCagI:Bl__4;a2nDq\BSH§;-0Vo=6-644N3W6bT4 2^AMs!o0KAAE4olL="1dP'@"F!Z:$OR!TT'N%inKq3$?-B@"5^-EoR1H35T"Ri-H\ciYu9-ZKJ+"=($)lV68-\f>+j a-8s:FOkY/Z/UF']>ijWlVT/p=r46DR"ZId;g\`OgC`,U«il8^?>CHlQu>O@`=uX^0$+6gYUqK=;K3.B\_u2JFpj§n Rt_%FPFAO"kjT!%c"0dOn1D[k5W0^Uk#2L-NFQIR!R4dbjTbe-_[u2884NdQS"Qe9F^3n7oa41u^NK9WW9c`V,-QP8 hk0)WEU5Hrp2"a1L.u]r3o,L0mr§0#m\`j:I5[%dS3OL§@h!p)j$YOq*[h7Sa#)M72WIYJ`[Bp0J`QYq*]Z=0cWnAM =M_«r]H4Q#O5Ug?*Pn0*Kb:O.j*n'`SGmM';0.b/?X)0_[=50*+\MMp0+"aqY["!(Z'MD@BNi9m3df*,UM[)6gR(Xg =MM;Ls.sS6X_gt^j9D3«Tp@Re-YoeZf%«R+LqP*Mli^I$/m4sWY'EpH].gQhBWZ/(B[n+mT>\"`-'!DC`3F6E3-MUG 0rGqC9n=PPkKV9>p3Q)jR.@Vm^=tGY$=t7(58Gp?MP5lNO9Keq_J':SaK]d>)GXf+aQ$-jP$;>dhdhte7gL.0!8A.@ Nt>It]*QI[G%mn@SN-lmB0iC*KRR"MF;=I'ddN(RlUJ:B'Rd.V]*djHlFMlP)3B;.3]jS=:P«§B3[n[b8*7^F$.`t[ mB`buW`k9Y*Bcg?\\q\7'!hrn^>FT8%/@S_?EOX(KtD6m!f`6",qZBHq2f1H$T2!0BR:TB>BL.[6_d-/GE)gUj2,RJ IGikk5RV_UaC7QnUIFcLL#I\8S=#@nO"Vp5@W#K(cp]h#g_?(kW++F4«Kmm0G=>Dak4,WEJtIZpjcr8i6quN`_*Y5( F:8lt*4Z%.JLa/KK6^#>:_!;4*;fei5,M_m3$^LdA7,Wt[,EkDCgKphGs^%=6QYDlW5n*.STB'3oYamUH«Rm."07]` lW`%NGseg"Yk>6Wo5WW_97'+d,m#(j3W_\cM"PWI"?/37VQL§W!7o5p@C2j§'rl-^0u*ba)gr1b,>YCm"S1TiiHsCA SdDG%V=.l\$It(«\C*)]h§>!!b_IfdK8di^`$s9CE']Y!XK!/S;A_O9VMAb6/jpoS-'.0\Y_%B("#-Z1L§~

本当はもう少し段階を踏んで、たとえばn=1から順に倍々にしていこうかと思ったが、面倒くさいその方が意外と難しかったりするので、申し訳ないがここは気合いで一気に。

 実行結果は、最初の16行が正FFTの結果の誤差の絶対値、最後の1行が逆FFTのsrcに対する各要素の誤差の2乗の和である。 誤差を計算して2乗して足すところはreduce一発でも書けるのだが、一時変数を作るかかっこを付けるかしないといけないので、一度mapで誤差を求めてからreduceで2乗和を計算している。

 少し変数を整理するとこうなる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(dst) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    for (let M = n / 2; M >= 1; M /= 2) {
        const D = 2 * M;
        const Q = 4 * M;
        for (let j = 0; j < M; j++) {
            const even = dst[j];
            const odd = dst[j + M];
            dst[j] = even + odd;
            dst[j + M] = even - odd;
        }
        for (let k = Q; k < n; k += Q) {
            const i = idx[n / D - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] * M / n);
            const wi = Math.sin(2 * Math.PI * idx[k] * M / n);
            for (let j = 0; j < M; j++) {
                const re0 = dst[j + k];
                const im0 = dst[j + i];
                const re = dst[j + k + M];
                const im = dst[j + i + M];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + M] = im1 - im0;
                dst[j + i] = re0 - re1;
                dst[j + i + M] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx;
}
async function invFFT(dst) {
    await STATUS("invFFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    for (let M = 1; M < n; M *= 2) {
        const D = 2 * M;
        const Q = 4 * M;
        for (let j = 0; j < M; j++) {
            const even = dst[j];
            const odd = dst[j + M];
            dst[j] = (even + odd) / 2;
            dst[j + M] = (even - odd) / 2;
        }
        for (let k = Q; k < n; k += Q) {
            const i = idx[n / D - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] * M / n);
            const wi = Math.sin(2 * Math.PI * idx[k] * M / n);
            for (let j = 0; j < M; j++) {
                const a = dst[j + k];
                const b = dst[j + k + M];
                const c = dst[j + i];
                const d = dst[j + i + M];
                dst[j + k] = (a + c) / 2;
                dst[j + i] = (d - b) / 2;
                const re1 = (a - c) / 2;
                const im1 = (d + b) / 2;
                dst[j + k + M] = re1 * wr + im1 * wi;
                dst[j + i + M] = im1 * wr - re1 * wi;
            }
        }
    }
    await STATUS("invFFT: done");
    return idx;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
let raw = Array.from(src);
FFT(raw).then(async idx => {
    const dft = await DFT(src.map(v => new Complex(v)));
    const fft = expandFFTResult(idx.map(i => raw[i]));
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    await invFFT(raw);
    LOG(raw.map((v, i) => v - src[i]).reduce((sum, v) => sum + v * v, 0));
    STATUS("finished");
});
GhS-UbAMqd§A70Vp.41@!pZVVOZ5ep!NCk2U)\DHh5#$9TM;Jes5(//)8\]lJSd6M1]§:#^inPS43n#§G4u#h!2AIW -?/aA8Vn5i-pP2q=">lF-2]=0=NNuI-!V$?§4Yd9I=]i;?r^K=T':pmfC$"IT>1K«oR$5J,mWa!/)a#"eimqH§CMnY '!e4G.^=f§jCEH;!N3UAN^FoZCe`MDF+§cXRk/m§L5BOZCjN9FAn!IAe?c(F;NoY_;bfoLA#dlpYTcC[9«+E#!ECBH VIUsTJ`8m/@o+Bb@ZZ5"0W:*@g`S[[_j!/,mY«e);an/§C!TgW"(R4Z1;KJSC(HBPmRCiGHESm%i0rsSeOdO*oX.bj U10aP/JWQ2b3r@GoaH!jVPp%8N_kk1A'M()72q5)7/]'q7;6e6k_G`ZXYS:e#)$W,;X4oc#oV'JT-64XR$dV%P+Kej ?$)u/7;4h)~
GhV7]gMZ%0§:Ml+§4R`igJ[C2>nlTO`M.((!d36S.JCS3]!=`-RFm[rMsY'6J+!e!§h-\$qREJTXL+KRB/mff$n9UT F%5^Ej§+3n>``Nb6j6@"Z:qT!=SuKC(l7Cl,@«§/r%_«;=[ETaOl#U1Ms3;VfS8o`§S;1eO$0-q?BAh@juN5dh/(^0 TJWE#!Ae:dU`%Do\L+'pi>[/ZDZ'#N5q*%TQp`K2C).J9^LG1R5k§:mSs-TZOFrX5CnYsE*]m]«3%#MZ1«h(%MY$qd Q@,Rle5UfZIIkDfTVD.?`$sXan+b!M§*'%kR`'MiK5:#r\>CVVVQX_a-o[RcEoCX(]9rOU12k=Cbq,0NLk$s"'5+C« ?W\Y(A[:3tBJ-4rP(A«F\NApYds7Z>cNUAQ?I)+p$J15KY4u;>jOuDu7lshj7IJ«mBiqHge]Tj§FT)\+Y?.\FND9;T [8@UGf/ndhoR]"oF*\"`'E@n"=*QfO6Q;3[U]h>9[EZVe-§HC§[s*licQ_#maUVfAp«9X7§f:^jY\)#?@Pokc1§P=@ fj'QRp)nJ%$M'.p@02K38-4jg^^O)M%s^G^*^;ddrBScg%2k9M8Io-Sj?;`ZpMk+BT"R]?)tO]ogC/u@WD%AIRS2*L 9NreG8-7S?+,/f?i;,.P99ON#D?B«J`oipZ$m#W-11*o*oZWS%b62iu8FI[s+Oc?T:A7$KT#«(B+RlauX>`-QFQ$VT ^lUq-«7LXm^1N§8\AG!+[FZ%PdcEM@VW!iX'§N]*DFerT'l1Lnf3$$A`Z'MN21aAKCGNmWCl>?;WON,18@[f^h1OWi >>XiSagHGH#uBMgVb'V!Fps!(d@«AE-OrB[a'§r_9cFqK#6;Rr5dUV9'K*t@6-§]K0uNJG%*'98n")\/d@XhiDh/e` TBAs6p).7"[^];S+d§m%Ffs9Lg]+)GgD@fp%3m+[cOG@4pGXFZGN)"XWTY>FBMt:X-_VCslcoUKond?BHf!"P_i\'« ZSoJi>Mr$@$DpSVFF'VaSAqKHb;H2IRZl?n,;IdSp=RID@s9(g=APcHWbRVJcDh(;o429nnunmuM0^L,n«ER[g9O36 ]§niYh-JfL?/6FGs*'=kS^eji$QQ^iZo>U$CXK:p;Ua:pDU$6TPdN[SPeFS4%(3dK/GLSb3"5m'WH@W*e8Q>D6l$+h GY>t9:lYg(+«(pW5WL%86«XQIEd;W($sPofC,§!«T8fQF/q?%O!§iS(2hJ*IhaYE8j-9C\X6J2JotO0X_/«7jX7EiE G,q?eD6$,F«P>bDPoIuWXie9I=Qh4"K8QdX+k1C/6.%lg=:\\§nZM/Ln§J_%W_srDjPil0)($Aa%F3;9iSK§I*RroX qg_K`%G5).-8FHY@X5L:BGU$j[n%?Xg`k2[Dil-I/:=/§E5U=E1iHXo5TL%D!^AXj:r]]Ok3X+0)r7+/DD,m]nj:7] [Z'@Yb8d,bM2s=B2u26B/#XWs)etPZ$fk@so?Jaa^qS!T'\Hi[T§=fQGm2Kf.T#/L?EiMV2''N`'i$smi5nK2_[P*> h,"XE2@@/c64«m^d(7%K"$)I:%*s")«^F!"$'j[:ENZmX«\Ie=)ThnRT*p«TESrTXfm003elrL#Ap3+S%+RA«b;a8p :^CIfIL`+fV6«8'4iI9la"R'B^e\>l'+nR@l)"6Fq0cgTc;C-3d5*eA"IOXOQ0pKr%YCK/rB\H_11Cr9qJ^fr@f5@B +S_W11e-2NhB,C^n9i2aYr@^4cbIg!Aj?~

 実は複素FFTの場合は/2が不要で、そうすると結果がn倍になるので最後にnで割るのが普通である。 DFT関数もn倍の値が得られるのでわざわざscaleメソッドを作ったのであった。

 これに対して実FFTの場合、この辺でなくなってしまったループの演算がない分、結果が合わなくなる。 元々はちゃんと回転因子を乗じて加減算していたのだから、ここの値を2倍にしないといけない。 これを補って、/2を取り除くとこうなる。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(dst) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    for (let M = n / 2; M >= 1; M /= 2) {
        const D = 2 * M;
        const Q = 4 * M;
        for (let j = 0; j < M; j++) {
            const even = dst[j];
            const odd = dst[j + M];
            dst[j] = even + odd;
            dst[j + M] = even - odd;
        }
        for (let k = Q; k < n; k += Q) {
            const i = idx[n / D - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] * M / n);
            const wi = Math.sin(2 * Math.PI * idx[k] * M / n);
            for (let j = 0; j < M; j++) {
                const re0 = dst[j + k];
                const im0 = dst[j + i];
                const re = dst[j + k + M];
                const im = dst[j + i + M];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + M] = im1 - im0;
                dst[j + i] = re0 - re1;
                dst[j + i + M] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx;
}
async function invFFT(dst) {
    await STATUS("invFFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    for (let M = 1; M < n; M *= 2) {
        const D = 2 * M;
        const Q = 4 * M;
        for (let j = 0; j < M; j++) {
            const even = dst[j];
            const odd = dst[j + M];
            dst[j] = even + odd;
            dst[j + M] = even - odd;
        }
        if (D < n) {
            for (let j = 0; j < M; j++) {
                dst[j + D] *= 2;
                dst[j + D + M] *= 2;
            }
        }
        for (let k = Q; k < n; k += Q) {
            const i = idx[n / D - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] * M / n);
            const wi = Math.sin(2 * Math.PI * idx[k] * M / n);
            for (let j = 0; j < M; j++) {
                const a = dst[j + k];
                const b = dst[j + k + M];
                const c = dst[j + i];
                const d = dst[j + i + M];
                dst[j + k] = a + c;
                dst[j + i] = d - b;
                const re1 = a - c;
                const im1 = d + b;
                dst[j + k + M] = re1 * wr + im1 * wi;
                dst[j + i + M] = im1 * wr - re1 * wi;
            }
        }
    }
    await STATUS("invFFT: done");
    return idx;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
let raw = Array.from(src);
FFT(raw).then(async idx => {
    const dft = await DFT(src.map(v => new Complex(v)));
    const fft = expandFFTResult(idx.map(i => raw[i]));
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    await invFFT(raw);
    LOG(raw.map((v, i) => v / raw.length - src[i]).reduce((sum, v) => sum + v * v, 0));
    STATUS("finished");
});
GhSFGbtdRY§;Bj@`JBe3dm/eJ5c#l"gp-r(6s639db(5tl«GWQb)cDeN9qrHD1Y.l)(5FGppat2#^TnrqBnON$2Y9§ §/QEDJTN=S,r,cnQK.LgYX3X)[0Ou1a^h)qQX%B5E«#063c"gP`i=H[E/RE9@;I"e`!B5"HZ08h(«j/;B[8tYRpmVp !`W?_«!08gPt6I2_)Y§7G?6$6EZf:*_$aut%C)G@/J^sf8b^0(.3+;TrVceuX^,VR\%AY']V:2Y+Vlo7$cQ1a,LZ,U O8W"9[ss.o0Bk@q-;Jqt/)l;(6P_7>]@VEf.E8ki>-`«C@7Z+ZbrSCBHHnl.g$=m0TNHkJOk,E^2j_Np2e2b'Hb57§ bS]Y4X=%LlpYW+/«"l;9%q([gdhcUF]":A\[6\m+Yn=c(1E_=$5JR:[$*iCn4UubMncC]3P1XadllAP3RfQ)Z(#\«S Mj$.ET0«NVn(lc)PL#n(c3R(nPF-Y^4=^];A7P/T)*.@Pce==ZYu'!/l'#p_=odH37Jrd>ZB?feDilD:0^!8?E'fHD _M?QUit="IicVKc8=r.sOZEXDoo7.-2?0_[G5a2+S3/usrDZ«bbW4G0hq.Yu"t;5gcN~
GhVOehf%7-§:X@\5o=sA/5*7#XVe§a6a810JU(Ic%"/D?«KX5KM(BP@@65q9^[I0s,tamOkHVf2Uf)KqI«RgEmX-AA 11qjc1*.->Da^«T#n!7N,YGDN?i3d()2RM5,%!hC4rGln/0TAL8?_i\L%LqtDZ'"4n?[_lk8$l67O3fg4=@n>lV5Us Q"H+)@§=?i1ntp5(M#EMI#Uq@N?92RP]FS98\ibFZ03>.'J#ceJgY'to_Lm?%RRmI$fr.m[DU)(l§GOMMFhOskq.Q? q*K5UWq^u@mV=n7't§0,7L«F+LX/#l+,9DV-W[t7(Z`Om\L.OV?7T)RXu#8§K:1C1@WkLW?C,Z$=WBF6!Nh«*``SF] YY:S@jJ69_5L«"nlkWdf*q0]qH)/-E$Wa0OI6Dn-+#B@RicbUPof[2r%^>d?\`jk]r*ncjSi:AWma"l§>.+2m9A31T 6qu«/AIu!."`KLboGg1Frq"`3GhZHO2C.HW%jDQbReuo%§iA1CY4$%q"d%«W(1TD7o[G5pXF]#`=n8CkH($"3^C,(s P\Z@%KZ:)9:KST!Gl0rBGhBGG,(JeIK§#42\GPHTU;9JX,«qAH]T1*\16-P:GY4PYYB(ZS'$jbmM[$.TF(q(9di;§j 8dLNgR%^.*jU'ili8tnK//OKMG'VE#Q:;9EF>mdFU7c5b-XRl-aGh@VL6.!\D7m:@0aQ[#0SL,B4)H0WH*7?VCbXXS a«>.qe9«Da1I>N`_W'.9>t[:PUY%48reA9%?NS§e9"'h=NS`+K*9.+\%Db(!i:ZYmbjc!RES`>`«r`sOY'XnB+S`Z§ W;.Os=ZqAS$[B%^8OL>§%"eT.]RO14#!a)]8e)eDF>-T'!.r6\ZiM9`(!(*A%Xlt!L>OgH*,G>/fm>?HIuf/n!bQh0 =rR.?b5aVci_g5P`,.B3F/@_hs.ekW+,JbmAm3EN3r@=5^XdCP§18Ag9kQFTSPj)_$I.4.FWLgk6U],NnR:PaNl2R% p#400"b$ZZ@-S"m>q`R8DTmj?>]DgF3I/M§'>AXoLX,i]$l>XkNN4R,«Ah9=`i953o4VQrno(A5M0^K]GX3\66«SA' AW8TjbnA",%oYE@2V"p]g925`qe_§8eu"!KEEKGhqrCpj§)?iYV)TT9(a.Z"K,U*UYMst?r^f!FiT/E,S5efA6iD1g «ESP(>@=ts]t-d§31Pa)Utuc8dM\k1])aV>25>TD9B"qc7ee@dlXP%_KO[@\l'$)NKPlEqU@X680[TY)rFE)-AOXDl hH?:B0\fpu[_h_W57%Aj2p9CSTUER\:EZlTQpB(4-]ejAk!)e0§[6FC>BY`):l!!sJ`S>3r§D0sP8i_/3X(k@6;/*a b:mk4'Un§/F*oto%_c>Ej@_(3;+3*\e0LCBb)'2l2l$f52d);qG*qAdn2D@J5%R##W;/NuNHcGl)2p`*2?1Xu.EY«g kE)iTUijc§r;d3Z"COR\\krZP+@@#q(Vm@tM\Sm`r2q/gnn%.Z_?eIYhN)F$'P9e?f!hrmXOd-!818Y"#Bk5hkGU#n XOKMOKnK9Q%p"q/^cZBUk4_)iUG"8!Hifmqn;=Kar[$pOg8sqOqNcaqPI1DtnG8hY\lH+A#Z«N6_LN_o!M_V#_H.6k `d-)]3+RW\$n^+%W6VdIrDm7s>if80':uC0.!"_Us83+SC)«l~

 この演算は、正FFTのmのループ初回ではやっていなかったので、逆FFTではループの最終回、つまりm=n/2の時は何もしなくてよい。 そこで、削ってしまったif文をとりあえず復活させて、変数を書き換える。

 どこを2倍するかだか、knまで行ったすぐ後に計算するはずの要素が対象だ。 ビット逆順なのだから、kのループで使っているビットのすぐ下のビットを1にすれば、元のビット順序で桁上げしたことになる。 どこのビットを使っているかというと、Qずつ増えるのだから、Qの位置がkが変化する場合の最下位ビットになる。 そのすぐ下のビットを1にすればよいので、Q/2、つまりdst[D]について演算する。

 もう一方のデータ、つまり虚部は、他のjループを見てみるとMだけ離れた要素について演算を行っているので、結局、D+jの位置とM+D+jの位置の要素を2倍すればよい。 結果はn倍になるのでnで割って比較する。

 しかし、やっぱり無駄に2倍しているのが悔しい。 どれくらいの演算量があるのか検討してみる。

 大体n回実行されていることが分かるのだが、よく見るとdst[2]からdst[n-1]までが2倍の対象で、dst[0]dst[1]は対象外だということが分かる。 それ以外の要素はどこかでふたつ目のループによって2倍され、以後は最初のループで処理されて出力されている。 これが結果が合わない原因である。

 ということは、2倍のループを無くし、dst[0]dst[1]をあらかじめ2で割っておくと全体が半分になる。 invFFTはビット逆順のデータを受け取っているので、 \(F(0)\) と \(F(N/2)\) の位置、つまり、実FFTとして見たときに複素数ではなく実数になっている部分に相当する。

"use strict";
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    conj() {
        this.im = -this.im;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(dst) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    for (let M = n / 2; M >= 1; M /= 2) {
        const D = 2 * M;
        const Q = 4 * M;
        for (let j = 0; j < M; j++) {
            const even = dst[j];
            const odd = dst[j + M];
            dst[j] = even + odd;
            dst[j + M] = even - odd;
        }
        for (let k = Q; k < n; k += Q) {
            const i = idx[n / D - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] * M / n);
            const wi = Math.sin(2 * Math.PI * idx[k] * M / n);
            for (let j = 0; j < M; j++) {
                const re0 = dst[j + k];
                const im0 = dst[j + i];
                const re = dst[j + k + M];
                const im = dst[j + i + M];
                const re1 = re * wr - im * wi;
                const im1 = re * wi + im * wr;
                dst[j + k] = re0 + re1;
                dst[j + k + M] = im1 - im0;
                dst[j + i] = re0 - re1;
                dst[j + i + M] = im0 + im1;
            }
        }
    }
    await STATUS("FFT: done");
    return idx;
}
async function invFFT(dst) {
    await STATUS("invFFT: doing", src.length);
    const n = src.length;
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    dst[0] /= 2;
    dst[1] /= 2;
    for (let M = 1; M < n; M *= 2) {
        const D = 2 * M;
        const Q = 4 * M;
        for (let j = 0; j < M; j++) {
            const even = dst[j];
            const odd = dst[j + M];
            dst[j] = even + odd;
            dst[j + M] = even - odd;
        }
        for (let k = Q; k < n; k += Q) {
            const i = idx[n / D - idx[k]];
            const wr = Math.cos(2 * Math.PI * idx[k] * M / n);
            const wi = Math.sin(2 * Math.PI * idx[k] * M / n);
            for (let j = 0; j < M; j++) {
                const a = dst[j + k];
                const b = dst[j + k + M];
                const c = dst[j + i];
                const d = dst[j + i + M];
                dst[j + k] = a + c;
                dst[j + i] = d - b;
                const re1 = a - c;
                const im1 = d + b;
                dst[j + k + M] = re1 * wr + im1 * wi;
                dst[j + i + M] = im1 * wr - re1 * wi;
            }
        }
    }
    await STATUS("invFFT: done");
    return idx;
}
function expandFFTResult(raw) {
    const n = raw.length;
    const out = Array(n);
    out[0] = new Complex(raw[0]);
    if (n < 2)
        return out;
    for (let i = 1; i < n / 2; i++) {
        out[i] = new Complex(raw[i], -raw[n - i]);
        out[n - i] = new Complex(out[i]).conj();
    }
    out[n / 2] = new Complex(raw[n / 2]);
    return out;
}
const src = Array(16).fill(0).map(_ => Math.random() - 0.5);
let raw = Array.from(src);
FFT(raw).then(async idx => {
    const dft = await DFT(src.map(v => new Complex(v)));
    const fft = expandFFTResult(idx.map(i => raw[i]));
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    await invFFT(raw);
    LOG(raw.map((v, i) => v * 2 / raw.length - src[i]).reduce((sum, v) => sum + v * v, 0));
    STATUS("finished");
});
GhSulb>,r/§A70V«h/O=6%Wc9$:R61j[+Y4j](=Q§r3tK%Vjd`L\gIuPD17m§e[BQmIbcf1YsN(2t6^i1"dl^.#KFP ?«'Vn>,>DgMPR8mj!i/$c0=%.N(kI^Jsabg]VSGc\tCiW]K%_LGoE.)bEn'W,gWQhiAo/]G,QaWp@i#;=uL6uHfi@j 32E«t,3TXD1G;hJQ9s%'cp«J«-V`fuUuQ#U>,k3D#ZXCEUIf,f7U^9§"?n(8""=,+`,6D9F)[_h5SUV§jAMCWiFgQh E]_r7Yu0Na0\e;uTA8VFn3_jb§>QdQJ$8eHi[A]OKteG2[!ieX.;c1/*#@IO)^7-«/]bs*]BIg5fm_pSd37@eS+Rtc GE*.7V[l/l-O«6[qc:gnHb'cG[IuM,E5'68F7"OsAWdgf]f@@K3P3#9@W,S\IUH«]B/8UUnk_'6P[jM513Uf#DnSn( M:C?OR?q+%"opaR$!kR9%^lRQPMLAZ9@YM/crVDqVh20grIeKnC9`GS24=^#ID5$ghf*pUX8cQH.«j7!l2;VaaJG§" +gM36hdZE^^T6H)~
GhVOegMYb8§:O:S/;-2«-'==K2CQGr78VT)J?#_e(O7]fL*g!Lg(Z"*,0:>emle!MMT_KjU+Z[R!KNP«R5:2#3Qi-V jJcO$Q:O4Z\7]W?7t)fFLb7Usj^l3f.fg7>=mX«L_^?%!Sj[DXAF[(_M1E@KS@'0gAmUYjl-odQrf$ZHQ_s,)GdS%# Y-@QY#]=ZIM\DZfG/5M8-«KhF@adh"(eH#q]uV#[U-p`$k`[T0l6r/ji7i)V'f^ke«>Xp7WjVe.(T)Q1"tPhC72FdT Cscm5L/ugedoToA"Y?=K4mS?RQ*pX§JpTf!?VKB5[a>8*EOs"c!]/FP'U"s,C§ZJ=D"f+--R,bbY`>fnS>h)U:>F,u Z_?s6m"«Ji_kXc9Koh:/g*Uu=HjJnt[k_$`G4G?RomAD,4R§m')[+cQ\A$[Ll91qK>r;/YV6bPUa'%([itBs)R.$$. rR`%«88QHUiWj^^lc1G=8*SNQIf"YnSMdRNUbZ\a.#h][^%=>U/+6QkG«ATIN-2M$RSGGUoN?i)Ln.@1T«kA08-8]p Yktt;_g!jVMMs\13Onk\N^`^]NQ`#\>>Jktp?qn8+1Ia[?Esn7Sdk0K@.2`UYmgnIp11^nDG5-*DmM!Lb/(L8Y,-:n iOVK%!-)[=5:XuHkRA-d)\uQd%0«cIQZL=M7Y]2L:R«§eQFZV+2F62§dVG']Z81rRdK9d4Yoj5S+k*HsQMX;%«J§§I 8rf>T«SoE?-lC!NThD@d=_iGK/j_4\H#§El^')5`bVg«Wb§2)S[W+e07WH^Z4C2?u.=G\WcJ)««"]a)lc!@r4i9Ru4 JH)SBl4F'Cn5XCj$:8P3'a*"!J"4+k$?6WAE[3JWk\*%+.%Cl`G:4lW;TZ'lmG"=5LYl§O:)n`3*PKtQ^SaD)!L]OO k+-d6'Bo.K\2L*.eZGD)Z8?K\s§§C:2oIft-:Z7P)Yk67Y=q+4X/?Gl"'il)RZJlU;:S'.bEE1M[@''r[^%!c;C>kS T![%K]TrI!O)T50YA>l_5[d@_F_-DD+30T\k![2Bd+pYWGMc1aOVeca;6UtmV3BFpRR`3/bQ"Eo2CaSCR4AoN(8g1[ Mu=@MW=bc-iI@!bCuap^8eiG1g*^rsh:§u;F§buE?U9It«iSL_T9\0V]TY>NiFJt]:ZCg#"?rtCVM>EV'a>)N.hB[G h1LT7SBD#M4e0=2"K9TO7AN)%6jN«YbEJb§OFNmT/'dLZi«'+=h:dbgkl39`4Nr'0D;4OE=R:96O)oVKJ"J"C1G=(a CIEsNf4p[_Z-]/[O3i18«qD=RgBM.[c'[eP>JE)=HF§brg#YuSa3:CcU*)HT-@nk_@`(lL=cU38RW%?B"TH"qBgKGV 3YVA_6XMY_s%oZY>K^0j0]WmP§9,A]8`u!MhurogYAj«Dfu3]@!3$*?;;+'h"EK$TN)!+$.jnPYM!Iu>SR.Yjh$Y"* 8%LpB9:9h$N\rVXNFgX8e:fYoFpFRfA1ku/f=«_jJV/sZdIW]p^Yn6eOj;:frkr.`qOmqPW:Hqrmbp+"D\$`.N5bYo AYs11!3#k;/)QSq)\fu#8lVn%BAm/:jM'Po\\a3*FQo5Q@WXP]q8=c.m69;K%c;bf9rXA0\«)hHQQf6n\Apg`!>]_F NW~

 全体を2で割っているので、スケーリングがnではなくn/2になる点に注意。

 FFTの実装の話はとりあえずこれで終わりである。


19 Feb 2026: 新規作成

ご意見・ご要望の送り先は あかもず仮店舗 の末尾をご覧ください。

Copyright (C) 2026 akamoz.jp