codex/discrete-convolution-sums.py

discrete-convolution-sums.py
"""Render finite convolution contributions and circular folding to cwd."""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np


def main():
    a = np.array([1, 2, 1])
    b = np.array([1, 1])
    c = np.convolve(a, b)
    assert c.tolist() == [1, 3, 3, 1]
    wrapped = np.array([sum(c[j::3]) for j in range(3)])
    assert wrapped.tolist() == [2, 3, 3]
    fft4 = np.fft.ifft(np.fft.fft(a, 4) * np.fft.fft(b, 4))
    fft3 = np.fft.ifft(np.fft.fft(a, 3) * np.fft.fft(b, 3))
    assert np.allclose(fft4, c) and np.allclose(fft3, wrapped)
    assert c.sum() == a.sum() * b.sum() == 8
    plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
    fig, axes = plt.subplots(2, 2, figsize=(10.5, 6.4), dpi=120, facecolor="white")
    ax = axes[0, 0]
    for values, offset, color, label in [(a, -0.08, "#2477aa", "a = (1, 2, 1)"),
                                          (b, 0.08, "#d77c21", "b = (1, 1)")]:
        indices = np.arange(len(values))
        ax.vlines(indices + offset, 0, values, color=color, lw=2)
        ax.scatter(indices + offset, values, s=75, color=color, zorder=3, label=label)
    ax.set_title("Two finite inputs; unspecified entries are zero")
    ax.set_xlabel("Input index")
    ax.set_ylabel("Value")
    ax.set_xticks(range(4))
    ax.set_xlim(-0.5, 3.4)
    ax.set_ylim(-0.05, 2.75)
    ax.set_yticks([0, 1, 2])
    ax.legend(loc="upper right", frameon=False, fontsize=10)

    ax = axes[0, 1]
    shades = ["#d4e7f2", "#cce5d9", "#ffe2b3", "#e7d7ef"]
    for m in range(3):
        for k in range(2):
            ax.add_patch(Rectangle((k - 0.5, m - 0.5), 1, 1,
                                   facecolor=shades[m + k], edgecolor="white", linewidth=2))
            ax.text(k, m, f"{a[m] * b[k]} → n = {m + k}", ha="center", va="center")
    ax.set_title("Every pair contributes to index n = m + k")
    ax.set_xticks([0, 1])
    ax.set_yticks([0, 1, 2])
    ax.set_xlim(-0.5, 1.5)
    ax.set_ylim(2.5, -0.5)
    ax.set_xlabel("Index k in b")
    ax.set_ylabel("Index m in a")
    ax.tick_params(length=0)
    for spine in ax.spines.values():
        spine.set_visible(False)

    ax = axes[1, 0]
    first = np.pad(a, (0, 1))
    shifted = np.pad(a, (1, 0))
    ax.bar(np.arange(4), first, color="#2477aa", width=0.6, label=r"$a_n$")
    ax.bar(np.arange(4), shifted, bottom=first, color="#d77c21", width=0.6, label=r"$a_{n-1}$")
    for n, value in enumerate(c):
        ax.text(n, value + 0.12, str(value), ha="center", fontweight="bold")
    ax.set_title("Add an original copy and a shifted copy")
    ax.set_xlabel("Output index n")
    ax.set_ylabel("Value; divide by 8 for probability")
    ax.set_xticks(range(4))
    ax.set_yticks([0, 1, 2, 3])
    ax.set_ylim(0, 4.2)
    ax.legend(loc="upper center", ncol=2, frameon=False)

    ax = axes[1, 1]
    ax.bar(np.arange(3), c[:3], width=0.6, color="#7c98ad")
    ax.bar(0, c[3], bottom=c[0], width=0.6, color="#925baf", label="Index 3 folds into index 0")
    for n, value in enumerate(wrapped):
        ax.text(n, value + 0.12, str(value), ha="center", fontweight="bold")
    ax.set_title("Length-3 circular convolution wraps the last term")
    ax.set_xlabel("Index modulo 3")
    ax.set_ylabel("Folded value")
    ax.set_xticks(range(3))
    ax.set_yticks([0, 1, 2, 3])
    ax.set_ylim(0, 4.2)
    ax.legend(loc="upper left", frameon=False, fontsize=9)
    for ax in [axes[0, 0], axes[1, 0], axes[1, 1]]:
        ax.spines[["top", "right"]].set_visible(False)
        ax.grid(axis="y", color="#e5e9eb", zorder=0)
        ax.set_axisbelow(True)
    fig.subplots_adjust(left=0.08, right=0.97, top=0.93, bottom=0.09, wspace=0.25, hspace=0.53)
    output = Path.cwd() / Path(__file__).with_suffix(".png").name
    fig.savefig(output, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()