# Trade the Ringing for a Softer Edge

*Task 3 of 5 · [Filtering in the Frequency Domain](https://gpu.rocks/learn/frequency-filtering-8c225e10.md) · GPU.js Learn*

The ringing came from the cliff. A rectangle in frequency has a discontinuity, and
a discontinuity in one domain is a long, slowly-decaying tail in the other. Round the cliff
off and the tail goes with it.

The most satisfying way to round it off is a **Gaussian**, because the
Fourier transform of a Gaussian is a Gaussian. The impulse response of this filter is
therefore a Gaussian too — *strictly positive*, with no negative lobes anywhere. A
filter that never subtracts cannot overshoot. The ringing does not shrink here; it
vanishes, from about 9.2% to about 0.00%.

You pay for it twice. A Gaussian never reaches zero, so it never removes anything
completely: at `σ = 12` the bin at `k = 26` is not deleted, it is
scaled by `exp(−26²/(2·12²)) ≈ 0.096`. And it starts attenuating immediately —
the harmonic at `k = 18`, which the brick wall passed at full strength, comes
through at `0.325`. The edge that swung in 7 samples now takes 13. That is the
whole trade, and it is the same one Windowing & Spectral Leakage makes from the other
end, one module back: a smooth taper buys clean tails at the cost of sharpness.

Convolution & Filters convolved a Gaussian blur directly, tap by tap. This is that
same filter, arriving from the other side.

## Goal

**Goal:** finish `rollOff` so every bin is scaled by
`exp(−dist²/(2σ²))`, where `dist` is the folded distance from DC —
and watch the overshoot collapse.

## Requirements

- Keep `output: [256, 2]`; the spectrum arrives as `spec[0][k]` / `spec[1][k]`
- Fold the bin index first, exactly as the brick wall did: `Math.min(k, this.constants.n - k)`
- Build the response `h = Math.exp(-(dist * dist) / (2 * sigma * sigma))` with `sigma` from `this.constants`
- Scale **both** planes by the same `h` — a response that touches only the real part is not a filter

## Hint 1 — the response, then the scaling

Compute `h` once, before you look at which plane you are on. Every
thread of a bin must agree on it:

```js
const k = this.thread.x;
const dist = Math.min(k, this.constants.n - k);
const s = this.constants.sigma;
const h = Math.exp(-(dist * dist) / (2 * s * s));
```

## Hint 2 — the finish

No branch on the cutoff at all — there is no cutoff. Every bin is scaled,
most of them by almost nothing:

```js
if (this.thread.y === 0) return spec[0][k] * h;
return spec[1][k] * h;
```

The `2` under the exponent is not decoration. Leave it out and you have a
filter `√2` narrower than the one you asked for.

## Same idea elsewhere

Choosing a window is the oldest trade in the field: rectangular, Hann, Hamming,
Blackman, Kaiser — each one moves ringing into resolution or back at a different exchange
rate. cuFFT and rocFFT will happily transform anything; the window is always yours to pick.
And the Gaussian's other half is why `cv::GaussianBlur` and every game engine's
bloom pass use a Gaussian rather than a box: the box is a brick wall in time, and it rings
in frequency.

## Starter code

```js
// The same wave, the same cutoff, a filter with no cliff in it.
const gpu = new GPU({ mode });

// GIVEN — the forward transform. A real signal in, a complex spectrum
// out: with output [n, 2] the result is indexed spec[p][k], where plane 0
// holds the real part of bin k and plane 1 the imaginary part.
const dft = gpu.createKernel(function (sig) {
  const k = this.thread.x;
  let re = 0;
  let im = 0;
  for (let t = 0; t < this.constants.n; t++) {
    const angle = (-2 * Math.PI * k * t) / this.constants.n;
    re += sig[t] * Math.cos(angle);
    im += sig[t] * Math.sin(angle);
  }
  if (this.thread.y === 0) return re;
  return im;
}, { output: [256, 2], constants: { n: 256 } });

// GIVEN — the inverse. A complex spectrum in as spec[0][k] / spec[1][k], a
// complex signal out in the same two planes. The 1/n lives HERE and nowhere
// else: divide a second time "to be safe" and everything comes back n times
// too small.
const idft = gpu.createKernel(function (spec) {
  const t = this.thread.x;
  let re = 0;
  let im = 0;
  for (let k = 0; k < this.constants.n; k++) {
    const angle = (2 * Math.PI * k * t) / this.constants.n;
    const c = Math.cos(angle);
    const s = Math.sin(angle);
    re += spec[0][k] * c - spec[1][k] * s;
    im += spec[0][k] * s + spec[1][k] * c;
  }
  if (this.thread.y === 0) return re / this.constants.n;
  return im / this.constants.n;
}, { output: [256, 2], constants: { n: 256 } });

// YOUR JOB — replace the cliff with a Gaussian roll-off.
const rollOff = gpu.createKernel(function (spec) {
  const k = this.thread.x;
  const dist = Math.min(k, this.constants.n - k);

  // TODO: this is still a brick wall — h is 1 inside the cutoff and 0
  // outside it. Make h fall off smoothly instead:
  //   h = exp(-(dist * dist) / (2 * sigma * sigma))
  let h = 0;
  if (dist <= this.constants.sigma) h = 1;

  if (this.thread.y === 0) return spec[0][k] * h;
  return spec[1][k] * h;
}, { output: [256, 2], constants: { n: 256, sigma: 12 } });

const filtered = idft(rollOff(dft(wave)));

let peak = filtered[0][0];
for (let i = 0; i < 256; i++) peak = Math.max(peak, filtered[0][i]);
let ripple = 0;
for (let i = 16; i < 48; i++) ripple = Math.max(ripple, Math.abs(Math.abs(filtered[0][i]) - 1));

console.log('overshoot:', (((peak - 1) / 2) * 100).toFixed(4), '% of the jump');
console.log('ripple in the flat stretch:', ripple.toFixed(6));
// the eight samples after the rising edge: does the climb ever turn back?
console.log('just after the edge:', Array.from({ length: 8 },
  (unused, j) => Number(filtered[0][j].toFixed(3))));
```

---

Interactive version: https://gpu.rocks/learn/frequency-filtering-8c225e10/3

[Previous task](https://gpu.rocks/learn/frequency-filtering-8c225e10/2.md) · [Next task](https://gpu.rocks/learn/frequency-filtering-8c225e10/4.md)
