codex/hermitian-quadratic-values.py

hermitian-quadratic-values.py
"""Compare directional values of indefinite, semidefinite and definite matrices.

Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Write an opaque white PNG with the script's basename in the working directory.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    theta = np.linspace(0, np.pi, 601)
    real_path = np.column_stack([np.cos(theta), np.sin(theta)])
    h = np.array([[2, 1j], [-1j, 2]])
    eigenbasis = np.array([[1, 1], [1j, -1j]]) / np.sqrt(2)
    complex_path = real_path @ eigenbasis.T
    cases = [
        (np.diag([1., -1.]), real_path, [-1, 1],
         'Self-adjoint, indefinite', r'$D=\mathrm{diag}(1,-1)$',
         r'$q_D=\cos(2\theta)$', '#a44b46'),
        (np.diag([1., 0.]), real_path, [0, 1],
         'Positive semidefinite', r'$P=\mathrm{diag}(1,0)$',
         r'$q_P=\cos^2\theta$', '#326c9d'),
        (h, complex_path, [1, 3],
         'Positive definite', r'$H=[2\ \ i;\ -i\ \ 2]$',
         r'$q_H=\cos^2\theta+3\sin^2\theta$', '#3b7955'),
    ]
    fig, axes = plt.subplots(1, 3, figsize=(11.8, 4.9), dpi=110,
                             sharey=True, facecolor='white')
    fig.subplots_adjust(left=.065, right=.98, bottom=.24, top=.75, wspace=.18)
    for ax, (matrix, path, eigenvalues, title, matrix_label, formula, color) in zip(axes, cases):
        assert np.allclose(matrix.conj().T, matrix)
        assert np.allclose(np.linalg.eigvalsh(matrix), eigenvalues)
        assert np.allclose(np.sum(np.abs(path)**2, axis=1), 1)
        values = np.einsum('ni,ij,nj->n', path.conj(), matrix, path)
        assert np.allclose(values.imag, 0)
        values = values.real
        assert np.isclose(values.min(), min(eigenvalues))
        assert np.isclose(values.max(), max(eigenvalues))
        ax.axhline(0, color='#5c6269', lw=1.3)
        ax.fill_between(theta, values, 0, where=values >= 0,
                        color='#e5f1e8', interpolate=True)
        ax.fill_between(theta, values, 0, where=values <= 0,
                        color='#f5e2df', interpolate=True)
        ax.plot(theta, values, color=color, lw=2.7)
        ax.scatter([0, np.pi/2, np.pi], values[[0, 300, 600]],
                   color=color, s=33, zorder=4)
        for level in eigenvalues:
            ax.axhline(level, color=color, lw=.8, ls=':', alpha=.65)
        ax.set(xlim=(-.07, np.pi + .07), ylim=(-1.35, 3.45),
               xlabel=r'$\theta$', title=title)
        ax.set_xticks([0, np.pi/2, np.pi], [r'$0$', r'$\pi/2$', r'$\pi$'])
        ax.set_yticks([-1, 0, 1, 2, 3])
        ax.grid(axis='x', color='#e2e6eb', lw=.7)
        ax.spines[['top', 'right']].set_visible(False)
        ax.title.set_fontsize(12)
        ax.text(.5, 1.14, matrix_label, transform=ax.transAxes,
                fontsize=13, ha='center', color=color)
        ax.text(.5, -.29, formula, transform=ax.transAxes,
                fontsize=12, ha='center', color=color)
    axes[0].set_ylabel(r'$q_T(x)=\langle Tx,x\rangle,\quad\|x\|=1$', fontsize=12)
    fig.suptitle('Real values do not guarantee positive values', fontsize=17, y=.98)
    fig.text(.5, .045,
             r'$D,P:\ x=(\cos\theta,\sin\theta)\qquad'
             r'H:\ x=\cos\theta\,e_1+\sin\theta\,e_3$',
             fontsize=12, ha='center')
    fig.savefig(Path.cwd() / (Path(__file__).stem + '.png'),
                facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()