Parity Decomposition Plots of Scalar Functions in Python

Mathematics
Parity
Function-Parity-Decomposition
Author

Galen Seilis

Published

October 26, 2023

Every function is even, odd, or neither. And by “neither” I also mean a bit of both. There is a decomposition theorem which states that every function equipped with addition on its image can be written as the sum of an even function and an odd function. For a suitable function \(f\) this can be written as:

\[f = \underbrace{f_e}_{\text{Even Function}} + \underbrace{f_o}_{\text{Odd Function}}\]

Further, when reflections of the input are defined it will also be expressible exactly as follows:

\[f_e(t) = \frac{f(t) + f(-t)}{2}\]

\[f_o(t) = \frac{f(t) - f(-t)}{2}\]

Strictly-speaking, the image of the function could be elements of a vector space, or matrices, or tensors, etc. But those are harder to visualize. Here is a snippet of code producing what I call a “parity decomposition plot of a scalar function”. It plots the parametric curve of

\[\vec p_f (t) \triangleq \begin{bmatrix} f_e(t) \\ f_o(t) \end{bmatrix}\]

which I call the parity decomposition vector of a scalar function.

Let us consider the function \(f(t) = \exp \left(\cos t + \sin t \right)\) as an example. The decompositions are as follows:

\[f_e(t) = \frac{\exp \left(\cos t + \sin t \right) + \exp \left(\cos (-t) + \sin (-t) \right)}{2}\]

\[f_o(t) = \frac{\exp \left(\cos t + \sin t \right) - \exp \left(\cos (-t) + \sin (-t) \right)}{2}\]

\[\vec p_f (t) \triangleq \begin{bmatrix} \frac{\exp \left(\cos t + \sin t \right) + \exp \left(\cos (-t) + \sin (-t) \right)}{2} \\ \frac{\exp \left(\cos t + \sin t \right) - \exp \left(\cos (-t) + \sin (-t) \right)}{2} \end{bmatrix}\]

Here is the decomposition using Python (NumPy):

"""Parity decomposition of scalar functions."""

from __future__ import annotations

from pathlib import Path
from typing import Callable

import matplotlib.pyplot as plt
import numpy as np


def even_part(f: Callable[[np.ndarray], np.ndarray], t: np.ndarray) -> np.ndarray:
    """Compute the even part of a scalar function."""
    return 0.5 * (f(t) + f(-t))


def odd_part(f: Callable[[np.ndarray], np.ndarray], t: np.ndarray) -> np.ndarray:
    """Compute the odd part of a scalar function."""
    return 0.5 * (f(t) - f(-t))


def parity_decomposition_vector(
    f: Callable[[np.ndarray], np.ndarray], t: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
    """Return (even_part(t), odd_part(t)) for function f."""
    return even_part(f, t), odd_part(f, t)


def plot_parity_decomposition(
    f: Callable[[np.ndarray], np.ndarray],
    t: np.ndarray,
    output: str | Path = "example_parity_decomposition.png",
    dpi: int = 300,
    transparent: bool = True,
) -> Path:
    """Plot the parity decomposition and save to output."""
    e = even_part(f, t)
    o = odd_part(f, t)
    plt.plot(e, o)
    plt.xlabel("Even Part")
    plt.ylabel("Odd Part")
    out_path = Path(output)
    plt.savefig(out_path, dpi=dpi, transparent=transparent)
    plt.close()
    return out_path


def example_f(t: np.ndarray) -> np.ndarray:
    """Example function from the blog post."""
    return np.exp(np.cos(t) + np.sin(t))


def main() -> None:
    """Generate the example plot and reproduce original output."""
    t = np.linspace(-2 * np.pi, 2 * np.pi, num=1000)
    plot_parity_decomposition(example_f, t)


if __name__ == "__main__":
    main()

I’ve wondered if parity decomposition could be useful in further understanding machine learning models, but things get complicated. Often machine learning models map many inputs to many outputs.

For the inputs there are the various combinations of signatures that could be reflected, although the nicest first approximation is to reflect all of them. But one must still choose what set of points to consider. A natural choice is the data itself. Other choices may involve choosing parametric curves, surfaces, or general manifolds.

For multiple outputs we are left with a decomposition vector for each output, which can be formatted into an array of some compatible shape. Most multivariable plots would become rather messy, with too many things overlapping. One option is to plot the parity decomposition plot for each output, producing a collection of plots of size equal to the number of scalar outputs of the ML model. Another option might be dimensionality reduction techniques such as principal components analysis or manifold-based methods as provided in scikit-learn.