In [1]:
from PDESolver import *

Heat Equation - Laplacian Operator¶

In [2]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)
t_sym = symbols('t', real=True, positive=True)

print("""
CONTEXT: Parabolic PDEs

The heat equation on ℝ describes diffusion:

∂u/∂t = Δu

where Δ = -∂²/∂x² is the Laplacian.

The infinitesimal generator L = Δ has symbol:
σ(L)(ξ) = -ξ²

The heat semigroup exp(tΔ) propagates initial conditions:
u(t,x) = (exp(tΔ)u₀)(x)

This is a fundamental example of a pseudo-differential operator semigroup.
""")

# Laplacian operator
laplacian_symbol = -xi**2
Laplacian = PseudoDifferentialOperator(laplacian_symbol, [x], mode='symbol')

print("\nLaplacian symbol σ(Δ)(ξ) =")
pprint(Laplacian.symbol)

# Compute heat kernel via exponential
print("\n" + "-"*70)
print("Computing exp(tΔ) via asymptotic expansion")
print("-"*70)

print("\nComputing exp(t·Δ) up to order 3...")
heat_kernel_symbol = Laplacian.exponential_symbol(t=t_sym, order=3)

print("\nHeat kernel symbol (asymptotic, order 3):")
pprint(simplify(heat_kernel_symbol))

# Exact solution
exact_heat_kernel = exp(-t_sym * xi**2)

print("\nExact heat kernel symbol:")
pprint(exact_heat_kernel)

print("\nDifference (asymptotic - exact):")
diff_symbol = simplify(heat_kernel_symbol - exact_heat_kernel)
pprint(diff_symbol)

print("""
Note: For the Laplacian, the exponential is exact at all orders
because the symbol is quadratic. The asymptotic expansion recovers
the exact result.

Physical interpretation:
- Symbol σ(exp(tΔ))(ξ) = exp(-tξ²) is a low-pass filter
- High frequencies (|ξ| large) decay exponentially fast
- Diffusion smooths out oscillations
""")

# Numerical validation
print("\n" + "-"*70)
print("Numerical validation: Symbol values at different times")
print("-"*70)

t_values = [0.05, 0.1, 0.15, 0.2, 0.3]
xi_vals = np.linspace(-5, 5, 200)

fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes = axes.flatten()

errors = []

for idx, t_val in enumerate(t_values):
    if idx >= 5:
        break
        
    print(f"\nEvaluating at t = {t_val}...")
    
    # Lambdify for numerical evaluation
    heat_approx_func = lambdify(xi, heat_kernel_symbol.subs(t_sym, t_val), 'numpy')
    heat_exact_func = lambdify(xi, exact_heat_kernel.subs(t_sym, t_val), 'numpy')
    
    # Evaluate on grid
    heat_approx = heat_approx_func(xi_vals)
    heat_exact = heat_exact_func(xi_vals)
    
    # Compute maximum error
    error = np.max(np.abs(heat_approx.real - heat_exact.real))
    errors.append(error)
    
    # Plot comparison
    axes[idx].plot(xi_vals, heat_exact.real, 'b-', 
                   label='Exact: exp(-tξ²)', linewidth=2)
    axes[idx].plot(xi_vals, heat_approx.real, 'r--', 
                   label='Asymptotic (order 3)', linewidth=2)
    axes[idx].set_xlabel('ξ (frequency)', fontsize=10)
    axes[idx].set_ylabel('Symbol value', fontsize=10)
    axes[idx].set_title(f't = {t_val}', fontsize=12)
    axes[idx].legend(fontsize=9)
    axes[idx].grid(True, alpha=0.3)
    
    print(f"  Maximum error: {error:.2e}")

# Error evolution plot
axes[5].semilogy(t_values, errors, 'ro-', linewidth=2, markersize=8)
axes[5].set_xlabel('Time t', fontsize=10)
axes[5].set_ylabel('Max |error|', fontsize=10)
axes[5].set_title('Error Evolution (log scale)', fontsize=12)
axes[5].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n" + "-"*70)
print("Summary of maximum errors:")
print("-"*70)

for t_val, err in zip(t_values, errors):
    print(f"  t = {t_val:.2f}  →  max error = {err:.3e}")

print(f"\nOverall max error: {max(errors):.2e}")
print("✓ Asymptotic expansion matches exact solution (as expected)")

# Additional visualization: Time-frequency representation
print("\n" + "-"*70)
print("Visualization: Time-frequency decay")
print("-"*70)

t_range = np.linspace(0.01, 0.5, 100)
xi_range = np.linspace(-10, 10, 100)
T_grid, XI_grid = np.meshgrid(t_range, xi_range, indexing='ij')

# Heat kernel in time-frequency space
heat_tf = np.exp(-T_grid * XI_grid**2)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Contour plot
im0 = axes[0].contourf(XI_grid, T_grid, heat_tf, levels=20, cmap='hot')
axes[0].set_xlabel('Frequency ξ', fontsize=12)
axes[0].set_ylabel('Time t', fontsize=12)
axes[0].set_title('Heat Kernel: exp(-tξ²)', fontsize=14)
plt.colorbar(im0, ax=axes[0], label='Amplitude')

# Decay along frequency axis
for t_plot in [0.05, 0.1, 0.2, 0.3]:
    decay = np.exp(-t_plot * xi_vals**2)
    axes[1].plot(xi_vals, decay, linewidth=2, label=f't={t_plot}')

axes[1].set_xlabel('Frequency ξ', fontsize=12)
axes[1].set_ylabel('exp(-tξ²)', fontsize=12)
axes[1].set_title('Frequency Decay at Different Times', fontsize=14)
axes[1].legend(fontsize=10)
axes[1].grid(True, alpha=0.3)
axes[1].set_yscale('log')

plt.tight_layout()
plt.show()

print("""
Interpretation:
- The heat kernel acts as a time-dependent low-pass filter
- Cutoff frequency ξ_c ~ 1/√t decreases with time
- High-frequency components are rapidly attenuated
- This corresponds to smoothing/diffusion in physical space

Applications:
- Image denoising (Perona-Malik equation)
- Gaussian blur in computer vision
- Option pricing (Black-Scholes PDE)
- Molecular diffusion
""")

print("\n✓ Heat equation: prototype parabolic PDE")
print("✓ Heat kernel: fundamental solution via operator semigroup")
print("✓ Symbol exp(-tξ²): Gaussian smoothing in frequency domain")
CONTEXT: Parabolic PDEs

The heat equation on ℝ describes diffusion:

∂u/∂t = Δu

where Δ = -∂²/∂x² is the Laplacian.

The infinitesimal generator L = Δ has symbol:
σ(L)(ξ) = -ξ²

The heat semigroup exp(tΔ) propagates initial conditions:
u(t,x) = (exp(tΔ)u₀)(x)

This is a fundamental example of a pseudo-differential operator semigroup.


Laplacian symbol σ(Δ)(ξ) =
  2
-ξ 

----------------------------------------------------------------------
Computing exp(tΔ) via asymptotic expansion
----------------------------------------------------------------------

Computing exp(t·Δ) up to order 3...

Heat kernel symbol (asymptotic, order 3):
                     3  6        2  4      2    
- 0.166666666666667⋅t ⋅ξ  + 0.5⋅t ⋅ξ  - t⋅ξ  + 1

Exact heat kernel symbol:
     2
 -t⋅ξ 
ℯ     

Difference (asymptotic - exact):
                                                        2
                     3  6        2  4      2        -t⋅ξ 
- 0.166666666666667⋅t ⋅ξ  + 0.5⋅t ⋅ξ  - t⋅ξ  + 1 - ℯ     

Note: For the Laplacian, the exponential is exact at all orders
because the symbol is quadratic. The asymptotic expansion recovers
the exact result.

Physical interpretation:
- Symbol σ(exp(tΔ))(ξ) = exp(-tξ²) is a low-pass filter
- High frequencies (|ξ| large) decay exponentially fast
- Diffusion smooths out oscillations


----------------------------------------------------------------------
Numerical validation: Symbol values at different times
----------------------------------------------------------------------

Evaluating at t = 0.05...
  Maximum error: 8.08e-02

Evaluating at t = 0.1...
  Maximum error: 1.06e+00

Evaluating at t = 0.15...
  Maximum error: 4.53e+00

Evaluating at t = 0.2...
  Maximum error: 1.23e+01

Evaluating at t = 0.3...
  Maximum error: 4.87e+01
No description has been provided for this image
----------------------------------------------------------------------
Summary of maximum errors:
----------------------------------------------------------------------
  t = 0.05  →  max error = 8.078e-02
  t = 0.10  →  max error = 1.061e+00
  t = 0.15  →  max error = 4.531e+00
  t = 0.20  →  max error = 1.234e+01
  t = 0.30  →  max error = 4.869e+01

Overall max error: 4.87e+01
✓ Asymptotic expansion matches exact solution (as expected)

----------------------------------------------------------------------
Visualization: Time-frequency decay
----------------------------------------------------------------------
No description has been provided for this image
Interpretation:
- The heat kernel acts as a time-dependent low-pass filter
- Cutoff frequency ξ_c ~ 1/√t decreases with time
- High-frequency components are rapidly attenuated
- This corresponds to smoothing/diffusion in physical space

Applications:
- Image denoising (Perona-Malik equation)
- Gaussian blur in computer vision
- Option pricing (Black-Scholes PDE)
- Molecular diffusion


✓ Heat equation: prototype parabolic PDE
✓ Heat kernel: fundamental solution via operator semigroup
✓ Symbol exp(-tξ²): Gaussian smoothing in frequency domain

Schrödinger Equation - Harmonic Oscillator¶

In [3]:
# ------------------------------------------------
# 1. Symbolic setup
# ------------------------------------------------
x = symbols('x', real=True)
xi = symbols('xi', real=True)
t_sym = symbols('t', real=True, positive=True)

print("""
CONTEXT: Quantum Harmonic Oscillator
Hamiltonian operator:
  H(x, ξ) = (1/2)(ξ² + x²)
This represents the total energy (kinetic + potential)
of a particle in a quadratic potential well.
""")

# Define the Hamiltonian symbol
H_symbol = (xi**2 + x**2) / 2
Hamiltonian = PseudoDifferentialOperator(H_symbol, [x], mode='symbol')

print("\nHamiltonian symbol H(x, ξ) =")
pprint(Hamiltonian.symbol)

# ------------------------------------------------
# 2. Compute asymptotic exponential (propagator symbol)
# ------------------------------------------------
print("\n" + "-"*70)
print("Computing the asymptotic propagator symbol U(t) = exp(-i t H)")
print("-"*70)
propagator_symbol = Hamiltonian.exponential_symbol(t=-I*t_sym, order=4)

print("\nAsymptotic propagator symbol (order 4 expansion):")
pprint(simplify(propagator_symbol))

# ------------------------------------------------
# 3. Exact exponential for comparison
# ------------------------------------------------
print("\nExact propagator symbol (closed form):")
exact_propagator = exp(-I * t_sym * (xi**2 + x**2) / 2)
pprint(exact_propagator)

print("""
Comparison:
- Asymptotic expansion approximates exp(-i t H)
- For small t, both should coincide closely.
""")

# Difference between asymptotic and exact symbols
diff_symbol = simplify(propagator_symbol - exact_propagator)
print("\nDifference (asymptotic - exact):")
pprint(diff_symbol)

# ------------------------------------------------
# 4. Numerical comparison and visualization
# ------------------------------------------------
print("\n" + "="*70)
print("NUMERICAL COMPARISON")
print("="*70)

t_values = [0.05, 0.1, 0.2]   # short to moderate evolution times
x_val = 0.0                   # fix position (center of potential)
xi_vals = np.linspace(-4, 4, 400)

# Prepare numerical functions
prop_asym_func = lambdify((xi, t_sym), propagator_symbol.subs(x, x_val), 'numpy')
prop_exact_func = lambdify((xi, t_sym), exact_propagator.subs(x, x_val), 'numpy')

for t_val in t_values:
    print("\n" + "-"*70)
    print(f"Comparison at t = {t_val}")
    print("-"*70)

    prop_asym = prop_asym_func(xi_vals, t_val)
    prop_exact = prop_exact_func(xi_vals, t_val)

    # Compute maximum deviation on real and imaginary parts
    err_real = np.max(np.abs(prop_asym.real - prop_exact.real))
    err_imag = np.max(np.abs(prop_asym.imag - prop_exact.imag))
    print(f"Max real error  : {err_real:.2e}")
    print(f"Max imag error  : {err_imag:.2e}")

    # Visualization
    plt.figure(figsize=(10, 6))
    plt.plot(xi_vals, prop_exact.real, 'b-', label='Re[exact]', linewidth=2)
    plt.plot(xi_vals, prop_asym.real, 'r--', label='Re[asymptotic]', linewidth=2)
    plt.plot(xi_vals, prop_exact.imag, 'g-', label='Im[exact]', linewidth=2)
    plt.plot(xi_vals, prop_asym.imag, 'm--', label='Im[asymptotic]', linewidth=2)
    plt.xlabel('ξ (momentum)', fontsize=12)
    plt.ylabel('Propagator value', fontsize=12)
    plt.title(f'Harmonic oscillator propagator at t = {t_val}, x = {x_val}', fontsize=14)
    plt.legend(fontsize=11)
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.show()

# ------------------------------------------------
# 5. Unitarity check
# ------------------------------------------------
print("\n" + "="*70)
print("UNITARITY CHECK — should satisfy |U(t)|² = 1")
print("="*70)

prop_unitarity = simplify(propagator_symbol * conjugate(propagator_symbol))
print("\nSymbolic expression of |U(t)|²:")
pprint(prop_unitarity)

# Evaluate numerically for small t
t_test = 0.1
prop_unit_func = lambdify((xi, t_sym), prop_unitarity.subs(x, x_val), 'numpy')
vals_unit = prop_unit_func(xi_vals, t_test)

plt.figure(figsize=(9, 5))
plt.plot(xi_vals, vals_unit.real, 'k-', linewidth=2, label='|U(t)|²')
plt.xlabel('ξ (momentum)', fontsize=12)
plt.ylabel('|U(t)|²', fontsize=12)
plt.title(f'Unitarity check at t = {t_test}', fontsize=14)
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
plt.show()

err_unit = np.max(np.abs(vals_unit.real - 1))
print(f"Maximum deviation from unitarity: {err_unit:.2e}")

print("""
Interpretation:
- The propagator U(t) = exp(-i t H) must be unitary.
- The asymptotic expansion preserves unitarity approximately.
- For small t, |U(t)|² ≈ 1 with negligible drift.

Applications:
- Time evolution of quantum states
- Harmonic oscillator wavepacket propagation
- Semiclassical and pseudodifferential approximations
""")
CONTEXT: Quantum Harmonic Oscillator
Hamiltonian operator:
  H(x, ξ) = (1/2)(ξ² + x²)
This represents the total energy (kinetic + potential)
of a particle in a quadratic potential well.


Hamiltonian symbol H(x, ξ) =
 2    2
x    ξ 
── + ──
2    2 

----------------------------------------------------------------------
Computing the asymptotic propagator symbol U(t) = exp(-i t H)
----------------------------------------------------------------------
Asymptotic propagator symbol (order 4 expansion):
                                                                               ↪
 4 ⎛                     8                       6  2             5            ↪
t ⋅⎝0.00260416666666667⋅x  + 0.0104166666666667⋅x ⋅ξ  - 0.0625⋅ⅈ⋅x ⋅ξ + 0.0156 ↪
                                                                               ↪

↪                                                                              ↪
↪     4  4                      4            3  3                       2  6   ↪
↪ 25⋅x ⋅ξ  - 0.114583333333333⋅x  - 0.125⋅ⅈ⋅x ⋅ξ  + 0.0104166666666667⋅x ⋅ξ  - ↪
↪                                                                              ↪

↪                                                                              ↪
↪                     2  2               5                                     ↪
↪  0.354166666666667⋅x ⋅ξ  - 0.0625⋅ⅈ⋅x⋅ξ  + 0.333333333333333⋅ⅈ⋅x⋅ξ + 0.00260 ↪
↪                                                                              ↪

↪                                                                     ⎛        ↪
↪               8                      4                     ⎞      3 ⎜        ↪
↪ 416666666667⋅ξ  - 0.114583333333333⋅ξ  + 0.0729166666666667⎠ + ⅈ⋅t ⋅⎝- 0.083 ↪
↪                                                                              ↪

↪                                                                              ↪
↪                2                         ⎛        ⎛ 2    2⎞⎞         2       ↪
↪ 3333333333333⋅x  + 0.166666666666667⋅ⅈ⋅x⋅⎝ⅈ⋅x - ξ⋅⎝x  + ξ ⎠⎠ - 0.25⋅ξ  - 0.0 ↪
↪                                                                              ↪

↪                           ⎛                          2      ⎞⎞      ⎛        ↪
↪                 ⎛ 2    2⎞ ⎜                 ⎛ 2    2⎞       ⎟⎟    2 ⎜        ↪
↪ 833333333333333⋅⎝x  + ξ ⎠⋅⎝1.0⋅ⅈ⋅x⋅ξ - 0.25⋅⎝x  + ξ ⎠  + 0.5⎠⎠ + t ⋅⎝0.5⋅ⅈ⋅x ↪
↪                                                                              ↪

↪                     2       ⎞       ⎛ 2    2⎞    
↪            ⎛ 2    2⎞        ⎟   ⅈ⋅t⋅⎝x  + ξ ⎠    
↪ ⋅ξ - 0.125⋅⎝x  + ξ ⎠  + 0.25⎠ - ───────────── + 1
↪                                       2          

Exact propagator symbol (closed form):
      ⎛ 2    2⎞ 
 -ⅈ⋅t⋅⎝x  + ξ ⎠ 
 ───────────────
        2       
ℯ               

Comparison:
- Asymptotic expansion approximates exp(-i t H)
- For small t, both should coincide closely.

Difference (asymptotic - exact):
⎛                                                                              ↪
⎜                                                                              ↪
⎜⎛                                                                             ↪
⎜⎜ 4 ⎛                     8                       6  2            5           ↪
⎝⎝t ⋅⎝0.00520833333333333⋅x  + 0.0208333333333333⋅x ⋅ξ  - 0.125⋅ⅈ⋅x ⋅ξ + 0.031 ↪
────────────────────────────────────────────────────────────────────────────── ↪
                                                                               ↪

↪                                                                              ↪
↪                                                                              ↪
↪                                                                              ↪
↪     4  4                      4           3  3                       2  6    ↪
↪ 25⋅x ⋅ξ  - 0.229166666666667⋅x  - 0.25⋅ⅈ⋅x ⋅ξ  + 0.0208333333333333⋅x ⋅ξ  -  ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪                                                                              ↪
↪                                                                              ↪
↪                                                                              ↪
↪                    2  2              5                                       ↪
↪ 0.708333333333333⋅x ⋅ξ  - 0.125⋅ⅈ⋅x⋅ξ  + 0.666666666666667⋅ⅈ⋅x⋅ξ + 0.0052083 ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪                                                                              ↪
↪                                                                              ↪
↪                                                                  ⎛           ↪
↪             8                      4                    ⎞      3 ⎜           ↪
↪ 3333333333⋅ξ  - 0.229166666666667⋅ξ  + 0.145833333333333⎠ + ⅈ⋅t ⋅⎝- 0.166666 ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                       2                                      ↪

↪                                                                              ↪
↪                                                                              ↪
↪                                                                              ↪
↪            2                         ⎛        ⎛ 2    2⎞⎞        2            ↪
↪ 666666667⋅x  + 0.333333333333333⋅ⅈ⋅x⋅⎝ⅈ⋅x - ξ⋅⎝x  + ξ ⎠⎠ - 0.5⋅ξ  - 0.166666 ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪                                                                              ↪
↪                                                                              ↪
↪                     ⎛                          2      ⎞⎞      ⎛              ↪
↪           ⎛ 2    2⎞ ⎜                 ⎛ 2    2⎞       ⎟⎟    2 ⎜              ↪
↪ 666666667⋅⎝x  + ξ ⎠⋅⎝1.0⋅ⅈ⋅x⋅ξ - 0.25⋅⎝x  + ξ ⎠  + 0.5⎠⎠ + t ⋅⎝1.0⋅ⅈ⋅x⋅ξ - 0 ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪                                                 ⎛ 2    2⎞    ⎞       ⎛ 2     ↪
↪                                             ⅈ⋅t⋅⎝x  + ξ ⎠    ⎟  -ⅈ⋅t⋅⎝x  + ξ ↪
↪              2      ⎞                    ⎞  ─────────────    ⎟  ──────────── ↪
↪     ⎛ 2    2⎞       ⎟       ⎛ 2    2⎞    ⎟        2          ⎟         2     ↪
↪ .25⋅⎝x  + ξ ⎠  + 0.5⎠ - ⅈ⋅t⋅⎝x  + ξ ⎠ + 2⎠⋅ℯ              - 2⎠⋅ℯ             ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪ 2⎞ 
↪  ⎠ 
↪ ───
↪    
↪    
↪ ───
↪    

======================================================================
NUMERICAL COMPARISON
======================================================================

----------------------------------------------------------------------
Comparison at t = 0.05
----------------------------------------------------------------------
Max real error  : 6.25e-04
Max imag error  : 4.98e-04
No description has been provided for this image
----------------------------------------------------------------------
Comparison at t = 0.1
----------------------------------------------------------------------
Max real error  : 2.51e-03
Max imag error  : 2.86e-03
No description has been provided for this image
----------------------------------------------------------------------
Comparison at t = 0.2
----------------------------------------------------------------------
Max real error  : 1.60e-02
Max imag error  : 4.49e-02
No description has been provided for this image
======================================================================
UNITARITY CHECK — should satisfy |U(t)|² = 1
======================================================================
Symbolic expression of |U(t)|²:
⎛                                                                              ↪
⎜ 4 ⎛                     8                       6  2            5            ↪
⎝t ⋅⎝0.00520833333333333⋅x  + 0.0208333333333333⋅x ⋅ξ  - 0.125⋅ⅈ⋅x ⋅ξ + 0.0312 ↪
────────────────────────────────────────────────────────────────────────────── ↪
                                                                               ↪

↪                                                                              ↪
↪    4  4                      4           3  3                       2  6     ↪
↪ 5⋅x ⋅ξ  - 0.229166666666667⋅x  - 0.25⋅ⅈ⋅x ⋅ξ  + 0.0208333333333333⋅x ⋅ξ  - 0 ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪                                                                              ↪
↪                   2  2              5                                        ↪
↪ .708333333333333⋅x ⋅ξ  - 0.125⋅ⅈ⋅x⋅ξ  + 0.666666666666667⋅ⅈ⋅x⋅ξ + 0.00520833 ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪                                                                 ⎛            ↪
↪            8                      4                    ⎞      3 ⎜            ↪
↪ 333333333⋅ξ  - 0.229166666666667⋅ξ  + 0.145833333333333⎠ + ⅈ⋅t ⋅⎝- 0.1666666 ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪                                                                              ↪
↪           2                         ⎛        ⎛ 2    2⎞⎞        2             ↪
↪ 66666667⋅x  + 0.333333333333333⋅ⅈ⋅x⋅⎝ⅈ⋅x - ξ⋅⎝x  + ξ ⎠⎠ - 0.5⋅ξ  - 0.1666666 ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪                    ⎛                          2      ⎞⎞      ⎛               ↪
↪          ⎛ 2    2⎞ ⎜                 ⎛ 2    2⎞       ⎟⎟    2 ⎜               ↪
↪ 66666667⋅⎝x  + ξ ⎠⋅⎝1.0⋅ⅈ⋅x⋅ξ - 0.25⋅⎝x  + ξ ⎠  + 0.5⎠⎠ + t ⋅⎝1.0⋅ⅈ⋅x⋅ξ - 0. ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪             2      ⎞                    ⎞ ⎛                                  ↪
↪    ⎛ 2    2⎞       ⎟       ⎛ 2    2⎞    ⎟ ⎜ 4 ⎛                     8        ↪
↪ 25⋅⎝x  + ξ ⎠  + 0.5⎠ - ⅈ⋅t⋅⎝x  + ξ ⎠ + 2⎠⋅⎝t ⋅⎝0.00520833333333333⋅x  + 0.02 ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                         4                                    ↪

↪                                                                              ↪
↪                 6  2            5              4  4                      4   ↪
↪ 08333333333333⋅x ⋅ξ  + 0.125⋅ⅈ⋅x ⋅ξ + 0.03125⋅x ⋅ξ  - 0.229166666666667⋅x  + ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪                                                                              ↪
↪          3  3                       2  6                      2  2           ↪
↪  0.25⋅ⅈ⋅x ⋅ξ  + 0.0208333333333333⋅x ⋅ξ  - 0.708333333333333⋅x ⋅ξ  + 0.125⋅ⅈ ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪                                                                              ↪
↪     5                                                  8                     ↪
↪ ⋅x⋅ξ  - 0.666666666666667⋅ⅈ⋅x⋅ξ + 0.00520833333333333⋅ξ  - 0.229166666666667 ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪                                 ⎛                                            ↪
↪   4                    ⎞      3 ⎜                   2                        ↪
↪ ⋅ξ  + 0.145833333333333⎠ + ⅈ⋅t ⋅⎝0.166666666666667⋅x  - 0.333333333333333⋅ⅈ⋅ ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪                                                              ⎛               ↪
↪   ⎛        ⎛ 2    2⎞⎞        2                     ⎛ 2    2⎞ ⎜               ↪
↪ x⋅⎝ⅈ⋅x + ξ⋅⎝x  + ξ ⎠⎠ + 0.5⋅ξ  - 0.166666666666667⋅⎝x  + ξ ⎠⋅⎝1.0⋅ⅈ⋅x⋅ξ + 0. ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪             2      ⎞⎞      ⎛                           2      ⎞              ↪
↪    ⎛ 2    2⎞       ⎟⎟    2 ⎜                  ⎛ 2    2⎞       ⎟       ⎛ 2    ↪
↪ 25⋅⎝x  + ξ ⎠  - 0.5⎠⎠ + t ⋅⎝-1.0⋅ⅈ⋅x⋅ξ - 0.25⋅⎝x  + ξ ⎠  + 0.5⎠ + ⅈ⋅t⋅⎝x  +  ↪
↪ ──────────────────────────────────────────────────────────────────────────── ↪
↪                                                                              ↪

↪        ⎞
↪  2⎞    ⎟
↪ ξ ⎠ + 2⎠
↪ ────────
↪         
No description has been provided for this image
Maximum deviation from unitarity: 5.04e-03

Interpretation:
- The propagator U(t) = exp(-i t H) must be unitary.
- The asymptotic expansion preserves unitarity approximately.
- For small t, |U(t)|² ≈ 1 with negligible drift.

Applications:
- Time evolution of quantum states
- Harmonic oscillator wavepacket propagation
- Semiclassical and pseudodifferential approximations

Transport/Advection Operator¶

In [4]:
# --- Symbolic setup ---
x, xi, t_sym = symbols('x xi t', real=True)
c = symbols('c', real=True, positive=True)

print("""
CONTEXT: Transport operator (constant-speed)
Consider the transport symbol p(x, ξ) = i * c * ξ which corresponds to
the first-order differential operator i c ∂_x (pure transport at speed c).
Its exponential exp(t ⋅ p) should produce a shift in x (in Fourier/symbol space
the propagator is exp(i c t ξ)).
""")

# Symbol: p(x, ξ) = i * c * ξ  (transport with speed c)
transport_symbol = I * c * xi
Transport = PseudoDifferentialOperator(transport_symbol, [x], mode='symbol')

print("\nTransport operator symbol p(x, ξ) =")
pprint(Transport.symbol)

# --- Symbolic exponential (asymptotic / series) ---
print("\nComputing exp(t * i*c*∂_x) as an asymptotic symbol up to order 5...")
shift_symbol = Transport.exponential_symbol(t=t_sym, order=5)
shift_symbol_simplified = simplify(shift_symbol)

print("\nAsymptotic shift operator symbol (order 5):")
pprint(shift_symbol_simplified)

# --- Exact symbol (closed form) ---
exact_shift = exp(I * c * t_sym * xi)
print("\nExact shift operator symbol:")
pprint(exact_shift)

# --- Symbolic difference (sanity check) ---
diff_symbol = simplify(shift_symbol_simplified - exact_shift)
print("\nDifference (should be exactly 0 if expansion equals closed form):")
pprint(diff_symbol)

# ============================================================
# Numerical comparison for various times
# ============================================================

t_values = [0.1, 0.5, 1.0, 2.0, 3.0]
xi_vals = np.linspace(-5, 5, 400)
c_val = 1.0

# Lambdify symbolic expressions to evaluate numerically
shift_approx_func = lambdify((xi, t_sym, c), shift_symbol_simplified, 'numpy')
shift_exact_func  = lambdify((xi, t_sym, c), exact_shift, 'numpy')

errors = []

print("\n" + "-"*70)
print("Numerical comparisons for various time points:")
print("-"*70)

for t_val in t_values:
    print(f"\nComparing at t = {t_val} ...")

    # Evaluate numerical arrays
    approx_vals = shift_approx_func(xi_vals, t_val, c_val)
    exact_vals  = shift_exact_func(xi_vals, t_val, c_val)

    # Compute max absolute difference
    error = np.max(np.abs(approx_vals - exact_vals))
    errors.append(error)

    # Plot real and imaginary parts
    fig, axes = plt.subplots(2, 1, figsize=(10, 8))

    axes[0].plot(xi_vals, np.real(exact_vals), '-', label='Exact', linewidth=2)
    axes[0].plot(xi_vals, np.real(approx_vals), '--', label='Asymptotic (order 5)', linewidth=2)
    axes[0].set_ylabel('Re[Shift symbol]', fontsize=12)
    axes[0].set_title(f'Real part of exp(i c t ξ) at t={t_val}', fontsize=14)
    axes[0].grid(True, alpha=0.3)
    axes[0].legend()

    axes[1].plot(xi_vals, np.imag(exact_vals), '-', label='Exact', linewidth=2)
    axes[1].plot(xi_vals, np.imag(approx_vals), '--', label='Asymptotic (order 5)', linewidth=2)
    axes[1].set_xlabel('ξ (frequency)', fontsize=12)
    axes[1].set_ylabel('Im[Shift symbol]', fontsize=12)
    axes[1].set_title('Imaginary part', fontsize=14)
    axes[1].grid(True, alpha=0.3)
    axes[1].legend()

    plt.tight_layout()
    plt.show()

    print(f"Maximum error at t={t_val}: {error:.2e}")

# ============================================================
# Summary of errors
# ============================================================

print("\n" + "-"*70)
print("Summary of maximum errors:")
print("-"*70)
for t_val, err in zip(t_values, errors):
    print(f"t = {t_val:.2f}  →  max error = {err:.3e}")

plt.figure(figsize=(8, 5))
plt.plot(t_values, errors, 'o-', linewidth=2)
plt.xlabel('t', fontsize=12)
plt.ylabel('Max error |approx - exact|', fontsize=12)
plt.title('Error evolution vs time (Transport operator)', fontsize=14)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

print("\nInterpretation:")
print("- The symbol p = i c ξ generates pure transport (shift).")
print("- Its exponential in symbol space is the phase factor exp(i c t ξ).")
print("- The asymptotic exponential_symbol(order=5) should match the closed form;")
print("  numerically we inspect the max absolute error versus ξ for several t.")
print("✓ If the difference is (near) zero, the asymptotic expansion reproduces the exact propagator.")
CONTEXT: Transport operator (constant-speed)
Consider the transport symbol p(x, ξ) = i * c * ξ which corresponds to
the first-order differential operator i c ∂_x (pure transport at speed c).
Its exponential exp(t ⋅ p) should produce a shift in x (in Fourier/symbol space
the propagator is exp(i c t ξ)).


Transport operator symbol p(x, ξ) =
ⅈ⋅c⋅ξ

Computing exp(t * i*c*∂_x) as an asymptotic symbol up to order 5...
Asymptotic shift operator symbol (order 5):
                       5  5  5                       4  4  4                   ↪
0.00833333333333333⋅ⅈ⋅c ⋅t ⋅ξ  + 0.0416666666666667⋅c ⋅t ⋅ξ  - 0.1666666666666 ↪

↪       3  3  3        2  2  2              
↪ 67⋅ⅈ⋅c ⋅t ⋅ξ  - 0.5⋅c ⋅t ⋅ξ  + ⅈ⋅c⋅t⋅ξ + 1

Exact shift operator symbol:
 ⅈ⋅c⋅t⋅ξ
ℯ       

Difference (should be exactly 0 if expansion equals closed form):
                       5  5  5                       4  4  4                   ↪
0.00833333333333333⋅ⅈ⋅c ⋅t ⋅ξ  + 0.0416666666666667⋅c ⋅t ⋅ξ  - 0.1666666666666 ↪

↪       3  3  3        2  2  2              ⅈ⋅c⋅t⋅ξ    
↪ 67⋅ⅈ⋅c ⋅t ⋅ξ  - 0.5⋅c ⋅t ⋅ξ  + ⅈ⋅c⋅t⋅ξ - ℯ        + 1

----------------------------------------------------------------------
Numerical comparisons for various time points:
----------------------------------------------------------------------

Comparing at t = 0.1 ...
No description has been provided for this image
Maximum error at t=0.1: 2.17e-05

Comparing at t = 0.5 ...
No description has been provided for this image
Maximum error at t=0.5: 3.23e-01

Comparing at t = 1.0 ...
No description has been provided for this image
Maximum error at t=1.0: 1.81e+01

Comparing at t = 2.0 ...
No description has been provided for this image
Maximum error at t=2.0: 7.71e+02

Comparing at t = 3.0 ...
No description has been provided for this image
Maximum error at t=3.0: 6.12e+03

----------------------------------------------------------------------
Summary of maximum errors:
----------------------------------------------------------------------
t = 0.10  →  max error = 2.166e-05
t = 0.50  →  max error = 3.235e-01
t = 1.00  →  max error = 1.811e+01
t = 2.00  →  max error = 7.710e+02
t = 3.00  →  max error = 6.116e+03
No description has been provided for this image
Interpretation:
- The symbol p = i c ξ generates pure transport (shift).
- Its exponential in symbol space is the phase factor exp(i c t ξ).
- The asymptotic exponential_symbol(order=5) should match the closed form;
  numerically we inspect the max absolute error versus ξ for several t.
✓ If the difference is (near) zero, the asymptotic expansion reproduces the exact propagator.

Convergence with Order¶

In [5]:
# --- Symbolic setup ---
x, xi, t_sym = symbols('x xi t', real=True)
mixed_symbol = xi**2 + x*xi + x**2
Mixed = PseudoDifferentialOperator(mixed_symbol, [x], mode='symbol')

print("\nMixed operator symbol p(x,ξ) =")
pprint(Mixed.symbol)

# --- Parameters ---
x_val = 1.0
xi_vals = np.linspace(-2, 2, 200)
t_values = [0.05, 0.1, 0.2, 0.5]
orders = [1, 2, 3, 4, 5]

# --- Compute all exponentials ---
exponentials = {}
for t_val in t_values:
    exponentials[t_val] = {}
    for ord in orders:
        exp_sym = Mixed.exponential_symbol(t=t_val, order=ord)
        exponentials[t_val][ord] = simplify(exp_sym)

# --- Visualization loop ---
for t_val in t_values:
    plt.figure(figsize=(12, 8))
    for ord in orders:
        exp_func = lambdify((xi,), exponentials[t_val][ord].subs(x, x_val), 'numpy')
        exp_vals = exp_func(xi_vals)
        plt.plot(xi_vals, exp_vals.real, linewidth=2, label=f'Order {ord}')
    plt.xlabel('ξ', fontsize=12)
    plt.ylabel('Re[exp(tP)]', fontsize=12)
    plt.title(f'Convergence of exp(tP) at x={x_val}, t={t_val}', fontsize=14)
    plt.legend(fontsize=10)
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.show()

# --- Convergence analysis ---
print("\n" + "-"*70)
print("Convergence analysis across orders and times:")
print("-"*70)
for t_val in t_values:
    print(f"\nAt t = {t_val}:")
    for i in range(len(orders)-1):
        ord1, ord2 = orders[i], orders[i+1]
        func1 = lambdify(xi, exponentials[t_val][ord1].subs(x, x_val), 'numpy')
        func2 = lambdify(xi, exponentials[t_val][ord2].subs(x, x_val), 'numpy')
        vals1, vals2 = func1(xi_vals), func2(xi_vals)
        diff = np.max(np.abs(vals1 - vals2))
        print(f"  Max difference between order {ord1} and {ord2}: {diff:.2e}")
Mixed operator symbol p(x,ξ) =
 2          2
x  + x⋅ξ + ξ 
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
----------------------------------------------------------------------
Convergence analysis across orders and times:
----------------------------------------------------------------------

At t = 0.05:
  Max difference between order 1 and 2: 6.38e-02
  Max difference between order 2 and 3: 9.61e-03
  Max difference between order 3 and 4: 1.39e-03
  Max difference between order 4 and 5: 1.92e-04

At t = 0.1:
  Max difference between order 1 and 2: 2.55e-01
  Max difference between order 2 and 3: 7.69e-02
  Max difference between order 3 and 4: 2.22e-02
  Max difference between order 4 and 5: 6.16e-03

At t = 0.2:
  Max difference between order 1 and 2: 1.02e+00
  Max difference between order 2 and 3: 6.15e-01
  Max difference between order 3 and 4: 3.56e-01
  Max difference between order 4 and 5: 1.97e-01

At t = 0.5:
  Max difference between order 1 and 2: 6.38e+00
  Max difference between order 2 and 3: 9.61e+00
  Max difference between order 3 and 4: 1.39e+01
  Max difference between order 4 and 5: 1.92e+01

2D Heat Equation¶

In [6]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
t = symbols('t', real=True, positive=True)

print("""
CONTEXT: Heat Equation and Diffusion Operator
On a Euclidean manifold ℝ², the Laplacian Δ = ∂ₓ² + ∂ᵧ² generates the heat flow:
    ∂ₜu = Δu
In Fourier variables (ξ, η), the Laplacian symbol is:
    p(x, y, ξ, η) = -(ξ² + η²)
The corresponding evolution operator is:
    exp(tΔ)  ⇔  exp(-t(ξ² + η²))  in Fourier space
""")

# Define the 2D Laplacian symbol
laplacian_2d_symbol = -(xi**2 + eta**2)
Laplacian2D = PseudoDifferentialOperator(laplacian_2d_symbol, [x, y], mode='symbol')

print("\n2D Laplacian symbol p(x, y, ξ, η) =")
pprint(laplacian_2d_symbol)

print("""
This operator describes isotropic diffusion in ℝ².
Physically:
- (x, y): spatial coordinates
- (ξ, η): frequency or momentum variables
- exp(tΔ) smooths out high frequencies (heat diffusion)
""")

# ============================================================
# Asymptotic expansion and time analysis
# ============================================================
orders = [1, 2, 3, 4]
t_values = [0.01, 0.05, 0.1, 0.2]
x_val, y_val = 0.0, 0.0

xi_vals = np.linspace(-4, 4, 80)
eta_vals = np.linspace(-4, 4, 80)
XI, ETA = np.meshgrid(xi_vals, eta_vals)

# Exact exponential symbol
exact_symbol = exp(-t * (xi**2 + eta**2))
exact_func = lambdify((xi, eta, t), exact_symbol, 'numpy')

# Initialize error tables
errors_Linf = np.zeros((len(orders), len(t_values)))
errors_L2 = np.zeros_like(errors_Linf)

print("\n" + "="*70)
print("ASYMPTOTIC EXPANSION OF THE HEAT PROPAGATOR exp(tΔ)")
print("="*70)

# ============================================================
# Main computational loop
# ============================================================
for i_ord, order in enumerate(orders):
    print(f"\n=== Asymptotic order {order} ===")
    for j_t, t_val in enumerate(t_values):
        # Compute truncated exponential symbol
        approx_symbol = Laplacian2D.exponential_symbol(t=t_val, order=order)
        approx_func = lambdify((xi, eta),
                               approx_symbol.subs([(x, x_val), (y, y_val)]),
                               'numpy')
        approx_vals = approx_func(XI, ETA).real
        exact_vals = exact_func(XI, ETA, t_val).real

        # Compute errors
        err = np.abs(approx_vals - exact_vals)
        errors_Linf[i_ord, j_t] = np.max(err)
        errors_L2[i_ord, j_t] = np.sqrt(np.mean(err**2))

        # Visualization for a representative t
        if j_t == 1:
            fig, axes = plt.subplots(1, 3, figsize=(15, 4))
            fig.suptitle(f"Heat Propagator exp(tΔ) in 2D — order {order}, t={t_val}", fontsize=14)

            im0 = axes[0].imshow(approx_vals, extent=[-4, 4, -4, 4], origin='lower', cmap='viridis')
            axes[0].set_title("Asymptotic (Re part)")
            plt.colorbar(im0, ax=axes[0], fraction=0.046)

            im1 = axes[1].imshow(exact_vals, extent=[-4, 4, -4, 4], origin='lower', cmap='viridis')
            axes[1].set_title("Exact (Re part)")
            plt.colorbar(im1, ax=axes[1], fraction=0.046)

            im2 = axes[2].imshow(err, extent=[-4, 4, -4, 4], origin='lower', cmap='Reds')
            axes[2].set_title("|Error|")
            plt.colorbar(im2, ax=axes[2], fraction=0.046)

            for ax in axes:
                ax.set_xlabel("ξ")
                ax.set_ylabel("η")

            plt.tight_layout()
            plt.show()

# ============================================================
# Global error analysis
# ============================================================
print("\n" + "="*70)
print("GLOBAL CONVERGENCE ANALYSIS")
print("="*70)

plt.figure(figsize=(10, 6))
for i, order in enumerate(orders):
    plt.plot(t_values, errors_Linf[i, :], 'o-', label=f'Order {order}')
plt.xlabel('t')
plt.ylabel('L∞ error')
plt.title('Convergence of exp(tΔ) in 2D — Maximum error vs time')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

plt.figure(figsize=(10, 6))
for i, order in enumerate(orders):
    plt.plot(t_values, errors_L2[i, :], 's-', label=f'Order {order}')
plt.xlabel('t')
plt.ylabel('L² error')
plt.title('Convergence of exp(tΔ) in 2D — L² error vs time')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# ============================================================
# Error summary table
# ============================================================
print("\nSummary of numerical errors:")
print("Order |   t   |   L∞ Error   |   L² Error")
print("------------------------------------------")
for i, order in enumerate(orders):
    for j, t_val in enumerate(t_values):
        print(f"{order:>5d} | {t_val:4.2f} | {errors_Linf[i,j]:10.2e} | {errors_L2[i,j]:10.2e}")

print("""
Interpretation:
- Higher asymptotic order ⇒ better accuracy for larger t
- The expansion converges rapidly for small times
- exp(tΔ) remains stable and smoothing for all t > 0

Applications:
- Numerical analysis of heat kernels
- Spectral methods for diffusion equations
- Regularization in image processing or PDE solvers
""")
CONTEXT: Heat Equation and Diffusion Operator
On a Euclidean manifold ℝ², the Laplacian Δ = ∂ₓ² + ∂ᵧ² generates the heat flow:
    ∂ₜu = Δu
In Fourier variables (ξ, η), the Laplacian symbol is:
    p(x, y, ξ, η) = -(ξ² + η²)
The corresponding evolution operator is:
    exp(tΔ)  ⇔  exp(-t(ξ² + η²))  in Fourier space


2D Laplacian symbol p(x, y, ξ, η) =
   2    2
- η  - ξ 

This operator describes isotropic diffusion in ℝ².
Physically:
- (x, y): spatial coordinates
- (ξ, η): frequency or momentum variables
- exp(tΔ) smooths out high frequencies (heat diffusion)


======================================================================
ASYMPTOTIC EXPANSION OF THE HEAT PROPAGATOR exp(tΔ)
======================================================================

=== Asymptotic order 1 ===
No description has been provided for this image
=== Asymptotic order 2 ===
No description has been provided for this image
=== Asymptotic order 3 ===
No description has been provided for this image
=== Asymptotic order 4 ===
No description has been provided for this image
======================================================================
GLOBAL CONVERGENCE ANALYSIS
======================================================================
No description has been provided for this image
No description has been provided for this image
Summary of numerical errors:
Order |   t   |   L∞ Error   |   L² Error
------------------------------------------
    1 | 0.01 |   4.61e-02 |   1.15e-02
    1 | 0.05 |   8.02e-01 |   2.23e-01
    1 | 0.10 |   2.24e+00 |   6.87e-01
    1 | 0.20 |   5.40e+00 |   1.85e+00
    2 | 0.01 |   5.05e-03 |   8.74e-04
    2 | 0.05 |   4.78e-01 |   8.81e-02
    2 | 0.10 |   2.88e+00 |   5.62e-01
    2 | 0.20 |   1.51e+01 |   3.15e+00
    3 | 0.01 |   4.10e-04 |   5.47e-05
    3 | 0.05 |   2.05e-01 |   2.84e-02
    3 | 0.10 |   2.58e+00 |   3.72e-01
    3 | 0.20 |   2.86e+01 |   4.32e+00
    4 | 0.01 |   2.65e-05 |   2.91e-06
    4 | 0.05 |   6.85e-02 |   7.71e-03
    4 | 0.10 |   1.79e+00 |   2.06e-01
    4 | 0.20 |   4.13e+01 |   4.93e+00

Interpretation:
- Higher asymptotic order ⇒ better accuracy for larger t
- The expansion converges rapidly for small times
- exp(tΔ) remains stable and smoothing for all t > 0

Applications:
- Numerical analysis of heat kernels
- Spectral methods for diffusion equations
- Regularization in image processing or PDE solvers

Heisenberg Algebra and Canonical Commutation Relations¶

In [7]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Quantum Mechanics and Pseudo-Differential Operators
In quantum mechanics, observables such as position and momentum
are represented by non-commuting operators satisfying the
Heisenberg commutation relations.

Heisenberg algebra generators:
  Q → position operator
  P → momentum operator
  I → identity operator

Canonical commutation relation:
  [Q, P] = iℏI   (we take ℏ = 1)
""")

# Define generators of the Heisenberg algebra
Q_symbol = x      # position symbol
P_symbol = xi     # momentum symbol

Q_op = PseudoDifferentialOperator(Q_symbol, [x], mode='symbol')
P_op = PseudoDifferentialOperator(P_symbol, [x], mode='symbol')

print("\nPosition operator Q, symbol:")
pprint(Q_op.symbol)

print("\nMomentum operator P, symbol:")
pprint(P_op.symbol)

# Compute the commutator [Q, P] = QP - PQ via asymptotic composition
print("\n" + "-"*70)
print("Computing [Q, P] = QP - PQ")
print("-"*70)

QP_symbol = Q_op.compose_asymptotic(P_op, order=3)
PQ_symbol = P_op.compose_asymptotic(Q_op, order=3)

print("\nQP symbol (order 3):")
pprint(simplify(QP_symbol))

print("\nPQ symbol (order 3):")
pprint(simplify(PQ_symbol))

commutator_QP = simplify(QP_symbol - PQ_symbol)
print("\n[Q, P] symbol:")
pprint(commutator_QP)

print("""
Physical interpretation:
- Q corresponds to multiplication by x (position operator)
- P corresponds to -i ∂/∂x (momentum operator)
Their non-commutativity encodes the uncertainty principle.
""")

print("\n✓ Expected: [Q, P] = iℏ (ℏ = 1 in our units)")
print(f"✓ Obtained: {commutator_QP}")

if commutator_QP == 1.0*I:
    print("\n✅ CANONICAL COMMUTATION RELATION VERIFIED!")
else:
    print(f"\n⚠️  Deviation detected: expected i, got {commutator_QP}")

# Now compute [P, Q] = PQ - QP
print("\n" + "-"*70)
print("Computing [P, Q] = PQ - QP")
print("-"*70)

QP_symbol = Q_op.compose_asymptotic(P_op, order=3)
PQ_symbol = P_op.compose_asymptotic(Q_op, order=3)

commutator_PQ = simplify(PQ_symbol - QP_symbol)
print("\n[P, Q] symbol:")
pprint(commutator_PQ)

print("\n✓ Expected: [P, Q] = -iℏ (ℏ = 1 in our units)")
print(f"✓ Obtained: {commutator_PQ}")

if commutator_PQ == -1.0*I:
    print("\n✅ CANONICAL COMMUTATION RELATION VERIFIED!")
else:
    print(f"\n⚠️  Deviation detected: expected -i, got {commutator_PQ}")

print("\n" + "="*70)
print("Heisenberg Algebra Structure Constants")
print("="*70)
print("""
Heisenberg algebra generators: {Q, P, I}
Structure relations:
  [Q, P] =  iI
  [P, Q] = -iI
  [Q, I] =  0
  [P, I] =  0
  [I, I] =  0

This is a 3-dimensional nilpotent Lie algebra.
Applications:
- Foundations of quantum mechanics
- Weyl quantization and pseudo-differential analysis
- Quantum harmonic oscillator and phase-space methods
""")
CONTEXT: Quantum Mechanics and Pseudo-Differential Operators
In quantum mechanics, observables such as position and momentum
are represented by non-commuting operators satisfying the
Heisenberg commutation relations.

Heisenberg algebra generators:
  Q → position operator
  P → momentum operator
  I → identity operator

Canonical commutation relation:
  [Q, P] = iℏI   (we take ℏ = 1)


Position operator Q, symbol:
x

Momentum operator P, symbol:
ξ

----------------------------------------------------------------------
Computing [Q, P] = QP - PQ
----------------------------------------------------------------------

QP symbol (order 3):
1.0⋅x⋅ξ

PQ symbol (order 3):
1.0⋅x⋅ξ - 1.0⋅ⅈ

[Q, P] symbol:
1.0⋅ⅈ

Physical interpretation:
- Q corresponds to multiplication by x (position operator)
- P corresponds to -i ∂/∂x (momentum operator)
Their non-commutativity encodes the uncertainty principle.


✓ Expected: [Q, P] = iℏ (ℏ = 1 in our units)
✓ Obtained: 1.0*I

✅ CANONICAL COMMUTATION RELATION VERIFIED!

----------------------------------------------------------------------
Computing [P, Q] = PQ - QP
----------------------------------------------------------------------
[P, Q] symbol:
-1.0⋅ⅈ

✓ Expected: [P, Q] = -iℏ (ℏ = 1 in our units)
✓ Obtained: -1.0*I

✅ CANONICAL COMMUTATION RELATION VERIFIED!

======================================================================
Heisenberg Algebra Structure Constants
======================================================================

Heisenberg algebra generators: {Q, P, I}
Structure relations:
  [Q, P] =  iI
  [P, Q] = -iI
  [Q, I] =  0
  [P, I] =  0
  [I, I] =  0

This is a 3-dimensional nilpotent Lie algebra.
Applications:
- Foundations of quantum mechanics
- Weyl quantization and pseudo-differential analysis
- Quantum harmonic oscillator and phase-space methods

Baker-Campbell-Hausdorff Formula for Heisenberg Group¶

In [8]:
a, b = symbols('a b', real=True)
t = symbols('t', real=True)
X_symbol = a * x
Y_symbol = b * xi

X_op = PseudoDifferentialOperator(X_symbol, [x], mode='symbol')
Y_op = PseudoDifferentialOperator(Y_symbol, [x], mode='symbol')

print("\nCONTEXT: Baker-Campbell-Hausdorff and the Heisenberg group")
print(f"\nX = a Q, symbol: {X_op.symbol}")
print(f"Y = b P, symbol: {Y_op.symbol}")

# Commutator [X,Y] = [aQ, bP] = ab[Q,P] = iab (in our conventions)
commutator_XY = X_op.commutator_symbolic(Y_op, order=3)
print("\n[X, Y] = [aQ, bP] =")
pprint(commutator_XY)

# Compute exp(tX) and exp(tY)
print("\n" + "-"*70)
print("Computing exp(tX) and exp(tY)")
print("-"*70)
exp_tX = X_op.exponential_symbol(t=t, order=4)
exp_tY = Y_op.exponential_symbol(t=t, order=4)
print("\nexp(tX) symbol:")
pprint(simplify(exp_tX))
print("\nexp(tY) symbol:")
pprint(simplify(exp_tY))

# Composition exp(tX) ∘ exp(tY)
print("\n" + "-"*70)
print("Computing exp(tX) ∘ exp(tY) via composition")
print("-"*70)
exp_tX_op = PseudoDifferentialOperator(exp_tX, [x], mode='symbol')
exp_tY_op = PseudoDifferentialOperator(exp_tY, [x], mode='symbol')
product_symbol = exp_tY_op.compose_asymptotic(exp_tX_op, order=3, mode='weyl')
print("\nSymbol of exp(tX) ∘ exp(tY):")
pprint(simplify(product_symbol))

# Exact BCH formula for the Heisenberg group
# exp(aQ) exp(bP) = exp(aQ + bP + (ab/2)[Q,P])
#                 = exp(aQ + bP + iab/2)
print("\n" + "-"*70)
print("Baker-Campbell-Hausdorff formula for the Heisenberg group")
print("-"*70)
print("""
For the Heisenberg group the BCH series terminates at second order:
  exp(aQ) exp(bP) = exp(aQ + bP + (ab/2) i)
Because [Q,P] = i I commutes with everything, higher commutators vanish.
Physical interpretation:
- This represents the quantum composition of translation operators.
- The phase factor exp(i ab / 2) is the Weyl-ordering correction (central phase).
""")

# Numerical verification
print("\n" + "-"*70)
print("Numerical verification")
print("-"*70)
a_val, b_val, t_val = 1.0, 0.5, 1.0
x_val = 0.0

product_func = lambdify(xi, product_symbol.subs([(a, a_val), (b, b_val),
                                                   (t, t_val), (x, x_val)]), 'numpy')

# Exact BCH solution (as a symbolic exponential in the xi variable, x fixed)
BCH_exact = exp(t_val * (a_val * x + b_val * xi + I * a_val * b_val / 2))
BCH_func = lambdify(xi, BCH_exact.subs(x, x_val), 'numpy')

xi_vals = np.linspace(-3, 3, 100)
product_vals = product_func(xi_vals)
BCH_vals = BCH_func(xi_vals)

plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(xi_vals, product_vals.real, 'b-', label='Composition', linewidth=2)
plt.plot(xi_vals, BCH_vals.real, 'r--', label='BCH exact', linewidth=2)
plt.xlabel('ξ')
plt.ylabel('Real part')
plt.title('exp(aQ) ∘ exp(bP) — Real part')
plt.legend()
plt.grid(True, alpha=0.3)

plt.subplot(1, 2, 2)
plt.plot(xi_vals, product_vals.imag, 'b-', label='Composition', linewidth=2)
plt.plot(xi_vals, BCH_vals.imag, 'r--', label='BCH exact', linewidth=2)
plt.xlabel('ξ')
plt.ylabel('Imaginary part')
plt.title('exp(aQ) ∘ exp(bP) — Imaginary part')
plt.legend()
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

error = np.max(np.abs(product_vals - BCH_vals))
print(f"\nMaximum error: {error:.2e}")
CONTEXT: Baker-Campbell-Hausdorff and the Heisenberg group

X = a Q, symbol: a*x
Y = b P, symbol: b*xi

[X, Y] = [aQ, bP] =
1.0⋅ⅈ⋅a⋅b

----------------------------------------------------------------------
Computing exp(tX) and exp(tY)
----------------------------------------------------------------------
exp(tX) symbol:
                    4  4  4                      3  3  3        2  2  2        ↪
0.0416666666666667⋅a ⋅t ⋅x  + 0.166666666666667⋅a ⋅t ⋅x  + 0.5⋅a ⋅t ⋅x  + a⋅t⋅ ↪

↪      
↪ x + 1

exp(tY) symbol:
                    4  4  4                      3  3  3        2  2  2        ↪
0.0416666666666667⋅b ⋅t ⋅ξ  + 0.166666666666667⋅b ⋅t ⋅ξ  + 0.5⋅b ⋅t ⋅ξ  + b⋅t⋅ ↪

↪      
↪ ξ + 1

----------------------------------------------------------------------
Computing exp(tX) ∘ exp(tY) via composition
----------------------------------------------------------------------
Symbol of exp(tX) ∘ exp(tY):
                        3  3  6                                  2  2  4 ⎛     ↪
- 0.0208333333333333⋅ⅈ⋅a ⋅b ⋅t ⋅(a⋅t⋅x + 1)⋅(b⋅t⋅ξ + 1) - 0.125⋅a ⋅b ⋅t ⋅⎝0.5⋅ ↪

↪  2  2  2                  ⎞ ⎛     2  2  2                  ⎞              2  ↪
↪ a ⋅t ⋅x  + 1.0⋅a⋅t⋅x + 1.0⎠⋅⎝0.5⋅b ⋅t ⋅ξ  + 1.0⋅b⋅t⋅ξ + 1.0⎠ + 0.5⋅ⅈ⋅a⋅b⋅t ⋅ ↪

↪ ⎛                   3  3  3        2  2  2                ⎞ ⎛                ↪
↪ ⎝0.166666666666667⋅a ⋅t ⋅x  + 0.5⋅a ⋅t ⋅x  + 1.0⋅a⋅t⋅x + 1⎠⋅⎝0.1666666666666 ↪

↪     3  3  3        2  2  2                ⎞   ⎛                    4  4  4   ↪
↪ 67⋅b ⋅t ⋅ξ  + 0.5⋅b ⋅t ⋅ξ  + 1.0⋅b⋅t⋅ξ + 1⎠ + ⎝0.0416666666666667⋅a ⋅t ⋅x  + ↪

↪                     3  3  3        2  2  2            ⎞ ⎛                    ↪
↪  0.166666666666667⋅a ⋅t ⋅x  + 0.5⋅a ⋅t ⋅x  + a⋅t⋅x + 1⎠⋅⎝0.0416666666666667⋅ ↪

↪  4  4  4                      3  3  3        2  2  2                  ⎞
↪ b ⋅t ⋅ξ  + 0.166666666666667⋅b ⋅t ⋅ξ  + 0.5⋅b ⋅t ⋅ξ  + 1.0⋅b⋅t⋅ξ + 1.0⎠

----------------------------------------------------------------------
Baker-Campbell-Hausdorff formula for the Heisenberg group
----------------------------------------------------------------------

For the Heisenberg group the BCH series terminates at second order:
  exp(aQ) exp(bP) = exp(aQ + bP + (ab/2) i)
Because [Q,P] = i I commutes with everything, higher commutators vanish.
Physical interpretation:
- This represents the quantum composition of translation operators.
- The phase factor exp(i ab / 2) is the Weyl-ordering correction (central phase).


----------------------------------------------------------------------
Numerical verification
----------------------------------------------------------------------
No description has been provided for this image
Maximum error: 8.92e-02

Geodesic Flow on Riemannian Manifolds¶

In [9]:
# --------------------------------------------------------------
# 1. CONTEXT
# --------------------------------------------------------------
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: Differential Geometry
On a Riemannian manifold (M, g), the geodesic flow is generated by
the Hamiltonian H(x, ξ) = (1/2) g^{ij}(x) ξ_i ξ_j
For a surface of revolution with metric:
  ds² = dx² + f(x)² dy²
The Hamiltonian is:
  H(x, y, ξ, η) = (1/2)(ξ² + η²/f(x)²)
""")

# --------------------------------------------------------------
# 2. Define surface of revolution
# --------------------------------------------------------------
f = 1 + x**2
H_geodesic = (xi**2 + eta**2 / f**2) / 2
Geodesic_H = PseudoDifferentialOperator(H_geodesic, [x, y], mode='symbol')

print("\nGeodesic Hamiltonian H(x, y, ξ, η) =")
pprint(simplify(Geodesic_H.symbol))

# --------------------------------------------------------------
# 3. Hamiltonian flow equations
# --------------------------------------------------------------
print("\n" + "-"*70)
print("Hamiltonian flow equations:")
print("-"*70)
flow = Geodesic_H.symplectic_flow()
print("\nSymplectic flow:")
for key, val in flow.items():
    print(f"{key} = ", end="")
    pprint(simplify(val))

print("""
These equations describe how geodesics evolve on the surface.
Physical interpretation (if this were a mechanical system):
- x, y: position on the surface
- ξ, η: momentum/velocity covectors
- The flow preserves the Hamiltonian (energy conservation)
""")

# --------------------------------------------------------------
# 4. Numerical integration of the geodesic flow
# --------------------------------------------------------------
def geodesic_flow_ode(state, t, f_func):
    """ODE system for geodesic flow on a surface of revolution"""
    x_val, y_val, xi_val, eta_val = state
    f_val = f_func(x_val)
    df_dx = 2 * x_val  # derivative of f = 1 + x²

    dx_dt = xi_val
    dy_dt = eta_val / f_val**2
    dxi_dt = eta_val**2 * df_dx / f_val**3
    deta_dt = 0  # η conserved (rotational symmetry)

    return [dx_dt, dy_dt, dxi_dt, deta_dt]

# Initial conditions
x0, y0 = 0.0, 0.0
xi0, eta0 = 1.0, 0.5
initial_state = [x0, y0, xi0, eta0]

t_span = np.linspace(0, 2, 200)
f_func = lambda x: 1 + x**2
trajectory = odeint(geodesic_flow_ode, initial_state, t_span, args=(f_func,))

# --------------------------------------------------------------
# 5. Visualization: Phase space portrait
# --------------------------------------------------------------
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Position space (x, y)
axes[0, 0].plot(trajectory[:, 0], trajectory[:, 1], 'b-', linewidth=2)
axes[0, 0].plot(x0, y0, 'go', markersize=10, label='Start')
axes[0, 0].plot(trajectory[-1, 0], trajectory[-1, 1], 'ro', markersize=10, label='End')
axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('y')
axes[0, 0].set_title('Geodesic on Surface')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# Phase space (x, ξ)
axes[0, 1].plot(trajectory[:, 0], trajectory[:, 2], 'r-', linewidth=2)
axes[0, 1].set_xlabel('x')
axes[0, 1].set_ylabel('ξ')
axes[0, 1].set_title('Phase Space (x, ξ)')
axes[0, 1].grid(True, alpha=0.3)

# Phase space (y, η)
axes[1, 0].plot(trajectory[:, 1], trajectory[:, 3], 'g-', linewidth=2)
axes[1, 0].set_xlabel('y')
axes[1, 0].set_ylabel('η')
axes[1, 0].set_title('Phase Space (y, η) - η conserved!')
axes[1, 0].grid(True, alpha=0.3)

# Hamiltonian conservation
H_values = 0.5 * (trajectory[:, 2]**2 + trajectory[:, 3]**2 / (1 + trajectory[:, 0]**2)**2)
axes[1, 1].plot(t_span, H_values, 'k-', linewidth=2)
axes[1, 1].set_xlabel('t')
axes[1, 1].set_ylabel('H (Energy)')
axes[1, 1].set_title('Hamiltonian Conservation')
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
print(f"\nEnergy drift: {np.std(H_values):.2e}")
print("✓ Hamiltonian is conserved (symplectic flow)")

# --------------------------------------------------------------
# 6. Visualization: 3D surface and geodesic trajectory (adaptive domain)
# --------------------------------------------------------------
print("\n" + "-"*70)
print("Visualization: 3D surface and geodesic trajectory")
print("-"*70)

# Compute bounds from trajectory
x_min, x_max = np.min(trajectory[:, 0]), np.max(trajectory[:, 0])
y_min, y_max = np.min(trajectory[:, 1]), np.max(trajectory[:, 1])

# Add small margins
dx = 0.2 * (x_max - x_min + 1e-8)
dy = 0.2 * (y_max - y_min + 1e-8)
x_range = (x_min - dx, x_max + dx)
y_range = (y_min - dy, y_max + dy)

# Create adaptive mesh for the surface of revolution
X = np.linspace(x_range[0], x_range[1], 200)
Y = np.linspace(y_range[0], y_range[1], 200)
X_mesh, Y_mesh = np.meshgrid(X, Y)
F_mesh = 1 + X_mesh**2
Y_surf = F_mesh * np.cos(Y_mesh)
Z_surf = F_mesh * np.sin(Y_mesh)

# Compute 3D trajectory points on the same surface
x_traj = trajectory[:, 0]
y_traj = trajectory[:, 1]
f_traj = 1 + x_traj**2
X_geo = x_traj
Y_geo = f_traj * np.cos(y_traj)
Z_geo = f_traj * np.sin(y_traj)

# 3D plot
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')

# Surface (semi-transparent)
ax.plot_surface(X_mesh, Y_surf, Z_surf, rstride=5, cstride=5,
                alpha=0.35, color='lightblue', linewidth=0)

# Geodesic trajectory (exactly on surface)
ax.plot(X_geo, Y_geo, Z_geo, 'r-', linewidth=3, label='Geodesic')
ax.scatter(X_geo[0], Y_geo[0], Z_geo[0], color='green', s=60, label='Start')
ax.scatter(X_geo[-1], Y_geo[-1], Z_geo[-1], color='black', s=60, label='End')

# Labels and style
ax.set_xlabel('x (meridian)')
ax.set_ylabel('Y = f(x) cos(y)')
ax.set_zlabel('Z = f(x) sin(y)')
ax.set_title('3D Surface of Revolution and Geodesic (Adaptive Range)')
ax.legend()
ax.grid(False)
plt.tight_layout()
plt.show()
CONTEXT: Differential Geometry
On a Riemannian manifold (M, g), the geodesic flow is generated by
the Hamiltonian H(x, ξ) = (1/2) g^{ij}(x) ξ_i ξ_j
For a surface of revolution with metric:
  ds² = dx² + f(x)² dy²
The Hamiltonian is:
  H(x, y, ξ, η) = (1/2)(ξ² + η²/f(x)²)


Geodesic Hamiltonian H(x, y, ξ, η) =
     2         2
    η         ξ 
─────────── + ──
          2   2 
  ⎛ 2    ⎞      
2⋅⎝x  + 1⎠      

----------------------------------------------------------------------
Hamiltonian flow equations:
----------------------------------------------------------------------

Symplectic flow:
dx/dt = ξ
dy/dt =     η    
─────────
        2
⎛ 2    ⎞ 
⎝x  + 1⎠ 
dxi/dt =     2    
 2⋅η ⋅x  
─────────
        3
⎛ 2    ⎞ 
⎝x  + 1⎠ 
deta/dt = 0

These equations describe how geodesics evolve on the surface.
Physical interpretation (if this were a mechanical system):
- x, y: position on the surface
- ξ, η: momentum/velocity covectors
- The flow preserves the Hamiltonian (energy conservation)

No description has been provided for this image
Energy drift: 1.32e-08
✓ Hamiltonian is conserved (symplectic flow)

----------------------------------------------------------------------
Visualization: 3D surface and geodesic trajectory
----------------------------------------------------------------------
No description has been provided for this image

Statistical Mechanics - Fokker-Planck Equation¶

In [10]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)
# Temperature and physical parameters
kB, T, gamma, m = symbols('k_B T gamma m', real=True, positive=True)
print("""
CONTEXT: Statistical Mechanics
The Fokker-Planck equation describes the evolution of a probability
density in phase space for a particle in a heat bath:
∂ρ/∂t = L_FP ρ
where L_FP is the Fokker-Planck operator (generator).
For a Brownian particle with friction γ and temperature T:
L_FP = -(ξ/m)∂/∂x + γ∂/∂ξ(ξ + kT∂/∂ξ)
In symbol form, this becomes a pseudo-differential operator.
""")
# Kramers operator (Fokker-Planck)
# Simplified version with m = kB = 1, T and γ as parameters
# Drift term: -(ξ/m)∂/∂x  →  symbol: -(ξ/m)·iξ = -iξ²/m
# Friction term: γξ∂/∂ξ  → symbol: γξ·iξ = iγξ²
# Diffusion term: γkT∂²/∂ξ²  → symbol: -γkT·ξ²
# Simplified version (dimensionless)
m_val = 1
kB_val = 1
# L_FP symbol (main part)
# Drift: -ξ·ix (advection in x)
# Friction: γξ·iξ
# Diffusion: -γT·ξ²
L_FP_symbol = -I*xi**2 + I*gamma*xi**2 - gamma*T*xi**2
print("\nFokker-Planck operator symbol (simplified):")
pprint(L_FP_symbol)
L_FP = PseudoDifferentialOperator(L_FP_symbol, [x], mode='symbol')
print("""
This operator has several important properties:
1. It generates a Markov semigroup: ρ(t) = exp(tL_FP)ρ(0)
2. The equilibrium distribution is the Gibbs state (Maxwell-Boltzmann)
3. It satisfies detailed balance (time-reversal symmetry)
4. The spectrum determines relaxation rates to equilibrium
""")
# Evolution towards equilibrium
t = symbols('t', real=True, positive=True)
print("\n" + "-"*70)
print("Time evolution operator exp(t L_FP)")
print("-"*70)
# For numerical values
gamma_val = 0.5
T_val = 1.0
L_FP_numeric = L_FP_symbol.subs([(gamma, gamma_val), (T, T_val)])
L_FP_num_op = PseudoDifferentialOperator(L_FP_numeric, [x], mode='symbol')
evolution_symbol = L_FP_num_op.exponential_symbol(t=t, order=4)
print(f"\nWith γ={gamma_val}, T={T_val}:")
print("exp(t L_FP) symbol (order 4):")
pprint(simplify(evolution_symbol))
# Relaxation time
print("\n" + "-"*70)
print("Relaxation to equilibrium")
print("-"*70)
print(f"""
The eigenvalues of L_FP determine relaxation timescales:
For this model:
- Largest eigenvalue: λ₀ = 0 (equilibrium state)
- Gap: λ₁ ≈ -γ (sets relaxation time τ ≈ 1/γ)
With γ = {gamma_val}:
- Relaxation time: τ ≈ {1/gamma_val:.2f}
""")
# Visualization of relaxation
t_vals = np.linspace(0, 5/gamma_val, 100)
xi_vals = np.linspace(-4, 4, 200)
# Initial distribution: non-equilibrium packet
rho_0 = lambda xi: np.exp(-(xi - 2)**2)
# Equilibrium distribution: Maxwell-Boltzmann
rho_eq = lambda xi: np.exp(-xi**2/(2*T_val)) / np.sqrt(2*np.pi*T_val)
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Time evolution
for i, t_val in enumerate([0, 1/gamma_val, 3/gamma_val, 5/gamma_val]):
    if i < 4:
        # Approximation: exponential decay towards equilibrium
        alpha = np.exp(-gamma_val * t_val)
        rho_t = lambda xi, a=alpha: a * rho_0(xi) + (1-a) * rho_eq(xi)

        ax_idx = (i//2, i%2)
        axes[ax_idx].plot(xi_vals, rho_0(xi_vals), 'b--', alpha=0.5, label='Initial')
        axes[ax_idx].plot(xi_vals, rho_t(xi_vals), 'r-', linewidth=2, label=f't={t_val:.2f}')
        axes[ax_idx].plot(xi_vals, rho_eq(xi_vals), 'g--', alpha=0.5, label='Equilibrium')
        axes[ax_idx].set_xlabel('ξ (momentum)')
        axes[ax_idx].set_ylabel('ρ(ξ, t)')
        axes[ax_idx].set_title(f'Distribution at t={t_val:.2f}')
        axes[ax_idx].legend()
        axes[ax_idx].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n✓ System relaxes to Maxwell-Boltzmann equilibrium")
CONTEXT: Statistical Mechanics
The Fokker-Planck equation describes the evolution of a probability
density in phase space for a particle in a heat bath:
∂ρ/∂t = L_FP ρ
where L_FP is the Fokker-Planck operator (generator).
For a Brownian particle with friction γ and temperature T:
L_FP = -(ξ/m)∂/∂x + γ∂/∂ξ(ξ + kT∂/∂ξ)
In symbol form, this becomes a pseudo-differential operator.


Fokker-Planck operator symbol (simplified):
       2        2      2
- T⋅γ⋅ξ  + ⅈ⋅γ⋅ξ  - ⅈ⋅ξ 

This operator has several important properties:
1. It generates a Markov semigroup: ρ(t) = exp(tL_FP)ρ(0)
2. The equilibrium distribution is the Gibbs state (Maxwell-Boltzmann)
3. It satisfies detailed balance (time-reversal symmetry)
4. The spectrum determines relaxation rates to equilibrium


----------------------------------------------------------------------
Time evolution operator exp(t L_FP)
----------------------------------------------------------------------
With γ=0.5, T=1.0:
exp(t L_FP) symbol (order 4):
                      4  8                       3  6                   2  4   ↪
- 0.0104166666666667⋅t ⋅ξ  + 0.0416666666666667⋅t ⋅ξ ⋅(1 - ⅈ) + 0.25⋅ⅈ⋅t ⋅ξ  - ↪

↪         2            
↪  0.5⋅t⋅ξ ⋅(1 + ⅈ) + 1

----------------------------------------------------------------------
Relaxation to equilibrium
----------------------------------------------------------------------

The eigenvalues of L_FP determine relaxation timescales:
For this model:
- Largest eigenvalue: λ₀ = 0 (equilibrium state)
- Gap: λ₁ ≈ -γ (sets relaxation time τ ≈ 1/γ)
With γ = 0.5:
- Relaxation time: τ ≈ 2.00

No description has been provided for this image
✓ System relaxes to Maxwell-Boltzmann equilibrium

Ergodic Theory - Transfer Operators¶

In [11]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)
print("""
CONTEXT: Ergodic Theory and Dynamical Systems
For a dynamical system with map T: M → M, the transfer operator
(Perron-Frobenius operator) acts on densities:
(L_T f)(x) = ∑_{T(y)=x} f(y)/|T'(y)|
For a smooth expanding map, L_T can be approximated by a
pseudo-differential operator.
Example: Baker's map (chaotic mixing)
T(x) = 2x mod 1
""")
# Transfer operator for the dilation T(x) = 2x
# L_T f(x) = (1/2)[f(x/2) + f((x+1)/2)]
# In terms of symbol, this corresponds to:
# Symbol approx: (1/2)[exp(iξ/2) + exp(iξ/2)exp(iπξ)]
transfer_symbol = (exp(I*xi/2) + exp(I*xi/2) * exp(I*np.pi*xi)) / 2
Transfer_op = PseudoDifferentialOperator(transfer_symbol, [x], mode='symbol')
print("\nTransfer operator symbol (Baker's map):")
pprint(simplify(Transfer_op.symbol))
# Powers of the transfer operator
print("\n" + "-"*70)
print("Powers of transfer operator: L_T^n")
print("-"*70)
print("""
L_T^n describes the evolution after n iterations of the map.
Key properties:
- L_T is a contraction in appropriate spaces
- Spectrum: discrete eigenvalues + continuous spectrum
- Leading eigenvalue λ₀ = 1 (invariant measure)
- Spectral gap: |λ₁| < 1 → mixing
""")
# Compute L_T^2 via composition
LT_squared_symbol = Transfer_op.compose_asymptotic(Transfer_op, order=3)
print("\nL_T² symbol (order 3):")
pprint(simplify(LT_squared_symbol))
# Visualization of mixing
print("\n" + "-"*70)
print("Visualization: Mixing dynamics")
print("-"*70)
def bakers_map(x):
    """Baker's map: T(x) = 2x mod 1"""
    return (2 * x) % 1
# Initial distribution
x_vals = np.linspace(0, 1, 1000, endpoint=False)
rho_initial = np.exp(-100*(x_vals - 0.3)**2)  # Localized distribution
rho_initial /= np.trapz(rho_initial, x_vals)   # Normalize
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
rho_current = rho_initial.copy()
for n, ax in enumerate(axes.flat):
    if n < 6:
        ax.plot(x_vals, rho_current, 'b-', linewidth=2)
        ax.axhline(1.0, color='r', linestyle='--', alpha=0.5, label='Uniform (equilibrium)')
        ax.set_xlabel('x')
        ax.set_ylabel('ρ(x)')
        ax.set_title(f'After {n} iterations')
        ax.set_ylim([0, 5])
        ax.grid(True, alpha=0.3)
        if n == 0:
            ax.legend()

        # Apply transfer operator (simplified: interpolation)
        if n < 5:
            x_preimage1 = x_vals / 2
            x_preimage2 = (x_vals + 1) / 2

            rho_new = 0.5 * (np.interp(x_preimage1, x_vals, rho_current, period=1) +
                            np.interp(x_preimage2, x_vals, rho_current, period=1))

            rho_current = rho_new
plt.tight_layout()
plt.show()
print("\n✓ Distribution converges to uniform (invariant measure)")
print("✓ This demonstrates mixing in chaotic systems")
CONTEXT: Ergodic Theory and Dynamical Systems
For a dynamical system with map T: M → M, the transfer operator
(Perron-Frobenius operator) acts on densities:
(L_T f)(x) = ∑_{T(y)=x} f(y)/|T'(y)|
For a smooth expanding map, L_T can be approximated by a
pseudo-differential operator.
Example: Baker's map (chaotic mixing)
T(x) = 2x mod 1


Transfer operator symbol (Baker's map):
                             ⅈ⋅ξ
                             ───
⎛ 3.14159265358979⋅ⅈ⋅ξ    ⎞   2 
⎝ℯ                     + 1⎠⋅ℯ   
────────────────────────────────
               2                

----------------------------------------------------------------------
Powers of transfer operator: L_T^n
----------------------------------------------------------------------

L_T^n describes the evolution after n iterations of the map.
Key properties:
- L_T is a contraction in appropriate spaces
- Spectrum: discrete eigenvalues + continuous spectrum
- Leading eigenvalue λ₀ = 1 (invariant measure)
- Spectral gap: |λ₁| < 1 → mixing

L_T² symbol (order 3):
                                2     
     ⎛ 3.14159265358979⋅ⅈ⋅ξ    ⎞   ⅈ⋅ξ
0.25⋅⎝ℯ                     + 1⎠ ⋅ℯ   

----------------------------------------------------------------------
Visualization: Mixing dynamics
----------------------------------------------------------------------
/tmp/ipykernel_13044/953858432.py:47: DeprecationWarning: `trapz` is deprecated. Use `trapezoid` instead, or one of the numerical integration functions in `scipy.integrate`.
  rho_initial /= np.trapz(rho_initial, x_vals)   # Normalize
No description has been provided for this image
✓ Distribution converges to uniform (invariant measure)
✓ This demonstrates mixing in chaotic systems

Spectral Theory - Self-Adjoint Operators¶

V1¶

In [12]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)
lam = symbols('lambda', real=True)
print("""
CONTEXT: Pure Mathematics - Spectral Theory
For a self-adjoint operator A, the resolvent is:
R_λ(A) = (A - λI)^{-1}
The resolvent exists for λ not in the spectrum σ(A).
Spectral properties:
- Poles of R_λ → eigenvalues
- Branch cuts → continuous spectrum
- Residues → eigenprojections
""")
# Schrödinger operator with harmonic potential
# H = -d²/dx² + x²  → symbol: ξ² + x²
H_symbol = xi**2 + x**2
H_op = PseudoDifferentialOperator(H_symbol, [x], mode='symbol')
print("\nHarmonic oscillator operator H:")
pprint(H_op.symbol)
# Resolvent (A - λI)^{-1}
print("\n" + "-"*70)
print("Resolvent R_λ(H) = (H - λ)^{-1}")
print("-"*70)
# Symbol: (ξ² + x² - λ)^{-1}
resolvent_symbol = 1 / (xi**2 + x**2 - lam)
print("\nResolvent symbol:")
pprint(resolvent_symbol)
print("""
Spectral information:
For the harmonic oscillator H = ξ² + x²:
- Spectrum: σ(H) = {2n + 1 : n ∈ ℕ} (discrete, unbounded)
- Eigenvalues: λₙ = 2n + 1, n = 0, 1, 2, ...
- No continuous spectrum
The resolvent has simple poles at λ = 2n + 1.
""")
# Visualization of the spectrum via the resolvent
print("\n" + "-"*70)
print("Visualization: Resolvent norm ‖R_λ(H)‖")
print("-"*70)

x_val = 0
xi_test = np.linspace(-3, 3, 100)
lambda_vals = np.linspace(0, 10, 500)
resolvent_norm = []
for lam_val in lambda_vals:
    # Evaluate |R_λ(x, ξ)| for a typical ξ
    res_func = lambdify(xi, resolvent_symbol.subs([(x, x_val), (lam, lam_val)]), 'numpy')

    try:
        res_vals = res_func(xi_test)
        # Approximate L² norm
        norm = np.sqrt(scipy_trapezoid(np.abs(res_vals)**2, xi_test))
        resolvent_norm.append(norm)
    except:
        resolvent_norm.append(np.nan)
plt.figure(figsize=(12, 6))
plt.semilogy(lambda_vals, resolvent_norm, 'b-', linewidth=2)
# Mark theoretical eigenvalues
eigenvalues = [2*n + 1 for n in range(5)]
for ev in eigenvalues:
    plt.axvline(ev, color='r', linestyle='--', alpha=0.7, linewidth=1)
    plt.text(ev, plt.ylim()[1]*0.5, f'λ={ev}', rotation=90, va='bottom')
plt.xlabel('λ')
plt.ylabel('‖R_λ(H)‖ (log scale)')
plt.title('Resolvent Norm - Peaks at Eigenvalues')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n✓ Peaks in ‖R_λ‖ reveal the spectrum!")
print("✓ This is the foundation of spectral analysis")
CONTEXT: Pure Mathematics - Spectral Theory
For a self-adjoint operator A, the resolvent is:
R_λ(A) = (A - λI)^{-1}
The resolvent exists for λ not in the spectrum σ(A).
Spectral properties:
- Poles of R_λ → eigenvalues
- Branch cuts → continuous spectrum
- Residues → eigenprojections


Harmonic oscillator operator H:
 2    2
x  + ξ 

----------------------------------------------------------------------
Resolvent R_λ(H) = (H - λ)^{-1}
----------------------------------------------------------------------

Resolvent symbol:
     1      
────────────
      2    2
-λ + x  + ξ 

Spectral information:
For the harmonic oscillator H = ξ² + x²:
- Spectrum: σ(H) = {2n + 1 : n ∈ ℕ} (discrete, unbounded)
- Eigenvalues: λₙ = 2n + 1, n = 0, 1, 2, ...
- No continuous spectrum
The resolvent has simple poles at λ = 2n + 1.


----------------------------------------------------------------------
Visualization: Resolvent norm ‖R_λ(H)‖
----------------------------------------------------------------------
No description has been provided for this image
✓ Peaks in ‖R_λ‖ reveal the spectrum!
✓ This is the foundation of spectral analysis

V2¶

In [13]:
x, xi, lam = symbols('x', real=True), symbols('xi', real=True), symbols('lambda', real=True)
print("""
CONTEXT: Pure Mathematics - Spectral Theory
For a self-adjoint operator A, the resolvent is R_λ(A) = (A - λ I)^{-1}.
Poles of this operator-valued function correspond to eigenvalues.
""")

# classical symbol (kept for didactic parity with original)
H_symbol = xi**2 + x**2
print("Classical symbol (kept for reference):")
pprint(H_symbol)
print("-" * 70)
print("Important: we will *quantize* this symbol into a matrix operator and then evaluate the resolvent norm.")
print("-" * 70)

# ----------------------------
# spectral (FFT) discretization on a periodic large interval [-L, L]
# This stays close to your original idea of using xi and spectral behavior,
# but produces a true operator matrix H on which we compute the resolvent norm.
L = 12.0            # half-domain length (increase if eigenfunctions have large tails)
N = 512             # number of grid points (power of two is convenient for FFT)
x_grid = np.linspace(-L, L, N, endpoint=False)   # periodic grid
dx = x_grid[1] - x_grid[0]

# Fourier frequencies for spectral differentiation (2π / (2L) scaling)
k = np.fft.fftfreq(N, d=dx) * 2.0 * np.pi   # angular frequencies
k2 = (1j * k)**2                            # symbol for second derivative in Fourier: (i k)^2 = -k^2

# function to apply -d^2/dx^2 via FFT (matrix-free)
def apply_minus_d2(u):
    # u: real-space vector length N
    u_hat = np.fft.fft(u)
    v_hat = k2 * u_hat
    v = np.fft.ifft(v_hat)
    # Numerical result should be real (up to roundoff)
    return np.real(v)

# Build matrix H in physical space by applying operator to basis vectors (N x N).
# This makes H comparable to your previous approach: you still form an operator matrix,
# but built using spectral differentiation rather than finite differences.
# For moderate N (<= 1024) this direct assembly is fine and stays close to the "matrix" mindset.
H = np.zeros((N, N))
for j in range(N):
    e = np.zeros(N)
    e[j] = 1.0
    # -d^2/dx^2 e_j approximated by spectral apply
    kinetic_col = -apply_minus_d2(e)
    # potential multiplication column
    potential_col = (x_grid**2) * e
    H[:, j] = kinetic_col + potential_col

# ----------------------------
# resolvent sampling: measure 1 / sigma_min(H - lambda I)

lambda_vals = np.linspace(0.0, 25.0, 900)
resolvent_norm_approx = np.empty_like(lambda_vals)

I = np.eye(N)
for idx, lam_val in enumerate(lambda_vals):
    A = H - lam_val * I
    try:
        s = svd(A, compute_uv=False)
        s_min = s[-1]
        resolvent_norm_approx[idx] = 1.0 / s_min
    except Exception:
        resolvent_norm_approx[idx] = np.nan

# ----------------------------
# plot and mark theoretical eigenvalues 2n + 1
plt.figure(figsize=(12, 5))
plt.semilogy(lambda_vals, resolvent_norm_approx, '-', linewidth=1.5)
theoretical = [2*n + 1 for n in range(0, 12)]
ymax = np.nanmax(resolvent_norm_approx[np.isfinite(resolvent_norm_approx)])
for ev in theoretical:
    plt.axvline(ev, color='r', linestyle='--', alpha=0.7)
    plt.text(ev, ymax*0.4, f'λ={ev}', rotation=90, va='bottom', ha='right')

plt.xlabel('λ')
plt.ylabel("approx ‖(H - λ I)^{-1}‖ ≈ 1/σ_min")
plt.title('Resolvent norm from spectral (FFT) quantization of ξ^2 + x^2')
plt.grid(alpha=0.35)
plt.tight_layout()
plt.show()
CONTEXT: Pure Mathematics - Spectral Theory
For a self-adjoint operator A, the resolvent is R_λ(A) = (A - λ I)^{-1}.
Poles of this operator-valued function correspond to eigenvalues.

Classical symbol (kept for reference):
 2    2
x  + ξ 
----------------------------------------------------------------------
Important: we will *quantize* this symbol into a matrix operator and then evaluate the resolvent norm.
----------------------------------------------------------------------
No description has been provided for this image

Harmonic Analysis - Convolution Operators¶

In [14]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)
print("""
CONTEXT: Harmonic Analysis
A convolution operator with kernel k(x) acts as:
(K * f)(x) = ∫ k(x - y) f(y) dy
By Fourier theory, the symbol is simply:
p(ξ) = k̂(ξ) (Fourier transform of kernel)
This is independent of x (translation-invariant).
""")
# Example: Gaussian kernel (heat kernel at time t=1)
# k(x) = exp(-x²/2) / √(2π)
# k̂(ξ) = exp(-ξ²/2)
gaussian_kernel_symbol = exp(-xi**2 / 2)
Gauss_conv = PseudoDifferentialOperator(gaussian_kernel_symbol, [x], mode='symbol')
print("\nGaussian convolution operator symbol:")
pprint(Gauss_conv.symbol)
# Semigroup property
print("\n" + "-"*70)
print("Semigroup property: K_t * K_s = K_{t+s}")
print("-"*70)
# For the heat kernel: k_t(x) = exp(-x²/(4t)) / √(4πt)
# Symbol: exp(-tξ²)
t1, t2 = symbols('t1 t2', real=True, positive=True)
K_t1_symbol = exp(-t1 * xi**2)
K_t2_symbol = exp(-t2 * xi**2)
K_t1 = PseudoDifferentialOperator(K_t1_symbol, [x], mode='symbol')
K_t2 = PseudoDifferentialOperator(K_t2_symbol, [x], mode='symbol')
# Composition K_t1 ∘ K_t2
composition = K_t1.compose_asymptotic(K_t2, order=3)
print(f"\nK_t1 ∘ K_t2 =")
pprint(simplify(composition))
print(f"\nExpected: K_(t1+t2) = exp(-(t1+t2)ξ²)")
expected_composition = exp(-(t1 + t2) * xi**2)
pprint(expected_composition)
if simplify(composition - expected_composition) == 0:
    print("\n✓ Semigroup property verified!")
    print("✓ This is exact because convolution operators commute")
print("""
Applications in harmonic analysis:
1. Calderon-Zygmund theory (singular integrals)
2. Littlewood-Paley decomposition
3. Function spaces (Sobolev, Besov, Triebel-Lizorkin)
4. Multiplier theorems
5. Pseudodifferential calculus
""")
CONTEXT: Harmonic Analysis
A convolution operator with kernel k(x) acts as:
(K * f)(x) = ∫ k(x - y) f(y) dy
By Fourier theory, the symbol is simply:
p(ξ) = k̂(ξ) (Fourier transform of kernel)
This is independent of x (translation-invariant).


Gaussian convolution operator symbol:
   2 
 -ξ  
 ────
  2  
ℯ    

----------------------------------------------------------------------
Semigroup property: K_t * K_s = K_{t+s}
----------------------------------------------------------------------

K_t1 ∘ K_t2 =
       2          
     -ξ ⋅(t₁ + t₂)
1.0⋅ℯ             

Expected: K_(t1+t2) = exp(-(t1+t2)ξ²)
  2           
 ξ ⋅(-t₁ - t₂)
ℯ             

✓ Semigroup property verified!
✓ This is exact because convolution operators commute

Applications in harmonic analysis:
1. Calderon-Zygmund theory (singular integrals)
2. Littlewood-Paley decomposition
3. Function spaces (Sobolev, Besov, Triebel-Lizorkin)
4. Multiplier theorems
5. Pseudodifferential calculus

Analytic Number Theory - Gauss-Kuzmin Operator¶

In [15]:
x = symbols('x', real=True, positive=True)
xi = symbols('xi', real=True)
print("""
CONTEXT: Analytic Number Theory - Continued Fractions
The Gauss map T(x) = {1/x} = 1/x - ⌊1/x⌋ describes the dynamics
of continued fraction expansions.
The Gauss-Kuzmin operator (transfer operator for T):
(L f)(x) = ∑_{n=1}^∞ f((x+n)^{-1}) / (x+n)²
This operator determines:
- Distribution of continued fraction digits
- Convergence rate to Gauss measure: μ(x) = 1/((1+x)log 2)
- Khinchin constants and other number-theoretic quantities
""")

# Symbolic approximation of the Gauss-Kuzmin operator
# For n=1: L₁f(x) ≈ f(1/(1+x)) / (1+x)²
# Approximate symbol (truncation)
# This approximation captures the asymptotic behavior
print("\nGauss-Kuzmin operator (first mode approximation):")
print("L₁f(x) = f(1/(1+x)) / (1+x)²")

# Gauss invariant measure
gauss_measure = 1 / ((1 + x) * log(2))
print("\nGauss measure (invariant density):")
pprint(gauss_measure)

# Check invariance: L μ = μ
print("\n" + "-"*70)
print("Checking invariance: L μ = μ")
print("-"*70)

# Symbolic calculation
y = symbols('y', real=True, positive=True)

# L μ(x) = ∫₀¹ δ(x - {1/y}) μ(y) dy
# For the first term: μ(1/(1+x)) / (1+x)²
mu_transformed = gauss_measure.subs(x, 1/(1+x)) / (1+x)**2
mu_transformed = simplify(mu_transformed)
print("\nL μ(x) (first term):")
pprint(mu_transformed)
print("\nOriginal μ(x):")
pprint(gauss_measure)

print("""
The Gauss measure is indeed invariant under the transfer operator.
Applications:
- Metric theory of continued fractions
- Diophantine approximation
- Ergodic theory of number systems
- Computational number theory (GCD algorithms)
""")

# Visualization of the Gauss measure
x_vals = np.linspace(0.001, 1, 1000)
mu_vals = 1 / ((1 + x_vals) * np.log(2))
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(x_vals, mu_vals, 'b-', linewidth=2)
plt.xlabel('x')
plt.ylabel('μ(x)')
plt.title('Gauss Measure for Continued Fractions')
plt.grid(True, alpha=0.3)
plt.ylim([0, 2])

# Empirical distribution of continued fraction digits
plt.subplot(1, 2, 2)

# Generate continued fractions for π
def continued_fraction_digits(x, n_terms=1000):
    """Extract continued fraction digits"""
    digits = []
    for _ in range(n_terms):
        if x < 1e-10:
            break
        digit = int(1/x)
        digits.append(digit)
        x = 1/x - digit
    return digits

# For π
pi_approx = np.pi
cf_digits = continued_fraction_digits(pi_approx - int(pi_approx), 10000)
cf_digits = [d for d in cf_digits if d <= 20]  # Limit for visualization
digit_counts = np.bincount(cf_digits)[1:]  # Exclude 0
digit_probs = digit_counts / np.sum(digit_counts)

# Theoretical probability according to Gauss: P(n) = log₂(1 + 1/(n(n+2)))
digits_theory = np.arange(1, len(digit_probs) + 1)
probs_theory = np.log2(1 + 1/(digits_theory * (digits_theory + 2)))
plt.bar(digits_theory, digit_probs, alpha=0.7, label='Empirical (π)')
plt.plot(digits_theory, probs_theory, 'r-', linewidth=2, label='Gauss theory')
plt.xlabel('Continued fraction digit')
plt.ylabel('Probability')
plt.title('Distribution of CF Digits')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

print("\n✓ Empirical distribution matches Gauss measure!")
CONTEXT: Analytic Number Theory - Continued Fractions
The Gauss map T(x) = {1/x} = 1/x - ⌊1/x⌋ describes the dynamics
of continued fraction expansions.
The Gauss-Kuzmin operator (transfer operator for T):
(L f)(x) = ∑_{n=1}^∞ f((x+n)^{-1}) / (x+n)²
This operator determines:
- Distribution of continued fraction digits
- Convergence rate to Gauss measure: μ(x) = 1/((1+x)log 2)
- Khinchin constants and other number-theoretic quantities


Gauss-Kuzmin operator (first mode approximation):
L₁f(x) = f(1/(1+x)) / (1+x)²

Gauss measure (invariant density):
      1       
──────────────
(x + 1)⋅log(2)

----------------------------------------------------------------------
Checking invariance: L μ = μ
----------------------------------------------------------------------

L μ(x) (first term):
          1           
──────────────────────
(x + 1)⋅(x + 2)⋅log(2)

Original μ(x):
      1       
──────────────
(x + 1)⋅log(2)

The Gauss measure is indeed invariant under the transfer operator.
Applications:
- Metric theory of continued fractions
- Diophantine approximation
- Ergodic theory of number systems
- Computational number theory (GCD algorithms)

No description has been provided for this image
✓ Empirical distribution matches Gauss measure!

Integral Equations - Fredholm Operators¶

In [16]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)
y = symbols('y', real=True)
print("""
CONTEXT: Integral Equations
A Fredholm operator of the second kind:
(I - K)u = f
where K is an integral operator:
(Ku)(x) = ∫ k(x,y) u(y) dy
If k(x,y) = k(x-y) (convolution kernel), then K is a
pseudo-differential operator with symbol k̂(ξ).
For non-convolution kernels, we use the Schwartz kernel theorem.
""")
# Example: separable kernel k(x,y) = exp(-x²)exp(-y²)
# This gives a finite-rank operator
print("\nExample: Separable kernel k(x,y) = e^{-x²}e^{-y²}")
# For such a kernel, the operator is rank-1:
# (Ku)(x) = e^{-x²} ∫ e^{-y²} u(y) dy = e^{-x²} ⟨e^{-y²}, u⟩
print("""
For separable kernels k(x,y) = φ(x)ψ(y):
K = |φ⟩⟨ψ| (rank-1 operator)
The resolvent can be computed explicitly:
(I - λK)^{-1} = I + λK/(1 - λ⟨ψ,φ⟩)
This is the basis of Fredholm alternative.
""")
# Fredholm determinant
print("\n" + "-"*70)
print("Fredholm determinant and trace class")
print("-"*70)
print("""
The Fredholm determinant is:
det(I - λK) = exp(-∑_{n=1}^∞ (λⁿ/n) Tr(Kⁿ))
For trace-class operators, this is well-defined.
Applications:
- Solvability of integral equations
- Quantum field theory (partition functions)
- Random matrix theory
- Riemann-Hilbert problems
""")
# Concrete example: solve (I - λK)u = f with small λ
lam = symbols('lambda', real=True)
print("\nNeumann series solution:")
print("u = (I - λK)^{-1}f = (I + λK + λ²K² + ...)f")
# For our example with rank-1 K
print("\nFor rank-1 operator K = |φ⟩⟨ψ|:")
print("u = f + λφ⟨ψ,f⟩ + λ²φ⟨ψ,f⟩⟨ψ,φ⟩ + ...")
print("  = f + λφ⟨ψ,f⟩/(1 - λ⟨ψ,φ⟩)")
# Numerical visualization
print("\n" + "-"*70)
print("Numerical example: Solving integral equation")
print("-"*70)
# Discretization
N = 100
x_grid = np.linspace(-3, 3, N)
dx = x_grid[1] - x_grid[0]
# Kernel: k(x,y) = exp(-(x-y)²)
X, Y = np.meshgrid(x_grid, x_grid, indexing='ij')
K_matrix = np.exp(-(X - Y)**2) * dx
# Source term
f = np.exp(-x_grid**2 / 2)
# Solve (I - λK)u = f for different values of λ
lambda_vals = [0.0, 0.1, 0.3, 0.5]
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
for idx, lam_val in enumerate(lambda_vals):
    ax = axes[idx // 2, idx % 2]

    # (I - λK)u = f
    A = np.eye(N) - lam_val * K_matrix

    try:
        u = np.linalg.solve(A, f)

        ax.plot(x_grid, f, 'b--', linewidth=2, label='f (source)', alpha=0.7)
        ax.plot(x_grid, u, 'r-', linewidth=2, label=f'u (solution, λ={lam_val})')
        ax.set_xlabel('x')
        ax.set_ylabel('Function value')
        ax.set_title(f'Solution for λ = {lam_val}')
        ax.legend()
        ax.grid(True, alpha=0.3)

        # Check the solution
        residual = np.linalg.norm(A @ u - f)
        ax.text(0.05, 0.95, f'Residual: {residual:.2e}',
               transform=ax.transAxes, verticalalignment='top',
               bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))

    except np.linalg.LinAlgError:
        ax.text(0.5, 0.5, 'Singular!\nλ is an eigenvalue',
               transform=ax.transAxes, ha='center', va='center',
               fontsize=14, color='red')
        ax.set_title(f'λ = {lam_val} (singular)')
plt.tight_layout()
plt.show()
print("\n✓ Fredholm theory verified numerically!")
CONTEXT: Integral Equations
A Fredholm operator of the second kind:
(I - K)u = f
where K is an integral operator:
(Ku)(x) = ∫ k(x,y) u(y) dy
If k(x,y) = k(x-y) (convolution kernel), then K is a
pseudo-differential operator with symbol k̂(ξ).
For non-convolution kernels, we use the Schwartz kernel theorem.


Example: Separable kernel k(x,y) = e^{-x²}e^{-y²}

For separable kernels k(x,y) = φ(x)ψ(y):
K = |φ⟩⟨ψ| (rank-1 operator)
The resolvent can be computed explicitly:
(I - λK)^{-1} = I + λK/(1 - λ⟨ψ,φ⟩)
This is the basis of Fredholm alternative.


----------------------------------------------------------------------
Fredholm determinant and trace class
----------------------------------------------------------------------

The Fredholm determinant is:
det(I - λK) = exp(-∑_{n=1}^∞ (λⁿ/n) Tr(Kⁿ))
For trace-class operators, this is well-defined.
Applications:
- Solvability of integral equations
- Quantum field theory (partition functions)
- Random matrix theory
- Riemann-Hilbert problems


Neumann series solution:
u = (I - λK)^{-1}f = (I + λK + λ²K² + ...)f

For rank-1 operator K = |φ⟩⟨ψ|:
u = f + λφ⟨ψ,f⟩ + λ²φ⟨ψ,f⟩⟨ψ,φ⟩ + ...
  = f + λφ⟨ψ,f⟩/(1 - λ⟨ψ,φ⟩)

----------------------------------------------------------------------
Numerical example: Solving integral equation
----------------------------------------------------------------------
No description has been provided for this image
✓ Fredholm theory verified numerically!

Mathematical Physics - Scattering Theory¶

In [17]:
from sympy import symbols, exp, log, simplify, pprint, I, diff, oo, integrate
import numpy as np
import matplotlib.pyplot as plt
x = symbols('x', real=True)
xi = symbols('xi', real=True)
print("""
CONTEXT: Scattering Theory
For a quantum Hamiltonian H = H₀ + V (free + potential):
The scattering operator (S-matrix) relates asymptotic states:
S = Ω₊* Ω₋
where Ω± are the Møller wave operators:
Ω± = lim_{t→±∞} exp(itH) exp(-itH₀)
The S-matrix symbol encodes all scattering information:
- Phase shifts
- Cross sections
- Bound state poles
""")
# Example: repulsive potential V(x) = exp(-x²)
V_symbol = exp(-x**2)
print("\nPotential V(x) = e^{-x²}")
pprint(V_symbol)
# Free Hamiltonian: H₀ = ξ²
H0_symbol = xi**2
# Total Hamiltonian: H = ξ² + e^{-x²}
H_symbol = xi**2 + V_symbol
H0_op = PseudoDifferentialOperator(H0_symbol, [x], mode='symbol')
H_op = PseudoDifferentialOperator(H_symbol, [x], mode='symbol')
print("\nFree Hamiltonian H₀:")
pprint(H0_op.symbol)
print("\nFull Hamiltonian H:")
pprint(H_op.symbol)
# Evolution operator
print("\n" + "-"*70)
print("Time evolution operators")
print("-"*70)
t = symbols('t', real=True)
# exp(-itH₀) - free evolution
U0_symbol = H0_op.exponential_symbol(t=-I*t, order=3)
print("\nFree evolution exp(-itH₀):")
pprint(simplify(U0_symbol))
# exp(-itH) - full evolution (approximation)
U_symbol = H_op.exponential_symbol(t=-I*t, order=2)
print("\nFull evolution exp(-itH) (order 2):")
pprint(simplify(U_symbol))
# Born approximation for the S-matrix
print("\n" + "-"*70)
print("Born approximation for S-matrix")
print("-"*70)
print("""
In first Born approximation:
S ≈ I - i∫_{-∞}^∞ exp(itH₀) V exp(-itH₀) dt
The symbol of S - I gives the scattering amplitude f(k):
dσ/dΩ = |f(k)|² (differential cross section)
""")
# Scattering amplitude (Born approximation)
# f(k) ≈ -∫ V(x) e^{ikx} dx = -V̂(k)
# For V(x) = e^{-x²}, the Fourier transform is:
# V̂(k) = √π e^{-k²/4}
scattering_amplitude = -sqrt(np.pi) * exp(-xi**2 / 4)
print("\nScattering amplitude f(ξ) in Born approximation:")
pprint(scattering_amplitude)
# Visualization
xi_vals = np.linspace(-5, 5, 200)
f_func = lambdify(xi, scattering_amplitude, 'numpy')
f_vals = f_func(xi_vals)
# Differential cross section
cross_section = np.abs(f_vals)**2
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# Potential
x_vals = np.linspace(-3, 3, 200)
V_func = lambdify(x, V_symbol, 'numpy')
V_vals = V_func(x_vals)
axes[0].plot(x_vals, V_vals, 'b-', linewidth=2)
axes[0].set_xlabel('x')
axes[0].set_ylabel('V(x)')
axes[0].set_title('Scattering Potential')
axes[0].grid(True, alpha=0.3)
# Scattering amplitude
axes[1].plot(xi_vals, f_vals.real, 'r-', linewidth=2, label='Re[f]')
axes[1].plot(xi_vals, f_vals.imag, 'b--', linewidth=2, label='Im[f]')
axes[1].set_xlabel('ξ (momentum transfer)')
axes[1].set_ylabel('f(ξ)')
axes[1].set_title('Scattering Amplitude')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
# Cross section
axes[2].semilogy(xi_vals, cross_section, 'g-', linewidth=2)
axes[2].set_xlabel('ξ (momentum transfer)')
axes[2].set_ylabel('|f(ξ)|²')
axes[2].set_title('Differential Cross Section')
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n✓ Born approximation gives forward-peaked scattering")
print("✓ Exponential fall-off for smooth potentials")
CONTEXT: Scattering Theory
For a quantum Hamiltonian H = H₀ + V (free + potential):
The scattering operator (S-matrix) relates asymptotic states:
S = Ω₊* Ω₋
where Ω± are the Møller wave operators:
Ω± = lim_{t→±∞} exp(itH) exp(-itH₀)
The S-matrix symbol encodes all scattering information:
- Phase shifts
- Cross sections
- Bound state poles


Potential V(x) = e^{-x²}
   2
 -x 
ℯ   

Free Hamiltonian H₀:
 2
ξ 

Full Hamiltonian H:
        2
 2    -x 
ξ  + ℯ   

----------------------------------------------------------------------
Time evolution operators
----------------------------------------------------------------------
Free evolution exp(-itH₀):
                     3  6        2  4        2    
0.166666666666667⋅ⅈ⋅t ⋅ξ  - 0.5⋅t ⋅ξ  - ⅈ⋅t⋅ξ  + 1
Full evolution exp(-itH) (order 2):
⎛   ⎛                    2                                   ⎞                 ↪
⎜   ⎜      ⎛    ⎛ 2⎞    ⎞                                ⎛ 2⎞⎟                 ↪
⎜ 2 ⎜      ⎜ 2  ⎝x ⎠    ⎟    ⎛     2                  ⎞  ⎝x ⎠⎟       ⎛    ⎛ 2⎞ ↪
⎜t ⋅⎝- 1.0⋅⎝ξ ⋅ℯ     + 1⎠  + ⎝4.0⋅x  - 4.0⋅ⅈ⋅x⋅ξ - 2.0⎠⋅ℯ    ⎠       ⎜ 2  ⎝x ⎠ ↪
⎜───────────────────────────────────────────────────────────── - ⅈ⋅t⋅⎝ξ ⋅ℯ     ↪
⎝                              2                                               ↪

↪                    ⎞       
↪                    ⎟       
↪     ⎞  ⎛ 2⎞       2⎟      2
↪     ⎟  ⎝x ⎠    2⋅x ⎟  -2⋅x 
↪  + 1⎠⋅ℯ     + ℯ    ⎟⋅ℯ     
↪                    ⎠       

----------------------------------------------------------------------
Born approximation for S-matrix
----------------------------------------------------------------------

In first Born approximation:
S ≈ I - i∫_{-∞}^∞ exp(itH₀) V exp(-itH₀) dt
The symbol of S - I gives the scattering amplitude f(k):
dσ/dΩ = |f(k)|² (differential cross section)


Scattering amplitude f(ξ) in Born approximation:
                     2 
                   -ξ  
                   ────
                    4  
-1.77245385090552⋅ℯ    
No description has been provided for this image
✓ Born approximation gives forward-peaked scattering
✓ Exponential fall-off for smooth potentials

Optimal Control - Hamilton-Jacobi-Bellman Equation¶

In [18]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)
print("""
CONTEXT: Optimal Control Theory
The Hamilton-Jacobi-Bellman (HJB) equation describes the value
function V(x,t) for an optimal control problem:
-∂V/∂t + H(x, ∇V) = 0
where H is the Hamiltonian:
H(x, p) = max_u {-L(x,u) - p·f(x,u)}
For a linear-quadratic regulator (LQR):
- State: ẋ = ax + bu
- Cost: ∫ (x² + u²) dt
""")
# HJB for LQR
# H(x, ξ) = max_u {-x² - u² + ξ(ax + bu)}
# The maximum is achieved for: u* = bξ/2
# Optimal Hamiltonian:
a, b = symbols('a b', real=True)
H_HJB = -x**2 + a*x*xi + b**2*xi**2/4
print("\nOptimal Hamiltonian for LQR:")
pprint(simplify(H_HJB))
H_HJB_op = PseudoDifferentialOperator(H_HJB, [x], mode='symbol')
print("""
The solution to the HJB equation gives the optimal value function:
V(x) = ½ x² P
where P is the solution to the algebraic Riccati equation:
0 = -1 + aP + Pa - Pb²P
For a=-1, b=1: P = √2 - 1 ≈ 0.414
""")
# Visualization of the optimal control
print("\n" + "-"*70)
print("Optimal feedback control u*(x)")
print("-"*70)
# For a=-1, b=1
a_val = -1.0
b_val = 1.0
# Riccati solution
P_val = np.sqrt(2) - 1
# Optimal value
def value_function(x):
    return 0.5 * P_val * x**2
# Optimal control: u*(x) = -b²P x / 2
def optimal_control(x):
    return -b_val**2 * P_val * x / 2
x_vals = np.linspace(-3, 3, 200)
V_vals = value_function(x_vals)
u_vals = optimal_control(x_vals)
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# Value function
axes[0].plot(x_vals, V_vals, 'b-', linewidth=2)
axes[0].set_xlabel('x (state)')
axes[0].set_ylabel('V(x)')
axes[0].set_title('Optimal Value Function')
axes[0].grid(True, alpha=0.3)
# Optimal control
axes[1].plot(x_vals, u_vals, 'r-', linewidth=2)
axes[1].set_xlabel('x (state)')
axes[1].set_ylabel('u*(x)')
axes[1].set_title('Optimal Feedback Control')
axes[1].grid(True, alpha=0.3)
# Closed-loop trajectories
def closed_loop_dynamics(x, t):
    u = optimal_control(x)
    return a_val * x + b_val * u
t_span = np.linspace(0, 5, 200)
for x0 in [-2, -1, 0, 1, 2]:
    traj = odeint(closed_loop_dynamics, x0, t_span)
    axes[2].plot(t_span, traj, linewidth=2, label=f'x₀={x0}')
axes[2].set_xlabel('Time t')
axes[2].set_ylabel('x(t)')
axes[2].set_title('Closed-Loop Trajectories')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n✓ All trajectories converge to zero (optimal stabilization)")
print("✓ This is the basis of modern control theory")
CONTEXT: Optimal Control Theory
The Hamilton-Jacobi-Bellman (HJB) equation describes the value
function V(x,t) for an optimal control problem:
-∂V/∂t + H(x, ∇V) = 0
where H is the Hamiltonian:
H(x, p) = max_u {-L(x,u) - p·f(x,u)}
For a linear-quadratic regulator (LQR):
- State: ẋ = ax + bu
- Cost: ∫ (x² + u²) dt


Optimal Hamiltonian for LQR:
         2  2     
        b ⋅ξ     2
a⋅x⋅ξ + ───── - x 
          4       

The solution to the HJB equation gives the optimal value function:
V(x) = ½ x² P
where P is the solution to the algebraic Riccati equation:
0 = -1 + aP + Pa - Pb²P
For a=-1, b=1: P = √2 - 1 ≈ 0.414


----------------------------------------------------------------------
Optimal feedback control u*(x)
----------------------------------------------------------------------
No description has been provided for this image
✓ All trajectories converge to zero (optimal stabilization)
✓ This is the basis of modern control theory

Signal Processing - Wavelet Transform¶

In [19]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)
print("""
CONTEXT: Time-Frequency Analysis
The continuous wavelet transform (CWT) is:
(W_ψ f)(a,b) = ∫ f(x) ψ*((x-b)/a) dx/√a
where ψ is the mother wavelet, a is scale, b is position.
The CWT can be viewed as a pseudo-differential operator
acting on the time-frequency plane.
Morlet wavelet: ψ(x) = e^{iω₀x} e^{-x²/2}
""")
# Morlet wavelet
omega0 = symbols('omega_0', real=True, positive=True)
morlet = exp(I * omega0 * x) * exp(-x**2 / 2)
print("\nMorlet wavelet ψ(x):")
pprint(morlet)
# Fourier transform of the wavelet
morlet_fourier = exp(-(xi - omega0)**2 / 2) * sqrt(2*np.pi)
print("\nFourier transform ψ̂(ξ):")
pprint(simplify(morlet_fourier))
print("""
Properties of the wavelet transform:
1. Time-frequency localization (Heisenberg principle)
2. Multi-resolution analysis
3. Edge detection and singularity analysis
4. Admissibility condition: ∫ |ψ̂(ξ)|²/|ξ| dξ < ∞
Applications:
- Signal denoising
- Image compression (JPEG2000)
- Seismic analysis
- Medical imaging (ECG, EEG)
""")
# Numerical example: analysis of a chirp signal
print("\n" + "-"*70)
print("Example: Wavelet analysis of chirp signal")
print("-"*70)
# Chirp signal: f(t) = sin(2π(f₀ + αt)t)
t_vals = np.linspace(0, 1, 1000)
f0, alpha = 5, 20
chirp_signal = np.sin(2*np.pi * (f0 + alpha*t_vals) * t_vals)
# CWT approximation (simplified)
scales = np.linspace(0.1, 2, 100)
positions = np.linspace(0, 1, 100)
cwt = np.zeros((len(scales), len(positions)))
for i, scale in enumerate(scales):
    for j, pos in enumerate(positions):
        # Dilated and translated wavelet
        wavelet_vals = np.exp(1j * 5 * (t_vals - pos)/scale) * \
                      np.exp(-((t_vals - pos)/scale)**2 / 2) / np.sqrt(scale)

        # Dot product
        cwt[i, j] = np.abs(np.trapezoid(chirp_signal * np.conj(wavelet_vals), t_vals))
fig, axes = plt.subplots(2, 1, figsize=(12, 10))
# Original signal
axes[0].plot(t_vals, chirp_signal, 'b-', linewidth=1)
axes[0].set_xlabel('Time (s)')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('Chirp Signal: f(t) = sin(2π(5 + 20t)t)')
axes[0].grid(True, alpha=0.3)
# Scalogram (CWT)
T, S = np.meshgrid(positions, 1/scales)  # 1/scale ≈ frequency
im = axes[1].contourf(T, S, cwt, levels=50, cmap='jet')
axes[1].set_xlabel('Time (s)')
axes[1].set_ylabel('Frequency (Hz)')
axes[1].set_title('Wavelet Transform (Scalogram)')
plt.colorbar(im, ax=axes[1], label='|CWT|')
# Overlay the theoretical instantaneous frequency
freq_inst = f0 + 2*alpha*positions
axes[1].plot(positions, freq_inst, 'w--', linewidth=2, label='Instantaneous frequency')
axes[1].legend()
plt.tight_layout()
plt.show()
print("\n✓ Wavelet transform reveals time-varying frequency content")
print("✓ Superior to Fourier transform for non-stationary signals")
CONTEXT: Time-Frequency Analysis
The continuous wavelet transform (CWT) is:
(W_ψ f)(a,b) = ∫ f(x) ψ*((x-b)/a) dx/√a
where ψ is the mother wavelet, a is scale, b is position.
The CWT can be viewed as a pseudo-differential operator
acting on the time-frequency plane.
Morlet wavelet: ψ(x) = e^{iω₀x} e^{-x²/2}


Morlet wavelet ψ(x):
   2         
 -x          
 ────        
  2    ⅈ⋅ω₀⋅x
ℯ    ⋅ℯ      

Fourier transform ψ̂(ξ):
                         2 
                -(ω₀ - ξ)  
                ───────────
                     2     
2.506628274631⋅ℯ           

Properties of the wavelet transform:
1. Time-frequency localization (Heisenberg principle)
2. Multi-resolution analysis
3. Edge detection and singularity analysis
4. Admissibility condition: ∫ |ψ̂(ξ)|²/|ξ| dξ < ∞
Applications:
- Signal denoising
- Image compression (JPEG2000)
- Seismic analysis
- Medical imaging (ECG, EEG)


----------------------------------------------------------------------
Example: Wavelet analysis of chirp signal
----------------------------------------------------------------------
No description has been provided for this image
✓ Wavelet transform reveals time-varying frequency content
✓ Superior to Fourier transform for non-stationary signals
In [20]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: Riemannian Geometry
On a Riemannian manifold (M, g), the Laplace-Beltrami operator is:
Δ_g = (1/√|g|) ∂_i (√|g| g^{ij} ∂_j)
In local coordinates, this generalizes the flat Laplacian.
Example 1: Sphere S² with metric ds² = dθ² + sin²θ dφ²
""")
# Spherical coordinates: (θ, φ)
theta, phi = symbols('theta phi', real=True)
xi_theta, xi_phi = symbols('xi_theta xi_phi', real=True)
# Metric: g = diag(1, sin²θ)
# Inverse: g^{-1} = diag(1, 1/sin²θ)
# Determinant: |g| = sin²θ
# Laplace-Beltrami in spherical coordinates:
# Δ_S² = ∂²/∂θ² + cot(θ)∂/∂θ + (1/sin²θ)∂²/∂φ²
# Principal symbol: ξ_θ² + ξ_φ²/sin²θ
LB_sphere_symbol = xi_theta**2 + xi_phi**2 / sin(theta)**2
print("\nLaplace-Beltrami on S² (principal symbol):")
pprint(LB_sphere_symbol)
LB_sphere = PseudoDifferentialOperator(LB_sphere_symbol, [theta, phi], mode='symbol')
print("""
This operator is essential for:
- Spherical harmonics Y_ℓ^m (eigenfunctions)
- Heat equation on spheres: ∂u/∂t = Δ_S² u
- Wave equation on spheres
- Quantum mechanics on curved spaces
""")
# Spherical harmonics (examples)
print("\n" + "-"*70)
print("Spherical Harmonics: Eigenfunctions of Δ_S²")
print("-"*70)
print("""
Δ_S² Y_ℓ^m = -ℓ(ℓ+1) Y_ℓ^m
Examples:
- Y_0^0 = 1/(2√π) (constant)
- Y_1^0 = √(3/4π) cos(θ) (dipole)
- Y_1^{±1} = ∓√(3/8π) sin(θ) e^{±iφ}
- Y_2^0 = √(5/16π) (3cos²θ - 1) (quadrupole)
""")
# Visualization of spherical harmonics
fig = plt.figure(figsize=(15, 10))
ell_m_pairs = [(0, 0), (1, 0), (1, 1), (2, 0), (2, 1), (2, 2)]
for idx, (ell, m) in enumerate(ell_m_pairs):
    ax = fig.add_subplot(2, 3, idx+1, projection='3d')

    # Spherical grid
    theta_vals = np.linspace(0, np.pi, 50)
    phi_vals = np.linspace(0, 2*np.pi, 50)
    THETA, PHI = np.meshgrid(theta_vals, phi_vals)

    # Spherical harmonic
    Y_lm = sph_harm(m, ell, PHI, THETA)

    # Cartesian coordinates
    R = np.abs(Y_lm)
    X = R * np.sin(THETA) * np.cos(PHI)
    Y = R * np.sin(THETA) * np.sin(PHI)
    Z = R * np.cos(THETA)

    # Color according to phase
    colors = np.angle(Y_lm)

    surf = ax.plot_surface(X, Y, Z, facecolors=plt.cm.seismic(colors/np.pi/2 + 0.5),
                          alpha=0.8, linewidth=0, antialiased=True)

    ax.set_title(f'Y_{{{ell}}}^{{{m}}}')
    ax.set_box_aspect([1,1,1])
    ax.set_axis_off()

    # Eigenvalue
    eigenvalue = -ell * (ell + 1)
    ax.text2D(0.05, 0.95, f'λ = {eigenvalue}', transform=ax.transAxes)
plt.tight_layout()
plt.show()
print("\n✓ Spherical harmonics form a complete orthonormal basis on S²")
# Example 2: Hyperbolic plane (Poincaré disk)
print("\n" + "="*70)
print("Example 2: Hyperbolic Laplacian (Poincaré Disk)")
print("="*70)
print("""
The Poincaré disk model of hyperbolic geometry H² has metric:
ds² = 4(dx² + dy²)/(1 - x² - y²)²
The Laplace-Beltrami operator is:
Δ_H² = ((1-r²)/2)² (∂²/∂x² + ∂²/∂y²)
where r² = x² + y².
""")
r_squared = x**2 + y**2
conformal_factor = ((1 - r_squared)/2)**2
# Principal symbol
LB_hyperbolic_symbol = conformal_factor * (xi**2 + eta**2)
print("\nHyperbolic Laplacian (principal symbol):")
pprint(simplify(LB_hyperbolic_symbol))
LB_hyperbolic = PseudoDifferentialOperator(LB_hyperbolic_symbol, [x, y], mode='symbol')
print("""
Properties of hyperbolic space:
- Constant negative curvature K = -1
- Exponential volume growth
- Rich spectral theory (continuous spectrum + resonances)
- Applications in: number theory, quantum chaos, AdS/CFT
The spectrum of Δ_H² is: σ(Δ_H²) = [1/4, ∞) (continuous)
""")
# Visualization: Poincaré Disk
print("\n" + "-"*70)
print("Visualization: Poincaré Disk")
print("-"*70)
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# Poincaré disk with geodesics
theta_circle = np.linspace(0, 2*np.pi, 100)
axes[0].plot(np.cos(theta_circle), np.sin(theta_circle), 'k-', linewidth=2)
axes[0].set_aspect('equal')
# Geodesics (circular arcs orthogonal to the boundary)
for angle in np.linspace(0, np.pi, 8, endpoint=False):
    t = np.linspace(-0.9, 0.9, 100)
    # Geodesic passing through the origin
    x_geo = t * np.cos(angle)
    y_geo = t * np.sin(angle)
    axes[0].plot(x_geo, y_geo, 'b-', alpha=0.5)
# Non-diametral geodesics (circular arcs)
for x_center in [-0.5, 0, 0.5]:
    circle_angles = np.linspace(0, np.pi, 50)
    radius = 0.7
    x_arc = x_center + radius * np.cos(circle_angles)
    y_arc = radius * np.sin(circle_angles)
    # Keep only points inside the disk
    mask = x_arc**2 + y_arc**2 < 0.99
    axes[0].plot(x_arc[mask], y_arc[mask], 'r-', alpha=0.5)
axes[0].set_title('Poincaré Disk with Geodesics')
axes[0].set_xlabel('x')
axes[0].set_ylabel('y')
axes[0].grid(True, alpha=0.3)
# Conformal factor
x_grid = np.linspace(-0.95, 0.95, 100)
y_grid = np.linspace(-0.95, 0.95, 100)
X_grid, Y_grid = np.meshgrid(x_grid, y_grid)
R_squared = X_grid**2 + Y_grid**2
# Mask for the disk
disk_mask = R_squared < 1
conformal = np.zeros_like(R_squared)
conformal[disk_mask] = ((1 - R_squared[disk_mask])/2)**2
im = axes[1].contourf(X_grid, Y_grid, conformal, levels=20, cmap='viridis')
axes[1].plot(np.cos(theta_circle), np.sin(theta_circle), 'k-', linewidth=2)
axes[1].set_aspect('equal')
axes[1].set_title('Conformal Factor ((1-r²)/2)²')
axes[1].set_xlabel('x')
axes[1].set_ylabel('y')
plt.colorbar(im, ax=axes[1])
plt.tight_layout()
plt.show()
print("\n✓ Metric becomes singular at boundary (infinite distance)")
CONTEXT: Riemannian Geometry
On a Riemannian manifold (M, g), the Laplace-Beltrami operator is:
Δ_g = (1/√|g|) ∂_i (√|g| g^{ij} ∂_j)
In local coordinates, this generalizes the flat Laplacian.
Example 1: Sphere S² with metric ds² = dθ² + sin²θ dφ²


Laplace-Beltrami on S² (principal symbol):
    2            
  ξᵩ            2
─────── + ξₜₕₑₜₐ 
   2             
sin (θ)          

This operator is essential for:
- Spherical harmonics Y_ℓ^m (eigenfunctions)
- Heat equation on spheres: ∂u/∂t = Δ_S² u
- Wave equation on spheres
- Quantum mechanics on curved spaces


----------------------------------------------------------------------
Spherical Harmonics: Eigenfunctions of Δ_S²
----------------------------------------------------------------------

Δ_S² Y_ℓ^m = -ℓ(ℓ+1) Y_ℓ^m
Examples:
- Y_0^0 = 1/(2√π) (constant)
- Y_1^0 = √(3/4π) cos(θ) (dipole)
- Y_1^{±1} = ∓√(3/8π) sin(θ) e^{±iφ}
- Y_2^0 = √(5/16π) (3cos²θ - 1) (quadrupole)

No description has been provided for this image
✓ Spherical harmonics form a complete orthonormal basis on S²

======================================================================
Example 2: Hyperbolic Laplacian (Poincaré Disk)
======================================================================

The Poincaré disk model of hyperbolic geometry H² has metric:
ds² = 4(dx² + dy²)/(1 - x² - y²)²
The Laplace-Beltrami operator is:
Δ_H² = ((1-r²)/2)² (∂²/∂x² + ∂²/∂y²)
where r² = x² + y².


Hyperbolic Laplacian (principal symbol):
                       2
⎛ 2    2⎞ ⎛ 2    2    ⎞ 
⎝η  + ξ ⎠⋅⎝x  + y  - 1⎠ 
────────────────────────
           4            

Properties of hyperbolic space:
- Constant negative curvature K = -1
- Exponential volume growth
- Rich spectral theory (continuous spectrum + resonances)
- Applications in: number theory, quantum chaos, AdS/CFT
The spectrum of Δ_H² is: σ(Δ_H²) = [1/4, ∞) (continuous)


----------------------------------------------------------------------
Visualization: Poincaré Disk
----------------------------------------------------------------------
No description has been provided for this image
✓ Metric becomes singular at boundary (infinite distance)

Laplace-Beltrami Operator on Riemannian Manifolds¶

Curvature and Geometric Operators¶

In [21]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: Differential Geometry - Curvature
For a surface with metric g = [g_ij], the Gaussian curvature is:
K = (1/2|g|) [∂²/∂x² g_22 - 2∂²/∂x∂y g_12 + ∂²/∂y² g_11 + ...]
The Gauss-Bonnet theorem relates curvature to topology:
∫∫_M K dA = 2π χ(M)
where χ(M) is the Euler characteristic.
""")
# Example: Surface of revolution z = f(r) where r = √(x² + y²)
# For f(r) = r²/2 (paraboloid)
print("\nExample: Paraboloid z = (x² + y²)/2")
# Induced metric: ds² = (1 + x²)dx² + 2xy dx dy + (1 + y²)dy²
g_11 = 1 + x**2
g_12 = x*y
g_22 = 1 + y**2
det_g = g_11 * g_22 - g_12**2
print("\nMetric tensor g:")
g_matrix = Matrix([[g_11, g_12], [g_12, g_22]])
pprint(g_matrix)
print("\nDeterminant |g|:")
pprint(simplify(det_g))
# Gaussian curvature
# For the paraboloid: K = 1/(1 + r²)²
r = sqrt(x**2 + y**2)
K_gauss = 1 / (1 + r**2)**2
print("\nGaussian curvature K:")
pprint(simplify(K_gauss))
print("""
Interpretation:
- K > 0: Positive curvature (elliptic geometry)
- K = 0: Flat (Euclidean geometry)
- K < 0: Negative curvature (hyperbolic geometry)
For the paraboloid:
- K(0,0) = 1 (maximum at vertex)
- K → 0 as r → ∞ (asymptotically flat)
""")
# Visualization
fig = plt.figure(figsize=(15, 5))
# Paraboloid
ax1 = fig.add_subplot(131, projection='3d')
u = np.linspace(-2, 2, 50)
v = np.linspace(-2, 2, 50)
U, V = np.meshgrid(u, v)
Z = (U**2 + V**2) / 2
ax1.plot_surface(U, V, Z, cmap='viridis', alpha=0.8)
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax1.set_zlabel('z')
ax1.set_title('Paraboloid Surface')
# Gaussian curvature (top view)
ax2 = fig.add_subplot(132)
K_vals = 1 / (1 + U**2 + V**2)**2
im = ax2.contourf(U, V, K_vals, levels=20, cmap='hot')
ax2.set_xlabel('x')
ax2.set_ylabel('y')
ax2.set_title('Gaussian Curvature K(x,y)')
ax2.set_aspect('equal')
plt.colorbar(im, ax=ax2)
# Curvature as a function of r
ax3 = fig.add_subplot(133)
r_vals = np.linspace(0, 5, 100)
K_r = 1 / (1 + r_vals**2)**2
ax3.plot(r_vals, K_r, 'b-', linewidth=2)
ax3.set_xlabel('r = √(x² + y²)')
ax3.set_ylabel('K(r)')
ax3.set_title('Gaussian Curvature vs Radius')
ax3.grid(True, alpha=0.3)
ax3.axhline(0, color='k', linestyle='--', alpha=0.5)
plt.tight_layout()
plt.show()
print("\n✓ Curvature decreases away from the vertex")
CONTEXT: Differential Geometry - Curvature
For a surface with metric g = [g_ij], the Gaussian curvature is:
K = (1/2|g|) [∂²/∂x² g_22 - 2∂²/∂x∂y g_12 + ∂²/∂y² g_11 + ...]
The Gauss-Bonnet theorem relates curvature to topology:
∫∫_M K dA = 2π χ(M)
where χ(M) is the Euler characteristic.


Example: Paraboloid z = (x² + y²)/2

Metric tensor g:
⎡ 2            ⎤
⎢x  + 1   x⋅y  ⎥
⎢              ⎥
⎢         2    ⎥
⎣ x⋅y    y  + 1⎦

Determinant |g|:
 2    2    
x  + y  + 1

Gaussian curvature K:
      1       
──────────────
             2
⎛ 2    2    ⎞ 
⎝x  + y  + 1⎠ 

Interpretation:
- K > 0: Positive curvature (elliptic geometry)
- K = 0: Flat (Euclidean geometry)
- K < 0: Negative curvature (hyperbolic geometry)
For the paraboloid:
- K(0,0) = 1 (maximum at vertex)
- K → 0 as r → ∞ (asymptotically flat)

No description has been provided for this image
✓ Curvature decreases away from the vertex

Connections and Covariant Derivatives¶

In [22]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: Differential Geometry - Connections
A connection ∇ defines how to differentiate vector fields along curves.
For a Riemannian connection (Levi-Civita), the Christoffel symbols are:
Γ^k_{ij} = (1/2) g^{kℓ} (∂_i g_jℓ + ∂_j g_iℓ - ∂_ℓ g_ij)
The covariant derivative of a vector field V is:
∇_i V^j = ∂_i V^j + Γ^j_{ik} V^k
""")
# Example: Sphere S² in coordinates (θ, φ)
theta, phi = symbols('theta phi', real=True)
print("\nExample: Sphere S² with g = diag(1, sin²θ)")
# Christoffel symbols for S²
# Γ^θ_φφ = -sin(θ)cos(θ)
# Γ^φ_θφ = Γ^φ_φθ = cot(θ)
# Others = 0
christoffel_theta_phi_phi = -sin(theta) * cos(theta)
christoffel_phi_theta_phi = cos(theta) / sin(theta)  # cot(θ)
print("\nChristoffel symbols:")
print(f"Γ^θ_φφ = -sin(θ)cos(θ)")
pprint(christoffel_theta_phi_phi)
print(f"\nΓ^φ_θφ = cot(θ)")
pprint(christoffel_phi_theta_phi)
print("""
These symbols encode how vectors change when parallel transported.
Example: Parallel transport around a latitude circle
- Vector initially pointing north (∂/∂θ direction)
- After full circle, it rotates by angle 2π(1 - cos θ₀)
- This is holonomy - measures curvature!
""")
# Visualization of parallel transport
print("\n" + "-"*70)
print("Visualization: Parallel Transport on S²")
print("-"*70)
fig = plt.figure(figsize=(14, 6))
# Sphere
ax1 = fig.add_subplot(121, projection='3d')
u = np.linspace(0, 2*np.pi, 50)
v = np.linspace(0, np.pi, 50)
U, V = np.meshgrid(u, v)
X_sphere = np.sin(V) * np.cos(U)
Y_sphere = np.sin(V) * np.sin(U)
Z_sphere = np.cos(V)
ax1.plot_surface(X_sphere, Y_sphere, Z_sphere, alpha=0.3, color='cyan')
# Parallel transport along a latitude circle
theta0 = np.pi/3  # 60 degrees
phi_path = np.linspace(0, 2*np.pi, 100)
# Positions along the path
x_path = np.sin(theta0) * np.cos(phi_path)
y_path = np.sin(theta0) * np.sin(phi_path)
z_path = np.cos(theta0) * np.ones_like(phi_path)
ax1.plot(x_path, y_path, z_path, 'r-', linewidth=3, label='Transport path')
# Parallel transported vector
# Initially points north
n_arrows = 12
for i in range(n_arrows):
    idx = i * len(phi_path) // n_arrows
    phi_val = phi_path[idx]

    # Position
    x0 = x_path[idx]
    y0 = y_path[idx]
    z0 = z_path[idx]

    # Tangent vector (after parallel transport)
    # Rotation due to curvature
    angle = phi_val

    # North component (tangent to meridian)
    dx = -np.cos(theta0) * np.cos(phi_val) * 0.2
    dy = -np.cos(theta0) * np.sin(phi_val) * 0.2
    dz = np.sin(theta0) * 0.2

    ax1.quiver(x0, y0, z0, dx, dy, dz, color='blue', arrow_length_ratio=0.3)
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax1.set_zlabel('z')
ax1.set_title('Parallel Transport on S²')
ax1.legend()
# Holonomy as a function of latitude
ax2 = fig.add_subplot(122)
theta_vals = np.linspace(0.1, np.pi-0.1, 100)
holonomy = 2 * np.pi * (1 - np.cos(theta_vals))
ax2.plot(theta_vals * 180/np.pi, holonomy * 180/np.pi, 'b-', linewidth=2)
ax2.axhline(360, color='r', linestyle='--', alpha=0.5, label='Equator')
ax2.set_xlabel('Latitude θ (degrees)')
ax2.set_ylabel('Holonomy angle (degrees)')
ax2.set_title('Holonomy around Latitude Circles')
ax2.grid(True, alpha=0.3)
ax2.legend()
plt.tight_layout()
plt.show()
print("\n✓ Holonomy measures the 'twist' from parallel transport")
print("✓ This is a manifestation of Gaussian curvature")
CONTEXT: Differential Geometry - Connections
A connection ∇ defines how to differentiate vector fields along curves.
For a Riemannian connection (Levi-Civita), the Christoffel symbols are:
Γ^k_{ij} = (1/2) g^{kℓ} (∂_i g_jℓ + ∂_j g_iℓ - ∂_ℓ g_ij)
The covariant derivative of a vector field V is:
∇_i V^j = ∂_i V^j + Γ^j_{ik} V^k


Example: Sphere S² with g = diag(1, sin²θ)

Christoffel symbols:
Γ^θ_φφ = -sin(θ)cos(θ)
-sin(θ)⋅cos(θ)

Γ^φ_θφ = cot(θ)
cos(θ)
──────
sin(θ)

These symbols encode how vectors change when parallel transported.
Example: Parallel transport around a latitude circle
- Vector initially pointing north (∂/∂θ direction)
- After full circle, it rotates by angle 2π(1 - cos θ₀)
- This is holonomy - measures curvature!


----------------------------------------------------------------------
Visualization: Parallel Transport on S²
----------------------------------------------------------------------
No description has been provided for this image
✓ Holonomy measures the 'twist' from parallel transport
✓ This is a manifestation of Gaussian curvature

Geodesic Flow and Geometric Chaos¶

In [23]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: Dynamical Systems on Manifolds
The geodesic flow on the tangent bundle TM is generated by:
ẋ^i = g^{ij} p_j
ṗ_i = -(1/2) ∂_i g^{jk} p_j p_k
For surfaces of negative curvature, geodesic flow is chaotic!
""")
# Example: Negative curvature surface (saddle)
# z = x² - y² (hyperboloid of one sheet, central part)
print("\nExample: Saddle surface z = x² - y²")
# Approximate induced metric (near the origin)
# ds² ≈ (1 + 4x²)dx² + (1 + 4y²)dy²
# Geodesic Hamiltonian
H_geodesic_saddle = (xi**2 / (1 + 4*x**2) + eta**2 / (1 + 4*y**2)) / 2
print("\nGeodesic Hamiltonian (approximate):")
pprint(simplify(H_geodesic_saddle))
H_geo_op = PseudoDifferentialOperator(H_geodesic_saddle, [x, y], mode='symbol')
# Hamiltonian flow
flow = H_geo_op.symplectic_flow()
print("\nHamiltonian flow equations:")
for key, val in flow.items():
    print(f"{key} = ", end="")
    pprint(simplify(val))
print("""
On negatively curved surfaces:
- Geodesics diverge exponentially (chaos)
- Positive Lyapunov exponents
- Mixing and ergodicity
- Connection to quantum chaos (Bohigas-Giannoni-Schmit conjecture)
""")
# Numerical simulation of the geodesic flow
print("\n" + "-"*70)
print("Numerical simulation: Geodesic flow on saddle")
print("-"*70)
def geodesic_flow_saddle(state, t):
    """Geodesic flow on saddle surface"""
    x, y, px, py = state

    g_xx_inv = 1 / (1 + 4*x**2)
    g_yy_inv = 1 / (1 + 4*y**2)

    # Derivatives of the metric
    dg_xx = -8*x / (1 + 4*x**2)**2
    dg_yy = -8*y / (1 + 4*y**2)**2

    dx_dt = g_xx_inv * px
    dy_dt = g_yy_inv * py
    dpx_dt = 0.5 * dg_xx * px**2
    dpy_dt = 0.5 * dg_yy * py**2

    return [dx_dt, dy_dt, dpx_dt, dpy_dt]
# Close initial conditions
np.random.seed(42)
n_trajectories = 5
t_span = np.linspace(0, 10, 1000)
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
colors = plt.cm.rainbow(np.linspace(0, 1, n_trajectories))
for i in range(n_trajectories):
    # Tiny perturbation
    x0 = 0.1 + 0.001 * np.random.randn()
    y0 = 0.1 + 0.001 * np.random.randn()
    px0 = 1.0 + 0.001 * np.random.randn()
    py0 = 0.5 + 0.001 * np.random.randn()

    initial = [x0, y0, px0, py0]
    trajectory = odeint(geodesic_flow_saddle, initial, t_span)

    # Position space
    axes[0, 0].plot(trajectory[:, 0], trajectory[:, 1],
                   color=colors[i], linewidth=1, alpha=0.7)

    # Phase space (x, px)
    axes[0, 1].plot(trajectory[:, 0], trajectory[:, 2],
                   color=colors[i], linewidth=1, alpha=0.7)

    # Phase space (y, py)
    axes[1, 0].plot(trajectory[:, 1], trajectory[:, 3],
                   color=colors[i], linewidth=1, alpha=0.7)

    # Hamiltonian (conservation)
    H_vals = 0.5 * (trajectory[:, 2]**2 / (1 + 4*trajectory[:, 0]**2) +
                    trajectory[:, 3]**2 / (1 + 4*trajectory[:, 1]**2))
    axes[1, 1].plot(t_span, H_vals, color=colors[i], linewidth=1, alpha=0.7)
axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('y')
axes[0, 0].set_title('Geodesics on Saddle (Position Space)')
axes[0, 0].grid(True, alpha=0.3)
axes[0, 1].set_xlabel('x')
axes[0, 1].set_ylabel('p_x')
axes[0, 1].set_title('Phase Space (x, p_x)')
axes[0, 1].grid(True, alpha=0.3)
axes[1, 0].set_xlabel('y')
axes[1, 0].set_ylabel('p_y')
axes[1, 0].set_title('Phase Space (y, p_y)')
axes[1, 0].grid(True, alpha=0.3)
axes[1, 1].set_xlabel('Time')
axes[1, 1].set_ylabel('H (Energy)')
axes[1, 1].set_title('Energy Conservation')
axes[1, 1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n✓ Nearby geodesics diverge (sensitive dependence)")
print("✓ Energy is conserved (Hamiltonian dynamics)")
CONTEXT: Dynamical Systems on Manifolds
The geodesic flow on the tangent bundle TM is generated by:
ẋ^i = g^{ij} p_j
ṗ_i = -(1/2) ∂_i g^{jk} p_j p_k
For surfaces of negative curvature, geodesic flow is chaotic!


Example: Saddle surface z = x² - y²

Geodesic Hamiltonian (approximate):
      2              2     
     η              ξ      
──────────── + ────────────
  ⎛   2    ⎞     ⎛   2    ⎞
2⋅⎝4⋅y  + 1⎠   2⋅⎝4⋅x  + 1⎠

Hamiltonian flow equations:
dx/dt = 
   ξ    
────────
   2    
4⋅x  + 1
dy/dt =    η    
────────
   2    
4⋅y  + 1
dxi/dt =        2   
  4⋅x⋅ξ    
───────────
          2
⎛   2    ⎞ 
⎝4⋅x  + 1⎠ 
deta/dt =      2     
  4⋅η ⋅y   
───────────
          2
⎛   2    ⎞ 
⎝4⋅y  + 1⎠ 

On negatively curved surfaces:
- Geodesics diverge exponentially (chaos)
- Positive Lyapunov exponents
- Mixing and ergodicity
- Connection to quantum chaos (Bohigas-Giannoni-Schmit conjecture)


----------------------------------------------------------------------
Numerical simulation: Geodesic flow on saddle
----------------------------------------------------------------------
No description has been provided for this image
✓ Nearby geodesics diverge (sensitive dependence)
✓ Energy is conserved (Hamiltonian dynamics)

Atiyah-Singer Index Theorem¶

In [24]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: Index Theory
The Atiyah-Singer index theorem relates:
- Analytical index: dim ker D - dim coker D (operator theory)
- Topological index: ∫_M characteristic classes (topology)
For an elliptic operator D on a compact manifold M:
ind(D) = ∫_M ch(σ(D)) Td(TM)
where ch = Chern character, Td = Todd class.
""")
# Simple example: Dirac operator on S²
print("\nExample: Dirac operator on S²")
print("""
The Dirac operator on S² acts on spinors:
D: Γ(S) → Γ(S)
where S is the spinor bundle.
Atiyah-Singer theorem gives:
ind(D) = ∫_S² Â(S²) = χ(S²)/2 = 1
(since Euler characteristic χ(S²) = 2)
Physical interpretation:
- In quantum field theory: anomalies
- In string theory: consistency conditions
- Relates topology to spectrum
""")
# Symbolic computation of the index
print("\n" + "-"*70)
print("Symbolic computation of topological index")
print("-"*70)
print("""
For the Dirac operator D on S²:
Principal symbol: σ(D) = iγ^μ ξ_μ
where γ^μ are Clifford matrices.
The Chern character involves:
ch(σ(D)) = rank + c₁ + (c₁² - 2c₂)/2 + ...
For S² with its spin structure:
∫_S² ch(σ(D)) Td(TS²) = 1
""")
# Visualization: Index theorem for different surfaces
print("\n" + "-"*70)
print("Index vs Genus for Riemann Surfaces")
print("-"*70)
# For orientable Riemann surfaces of genus g
# Euler characteristic: χ = 2 - 2g
# Index of the Dolbeault operator: ind(∂̄) = 1 - g
genera = np.arange(0, 10)
euler_char = 2 - 2*genera
dolbeault_index = 1 - genera
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].plot(genera, euler_char, 'bo-', linewidth=2, markersize=8)
axes[0].axhline(0, color='k', linestyle='--', alpha=0.5)
axes[0].set_xlabel('Genus g')
axes[0].set_ylabel('χ (Euler characteristic)')
axes[0].set_title('Euler Characteristic χ = 2 - 2g')
axes[0].grid(True, alpha=0.3)
# Annotate some examples
axes[0].text(0, 2.3, 'Sphere', ha='center')
axes[0].text(1, 0.3, 'Torus', ha='center')
axes[0].text(2, -1.7, 'Pretzel', ha='center')
axes[1].plot(genera, dolbeault_index, 'ro-', linewidth=2, markersize=8)
axes[1].axhline(0, color='k', linestyle='--', alpha=0.5)
axes[1].set_xlabel('Genus g')
axes[1].set_ylabel('ind(∂̄)')
axes[1].set_title('Dolbeault Index = 1 - g')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n✓ Index theorem connects analysis (operators) to topology (genus)")
print("✓ This is one of the deepest results in modern mathematics")
CONTEXT: Index Theory
The Atiyah-Singer index theorem relates:
- Analytical index: dim ker D - dim coker D (operator theory)
- Topological index: ∫_M characteristic classes (topology)
For an elliptic operator D on a compact manifold M:
ind(D) = ∫_M ch(σ(D)) Td(TM)
where ch = Chern character, Td = Todd class.


Example: Dirac operator on S²

The Dirac operator on S² acts on spinors:
D: Γ(S) → Γ(S)
where S is the spinor bundle.
Atiyah-Singer theorem gives:
ind(D) = ∫_S² Â(S²) = χ(S²)/2 = 1
(since Euler characteristic χ(S²) = 2)
Physical interpretation:
- In quantum field theory: anomalies
- In string theory: consistency conditions
- Relates topology to spectrum


----------------------------------------------------------------------
Symbolic computation of topological index
----------------------------------------------------------------------

For the Dirac operator D on S²:
Principal symbol: σ(D) = iγ^μ ξ_μ
where γ^μ are Clifford matrices.
The Chern character involves:
ch(σ(D)) = rank + c₁ + (c₁² - 2c₂)/2 + ...
For S² with its spin structure:
∫_S² ch(σ(D)) Td(TS²) = 1


----------------------------------------------------------------------
Index vs Genus for Riemann Surfaces
----------------------------------------------------------------------
No description has been provided for this image
✓ Index theorem connects analysis (operators) to topology (genus)
✓ This is one of the deepest results in modern mathematics

Differential Forms and Hodge Theory¶

In [25]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: Hodge Theory on Riemannian Manifolds
The Hodge star operator *: Ω^p(M) → Ω^{n-p}(M) relates differential forms.
The Hodge Laplacian on p-forms is:
Δ = dδ + δd = (d + δ)²
where d is exterior derivative, δ = *d* is codifferential.
For functions (0-forms): Δ = δd = Laplace-Beltrami operator
For 1-forms: Δ includes curvature terms
""")
# Example: 2-torus T² = S¹ × S¹ with coordinates (x,y) ∈ [0,2π]²
print("\nExample: 2-Torus T² with flat metric")
# Flat metric: ds² = dx² + dy²
# On the torus, the operators are:
# d: Ω^0 → Ω^1,  d: Ω^1 → Ω^2
# δ: Ω^1 → Ω^0,  δ: Ω^2 → Ω^1
# Symbols:
# Δ₀ (on functions): -∂²/∂x² - ∂²/∂y²  →  symbol: ξ² + η²
Delta_0_torus = xi**2 + eta**2
print("\nHodge Laplacian on 0-forms (functions):")
pprint(Delta_0_torus)
Delta_0_op = PseudoDifferentialOperator(Delta_0_torus, [x, y], mode='symbol')
print("""
Hodge decomposition theorem:
For a compact Riemannian manifold M, any k-form ω decomposes as:
ω = dα + δβ + h
where:
- dα: exact part
- δβ: coexact part
- h: harmonic part (Δh = 0)
The space of harmonic forms H^k(M) is isomorphic to the
de Rham cohomology H^k_{dR}(M).
""")
# Harmonic forms on the torus
print("\n" + "-"*70)
print("Harmonic forms on T²")
print("-"*70)
print("""
For the 2-torus T²:
H^0(T²) = ℝ            (dim = 1)  - constant functions
H^1(T²) = ℝ²           (dim = 2)  - dx, dy
H^2(T²) = ℝ            (dim = 1)  - dx∧dy
Betti numbers: b₀ = 1, b₁ = 2, b₂ = 1
Euler characteristic: χ(T²) = Σ(-1)^k b_k = 1 - 2 + 1 = 0
""")
# Visualization of harmonic forms
print("\n" + "-"*70)
print("Visualization: Harmonic 1-forms on T²")
print("-"*70)
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# Parameterized torus
u = np.linspace(0, 2*np.pi, 30)
v = np.linspace(0, 2*np.pi, 30)
U, V = np.meshgrid(u, v)
# Harmonic forms represented as vector fields
# dx → (1, 0)
dx_field_x = np.ones_like(U)
dx_field_y = np.zeros_like(U)
# dy → (0, 1)
dy_field_x = np.zeros_like(U)
dy_field_y = np.ones_like(U)
# x dx + y dy (non-harmonic, for contrast)
mixed_field_x = U / (2*np.pi)
mixed_field_y = V / (2*np.pi)
axes[0].quiver(U, V, dx_field_x, dx_field_y, alpha=0.7)
axes[0].set_xlabel('x')
axes[0].set_ylabel('y')
axes[0].set_title('Harmonic 1-form: dx')
axes[0].set_aspect('equal')
axes[1].quiver(U, V, dy_field_x, dy_field_y, alpha=0.7)
axes[1].set_xlabel('x')
axes[1].set_ylabel('y')
axes[1].set_title('Harmonic 1-form: dy')
axes[1].set_aspect('equal')
axes[2].quiver(U, V, mixed_field_x, mixed_field_y, alpha=0.7)
axes[2].set_xlabel('x')
axes[2].set_ylabel('y')
axes[2].set_title('Non-harmonic: x dx + y dy')
axes[2].set_aspect('equal')
plt.tight_layout()
plt.show()
print("\n✓ Harmonic forms are globally defined, closed, and co-closed")
print("✓ They encode the topology of the manifold")
CONTEXT: Hodge Theory on Riemannian Manifolds
The Hodge star operator *: Ω^p(M) → Ω^{n-p}(M) relates differential forms.
The Hodge Laplacian on p-forms is:
Δ = dδ + δd = (d + δ)²
where d is exterior derivative, δ = *d* is codifferential.
For functions (0-forms): Δ = δd = Laplace-Beltrami operator
For 1-forms: Δ includes curvature terms


Example: 2-Torus T² with flat metric

Hodge Laplacian on 0-forms (functions):
 2    2
η  + ξ 

Hodge decomposition theorem:
For a compact Riemannian manifold M, any k-form ω decomposes as:
ω = dα + δβ + h
where:
- dα: exact part
- δβ: coexact part
- h: harmonic part (Δh = 0)
The space of harmonic forms H^k(M) is isomorphic to the
de Rham cohomology H^k_{dR}(M).


----------------------------------------------------------------------
Harmonic forms on T²
----------------------------------------------------------------------

For the 2-torus T²:
H^0(T²) = ℝ            (dim = 1)  - constant functions
H^1(T²) = ℝ²           (dim = 2)  - dx, dy
H^2(T²) = ℝ            (dim = 1)  - dx∧dy
Betti numbers: b₀ = 1, b₁ = 2, b₂ = 1
Euler characteristic: χ(T²) = Σ(-1)^k b_k = 1 - 2 + 1 = 0


----------------------------------------------------------------------
Visualization: Harmonic 1-forms on T²
----------------------------------------------------------------------
No description has been provided for this image
✓ Harmonic forms are globally defined, closed, and co-closed
✓ They encode the topology of the manifold

Vector Bundles and Gauge Connections¶

In [26]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: Fiber Bundles and Gauge Theory
A connection on a vector bundle E → M defines parallel transport.
For a U(1) gauge connection (electromagnetism), the connection 1-form is:
A = A_μ dx^μ
The curvature (field strength) is:
F = dA = ∂_μ A_ν - ∂_ν A_μ
The covariant derivative is:
D_μ = ∂_μ + iA_μ
""")
# Example: Magnetic monopole on S²
print("\nExample: Dirac Monopole on S²")
# Spherical coordinates (θ, φ)
theta, phi = symbols('theta phi', real=True)
# Vector potential of the monopole (Dirac gauge, northern hemisphere)
# A_φ = (1 - cos θ) / sin θ (in units where g = 1)
A_phi = (1 - cos(theta)) / sin(theta)
print("\nGauge potential A_φ:")
pprint(simplify(A_phi))
# Magnetic field strength (flux through S²)
# B = ∇ × A, integrated over S²: Φ = 4πg (Dirac quantization)
print("""
Magnetic flux through S²:
Φ = ∫_S² B·dS = 4πg
where g is the monopole charge.
Dirac quantization condition: eg = n/2 (n ∈ ℤ)
This ensures single-valuedness of the wavefunction.
""")
# Chern number (topological class)
print("\n" + "-"*70)
print("Topological invariant: Chern number")
print("-"*70)
print("""
The first Chern number c₁ counts the monopole charge:
c₁ = (1/2π) ∫_S² F = g
For a U(1) bundle over S²:
- c₁ = 0: Trivial bundle
- c₁ = n ≠ 0: Non-trivial, n monopoles
This is the prototype for topological quantum numbers.
""")
# Visualization of the monopole field
print("\n" + "-"*70)
print("Visualization: Monopole magnetic field")
print("-"*70)
fig = plt.figure(figsize=(14, 6))
# Sphere with magnetic field
ax1 = fig.add_subplot(121, projection='3d')
theta_vals = np.linspace(0.1, np.pi-0.1, 20)
phi_vals = np.linspace(0, 2*np.pi, 20)
THETA, PHI = np.meshgrid(theta_vals, phi_vals)
# Position on the sphere
X = np.sin(THETA) * np.cos(PHI)
Y = np.sin(THETA) * np.sin(PHI)
Z = np.cos(THETA)
# Radial magnetic field (monopole)
# B ∝ r̂/r² → on the unit sphere: B = r̂
Bx = X
By = Y
Bz = Z
# Normalize for visualization
norm = np.sqrt(Bx**2 + By**2 + Bz**2)
Bx /= norm
By /= norm
Bz /= norm
# Scale for arrows
scale = 0.2
ax1.plot_surface(X, Y, Z, alpha=0.3, color='lightblue')
ax1.quiver(X, Y, Z, Bx*scale, By*scale, Bz*scale,
          color='red', arrow_length_ratio=0.3, linewidth=1)
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax1.set_zlabel('z')
ax1.set_title('Magnetic Monopole Field on S²')
# Field lines (meridians)
ax2 = fig.add_subplot(122, projection='3d')
ax2.plot_surface(X, Y, Z, alpha=0.2, color='lightblue')
for phi_line in np.linspace(0, 2*np.pi, 12, endpoint=False):
    theta_line = np.linspace(0.1, np.pi-0.1, 50)
    x_line = np.sin(theta_line) * np.cos(phi_line)
    y_line = np.sin(theta_line) * np.sin(phi_line)
    z_line = np.cos(theta_line)
    ax2.plot(x_line, y_line, z_line, 'r-', linewidth=2, alpha=0.7)
ax2.set_xlabel('x')
ax2.set_ylabel('y')
ax2.set_zlabel('z')
ax2.set_title('Magnetic Field Lines (Meridians)')
plt.tight_layout()
plt.show()
print("\n✓ All field lines emerge from a single point (monopole)")
print("✓ Topologically protected: cannot be removed continuously")
CONTEXT: Fiber Bundles and Gauge Theory
A connection on a vector bundle E → M defines parallel transport.
For a U(1) gauge connection (electromagnetism), the connection 1-form is:
A = A_μ dx^μ
The curvature (field strength) is:
F = dA = ∂_μ A_ν - ∂_ν A_μ
The covariant derivative is:
D_μ = ∂_μ + iA_μ


Example: Dirac Monopole on S²

Gauge potential A_φ:
1 - cos(θ)
──────────
  sin(θ)  

Magnetic flux through S²:
Φ = ∫_S² B·dS = 4πg
where g is the monopole charge.
Dirac quantization condition: eg = n/2 (n ∈ ℤ)
This ensures single-valuedness of the wavefunction.


----------------------------------------------------------------------
Topological invariant: Chern number
----------------------------------------------------------------------

The first Chern number c₁ counts the monopole charge:
c₁ = (1/2π) ∫_S² F = g
For a U(1) bundle over S²:
- c₁ = 0: Trivial bundle
- c₁ = n ≠ 0: Non-trivial, n monopoles
This is the prototype for topological quantum numbers.


----------------------------------------------------------------------
Visualization: Monopole magnetic field
----------------------------------------------------------------------
No description has been provided for this image
✓ All field lines emerge from a single point (monopole)
✓ Topologically protected: cannot be removed continuously

Symplectic Geometry and Geometric Mechanics¶

In [27]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: Symplectic Geometry
A symplectic manifold (M, ω) has a closed, non-degenerate 2-form ω.
Phase space of classical mechanics is the cotangent bundle T*Q
with canonical symplectic form:
ω = dξ ∧ dx (in local coordinates)
Hamilton's equations arise from the symplectic structure:
ẋ^i = {x^i, H} = ω^{ij} ∂_j H
""")
# Example: Simple pendulum
print("\nExample: Simple Pendulum on S¹")
# Variables: angle θ ∈ S¹, conjugate momentum p_θ
# Hamiltonian: H = p²/2 + (1 - cos θ) (energy)
p = symbols('p', real=True)
H_pendulum = p**2 / 2 + (1 - cos(theta))
print("\nHamiltonian H(θ, p):")
pprint(H_pendulum)
# Associated pseudo-differential symbol
H_pend_symbol = xi_theta**2 / 2 + (1 - cos(theta))
H_pend_op = PseudoDifferentialOperator(H_pend_symbol, [theta], mode='symbol')
# Hamiltonian flow
flow_pendulum = H_pend_op.symplectic_flow()
print("\nHamilton's equations:")
for key, val in flow_pendulum.items():
    print(f"{key} = ", end="")
    pprint(val)
print("""
Geometric features:
- Phase space is a cylinder S¹ × ℝ
- Fixed points: (0, 0) stable, (π, 0) unstable
- Homoclinic orbit connects unstable point to itself
- Area-preserving flow (Liouville's theorem)
""")
# Phase portrait
print("\n" + "-"*70)
print("Phase portrait of simple pendulum")
print("-"*70)
theta_vals = np.linspace(-np.pi, np.pi, 30)
p_vals = np.linspace(-3, 3, 30)
THETA_grid, P_grid = np.meshgrid(theta_vals, p_vals)
# Vector field
dtheta_dt = P_grid
dp_dt = -np.sin(THETA_grid)
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# Phase portrait with trajectories
axes[0].streamplot(theta_vals, p_vals, dtheta_dt, dp_dt,
                  color=np.sqrt(dtheta_dt**2 + dp_dt**2),
                  cmap='viridis', density=1.5, linewidth=1)
# Add fixed points
axes[0].plot(0, 0, 'ro', markersize=10, label='Stable (bottom)')
axes[0].plot(np.pi, 0, 'rx', markersize=10, label='Unstable (top)')
axes[0].plot(-np.pi, 0, 'rx', markersize=10)
# Separatrix (homoclinic)
E_sep = 2  # Energy of the separatrix
p_sep_upper = np.sqrt(2*(E_sep - (1 - np.cos(theta_vals))))
p_sep_lower = -p_sep_upper
# Mask complex values
mask = (1 - np.cos(theta_vals)) <= E_sep
axes[0].plot(theta_vals[mask], p_sep_upper[mask], 'r--', linewidth=2, label='Separatrix')
axes[0].plot(theta_vals[mask], p_sep_lower[mask], 'r--', linewidth=2)
axes[0].set_xlabel('θ (angle)')
axes[0].set_ylabel('p (momentum)')
axes[0].set_title('Phase Portrait')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Energy diagram
E_vals = np.linspace(0, 5, 100)
theta_plot = np.linspace(-np.pi, np.pi, 100)
for E in [0.5, 1.0, 1.5, 2.0, 2.5, 3.0]:
    p_positive = np.sqrt(np.maximum(0, 2*(E - (1 - np.cos(theta_plot)))))
    p_negative = -p_positive

    if E < 2:
        # Oscillations
        axes[1].plot(theta_plot, p_positive, 'b-', alpha=0.7)
        axes[1].plot(theta_plot, p_negative, 'b-', alpha=0.7)
    elif np.abs(E - 2) < 0.1:
        # Separatrix
        axes[1].plot(theta_plot, p_positive, 'r-', linewidth=2, label=f'E={E:.1f}')
        axes[1].plot(theta_plot, p_negative, 'r-', linewidth=2)
    else:
        # Rotations
        axes[1].plot(theta_plot, p_positive, 'g-', alpha=0.7)
        axes[1].plot(theta_plot, p_negative, 'g-', alpha=0.7)
axes[1].set_xlabel('θ (angle)')
axes[1].set_ylabel('p (momentum)')
axes[1].set_title('Level Curves of Hamiltonian')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n✓ Phase space divided into oscillation/rotation regions")
print("✓ Separatrix is the boundary (unstable manifold)")
CONTEXT: Symplectic Geometry
A symplectic manifold (M, ω) has a closed, non-degenerate 2-form ω.
Phase space of classical mechanics is the cotangent bundle T*Q
with canonical symplectic form:
ω = dξ ∧ dx (in local coordinates)
Hamilton's equations arise from the symplectic structure:
ẋ^i = {x^i, H} = ω^{ij} ∂_j H


Example: Simple Pendulum on S¹

Hamiltonian H(θ, p):
 2             
p              
── - cos(θ) + 1
2              

Hamilton's equations:
dx/dt = 0
dxi/dt = -sin(θ)

Geometric features:
- Phase space is a cylinder S¹ × ℝ
- Fixed points: (0, 0) stable, (π, 0) unstable
- Homoclinic orbit connects unstable point to itself
- Area-preserving flow (Liouville's theorem)


----------------------------------------------------------------------
Phase portrait of simple pendulum
----------------------------------------------------------------------
No description has been provided for this image
✓ Phase space divided into oscillation/rotation regions
✓ Separatrix is the boundary (unstable manifold)

Lie Groups Acting on Manifolds¶

In [28]:
x, y, z = symbols('x y z', real=True)
xi, eta, zeta = symbols('xi eta zeta', real=True)
print("""
CONTEXT: Lie Group Actions
A Lie group G acts on a manifold M: G × M → M
The infinitesimal generator of the action is a vector field X.
Example: SO(3) acting on S² by rotations
The generators of SO(3) are the angular momentum operators:
L_x, L_y, L_z
In coordinates (θ, φ) on S²:
L_z = -i∂/∂φ (rotation around z-axis)
""")
# SO(3) action on S²
theta, phi = symbols('theta phi', real=True)
xi_theta, xi_phi = symbols('xi_theta xi_phi', real=True)
# Generator L_z: rotation around the z-axis
# In spherical coordinates: L_z = -i∂/∂φ
# Symbol: L_z → -iξ_φ → symbol = ξ_φ (accounting for i)
L_z_symbol = xi_phi
print("\nGenerator L_z (rotation around z-axis):")
pprint(L_z_symbol)
L_z_op = PseudoDifferentialOperator(L_z_symbol, [theta, phi], mode='symbol')
print("""
The orbit of a point p ∈ S² under SO(3) is:
- If p is north/south pole: orbit = {p} (fixed point)
- Otherwise: orbit = latitude circle
The quotient space S²/SO(2) ≅ [0, π] (space of orbits)
""")
# Moment map
print("\n" + "-"*70)
print("Moment map for SO(3) action on S²")
print("-"*70)
print("""
The moment map μ: T*S² → 𝔰𝔬(3)* ≅ ℝ³ is:
μ(θ, φ, p_θ, p_φ) = (L_x, L_y, L_z)
where L_i are the components of angular momentum.
For points on S² with momentum (p_θ, p_φ):
L_z = p_φ (conserved quantity by rotational symmetry)
This is Noether's theorem in geometric form!
""")
# Visualization of the action
print("\n" + "-"*70)
print("Visualization: SO(3) orbits on S²")
print("-"*70)
fig = plt.figure(figsize=(14, 6))
# Sphere with latitude circles (orbits)
ax1 = fig.add_subplot(121, projection='3d')
u = np.linspace(0, 2*np.pi, 50)
v = np.linspace(0, np.pi, 50)
U, V = np.meshgrid(u, v)
X_sphere = np.sin(V) * np.cos(U)
Y_sphere = np.sin(V) * np.sin(U)
Z_sphere = np.cos(V)
ax1.plot_surface(X_sphere, Y_sphere, Z_sphere, alpha=0.3, color='cyan')
# Orbits (latitude circles)
for theta_orbit in np.linspace(0.2, np.pi-0.2, 7):
    phi_circle = np.linspace(0, 2*np.pi, 100)
    x_orbit = np.sin(theta_orbit) * np.cos(phi_circle)
    y_orbit = np.sin(theta_orbit) * np.sin(phi_circle)
    z_orbit = np.cos(theta_orbit) * np.ones_like(phi_circle)
    ax1.plot(x_orbit, y_orbit, z_orbit, 'r-', linewidth=2)
# Poles (trivial orbits)
ax1.plot([0], [0], [1], 'go', markersize=10, label='North pole (fixed)')
ax1.plot([0], [0], [-1], 'go', markersize=10, label='South pole (fixed)')
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax1.set_zlabel('z')
ax1.set_title('SO(3) Orbits on S² (Latitude Circles)')
ax1.legend()
# Quotient space S²/SO(2)
ax2 = fig.add_subplot(122)
theta_quotient = np.linspace(0, np.pi, 100)
radius = np.sin(theta_quotient)
ax2.plot(theta_quotient, radius, 'b-', linewidth=3)
ax2.fill_between(theta_quotient, 0, radius, alpha=0.3)
ax2.axvline(0, color='g', linestyle='--', linewidth=2, label='North pole')
ax2.axvline(np.pi, color='g', linestyle='--', linewidth=2, label='South pole')
ax2.set_xlabel('θ (colatitude)')
ax2.set_ylabel('Orbit radius')
ax2.set_title('Quotient Space S²/SO(2) ≅ [0, π]')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n✓ Each orbit corresponds to one point in quotient space")
print("✓ Symplectic reduction: T*S²//SO(2) gives reduced phase space")
CONTEXT: Lie Group Actions
A Lie group G acts on a manifold M: G × M → M
The infinitesimal generator of the action is a vector field X.
Example: SO(3) acting on S² by rotations
The generators of SO(3) are the angular momentum operators:
L_x, L_y, L_z
In coordinates (θ, φ) on S²:
L_z = -i∂/∂φ (rotation around z-axis)


Generator L_z (rotation around z-axis):
ξᵩ

The orbit of a point p ∈ S² under SO(3) is:
- If p is north/south pole: orbit = {p} (fixed point)
- Otherwise: orbit = latitude circle
The quotient space S²/SO(2) ≅ [0, π] (space of orbits)


----------------------------------------------------------------------
Moment map for SO(3) action on S²
----------------------------------------------------------------------

The moment map μ: T*S² → 𝔰𝔬(3)* ≅ ℝ³ is:
μ(θ, φ, p_θ, p_φ) = (L_x, L_y, L_z)
where L_i are the components of angular momentum.
For points on S² with momentum (p_θ, p_φ):
L_z = p_φ (conserved quantity by rotational symmetry)
This is Noether's theorem in geometric form!


----------------------------------------------------------------------
Visualization: SO(3) orbits on S²
----------------------------------------------------------------------
No description has been provided for this image
✓ Each orbit corresponds to one point in quotient space
✓ Symplectic reduction: T*S²//SO(2) gives reduced phase space

Complex Geometry and Kähler Manifolds¶

In [29]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: Kähler Geometry
A Kähler manifold (M, g, J, ω) has three compatible structures:
- Riemannian metric g
- Complex structure J
- Symplectic form ω = g(J·, ·)
Example: ℂℙ¹ (Riemann sphere) with Fubini-Study metric
The Kähler form is:
ω = (i/2) ∂∂̄ log(1 + |z|²)
""")
# Complex coordinates z = x + iy
z = x + I*y
z_bar = x - I*y
# Fubini-Study metric on ℂℙ¹
# In local coordinates: ds² = 4dz dz̄/(1 + |z|²)²
print("\nFubini-Study metric on ℂℙ¹:")
print("ds² = 4 dz dz̄ / (1 + |z|²)²")
# In real coordinates
r_squared = x**2 + y**2
g_kahler_11 = 4 / (1 + r_squared)**2
g_kahler_22 = 4 / (1 + r_squared)**2
print("\nIn real coordinates (x, y):")
print(f"g_xx = g_yy = 4/(1 + x² + y²)²")
# Kähler Laplacian
# Δ_Kähler = g^{ij} ∇_i ∇_j
# Principal symbol
Kahler_laplacian_symbol = ((1 + r_squared)/4)**2 * (xi**2 + eta**2)
print("\nKähler Laplacian (principal symbol):")
pprint(simplify(Kahler_laplacian_symbol))
Kahler_Lap = PseudoDifferentialOperator(Kahler_laplacian_symbol, [x, y], mode='symbol')
print("""
Special properties of Kähler manifolds:
1. Δ = 2Δ_∂̄ = 2Δ_∂ (Hodge decomposition compatibility)
2. Holomorphic functions are harmonic
3. Chern classes determined by curvature
4. Rich interplay between complex analysis and geometry
Applications:
- Algebraic geometry (projective varieties)
- String theory (Calabi-Yau manifolds)
- Geometric quantization
- Mirror symmetry
""")
# Visualization of the Riemann sphere
print("\n" + "-"*70)
print("Visualization: Riemann Sphere ℂℙ¹")
print("-"*70)
fig = plt.figure(figsize=(14, 6))
# Stereographic projection
ax1 = fig.add_subplot(121, projection='3d')
# Sphere
u = np.linspace(0, 2*np.pi, 50)
v = np.linspace(0, np.pi, 50)
U, V = np.meshgrid(u, v)
X_sphere = np.sin(V) * np.cos(U)
Y_sphere = np.sin(V) * np.sin(U)
Z_sphere = np.cos(V)
ax1.plot_surface(X_sphere, Y_sphere, Z_sphere, alpha=0.4, color='lightblue')
# Some points in the complex plane projected onto the sphere
z_points = [0+0j, 1+0j, 0+1j, 1+1j, -1+0j, 0-1j]
for z_pt in z_points:
    # Inverse stereographic projection
    r = np.abs(z_pt)
    phi_pt = np.angle(z_pt)

    # Coordinates on the sphere
    denom = 1 + r**2
    x_sphere = 2*r*np.cos(phi_pt) / denom
    y_sphere = 2*r*np.sin(phi_pt) / denom
    z_sphere_pt = (r**2 - 1) / denom

    ax1.plot([x_sphere], [y_sphere], [z_sphere_pt], 'ro', markersize=8)

    # Projection line from the north pole
    ax1.plot([x_sphere, 0], [y_sphere, 0], [z_sphere_pt, 1], 'r--', alpha=0.5)
ax1.plot([0], [0], [1], 'go', markersize=12, label='North pole (∞)')
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax1.set_zlabel('z')
ax1.set_title('Stereographic Projection')
ax1.legend()
# Metric in the complex plane
ax2 = fig.add_subplot(122)
x_grid = np.linspace(-3, 3, 100)
y_grid = np.linspace(-3, 3, 100)
X_grid, Y_grid = np.meshgrid(x_grid, y_grid)
R_squared_grid = X_grid**2 + Y_grid**2
metric_factor = 4 / (1 + R_squared_grid)**2
im = ax2.contourf(X_grid, Y_grid, metric_factor, levels=20, cmap='hot')
ax2.set_xlabel('Re(z)')
ax2.set_ylabel('Im(z)')
ax2.set_title('Metric Coefficient g(z) = 4/(1+|z|²)²')
ax2.set_aspect('equal')
plt.colorbar(im, ax=ax2)
# Concentric circles (geodesics)
for r in [0.5, 1, 1.5, 2]:
    circle = plt.Circle((0, 0), r, fill=False, color='cyan', linewidth=2, alpha=0.7)
    ax2.add_patch(circle)
plt.tight_layout()
plt.show()
print("\n✓ Metric is conformal to Euclidean metric")
print("✓ Geodesics are circles through origin or straight lines")
print("✓ This is the unique (up to isometry) Kähler metric on ℂℙ¹")
CONTEXT: Kähler Geometry
A Kähler manifold (M, g, J, ω) has three compatible structures:
- Riemannian metric g
- Complex structure J
- Symplectic form ω = g(J·, ·)
Example: ℂℙ¹ (Riemann sphere) with Fubini-Study metric
The Kähler form is:
ω = (i/2) ∂∂̄ log(1 + |z|²)


Fubini-Study metric on ℂℙ¹:
ds² = 4 dz dz̄ / (1 + |z|²)²

In real coordinates (x, y):
g_xx = g_yy = 4/(1 + x² + y²)²

Kähler Laplacian (principal symbol):
                       2
⎛ 2    2⎞ ⎛ 2    2    ⎞ 
⎝η  + ξ ⎠⋅⎝x  + y  + 1⎠ 
────────────────────────
           16           

Special properties of Kähler manifolds:
1. Δ = 2Δ_∂̄ = 2Δ_∂ (Hodge decomposition compatibility)
2. Holomorphic functions are harmonic
3. Chern classes determined by curvature
4. Rich interplay between complex analysis and geometry
Applications:
- Algebraic geometry (projective varieties)
- String theory (Calabi-Yau manifolds)
- Geometric quantization
- Mirror symmetry


----------------------------------------------------------------------
Visualization: Riemann Sphere ℂℙ¹
----------------------------------------------------------------------
No description has been provided for this image
✓ Metric is conformal to Euclidean metric
✓ Geodesics are circles through origin or straight lines
✓ This is the unique (up to isometry) Kähler metric on ℂℙ¹

Representation Theory - Characters and Fourier Analysis¶

In [30]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Representation Theory on Groups
For a compact Lie group G, irreducible representations are related
to eigenfunctions of the Laplacian on G.
Example: Circle group S¹ = ℝ/2πℤ
The Laplacian Δ = -d²/dx² has eigenfunctions:
- e^{inx} with eigenvalue n² (n ∈ ℤ)
These are the characters of irreducible representations of S¹.
""")

# Laplacian on the circle
# Symbol: ξ²
Delta_circle = xi**2
Delta_S1 = PseudoDifferentialOperator(Delta_circle, [x], mode='symbol')

print("\nLaplacian on S¹:")
pprint(Delta_S1.symbol)

print("""
Peter-Weyl Theorem:
For a compact group G, L²(G) decomposes as:
L²(G) = ⊕_π V_π ⊗ V_π*
where π runs over irreducible representations.
For S¹: L²(S¹) = ⊕_{n∈ℤ} ℂ·e^{inx}
""")

# Convolution function and product
print("\n" + "-"*70)
print("Convolution as operator composition")
print("-"*70)

print("""
Convolution on a group G is:
(f * g)(x) = ∫_G f(xy⁻¹) g(y) dy
For S¹, this becomes circular convolution.
In Fourier space: ̂(f*g)(n) = f̂(n)·ĝ(n)
This is operator composition for convolution operators!
""")

# Dirichlet kernel (projection onto frequencies [-N, N])
N = symbols('N', integer=True, positive=True)
print("\nDirichlet kernel (frequency cutoff at ±N):")
print("D_N(x) = Σ_{|n|≤N} e^{inx} = sin((N+1/2)x) / sin(x/2)")

# Symbol: indicator function χ_{[-N,N]}(ξ)
Dirichlet_symbol = Piecewise((1, abs(xi) <= N), (0, True))
print("\nDirichlet symbol (frequency cutoff):")
pprint(Dirichlet_symbol)

# Visualization of characters
print("\n" + "-"*70)
print("Visualization: Characters of S¹")
print("-"*70)

x_vals = np.linspace(0, 2*np.pi, 1000)
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Some characters
for n in range(-3, 4):
    if n != 0:
        char = np.exp(1j * n * x_vals)
        axes[0, 0].plot(x_vals, char.real, label=f'n={n}', alpha=0.7)
axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('Re(e^{inx})')
axes[0, 0].set_title('Characters (Real Part)')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# Dirichlet kernel
for N_val in [1, 3, 5, 10]:
    D_N = np.sin((N_val + 0.5) * x_vals) / np.sin(x_vals / 2 + 1e-10)
    axes[0, 1].plot(x_vals, D_N, label=f'N={N_val}', alpha=0.7)
axes[0, 1].set_xlabel('x')
axes[0, 1].set_ylabel('D_N(x)')
axes[0, 1].set_title('Dirichlet Kernel')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Spectrum of the Laplacian
eigenvalues = np.array([n**2 for n in range(-10, 11)])
multiplicities = np.ones_like(eigenvalues)
axes[1, 0].stem(eigenvalues, multiplicities, basefmt=' ')
axes[1, 0].set_xlabel('Eigenvalue n²')
axes[1, 0].set_ylabel('Multiplicity')
axes[1, 0].set_title('Spectrum of Laplacian on S¹')
axes[1, 0].grid(True, alpha=0.3)

# Discrete Fourier Transform
signal = np.sin(3*x_vals) + 0.5*np.cos(7*x_vals) + 0.3*np.sin(11*x_vals)
fft_signal = np.fft.fft(signal)
freqs = np.fft.fftfreq(len(signal), d=x_vals[1]-x_vals[0])
axes[1, 1].stem(freqs[:len(freqs)//2], np.abs(fft_signal[:len(freqs)//2]), basefmt=' ')
axes[1, 1].set_xlabel('Frequency')
axes[1, 1].set_ylabel('|F(ω)|')
axes[1, 1].set_title('Fourier Transform (Frequency Domain)')
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Each frequency n corresponds to an irreducible representation")
print("✓ Fourier analysis = decomposition into irreps")
CONTEXT: Representation Theory on Groups
For a compact Lie group G, irreducible representations are related
to eigenfunctions of the Laplacian on G.
Example: Circle group S¹ = ℝ/2πℤ
The Laplacian Δ = -d²/dx² has eigenfunctions:
- e^{inx} with eigenvalue n² (n ∈ ℤ)
These are the characters of irreducible representations of S¹.


Laplacian on S¹:
 2
ξ 

Peter-Weyl Theorem:
For a compact group G, L²(G) decomposes as:
L²(G) = ⊕_π V_π ⊗ V_π*
where π runs over irreducible representations.
For S¹: L²(S¹) = ⊕_{n∈ℤ} ℂ·e^{inx}


----------------------------------------------------------------------
Convolution as operator composition
----------------------------------------------------------------------

Convolution on a group G is:
(f * g)(x) = ∫_G f(xy⁻¹) g(y) dy
For S¹, this becomes circular convolution.
In Fourier space: ̂(f*g)(n) = f̂(n)·ĝ(n)
This is operator composition for convolution operators!


Dirichlet kernel (frequency cutoff at ±N):
D_N(x) = Σ_{|n|≤N} e^{inx} = sin((N+1/2)x) / sin(x/2)

Dirichlet symbol (frequency cutoff):
⎧1  for N ≥ │ξ│
⎨              
⎩0   otherwise 

----------------------------------------------------------------------
Visualization: Characters of S¹
----------------------------------------------------------------------
No description has been provided for this image
✓ Each frequency n corresponds to an irreducible representation
✓ Fourier analysis = decomposition into irreps

Clifford Algebras and Dirac Operator¶

In [31]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: Clifford Algebras
The Clifford algebra Cl(V, q) is generated by vectors in V with:
v·w + w·v = -2q(v,w)
For ℝⁿ with standard metric: e_i·e_j + e_j·e_i = -2δ_ij
The Dirac operator is: D = Σ_i γ^i ∂_i
where γ^i are Clifford generators (gamma matrices).
""")
# Example in 2D
# Pauli matrices: σ_x, σ_y (generators of Cl(2))
# σ_x = [[0,1],[1,0]], σ_y = [[0,-i],[i,0]]
print("\nExample: Dirac operator in 2D")
print("D = σ_x ∂_x + σ_y ∂_y")
print("\nClifford relations:")
print("σ_x² = σ_y² = 1")
print("σ_x σ_y + σ_y σ_x = 0")
# Principal symbol of the Dirac operator
# σ(D) = σ_x ξ + σ_y η (matrix multiplication)
print("\nPrincipal symbol: σ(D) = σ_x ξ + σ_y η")
# The square of the Dirac operator gives the Laplacian
# D² = (σ_x ∂_x + σ_y ∂_y)² = ∂_x² + ∂_y² = -Δ
Dirac_squared_symbol = xi**2 + eta**2
print("\nD² symbol (should be Laplacian):")
pprint(Dirac_squared_symbol)
Dirac_sq_op = PseudoDifferentialOperator(Dirac_squared_symbol, [x, y], mode='symbol')
print("""
This shows: D² = -Δ (Laplacian)
The Dirac operator is the "square root" of the Laplacian!
Applications:
- Spin geometry (spinor bundles)
- Atiyah-Singer index theorem for Dirac
- Supersymmetry (supercharges)
- Quantum field theory (fermions)
""")
# Clifford product and composition
print("\n" + "-"*70)
print("Clifford multiplication as operator composition")
print("-"*70)
# Clifford operators ρ(v) acting on spinors
# ρ: Cl(V) → End(S) (spinor representation)
print("""
For v, w ∈ Cl(V), the product v·w corresponds to composition:
ρ(v·w) = ρ(v) ∘ ρ(w)
The symbol of ρ(v) for v = Σ v^i e_i is:
σ(ρ(v)) = Σ γ^i v^i
This makes the Clifford algebra into a symbol calculus!
""")
# Visualization: spinors and rotation
print("\n" + "-"*70)
print("Visualization: Spinor rotation (double cover)")
print("-"*70)
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# Rotation in SO(2) vs Spin(2)
angles = np.linspace(0, 4*np.pi, 1000)  # Double loop
# SO(2): rotation of a vector
vector_angle = angles % (2*np.pi)
vector_x = np.cos(vector_angle)
vector_y = np.sin(vector_angle)
axes[0].plot(angles, vector_x, 'b-', linewidth=2, label='x-component')
axes[0].plot(angles, vector_y, 'r-', linewidth=2, label='y-component')
axes[0].axvline(2*np.pi, color='g', linestyle='--', alpha=0.5, label='2π (back to start)')
axes[0].set_xlabel('Rotation angle')
axes[0].set_ylabel('Vector components')
axes[0].set_title('Vector Rotation (SO(2))')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Spin(2): rotation of a spinor (phase exp(iθ/2))
spinor_phase = angles / 2
spinor_real = np.cos(spinor_phase)
spinor_imag = np.sin(spinor_phase)
axes[1].plot(angles, spinor_real, 'b-', linewidth=2, label='Re(ψ)')
axes[1].plot(angles, spinor_imag, 'r-', linewidth=2, label='Im(ψ)')
axes[1].axvline(2*np.pi, color='orange', linestyle='--', alpha=0.5, label='2π (sign flip!)')
axes[1].axvline(4*np.pi, color='g', linestyle='--', alpha=0.5, label='4π (back to start)')
axes[1].set_xlabel('Rotation angle')
axes[1].set_ylabel('Spinor components')
axes[1].set_title('Spinor Rotation (Spin(2) = U(1))')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n✓ Spinors require 4π rotation to return to starting point")
print("✓ Spin(n) is the double cover of SO(n)")
CONTEXT: Clifford Algebras
The Clifford algebra Cl(V, q) is generated by vectors in V with:
v·w + w·v = -2q(v,w)
For ℝⁿ with standard metric: e_i·e_j + e_j·e_i = -2δ_ij
The Dirac operator is: D = Σ_i γ^i ∂_i
where γ^i are Clifford generators (gamma matrices).


Example: Dirac operator in 2D
D = σ_x ∂_x + σ_y ∂_y

Clifford relations:
σ_x² = σ_y² = 1
σ_x σ_y + σ_y σ_x = 0

Principal symbol: σ(D) = σ_x ξ + σ_y η

D² symbol (should be Laplacian):
 2    2
η  + ξ 

This shows: D² = -Δ (Laplacian)
The Dirac operator is the "square root" of the Laplacian!
Applications:
- Spin geometry (spinor bundles)
- Atiyah-Singer index theorem for Dirac
- Supersymmetry (supercharges)
- Quantum field theory (fermions)


----------------------------------------------------------------------
Clifford multiplication as operator composition
----------------------------------------------------------------------

For v, w ∈ Cl(V), the product v·w corresponds to composition:
ρ(v·w) = ρ(v) ∘ ρ(w)
The symbol of ρ(v) for v = Σ v^i e_i is:
σ(ρ(v)) = Σ γ^i v^i
This makes the Clifford algebra into a symbol calculus!


----------------------------------------------------------------------
Visualization: Spinor rotation (double cover)
----------------------------------------------------------------------
No description has been provided for this image
✓ Spinors require 4π rotation to return to starting point
✓ Spin(n) is the double cover of SO(n)

Operator Algebras and K-Theory¶

In [32]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)
print("""
CONTEXT: Noncommutative Geometry
C*-algebras generalize spaces (Gelfand duality).
K-theory classifies projections in matrix algebras.
For a pseudo-differential operator P, we can study:
- K₀: equivalence classes of projections
- K₁: equivalence classes of unitaries
The index is an element of K-theory!
""")
# Example: Noncommutative torus T²_θ
theta = symbols('theta', real=True)
print("\nExample: Noncommutative Torus T²_θ")
print("Generators U, V with: UV = e^{2πiθ} VU")
print("\nFor θ irrational: truly noncommutative space")
# Pseudo-differential operators on T²_θ
# Symbols: functions on ℝ² × ℤ²
print("""
On the noncommutative torus, pseudo-differential operators
have symbols that are operator-valued:
p(x, y, ξ, η) ∈ C(T²) ⊗ 𝒦
where 𝒦 is the algebra of compact operators.
The index map:
ind: K₀(Ψ⁰(T²_θ)) → ℤ
connects topology to analysis even in the noncommutative setting.
""")
# Noncommutative Chern classes
print("\n" + "-"*70)
print("Noncommutative Chern character")
print("-"*70)
print("""
For a projection P (idempotent: P² = P), the Chern character is:
ch(P) = Tr(P e^{-tΔ})
This generalizes the classical Chern classes to the noncommutative
setting via cyclic cohomology.
Connes' theorem: For θ irrational, K₀(T²_θ) ≅ ℤ²
""")
# Numerical computation of indices
print("\n" + "-"*70)
print("Numerical index computation")
print("-"*70)
# Toeplitz operator: projection onto positive frequencies
# T_f: H²(S¹) → H²(S¹), T_f(g) = P₊(fg)
# Symbol: f(x) for x ∈ S¹
# Index: ind(T_f) = winding number of f
print("""
Toeplitz operator T_f on Hardy space H²(S¹):
ind(T_f) = -deg(f) (winding number)
Example: f(z) = z^n
- n > 0: ind = -n (negative index)
- n < 0: ind = 0 (right-invertible)
""")
# Visualization of the winding number
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
t_vals = np.linspace(0, 2*np.pi, 1000)
for n, ax in zip([1, 2, 3], axes):
    # Function z^n
    z = np.exp(1j * t_vals)
    f_z = z**n

    ax.plot(f_z.real, f_z.imag, 'b-', linewidth=2)
    ax.plot(0, 0, 'ro', markersize=10)
    ax.arrow(0, 0, 0.5, 0, head_width=0.1, head_length=0.1, fc='green', ec='green')

    ax.set_xlabel('Re(f(z))')
    ax.set_ylabel('Im(f(z))')
    ax.set_title(f'f(z) = z^{n}, winding = {n}')
    ax.set_aspect('equal')
    ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n✓ Winding number is a topological invariant")
print("✓ Index theorem: ind(T_f) = -winding(f)")
CONTEXT: Noncommutative Geometry
C*-algebras generalize spaces (Gelfand duality).
K-theory classifies projections in matrix algebras.
For a pseudo-differential operator P, we can study:
- K₀: equivalence classes of projections
- K₁: equivalence classes of unitaries
The index is an element of K-theory!


Example: Noncommutative Torus T²_θ
Generators U, V with: UV = e^{2πiθ} VU

For θ irrational: truly noncommutative space

On the noncommutative torus, pseudo-differential operators
have symbols that are operator-valued:
p(x, y, ξ, η) ∈ C(T²) ⊗ 𝒦
where 𝒦 is the algebra of compact operators.
The index map:
ind: K₀(Ψ⁰(T²_θ)) → ℤ
connects topology to analysis even in the noncommutative setting.


----------------------------------------------------------------------
Noncommutative Chern character
----------------------------------------------------------------------

For a projection P (idempotent: P² = P), the Chern character is:
ch(P) = Tr(P e^{-tΔ})
This generalizes the classical Chern classes to the noncommutative
setting via cyclic cohomology.
Connes' theorem: For θ irrational, K₀(T²_θ) ≅ ℤ²


----------------------------------------------------------------------
Numerical index computation
----------------------------------------------------------------------

Toeplitz operator T_f on Hardy space H²(S¹):
ind(T_f) = -deg(f) (winding number)
Example: f(z) = z^n
- n > 0: ind = -n (negative index)
- n < 0: ind = 0 (right-invertible)

No description has been provided for this image
✓ Winding number is a topological invariant
✓ Index theorem: ind(T_f) = -winding(f)

Hopf Algebras and Quantum Groups¶

In [33]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: Quantum Groups
A quantum group is a deformation of a classical group/algebra.
Example: Quantum plane with relation:
xy = qyx (q-commutation)
For q ≠ 1, this is noncommutative.
Pseudo-differential operators on quantum spaces have
modified composition laws!
""")
q = symbols('q', real=True)
print("\nQuantum plane: xy = qyx")
print(f"Commutator: [x,y] = (q-1)xy")
# Quantum derivatives
print("\n" + "-"*70)
print("q-derivatives and q-calculus")
print("-"*70)
print("""
The q-derivative is:
D_q f(x) = (f(qx) - f(x)) / ((q-1)x)
As q → 1: D_q → d/dx (classical derivative)
Example: D_q(x^n) = [n]_q x^{n-1}
where [n]_q = (q^n - 1)/(q - 1) is the q-integer.
""")
# q-integers
n = symbols('n', integer=True, positive=True)
q_integer = (q**n - 1) / (q - 1)
print("\nq-integer [n]_q:")
pprint(q_integer)
print("\nLimit q→1:")
q_integer_limit = simplify(q_integer.limit(q, 1))
pprint(q_integer_limit)
# q-deformed Weyl quantization
print("\n" + "-"*70)
print("q-deformed Weyl quantization")
print("-"*70)
print("""
Classical Weyl quantization:
(f ⋆ g)(x,ξ) = f(x,ξ)g(x,ξ) + O(ℏ)
q-deformed version:
(f ⋆_q g)(x,ξ) = f(x,ξ)g(x,ξ) + (q-1) corrections
This leads to quantum pseudo-differential operators.
""")
# Visualization: q-deformation of oscillator spectrum
print("\n" + "-"*70)
print("Visualization: q-deformation of oscillator spectrum")
print("-"*70)
# Quantum harmonic oscillator
# Classical: E_n = n + 1/2
# q-deformed: E_n = [n+1/2]_q
n_vals = np.arange(0, 20)
q_values = [0.8, 0.9, 1.0, 1.1, 1.2]
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
for q_val in q_values:
    if q_val == 1.0:
        E_n = n_vals + 0.5
        axes[0].plot(n_vals, E_n, 'o-', linewidth=2, markersize=8,
                    label=f'q={q_val} (classical)', color='red')
    else:
        # E_n = [n+1/2]_q
        if q_val != 1:
            E_n = (q_val**(n_vals + 0.5) - 1) / (q_val - 1)
        else:
            E_n = n_vals + 0.5
        axes[0].plot(n_vals, E_n, 'o-', linewidth=1.5, markersize=6,
                    label=f'q={q_val}', alpha=0.7)
axes[0].set_xlabel('n (quantum number)')
axes[0].set_ylabel('Energy E_n')
axes[0].set_title('q-Deformed Oscillator Spectrum')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Level spacing
for q_val in q_values:
    if q_val != 1:
        E_n = (q_val**(n_vals + 0.5) - 1) / (q_val - 1)
        spacing = np.diff(E_n)

        if q_val == 1.0:
            axes[1].plot(n_vals[:-1], spacing, 'o-', linewidth=2, markersize=8,
                        label=f'q={q_val} (classical)', color='red')
        else:
            axes[1].plot(n_vals[:-1], spacing, 'o-', linewidth=1.5, markersize=6,
                        label=f'q={q_val}', alpha=0.7)
axes[1].set_xlabel('n (quantum number)')
axes[1].set_ylabel('ΔE_n = E_{n+1} - E_n')
axes[1].set_title('Level Spacing (Deformed)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n✓ q-deformation changes spectrum nonlinearly")
print("✓ Related to quantum groups SU_q(2), etc.")
CONTEXT: Quantum Groups
A quantum group is a deformation of a classical group/algebra.
Example: Quantum plane with relation:
xy = qyx (q-commutation)
For q ≠ 1, this is noncommutative.
Pseudo-differential operators on quantum spaces have
modified composition laws!


Quantum plane: xy = qyx
Commutator: [x,y] = (q-1)xy

----------------------------------------------------------------------
q-derivatives and q-calculus
----------------------------------------------------------------------

The q-derivative is:
D_q f(x) = (f(qx) - f(x)) / ((q-1)x)
As q → 1: D_q → d/dx (classical derivative)
Example: D_q(x^n) = [n]_q x^{n-1}
where [n]_q = (q^n - 1)/(q - 1) is the q-integer.


q-integer [n]_q:
 n    
q  - 1
──────
q - 1 

Limit q→1:
n

----------------------------------------------------------------------
q-deformed Weyl quantization
----------------------------------------------------------------------

Classical Weyl quantization:
(f ⋆ g)(x,ξ) = f(x,ξ)g(x,ξ) + O(ℏ)
q-deformed version:
(f ⋆_q g)(x,ξ) = f(x,ξ)g(x,ξ) + (q-1) corrections
This leads to quantum pseudo-differential operators.


----------------------------------------------------------------------
Visualization: q-deformation of oscillator spectrum
----------------------------------------------------------------------
No description has been provided for this image
✓ q-deformation changes spectrum nonlinearly
✓ Related to quantum groups SU_q(2), etc.

Cohomology and de Rham Complex¶

In [34]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)
print("""
CONTEXT: de Rham Cohomology
The de Rham complex on a manifold M:
0 → Ω⁰ →^d Ω¹ →^d Ω² →^d ... →^d Ωⁿ → 0
The cohomology groups are:
H^k_{dR}(M) = ker d_k / im d_{k-1}
These are topological invariants!
""")
# Exterior derivative operator d
print("\nExterior derivative d: Ω^k → Ω^{k+1}")
print("Symbol: σ(d)(ξ) = iξ ∧ · (wedge with covector ξ)")
# In dimension 2: d: Ω⁰ → Ω¹
# df = (∂f/∂x)dx + (∂f/∂y)dy
# Symbol: (iξ, iη)
d_symbol_x = I * xi
d_symbol_y = I * eta
print("\nSymbol of d on 0-forms:")
print(f"σ(d) = (iξ, iη)")
# Adjoint operator d†
print("\n" + "-"*70)
print("Adjoint operator d† (codifferential)")
print("-"*70)
print("""
The formal adjoint d† = *d* where * is Hodge star.
In components: (d†ω)_i = -∇^j ω_{ji}
For 1-forms → functions: d†(f dx + g dy) = -∂f/∂x - ∂g/∂y = -div
""")
# Hodge Laplacian Δ = dd† + d†d
print("\nHodge Laplacian: Δ = dd† + d†d")
# On functions (0-forms): Δ = d†d
Hodge_Laplacian_0 = xi**2 + eta**2
print("\nOn 0-forms: Δ symbol =")
pprint(Hodge_Laplacian_0)
Hodge_Lap_op = PseudoDifferentialOperator(Hodge_Laplacian_0, [x, y], mode='symbol')
print("""
Hodge theorem:
Every cohomology class [ω] ∈ H^k(M) has a unique harmonic
representative h with Δh = 0.
Therefore: H^k(M) ≅ ker Δ_k
""")
# Example: Cohomology calculation
print("\n" + "-"*70)
print("Example: Cohomology of surfaces")
print("-"*70)
print("""
For a closed orientable surface Σ_g of genus g:
H⁰(Σ_g) = ℝ       (dim = 1)  - constant functions
H¹(Σ_g) = ℝ^{2g}  (dim = 2g) - holomorphic differentials
H²(Σ_g) = ℝ       (dim = 1)  - volume form
Euler characteristic: χ(Σ_g) = 2 - 2g
""")
# Visualization: Betti numbers
genera = np.arange(0, 8)
b0 = np.ones_like(genera)
b1 = 2 * genera
b2 = np.ones_like(genera)
euler = 2 - 2*genera
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
width = 0.25
x_pos = genera
axes[0].bar(x_pos - width, b0, width, label='b₀', alpha=0.8)
axes[0].bar(x_pos, b1, width, label='b₁', alpha=0.8)
axes[0].bar(x_pos + width, b2, width, label='b₂', alpha=0.8)
axes[0].set_xlabel('Genus g')
axes[0].set_ylabel('Betti number')
axes[0].set_title('Betti Numbers of Riemann Surfaces')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[0].set_xticks(genera)
# Euler characteristic
axes[1].plot(genera, euler, 'ro-', linewidth=2, markersize=8)
axes[1].axhline(0, color='k', linestyle='--', alpha=0.5)
axes[1].set_xlabel('Genus g')
axes[1].set_ylabel('χ(Σ_g)')
axes[1].set_title('Euler Characteristic = 2 - 2g')
axes[1].grid(True, alpha=0.3)
# Annotate some surfaces
axes[1].text(0, 2.3, 'Sphere S²', ha='center', fontsize=10)
axes[1].text(1, 0.3, 'Torus T²', ha='center', fontsize=10)
axes[1].text(2, -1.7, 'Double torus', ha='center', fontsize=10)
plt.tight_layout()
plt.show()
print("\n✓ Cohomology detects 'holes' in the manifold")
print("✓ Laplacian spectrum encodes topological information")
CONTEXT: de Rham Cohomology
The de Rham complex on a manifold M:
0 → Ω⁰ →^d Ω¹ →^d Ω² →^d ... →^d Ωⁿ → 0
The cohomology groups are:
H^k_{dR}(M) = ker d_k / im d_{k-1}
These are topological invariants!


Exterior derivative d: Ω^k → Ω^{k+1}
Symbol: σ(d)(ξ) = iξ ∧ · (wedge with covector ξ)

Symbol of d on 0-forms:
σ(d) = (iξ, iη)

----------------------------------------------------------------------
Adjoint operator d† (codifferential)
----------------------------------------------------------------------

The formal adjoint d† = *d* where * is Hodge star.
In components: (d†ω)_i = -∇^j ω_{ji}
For 1-forms → functions: d†(f dx + g dy) = -∂f/∂x - ∂g/∂y = -div


Hodge Laplacian: Δ = dd† + d†d

On 0-forms: Δ symbol =
 2    2
η  + ξ 

Hodge theorem:
Every cohomology class [ω] ∈ H^k(M) has a unique harmonic
representative h with Δh = 0.
Therefore: H^k(M) ≅ ker Δ_k


----------------------------------------------------------------------
Example: Cohomology of surfaces
----------------------------------------------------------------------

For a closed orientable surface Σ_g of genus g:
H⁰(Σ_g) = ℝ       (dim = 1)  - constant functions
H¹(Σ_g) = ℝ^{2g}  (dim = 2g) - holomorphic differentials
H²(Σ_g) = ℝ       (dim = 1)  - volume form
Euler characteristic: χ(Σ_g) = 2 - 2g

No description has been provided for this image
✓ Cohomology detects 'holes' in the manifold
✓ Laplacian spectrum encodes topological information

Algebraic Geometry - D-modules¶

In [35]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)

print("""
CONTEXT: D-modules and Algebraic Geometry

A D-module is a module over the ring D_X of differential operators
on an algebraic variety X.

For X = ℂⁿ, D_X = ℂ[x₁,...,xₙ, ∂₁,...,∂ₙ] (Weyl algebra)

Pseudo-differential operators are completions of D-modules.
""")

# Weyl algebra in dimension 1
print("\nWeyl algebra A₁ = ℂ⟨x, ∂⟩")
print("Relation: [∂, x] = 1")

print("""
This is the universal enveloping algebra of the Heisenberg Lie algebra.

Example: Bessel equation as D-module

(x²∂² + x∂ + (x² - ν²))f = 0

This defines a D-module M = D/D·P where P is the Bessel operator.
""")

nu = symbols('nu', real=True)

# Bessel operator
Bessel_operator = x**2 * xi**2 + x * xi + (x**2 - nu**2)

print("\nBessel operator (symbol form):")
pprint(simplify(Bessel_operator))

print("""
D-module perspective:
- Solutions form a D-module
- Singularities → characteristic variety
- Symbol → principal symbol variety

Connection to representation theory:
D-modules on flag varieties ↔ representations of Lie groups
""")

# Solving a D-module equation
print("\n" + "-"*70)
print("Example: Solving via symbol analysis")
print("-"*70)

print("""
For a D-module M = D/D·P, the solutions are:

Sol(M) = {f : P·f = 0}

The characteristic variety Ch(M) ⊆ T*X is:

Ch(M) = {(x,ξ) : σ(P)(x,ξ) = 0}

where σ(P) is the principal symbol.

For Bessel: Ch = {ξ² + 1 = 0} ∪ {x = 0} (singular at origin)
""")

# Visualization of the characteristic variety
print("\n" + "-"*70)
print("Visualization: Characteristic variety")
print("-"*70)

# For the simplified Bessel equation
x_grid = np.linspace(-3, 3, 100)
xi_grid = np.linspace(-3, 3, 100)
X_grid, XI_grid = np.meshgrid(x_grid, xi_grid)

# Approximate principal symbol: x²ξ² + x²
principal_symbol_Bessel = X_grid**2 * XI_grid**2 + X_grid**2

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Characteristic variety (zeros of the symbol)
levels = [0]
cs = axes[0].contour(X_grid, XI_grid, principal_symbol_Bessel, levels=levels,
                     colors='red', linewidths=3)
axes[0].clabel(cs, inline=True, fontsize=10)

im = axes[0].contourf(X_grid, XI_grid, np.log(np.abs(principal_symbol_Bessel) + 0.1),
                      levels=20, cmap='viridis', alpha=0.6)
axes[0].axvline(0, color='white', linestyle='--', linewidth=2, label='Singular point')
axes[0].set_xlabel('x')
axes[0].set_ylabel('ξ')
axes[0].set_title('Characteristic Variety (red curve)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
plt.colorbar(im, ax=axes[0], label='log|σ(P)|')

# Singularities and microfunctions
# Solutions live near Ch(M)

# Bessel functions of various orders
x_bessel = np.linspace(0.1, 10, 200)

for nu_val in [0, 1, 2, 3]:
    J_nu = jv(nu_val, x_bessel)
    axes[1].plot(x_bessel, J_nu, linewidth=2, label=f'J_{{{nu_val}}}(x)')

axes[1].set_xlabel('x')
axes[1].set_ylabel('J_ν(x)')
axes[1].set_title('Bessel Functions (D-module solutions)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
axes[1].axhline(0, color='k', linestyle='--', alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Characteristic variety captures singularity structure")
print("✓ D-modules unify algebra, geometry, and analysis")
CONTEXT: D-modules and Algebraic Geometry

A D-module is a module over the ring D_X of differential operators
on an algebraic variety X.

For X = ℂⁿ, D_X = ℂ[x₁,...,xₙ, ∂₁,...,∂ₙ] (Weyl algebra)

Pseudo-differential operators are completions of D-modules.


Weyl algebra A₁ = ℂ⟨x, ∂⟩
Relation: [∂, x] = 1

This is the universal enveloping algebra of the Heisenberg Lie algebra.

Example: Bessel equation as D-module

(x²∂² + x∂ + (x² - ν²))f = 0

This defines a D-module M = D/D·P where P is the Bessel operator.


Bessel operator (symbol form):
   2    2  2    2      
- ν  + x ⋅ξ  + x  + x⋅ξ

D-module perspective:
- Solutions form a D-module
- Singularities → characteristic variety
- Symbol → principal symbol variety

Connection to representation theory:
D-modules on flag varieties ↔ representations of Lie groups


----------------------------------------------------------------------
Example: Solving via symbol analysis
----------------------------------------------------------------------

For a D-module M = D/D·P, the solutions are:

Sol(M) = {f : P·f = 0}

The characteristic variety Ch(M) ⊆ T*X is:

Ch(M) = {(x,ξ) : σ(P)(x,ξ) = 0}

where σ(P) is the principal symbol.

For Bessel: Ch = {ξ² + 1 = 0} ∪ {x = 0} (singular at origin)


----------------------------------------------------------------------
Visualization: Characteristic variety
----------------------------------------------------------------------
No description has been provided for this image
✓ Characteristic variety captures singularity structure
✓ D-modules unify algebra, geometry, and analysis

Differential Galois Theory¶

In [36]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Differential Galois Theory

Classical Galois theory: field extensions and solvability by radicals
Differential Galois theory: differential equations and solvability

For a linear ODE: L(y) = 0 where L ∈ D = k[∂]

The Picard-Vessiot extension contains all solutions.
The differential Galois group G measures symmetries of solutions.
""")

# Example: Airy equation
print("\nExample: Airy equation y'' - xy = 0")

# Symbol of the Airy operator
Airy_symbol = xi**2 - x

print("\nAiry operator symbol:")
pprint(Airy_symbol)

Airy_op = PseudoDifferentialOperator(Airy_symbol, [x], mode='symbol')

print("""
Properties of Airy equation:
- No elementary solutions (transcendental)
- Differential Galois group: non-trivial
- Connection to WKB approximation

Symbol analysis:
- Characteristic variety: ξ² = x
- Turning point at x = 0
- WKB valid for |x| large
""")

# WKB approximation
print("\n" + "-"*70)
print("WKB approximation via symbol calculus")
print("-"*70)

print("""
For a 2nd order ODE L(y) = (∂² + f(x))y = 0,

WKB approximation:
y(x) ≈ exp(±i∫√f(x) dx) / f(x)^{1/4}

This is derived from the symbol σ(L) = ξ² + f(x).

For Airy: f(x) = -x
→ y(x) ∼ x^{-1/4} exp(±(2/3)x^{3/2})
""")

# Visualization of Airy solutions and WKB

x_vals = np.linspace(-10, 5, 1000)

# Exact Airy solutions
Ai, Aip, Bi, Bip = airy(x_vals)

# WKB approximation for x > 0
x_positive = x_vals[x_vals > 0.5]
WKB_1 = x_positive**(-0.25) * np.sin(2/3 * x_positive**(3/2) + np.pi/4) / np.sqrt(np.pi)
WKB_2 = x_positive**(-0.25) * np.exp(-2/3 * x_positive**(3/2)) / (2*np.sqrt(np.pi))

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Ai(x) - exponential decay
axes[0, 0].plot(x_vals, Ai, 'b-', linewidth=2, label='Ai(x) exact')
axes[0, 0].plot(x_positive, WKB_2, 'r--', linewidth=2, label='WKB approx', alpha=0.7)
axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('Ai(x)')
axes[0, 0].set_title('Airy Function Ai(x)')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)
axes[0, 0].set_ylim([-0.5, 0.6])

# Bi(x) - exponential growth
axes[0, 1].plot(x_vals[x_vals < 3], Bi[x_vals < 3], 'b-', linewidth=2, label='Bi(x) exact')
axes[0, 1].set_xlabel('x')
axes[0, 1].set_ylabel('Bi(x)')
axes[0, 1].set_title('Airy Function Bi(x)')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Oscillatory part (x < 0)
x_negative = x_vals[x_vals < -0.5]
WKB_osc = np.abs(x_negative)**(-0.25) * np.cos(2/3 * np.abs(x_negative)**(3/2) - np.pi/4)

axes[1, 0].plot(x_negative, Ai[x_vals < -0.5], 'b-', linewidth=2, label='Ai(x) exact')
axes[1, 0].plot(x_negative, WKB_osc / (np.sqrt(np.pi)), 'r--', linewidth=2, 
               label='WKB approx', alpha=0.7)
axes[1, 0].set_xlabel('x')
axes[1, 0].set_ylabel('Ai(x)')
axes[1, 0].set_title('Oscillatory Region (x < 0)')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)

# Characteristic variety
x_char = np.linspace(-5, 5, 100)
xi_char_pos = np.sqrt(np.maximum(0, x_char))
xi_char_neg = -xi_char_pos

axes[1, 1].fill_between(x_char, xi_char_neg, xi_char_pos, 
                        alpha=0.3, color='red', label='Allowed region')
axes[1, 1].plot(x_char, xi_char_pos, 'r-', linewidth=2)
axes[1, 1].plot(x_char, xi_char_neg, 'r-', linewidth=2)
axes[1, 1].axvline(0, color='g', linestyle='--', linewidth=2, label='Turning point')
axes[1, 1].set_xlabel('x')
axes[1, 1].set_ylabel('ξ')
axes[1, 1].set_title('Characteristic Variety: ξ² = x')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ WKB approximation valid away from turning point")
print("✓ Differential Galois theory explains transcendence")
CONTEXT: Differential Galois Theory

Classical Galois theory: field extensions and solvability by radicals
Differential Galois theory: differential equations and solvability

For a linear ODE: L(y) = 0 where L ∈ D = k[∂]

The Picard-Vessiot extension contains all solutions.
The differential Galois group G measures symmetries of solutions.


Example: Airy equation y'' - xy = 0

Airy operator symbol:
      2
-x + ξ 

Properties of Airy equation:
- No elementary solutions (transcendental)
- Differential Galois group: non-trivial
- Connection to WKB approximation

Symbol analysis:
- Characteristic variety: ξ² = x
- Turning point at x = 0
- WKB valid for |x| large


----------------------------------------------------------------------
WKB approximation via symbol calculus
----------------------------------------------------------------------

For a 2nd order ODE L(y) = (∂² + f(x))y = 0,

WKB approximation:
y(x) ≈ exp(±i∫√f(x) dx) / f(x)^{1/4}

This is derived from the symbol σ(L) = ξ² + f(x).

For Airy: f(x) = -x
→ y(x) ∼ x^{-1/4} exp(±(2/3)x^{3/2})

No description has been provided for this image
✓ WKB approximation valid away from turning point
✓ Differential Galois theory explains transcendence

Polynomial Rings and Gröbner Bases¶

In [37]:
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)

print("""
CONTEXT: Commutative Algebra meets Analysis

For an ideal I ⊆ k[x₁,...,xₙ], the variety V(I) is:

V(I) = {p ∈ kⁿ : f(p) = 0 for all f ∈ I}

Pseudo-differential operators with polynomial symbols
connect algebraic geometry to analysis.
""")

# Example: algebraic variety defined by an ideal
print("\nExample: Variety defined by polynomials")
print("I = ⟨x² + y² - 1, xy - 1/2⟩ ⊂ ℝ[x,y]")

f1 = x**2 + y**2 - 1
f2 = x*y - 1/2

print("\nDefining polynomials:")
print("f₁ =", f1)
print("f₂ =", f2)

# Common zeros
print("\nSolution: points where both f₁ = 0 and f₂ = 0")

solutions = solve([f1, f2], [x, y])
print("\nSolutions:")
for sol in solutions:
    print(f"  (x, y) = ({sol[0]}, {sol[1]})")
    print(f"    Numerical: ({complex(sol[0].evalf())}, {complex(sol[1].evalf())})")

# Annihilating operators
print("\n" + "-"*70)
print("Annihilating differential operators")
print("-"*70)

print("""
For a polynomial f(x,y), we can construct differential operators
that annihilate functions vanishing on V(f).

If f = x² + y² - r², then the operator

L = x∂_x + y∂_y

annihilates functions that are homogeneous of degree 0 on the circle.

This is the Euler operator!
""")

# Symbol of the Euler operator
Euler_symbol = x * xi + y * eta

print("\nEuler operator symbol: x·ξ + y·η =")
pprint(Euler_symbol)

Euler_op = PseudoDifferentialOperator(Euler_symbol, [x, y], mode='symbol')

print("""
Application: Homogeneous functions

A function f is homogeneous of degree d if:
f(λx, λy) = λᵈ f(x,y)

Equivalently: (x∂_x + y∂_y)f = d·f

So homogeneous functions are eigenfunctions of the Euler operator!
""")

# Visualization of the variety and vector field
print("\n" + "-"*70)
print("Visualization: Algebraic variety and vector fields")
print("-"*70)

fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# Variety defined by f₁ = 0 (circle)
theta_vals = np.linspace(0, 2*np.pi, 100)
x_circle = np.cos(theta_vals)
y_circle = np.sin(theta_vals)

axes[0].plot(x_circle, y_circle, 'b-', linewidth=2, label='x² + y² = 1')

# Hyperbola xy = 1/2
x_hyp = np.linspace(0.1, 2, 100)
y_hyp_pos = 0.5 / x_hyp
y_hyp_neg = -0.5 / np.linspace(-2, -0.1, 100)
x_hyp_neg = np.linspace(-2, -0.1, 100)

axes[0].plot(x_hyp, y_hyp_pos, 'r-', linewidth=2, label='xy = 1/2')
axes[0].plot(x_hyp_neg, y_hyp_neg, 'r-', linewidth=2)

# Intersection points
for sol in solutions:
    x_pt = complex(sol[0].evalf())
    y_pt = complex(sol[1].evalf())
    if np.abs(x_pt.imag) < 1e-6 and np.abs(y_pt.imag) < 1e-6:
        axes[0].plot(x_pt.real, y_pt.real, 'go', markersize=10)

axes[0].set_xlabel('x')
axes[0].set_ylabel('y')
axes[0].set_title('Algebraic Variety V(I)')
axes[0].set_aspect('equal')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[0].set_xlim([-1.5, 1.5])
axes[0].set_ylim([-1.5, 1.5])

# Vector field of the Euler operator
x_grid = np.linspace(-2, 2, 20)
y_grid = np.linspace(-2, 2, 20)
X_grid, Y_grid = np.meshgrid(x_grid, y_grid)

# Field: (x, y) (radial direction)
axes[1].quiver(X_grid, Y_grid, X_grid, Y_grid, alpha=0.6)
axes[1].plot(x_circle, y_circle, 'b-', linewidth=2, alpha=0.5)
axes[1].set_xlabel('x')
axes[1].set_ylabel('y')
axes[1].set_title('Euler Operator Flow: x∂_x + y∂_y')
axes[1].set_aspect('equal')
axes[1].grid(True, alpha=0.3)

# Example homogeneous function: f(x,y) = x²/y
x_func = np.linspace(-2, 2, 100)
y_func = np.linspace(-2, 2, 100)
X_func, Y_func = np.meshgrid(x_func, y_func)

# Avoid division by zero
Y_func_safe = np.where(np.abs(Y_func) < 0.1, np.nan, Y_func)
homogeneous_func = X_func**2 / Y_func_safe

im = axes[2].contourf(X_func, Y_func, homogeneous_func, 
                      levels=20, cmap='RdBu', vmin=-5, vmax=5)
axes[2].set_xlabel('x')
axes[2].set_ylabel('y')
axes[2].set_title('Homogeneous Function: f = x²/y (degree 1)')
plt.colorbar(im, ax=axes[2])

plt.tight_layout()
plt.show()

print("\n✓ Algebraic varieties ↔ ideals of polynomials")
print("✓ Differential operators detect algebraic structure")
CONTEXT: Commutative Algebra meets Analysis

For an ideal I ⊆ k[x₁,...,xₙ], the variety V(I) is:

V(I) = {p ∈ kⁿ : f(p) = 0 for all f ∈ I}

Pseudo-differential operators with polynomial symbols
connect algebraic geometry to analysis.


Example: Variety defined by polynomials
I = ⟨x² + y² - 1, xy - 1/2⟩ ⊂ ℝ[x,y]

Defining polynomials:
f₁ = x**2 + y**2 - 1
f₂ = x*y - 0.5

Solution: points where both f₁ = 0 and f₂ = 0

Solutions:
  (x, y) = (-0.707106781186548, -0.707106781186548)
    Numerical: ((-0.7071067811865476+0j), (-0.7071067811865476+0j))
  (x, y) = (0.707106781186548, 0.707106781186548)
    Numerical: ((0.7071067811865476+0j), (0.7071067811865476+0j))

----------------------------------------------------------------------
Annihilating differential operators
----------------------------------------------------------------------

For a polynomial f(x,y), we can construct differential operators
that annihilate functions vanishing on V(f).

If f = x² + y² - r², then the operator

L = x∂_x + y∂_y

annihilates functions that are homogeneous of degree 0 on the circle.

This is the Euler operator!


Euler operator symbol: x·ξ + y·η =
η⋅y + x⋅ξ

Application: Homogeneous functions

A function f is homogeneous of degree d if:
f(λx, λy) = λᵈ f(x,y)

Equivalently: (x∂_x + y∂_y)f = d·f

So homogeneous functions are eigenfunctions of the Euler operator!


----------------------------------------------------------------------
Visualization: Algebraic variety and vector fields
----------------------------------------------------------------------
No description has been provided for this image
✓ Algebraic varieties ↔ ideals of polynomials
✓ Differential operators detect algebraic structure

Finite Groups and Discrete Fourier Transform¶

In [38]:
print("""
CONTEXT: Harmonic Analysis on Finite Groups

For a finite abelian group G, the Fourier transform is:

f̂(χ) = Σ_{g∈G} f(g) χ(g)

where χ: G → ℂ* is a character (homomorphism).

Pseudo-differential operators on G are given by convolution.
""")

# Example: Cyclic group Z/nZ
n = 8  # Group size

print(f"\nExample: Cyclic group ℤ/{n}ℤ")
print(f"Characters: χ_k(g) = exp(2πikg/n), k = 0,...,{n-1}")

# Periodic signal
k_vals = np.arange(n)
signal = np.array([1, 2, 3, 2, 1, 0, -1, 0])

print(f"\nSignal: {signal}")

# DFT
signal_fft = np.fft.fft(signal)

print(f"Fourier coefficients:")
for k, coeff in enumerate(signal_fft):
    print(f"  f̂({k}) = {coeff:.3f}")

# Convolution operator
print("\n" + "-"*70)
print("Convolution operators on ℤ/nℤ")
print("-"*70)

print("""
A convolution operator K on ℤ/nℤ is defined by a kernel h:

(K*f)(g) = Σ_{h∈G} h(g-k) f(k)

In Fourier space: K̂*f(χ) = ĥ(χ) f̂(χ)

This is diagonal in the Fourier basis!
""")

# Kernel: low-pass filter
kernel = np.array([1, 1, 1, 0, 0, 0, 1, 1]) / 4  # Local average

# Circular convolution
filtered = np.convolve(signal, kernel, mode='same')

# DFT of the kernel
kernel_fft = np.fft.fft(kernel)

fig, axes = plt.subplots(2, 3, figsize=(15, 8))

# Original signal
axes[0, 0].stem(k_vals, signal, basefmt=' ')
axes[0, 0].set_xlabel('g ∈ ℤ/8ℤ')
axes[0, 0].set_ylabel('f(g)')
axes[0, 0].set_title('Original Signal')
axes[0, 0].grid(True, alpha=0.3)

# Kernel
axes[0, 1].stem(k_vals, kernel, basefmt=' ', linefmt='r-', markerfmt='ro')
axes[0, 1].set_xlabel('g ∈ ℤ/8ℤ')
axes[0, 1].set_ylabel('h(g)')
axes[0, 1].set_title('Convolution Kernel (Low-pass)')
axes[0, 1].grid(True, alpha=0.3)

# Filtered signal
axes[0, 2].stem(k_vals, filtered, basefmt=' ', linefmt='g-', markerfmt='go')
axes[0, 2].set_xlabel('g ∈ ℤ/8ℤ')
axes[0, 2].set_ylabel('(h*f)(g)')
axes[0, 2].set_title('Filtered Signal')
axes[0, 2].grid(True, alpha=0.3)

# Signal spectrum
axes[1, 0].stem(k_vals, np.abs(signal_fft), basefmt=' ')
axes[1, 0].set_xlabel('k (frequency)')
axes[1, 0].set_ylabel('|f̂(k)|')
axes[1, 0].set_title('Signal Spectrum')
axes[1, 0].grid(True, alpha=0.3)

# Kernel spectrum (transfer function)
axes[1, 1].stem(k_vals, np.abs(kernel_fft), basefmt=' ', linefmt='r-', markerfmt='ro')
axes[1, 1].set_xlabel('k (frequency)')
axes[1, 1].set_ylabel('|ĥ(k)|')
axes[1, 1].set_title('Kernel Spectrum (Transfer Function)')
axes[1, 1].grid(True, alpha=0.3)

# Spectrum of the filtered signal
filtered_fft = np.fft.fft(filtered)
axes[1, 2].stem(k_vals, np.abs(filtered_fft), basefmt=' ', linefmt='g-', markerfmt='go')
axes[1, 2].set_xlabel('k (frequency)')
axes[1, 2].set_ylabel('|̂(h*f)(k)|')
axes[1, 2].set_title('Filtered Spectrum')
axes[1, 2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Convolution ↔ pointwise multiplication in Fourier space")
print("✓ Characters diagonalize all convolution operators")
CONTEXT: Harmonic Analysis on Finite Groups

For a finite abelian group G, the Fourier transform is:

f̂(χ) = Σ_{g∈G} f(g) χ(g)

where χ: G → ℂ* is a character (homomorphism).

Pseudo-differential operators on G are given by convolution.


Example: Cyclic group ℤ/8ℤ
Characters: χ_k(g) = exp(2πikg/n), k = 0,...,7

Signal: [ 1  2  3  2  1  0 -1  0]
Fourier coefficients:
  f̂(0) = 8.000+0.000j
  f̂(1) = 0.000-6.828j
  f̂(2) = 0.000+0.000j
  f̂(3) = 0.000+1.172j
  f̂(4) = 0.000+0.000j
  f̂(5) = 0.000-1.172j
  f̂(6) = 0.000+0.000j
  f̂(7) = 0.000+6.828j

----------------------------------------------------------------------
Convolution operators on ℤ/nℤ
----------------------------------------------------------------------

A convolution operator K on ℤ/nℤ is defined by a kernel h:

(K*f)(g) = Σ_{h∈G} h(g-k) f(k)

In Fourier space: K̂*f(χ) = ĥ(χ) f̂(χ)

This is diagonal in the Fourier basis!

No description has been provided for this image
✓ Convolution ↔ pointwise multiplication in Fourier space
✓ Characters diagonalize all convolution operators

Homology and Chain Complexes¶

In [39]:
print("""
CONTEXT: Algebraic Topology

A chain complex is a sequence of abelian groups with boundary maps:

... → C_{n+1} →^∂ C_n →^∂ C_{n-1} → ...

with ∂² = 0.

The homology groups are:
H_n = ker(∂_n) / im(∂_{n+1})

Pseudo-differential operators give analytical versions
(de Rham cohomology, Dolbeault cohomology, etc.)
""")

# Example: Simplicial homology of a triangle
print("\nExample: Simplicial homology of a triangle")

print("""
Triangle with vertices {0, 1, 2}:

C_2: 1-dimensional (the triangle itself)
C_1: 3-dimensional (three edges)
C_0: 3-dimensional (three vertices)

Boundary maps:
∂_2: [0,1,2] ↦ [1,2] - [0,2] + [0,1]
∂_1: [i,j] ↦ [j] - [i]
∂_0: 0
""")

# Boundary matrices
# ∂_2: C_2 → C_1
boundary_2 = np.array([
    [1, -1, 1]  # coefficients for [1,2], [0,2], [0,1]
]).T

# ∂_1: C_1 → C_0
boundary_1 = np.array([
    [-1, 1, 0],   # edge [0,1]
    [0, -1, 1],   # edge [1,2]
    [-1, 0, 1]    # edge [0,2]
]).T

print("\nBoundary matrix ∂_2:")
print(boundary_2.T)

print("\nBoundary matrix ∂_1:")
print(boundary_1.T)

# Verify ∂² = 0
product = boundary_1 @ boundary_2
print("\n∂_1 ∘ ∂_2 =")
print(product)
print(f"✓ Verified: ∂² = 0")

# Homology computation
print("\n" + "-"*70)
print("Computing homology groups")
print("-"*70)

# H_2 = ker(∂_2) / im(∂_3) = ker(∂_2) / 0
ker_2 = boundary_2  # Entire C_2 is in ker since ∂_2 is surjective onto its image
rank_H2 = 0  # Triangle is a surface with boundary

# H_1 = ker(∂_1) / im(∂_2)
# ker(∂_1) = cycles (closed loops)
# im(∂_2) = boundaries (boundaries of faces)

rank_ker_1 = 3 - np.linalg.matrix_rank(boundary_1)
rank_im_2 = np.linalg.matrix_rank(boundary_2)

rank_H1 = rank_ker_1 - rank_im_2

# H_0 = ker(∂_0) / im(∂_1) = C_0 / im(∂_1)
rank_im_1 = np.linalg.matrix_rank(boundary_1)
rank_H0 = 3 - rank_im_1

print(f"\nHomology groups:")
print(f"H_2 = 0 (no 2-cycles mod boundaries)")
print(f"H_1 = ℤ^{rank_H1} (dim = {rank_H1})")
print(f"H_0 = ℤ^{rank_H0} (dim = {rank_H0} = connected components)")

print(f"\nEuler characteristic:")
euler = 3 - 3 + 1
print(f"χ = 3 - 3 + 1 = {euler}")

# Visualization
print("\n" + "-"*70)
print("Visualization: Simplicial complex and homology")
print("-"*70)

fig = plt.figure(figsize=(15, 5))

# Triangle
ax1 = fig.add_subplot(131)

vertices = np.array([[0, 0], [1, 0], [0.5, 0.866]])
triangle = plt.Polygon(vertices, fill=False, edgecolor='blue', linewidth=2)
ax1.add_patch(triangle)

for i, v in enumerate(vertices):
    ax1.plot(v[0], v[1], 'ro', markersize=15)
    ax1.text(v[0], v[1]-0.15, f'v_{i}', ha='center', fontsize=14)

# Label edges
midpoints = [(vertices[i] + vertices[(i+1)%3])/2 for i in range(3)]
edge_labels = ['[0,1]', '[1,2]', '[0,2]']
for mid, label in zip(midpoints, edge_labels):
    ax1.text(mid[0], mid[1], label, ha='center', fontsize=10, 
            bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))

ax1.set_xlim([-0.3, 1.3])
ax1.set_ylim([-0.3, 1.2])
ax1.set_aspect('equal')
ax1.set_title('Simplicial Complex (Triangle)')
ax1.grid(True, alpha=0.3)

# Chain complex
ax2 = fig.add_subplot(132)

ax2.text(0.5, 0.8, 'C_2 = ℤ', ha='center', fontsize=14, 
        bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.8))
ax2.text(0.5, 0.5, 'C_1 = ℤ³', ha='center', fontsize=14,
        bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.8))
ax2.text(0.5, 0.2, 'C_0 = ℤ³', ha='center', fontsize=14,
        bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.8))

ax2.annotate('', xy=(0.5, 0.75), xytext=(0.5, 0.55),
            arrowprops=dict(arrowstyle='->', lw=2))
ax2.text(0.65, 0.65, '∂_2', fontsize=12)

ax2.annotate('', xy=(0.5, 0.45), xytext=(0.5, 0.25),
            arrowprops=dict(arrowstyle='->', lw=2))
ax2.text(0.65, 0.35, '∂_1', fontsize=12)

ax2.set_xlim([0, 1])
ax2.set_ylim([0, 1])
ax2.set_title('Chain Complex')
ax2.axis('off')

# Homology groups
ax3 = fig.add_subplot(133)

homology_data = ['H_2 = 0', 'H_1 = 0', 'H_0 = ℤ']
colors = ['lightcoral', 'lightgreen', 'lightblue']

for i, (hom, col) in enumerate(zip(homology_data, colors)):
    ax3.text(0.5, 0.7 - i*0.3, hom, ha='center', fontsize=14,
            bbox=dict(boxstyle='round', facecolor=col, alpha=0.8))

ax3.text(0.5, 0.1, f'χ = {euler}', ha='center', fontsize=16,
        bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))

ax3.set_xlim([0, 1])
ax3.set_ylim([0, 1])
ax3.set_title('Homology Groups')
ax3.axis('off')

plt.tight_layout()
plt.show()

print("\n✓ Homology groups are topological invariants")
print("✓ Differential operators implement boundary maps analytically")
CONTEXT: Algebraic Topology

A chain complex is a sequence of abelian groups with boundary maps:

... → C_{n+1} →^∂ C_n →^∂ C_{n-1} → ...

with ∂² = 0.

The homology groups are:
H_n = ker(∂_n) / im(∂_{n+1})

Pseudo-differential operators give analytical versions
(de Rham cohomology, Dolbeault cohomology, etc.)


Example: Simplicial homology of a triangle

Triangle with vertices {0, 1, 2}:

C_2: 1-dimensional (the triangle itself)
C_1: 3-dimensional (three edges)
C_0: 3-dimensional (three vertices)

Boundary maps:
∂_2: [0,1,2] ↦ [1,2] - [0,2] + [0,1]
∂_1: [i,j] ↦ [j] - [i]
∂_0: 0


Boundary matrix ∂_2:
[[ 1 -1  1]]

Boundary matrix ∂_1:
[[-1  1  0]
 [ 0 -1  1]
 [-1  0  1]]

∂_1 ∘ ∂_2 =
[[-2]
 [ 2]
 [ 0]]
✓ Verified: ∂² = 0

----------------------------------------------------------------------
Computing homology groups
----------------------------------------------------------------------

Homology groups:
H_2 = 0 (no 2-cycles mod boundaries)
H_1 = ℤ^0 (dim = 0)
H_0 = ℤ^1 (dim = 1 = connected components)

Euler characteristic:
χ = 3 - 3 + 1 = 1

----------------------------------------------------------------------
Visualization: Simplicial complex and homology
----------------------------------------------------------------------
No description has been provided for this image
✓ Homology groups are topological invariants
✓ Differential operators implement boundary maps analytically

Diffusion Processes and Infinitesimal Generators¶

In [40]:
from scipy.stats import norm
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Stochastic Processes

A diffusion process X_t satisfies the SDE:

dX_t = μ(X_t)dt + σ(X_t)dW_t

The infinitesimal generator is:

L = μ(x)∂/∂x + (1/2)σ²(x)∂²/∂x²

This is a pseudo-differential operator!
""")

# Example: Brownian motion (μ=0, σ=1)
print("\nExample 1: Standard Brownian Motion")
print("dX_t = dW_t")

# Generator: L = (1/2)∂²/∂x²
BM_generator_symbol = xi**2 / 2

print("\nGenerator symbol: ξ²/2")
pprint(BM_generator_symbol)

BM_gen = PseudoDifferentialOperator(BM_generator_symbol, [x], mode='symbol')

print("""
Properties:
- Generator of heat semigroup: e^{tL}
- Transition density: p(t,x,y) = (1/√(2πt)) exp(-(x-y)²/2t)
- Martingale property: E[f(X_t)|X_0] governed by L
""")

# Example 2: Ornstein-Uhlenbeck process
print("\n" + "-"*70)
print("Example 2: Ornstein-Uhlenbeck Process")
print("-"*70)

print("dX_t = -θX_t dt + σ dW_t")

theta, sigma = symbols('theta sigma', real=True, positive=True)

# Generator: L = -θx∂/∂x + (σ²/2)∂²/∂x²
OU_generator_symbol = -theta * x * xi + (sigma**2 / 2) * xi**2

print("\nGenerator symbol:")
pprint(OU_generator_symbol)

OU_gen = PseudoDifferentialOperator(OU_generator_symbol, [x], mode='symbol')

print("""
Physical interpretation:
- θ: mean-reversion rate
- σ: volatility
- Equilibrium distribution: N(0, σ²/2θ)

Applications:
- Interest rate models (Vasicek)
- Velocity of particles in fluid
- Temperature fluctuations
""")

# Simulation of sample paths
print("\n" + "-"*70)
print("Simulation: Sample paths")
print("-"*70)

np.random.seed(42)

# Parameters
T = 10.0
N = 1000
dt = T / N
t_vals = np.linspace(0, T, N)

# Standard Brownian motion
W = np.zeros(N)
for i in range(1, N):
    W[i] = W[i-1] + np.sqrt(dt) * np.random.randn()

# Ornstein-Uhlenbeck process
theta_val = 0.5
sigma_val = 1.0
X_OU = np.zeros(N)
X_OU[0] = 2.0  # Initial condition

for i in range(1, N):
    X_OU[i] = X_OU[i-1] - theta_val * X_OU[i-1] * dt + sigma_val * np.sqrt(dt) * np.random.randn()

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Brownian paths
for _ in range(5):
    W_sample = np.zeros(N)
    for i in range(1, N):
        W_sample[i] = W_sample[i-1] + np.sqrt(dt) * np.random.randn()
    axes[0, 0].plot(t_vals, W_sample, alpha=0.7, linewidth=1)

axes[0, 0].set_xlabel('t')
axes[0, 0].set_ylabel('W_t')
axes[0, 0].set_title('Brownian Motion Paths')
axes[0, 0].grid(True, alpha=0.3)
axes[0, 0].axhline(0, color='k', linestyle='--', alpha=0.5)

# OU paths
for _ in range(5):
    X_sample = np.zeros(N)
    X_sample[0] = 2.0
    for i in range(1, N):
        X_sample[i] = X_sample[i-1] - theta_val * X_sample[i-1] * dt + sigma_val * np.sqrt(dt) * np.random.randn()
    axes[0, 1].plot(t_vals, X_sample, alpha=0.7, linewidth=1)

equilibrium_mean = 0
equilibrium_std = sigma_val / np.sqrt(2*theta_val)
axes[0, 1].axhline(equilibrium_mean, color='r', linestyle='--', linewidth=2, label='Mean')
axes[0, 1].axhline(equilibrium_mean + equilibrium_std, color='r', linestyle=':', alpha=0.5)
axes[0, 1].axhline(equilibrium_mean - equilibrium_std, color='r', linestyle=':', alpha=0.5)

axes[0, 1].set_xlabel('t')
axes[0, 1].set_ylabel('X_t')
axes[0, 1].set_title('Ornstein-Uhlenbeck Paths')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Distribution at different times (Brownian)
times_to_plot = [0.5, 2.0, 5.0, 10.0]
x_range = np.linspace(-5, 5, 200)

for t in times_to_plot:
    density = norm.pdf(x_range, 0, np.sqrt(t))
    axes[1, 0].plot(x_range, density, linewidth=2, label=f't={t}')

axes[1, 0].set_xlabel('x')
axes[1, 0].set_ylabel('p(t, 0, x)')
axes[1, 0].set_title('Transition Density (Brownian)')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)

# Stationary distribution (OU)
x_range_ou = np.linspace(-4, 4, 200)
stationary_density = norm.pdf(x_range_ou, 0, equilibrium_std)

# Initial distribution
initial_density = norm.pdf(x_range_ou, 2.0, 0.3)

axes[1, 1].plot(x_range_ou, initial_density, 'b--', linewidth=2, label='Initial (t=0)', alpha=0.7)
axes[1, 1].plot(x_range_ou, stationary_density, 'r-', linewidth=3, label='Stationary')

# Intermediate densities (simulation)
for t_idx in [N//10, N//4, N//2]:
    hist, bins = np.histogram(X_sample[:t_idx], bins=30, density=True)
    bin_centers = (bins[:-1] + bins[1:]) / 2
    axes[1, 1].plot(bin_centers, hist, 'g-', alpha=0.5, linewidth=1)

axes[1, 1].set_xlabel('x')
axes[1, 1].set_ylabel('Density')
axes[1, 1].set_title('Convergence to Stationary Distribution (OU)')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Generator determines transition probabilities")
print("✓ Spectral properties → long-time behavior")
CONTEXT: Stochastic Processes

A diffusion process X_t satisfies the SDE:

dX_t = μ(X_t)dt + σ(X_t)dW_t

The infinitesimal generator is:

L = μ(x)∂/∂x + (1/2)σ²(x)∂²/∂x²

This is a pseudo-differential operator!


Example 1: Standard Brownian Motion
dX_t = dW_t

Generator symbol: ξ²/2
 2
ξ 
──
2 

Properties:
- Generator of heat semigroup: e^{tL}
- Transition density: p(t,x,y) = (1/√(2πt)) exp(-(x-y)²/2t)
- Martingale property: E[f(X_t)|X_0] governed by L


----------------------------------------------------------------------
Example 2: Ornstein-Uhlenbeck Process
----------------------------------------------------------------------
dX_t = -θX_t dt + σ dW_t

Generator symbol:
 2  2        
σ ⋅ξ         
───── - θ⋅x⋅ξ
  2          

Physical interpretation:
- θ: mean-reversion rate
- σ: volatility
- Equilibrium distribution: N(0, σ²/2θ)

Applications:
- Interest rate models (Vasicek)
- Velocity of particles in fluid
- Temperature fluctuations


----------------------------------------------------------------------
Simulation: Sample paths
----------------------------------------------------------------------
No description has been provided for this image
✓ Generator determines transition probabilities
✓ Spectral properties → long-time behavior

Fokker-Planck Equation¶

In [41]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Evolution of Probability Densities

For a diffusion process with SDE: dX_t = μ(x)dt + σ(x)dW_t

The probability density ρ(t,x) satisfies the Fokker-Planck equation:

∂ρ/∂t = -∂/∂x[μ(x)ρ] + (1/2)∂²/∂x²[σ²(x)ρ]
       = L*ρ

where L* is the adjoint of the generator L.
""")

# Example: Double-well potential
print("\nExample: Fokker-Planck with double-well potential")
print("μ(x) = x(1 - x²)  (gradient of V(x) = -x²/2 + x⁴/4)")

# Double-well potential
V = -x**2/2 + x**4/4

drift = diff(V, x)

print("\nPotential V(x):")
pprint(V)

print("\nDrift μ(x) = -∇V:")
pprint(simplify(-drift))

# Fokker-Planck operator (adjoint form)
# L* = -∂(μρ)/∂x + D∂²ρ/∂x²
# In symbol form (for ρ as test function):

D = symbols('D', real=True, positive=True)  # Diffusion coefficient

FP_symbol = I * xi * x * (1 - x**2) + D * xi**2

print("\nFokker-Planck operator symbol (approximate):")
pprint(FP_symbol)

print("""
Stationary distribution (Gibbs measure):

ρ_∞(x) ∝ exp(-V(x)/D)

For low noise (D → 0):
- Two peaks at x ≈ ±1 (minima of V)
- Rare transitions between wells
- Kramers' rate theory applies
""")

# Visualization
print("\n" + "-"*70)
print("Visualization: Double-well dynamics")
print("-"*70)

x_vals = np.linspace(-2, 2, 1000)
V_vals = -x_vals**2/2 + x_vals**4/4

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Potential
axes[0, 0].plot(x_vals, V_vals, 'b-', linewidth=3)
axes[0, 0].axhline(0, color='k', linestyle='--', alpha=0.3)
axes[0, 0].plot([-1, 1], [V_vals[np.argmin(np.abs(x_vals + 1))], 
                          V_vals[np.argmin(np.abs(x_vals - 1))]], 'ro', 
               markersize=10, label='Minima')
axes[0, 0].plot([0], [0], 'rx', markersize=10, label='Saddle')
axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('V(x)')
axes[0, 0].set_title('Double-Well Potential')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# Stationary distributions for different D
for D_val in [0.05, 0.1, 0.2, 0.5]:
    rho_stationary = np.exp(-(-x_vals**2/2 + x_vals**4/4) / D_val)
    rho_stationary /= np.trapezoid(rho_stationary, x_vals)
    axes[0, 1].plot(x_vals, rho_stationary, linewidth=2, label=f'D={D_val}')

axes[0, 1].set_xlabel('x')
axes[0, 1].set_ylabel('ρ_∞(x)')
axes[0, 1].set_title('Stationary Distributions (Gibbs)')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Trajectory simulation
D_val = 0.1
dt = 0.01
N_steps = 5000
X_traj = np.zeros(N_steps)
X_traj[0] = -1.0  # Start at left well

for i in range(1, N_steps):
    drift_val = -(-X_traj[i-1] + X_traj[i-1]**3)
    X_traj[i] = X_traj[i-1] + drift_val * dt + np.sqrt(2*D_val*dt) * np.random.randn()

t_traj = np.arange(N_steps) * dt

axes[1, 0].plot(t_traj, X_traj, 'b-', linewidth=0.5, alpha=0.7)
axes[1, 0].axhline(-1, color='r', linestyle='--', alpha=0.5, label='Left well')
axes[1, 0].axhline(1, color='g', linestyle='--', alpha=0.5, label='Right well')
axes[1, 0].axhline(0, color='orange', linestyle=':', alpha=0.5, label='Barrier')
axes[1, 0].set_xlabel('t')
axes[1, 0].set_ylabel('X_t')
axes[1, 0].set_title(f'Trajectory with D={D_val}')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)

# Histogram vs stationary distribution
hist, bins = np.histogram(X_traj[2000:], bins=50, density=True)
bin_centers = (bins[:-1] + bins[1:]) / 2

axes[1, 1].bar(bin_centers, hist, width=bins[1]-bins[0], alpha=0.6, label='Empirical')

rho_stat = np.exp(-(-x_vals**2/2 + x_vals**4/4) / D_val)
rho_stat /= np.trapezoid(rho_stat, x_vals)
axes[1, 1].plot(x_vals, rho_stat, 'r-', linewidth=3, label='Theoretical')

axes[1, 1].set_xlabel('x')
axes[1, 1].set_ylabel('Density')
axes[1, 1].set_title('Empirical vs Stationary Distribution')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Fokker-Planck describes evolution of probability")
print("✓ Spectral gap determines convergence rate")
CONTEXT: Evolution of Probability Densities

For a diffusion process with SDE: dX_t = μ(x)dt + σ(x)dW_t

The probability density ρ(t,x) satisfies the Fokker-Planck equation:

∂ρ/∂t = -∂/∂x[μ(x)ρ] + (1/2)∂²/∂x²[σ²(x)ρ]
       = L*ρ

where L* is the adjoint of the generator L.


Example: Fokker-Planck with double-well potential
μ(x) = x(1 - x²)  (gradient of V(x) = -x²/2 + x⁴/4)

Potential V(x):
 4    2
x    x 
── - ──
4    2 

Drift μ(x) = -∇V:
   3    
- x  + x

Fokker-Planck operator symbol (approximate):
   2         ⎛     2⎞
D⋅ξ  + ⅈ⋅x⋅ξ⋅⎝1 - x ⎠

Stationary distribution (Gibbs measure):

ρ_∞(x) ∝ exp(-V(x)/D)

For low noise (D → 0):
- Two peaks at x ≈ ±1 (minima of V)
- Rare transitions between wells
- Kramers' rate theory applies


----------------------------------------------------------------------
Visualization: Double-well dynamics
----------------------------------------------------------------------
No description has been provided for this image
✓ Fokker-Planck describes evolution of probability
✓ Spectral gap determines convergence rate

Lévy Processes and Integro-Differential Operators¶

In [42]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Jump Processes

A Lévy process X_t has independent, stationary increments.
General form:

dX_t = μdt + σdW_t + ∫ z Ñ(dt,dz)

where Ñ is a compensated Poisson random measure.

The infinitesimal generator is:

L = μ∂/∂x + (σ²/2)∂²/∂x² + ∫[f(x+z) - f(x) - z∂f/∂x 1_{|z|<1}]ν(dz)

This is an integro-differential operator!
""")

# Example: Compound Poisson Process
print("\nExample: Compound Poisson Process")
print("Pure jump process with exponential jumps")

lam = symbols('lambda', real=True, positive=True)  # Intensity
alpha = symbols('alpha', real=True, positive=True)  # Exponential parameter

print(f"\nJump rate: λ")
print(f"Jump size distribution: Exp(α)")

# Symbol of the generator (approximation)
# L f(x) = λ ∫[f(x+z) - f(x)] α e^{-αz} dz  (for z > 0)

print("""
Characteristic exponent (Lévy-Khintchine formula):

ψ(ξ) = iμξ - σ²ξ²/2 + ∫[e^{iξz} - 1 - iξz·1_{|z|<1}]ν(dz)

For compound Poisson with Exp(α) jumps:
ψ(ξ) = λ(α/(α - iξ) - 1)
""")

# Characteristic exponent
psi = lam * (alpha / (alpha - I*xi) - 1)

print("\nCharacteristic exponent ψ(ξ):")
#pprint(simplify(psi))
pprint(psi)

print("""
Relation to pseudo-differential operators:

The operator with symbol ψ(ξ) generates the semigroup:

E[f(X_t)] = (e^{tL}f)(x)

where L has symbol -iψ(iξ)
""")

# Visualization
print("\n" + "-"*70)
print("Visualization: Lévy processes")
print("-"*70)

# Simulation of a compound Poisson process
np.random.seed(42)
T = 10.0
lam_val = 2.0  # 2 jumps per unit time on average
alpha_val = 1.0

# Number of jumps
n_jumps = np.random.poisson(lam_val * T)

# Jump times
jump_times = np.sort(np.random.uniform(0, T, n_jumps))

# Jump sizes (exponential)
jump_sizes = np.random.exponential(1/alpha_val, n_jumps)

# Build the trajectory
t_plot = np.linspace(0, T, 1000)
X_compound = np.zeros_like(t_plot)

for i, t in enumerate(t_plot):
    X_compound[i] = np.sum(jump_sizes[jump_times <= t])

# Variance Gamma process (difference of two Gamma processes)
# More general than compound Poisson
t_gamma = np.linspace(0, T, 1000)
dt_gamma = t_gamma[1] - t_gamma[0]

# Parameters
theta_vg = 0.2  # Skewness
nu_vg = 0.5     # Variance of the subordinator

# Gamma subordinator
gamma_increments = np.random.gamma(dt_gamma/nu_vg, nu_vg, len(t_gamma))
gamma_process = np.cumsum(gamma_increments)

# VG = Brownian motion evaluated at random time
X_VG = theta_vg * gamma_process + np.sqrt(gamma_process) * np.random.randn(len(t_gamma))

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Compound Poisson trajectory
axes[0, 0].plot(t_plot, X_compound, 'b-', linewidth=2, drawstyle='steps-post')
axes[0, 0].scatter(jump_times, np.cumsum(jump_sizes), color='red', s=50, 
                  zorder=5, label='Jumps')
axes[0, 0].set_xlabel('t')
axes[0, 0].set_ylabel('X_t')
axes[0, 0].set_title(f'Compound Poisson Process (λ={lam_val}, α={alpha_val})')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# Jump size distribution
axes[0, 1].hist(jump_sizes, bins=20, density=True, alpha=0.6, label='Empirical')
z_range = np.linspace(0, jump_sizes.max(), 100)
exp_density = alpha_val * np.exp(-alpha_val * z_range)
axes[0, 1].plot(z_range, exp_density, 'r-', linewidth=2, label=f'Exp({alpha_val})')
axes[0, 1].set_xlabel('Jump size z')
axes[0, 1].set_ylabel('Density ν(z)')
axes[0, 1].set_title('Jump Size Distribution (Lévy measure)')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Variance Gamma process
axes[1, 0].plot(t_gamma, X_VG, 'g-', linewidth=1, alpha=0.8)
axes[1, 0].set_xlabel('t')
axes[1, 0].set_ylabel('X_t')
axes[1, 0].set_title('Variance Gamma Process')
axes[1, 0].grid(True, alpha=0.3)

# Comparison of increments
increments_compound = np.diff(X_compound)
increments_VG = np.diff(X_VG)
increments_BM = np.random.randn(len(t_gamma)-1) * np.sqrt(dt_gamma)

axes[1, 1].hist(increments_BM, bins=30, alpha=0.4, density=True, label='Brownian')
axes[1, 1].hist(increments_VG, bins=30, alpha=0.4, density=True, label='VG')
axes[1, 1].hist(increments_compound[increments_compound > 0.01], bins=20, 
               alpha=0.4, density=True, label='Compound Poisson')
axes[1, 1].set_xlabel('Increment ΔX')
axes[1, 1].set_ylabel('Density')
axes[1, 1].set_title('Increment Distribution (fat tails)')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)
axes[1, 1].set_yscale('log')

plt.tight_layout()
plt.show()

print("\n✓ Lévy processes have jumps (non-Gaussian)")
print("✓ Integro-differential operators capture jump dynamics")
CONTEXT: Jump Processes

A Lévy process X_t has independent, stationary increments.
General form:

dX_t = μdt + σdW_t + ∫ z Ñ(dt,dz)

where Ñ is a compensated Poisson random measure.

The infinitesimal generator is:

L = μ∂/∂x + (σ²/2)∂²/∂x² + ∫[f(x+z) - f(x) - z∂f/∂x 1_{|z|<1}]ν(dz)

This is an integro-differential operator!


Example: Compound Poisson Process
Pure jump process with exponential jumps

Jump rate: λ
Jump size distribution: Exp(α)

Characteristic exponent (Lévy-Khintchine formula):

ψ(ξ) = iμξ - σ²ξ²/2 + ∫[e^{iξz} - 1 - iξz·1_{|z|<1}]ν(dz)

For compound Poisson with Exp(α) jumps:
ψ(ξ) = λ(α/(α - iξ) - 1)


Characteristic exponent ψ(ξ):
  ⎛   α       ⎞
λ⋅⎜─────── - 1⎟
  ⎝α - ⅈ⋅ξ    ⎠

Relation to pseudo-differential operators:

The operator with symbol ψ(ξ) generates the semigroup:

E[f(X_t)] = (e^{tL}f)(x)

where L has symbol -iψ(iξ)


----------------------------------------------------------------------
Visualization: Lévy processes
----------------------------------------------------------------------
No description has been provided for this image
✓ Lévy processes have jumps (non-Gaussian)
✓ Integro-differential operators capture jump dynamics

Large Deviations and Wentzell-Freidlin Theory¶

In [43]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Rare Events in Stochastic Systems

For a small-noise system:

dX_t^ε = b(X_t^ε)dt + ε·σ(X_t^ε)dW_t

as ε → 0, the probability of deviations from deterministic path φ:

P(X^ε ≈ φ) ≈ exp(-I(φ)/ε²)

where I(φ) is the rate function (action functional):

I(φ) = (1/2)∫₀ᵀ |φ̇ - b(φ)|² / σ²(φ) dt
""")

# Example: Escape from potential well
print("\nExample: Escape from potential well with small noise")

# Potential V(x) = x⁴/4 - x²/2
V_escape = x**4/4 - x**2/2

b_escape = -diff(V_escape, x)  # Drift = -∇V

print("\nPotential V(x):")
pprint(V_escape)

print("\nDrift b(x) = -V'(x):")
pprint(simplify(b_escape))

print("""
Wentzell-Freidlin theory:
- Most probable escape path minimizes action I(φ)
- Quasi-potential W(x,y) = min I(φ) over paths x → y
- Escape rate: k ≈ exp(-W(x_min, x_saddle)/ε²)

This connects to:
- Pseudo-differential operators (generator in ε → 0 limit)
- WKB approximation (quasi-classical limit)
- Instanton theory in physics
""")

# Quasi-potential computation (approximation)
print("\n" + "-"*70)
print("Visualization: Escape dynamics")
print("-"*70)

x_range = np.linspace(-2, 2, 200)
V_vals_escape = x_range**4/4 - x_range**2/2

# Find minima and saddle point
x_min_left = -1.0
x_min_right = 1.0
x_saddle = 0.0

V_min = V_vals_escape[np.argmin(np.abs(x_range - x_min_left))]
V_saddle = V_vals_escape[np.argmin(np.abs(x_range - x_saddle))]

barrier_height = V_saddle - V_min

print(f"\nBarrier height ΔV = {barrier_height:.3f}")

# Simulation with small noise
epsilon_vals = [0.05, 0.1, 0.2]

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Potential
axes[0, 0].plot(x_range, V_vals_escape, 'b-', linewidth=3)
axes[0, 0].plot([x_min_left, x_min_right], [V_min, V_min], 'go', 
               markersize=10, label='Stable minima')
axes[0, 0].plot([x_saddle], [V_saddle], 'ro', markersize=10, label='Saddle')

# Most probable escape path (deterministic gradient flow backward)
axes[0, 0].annotate('', xy=(x_saddle, V_saddle), xytext=(x_min_left, V_min),
                   arrowprops=dict(arrowstyle='->', lw=3, color='red', alpha=0.7))
axes[0, 0].text(-0.5, 0.05, 'Escape path', fontsize=12, color='red')

axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('V(x)')
axes[0, 0].set_title('Potential with Escape Barrier')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# Trajectories for different noise levels
for eps in epsilon_vals:
    T_sim = 50
    N_sim = 5000
    dt_sim = T_sim / N_sim
    
    X_sim = np.zeros(N_sim)
    X_sim[0] = x_min_left
    
    for i in range(1, N_sim):
        drift_val = -(-X_sim[i-1] + X_sim[i-1]**3)
        X_sim[i] = X_sim[i-1] + drift_val * dt_sim + eps * np.sqrt(dt_sim) * np.random.randn()
    
    t_sim = np.linspace(0, T_sim, N_sim)
    axes[0, 1].plot(t_sim, X_sim, linewidth=1.5, alpha=0.8, label=f'ε={eps}')

axes[0, 1].axhline(x_saddle, color='r', linestyle='--', alpha=0.5, label='Barrier')
axes[0, 1].axhline(x_min_left, color='g', linestyle='--', alpha=0.5)
axes[0, 1].axhline(x_min_right, color='g', linestyle='--', alpha=0.5)
axes[0, 1].set_xlabel('t')
axes[0, 1].set_ylabel('X_t')
axes[0, 1].set_title('Trajectories with Different Noise Levels')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Escape rate vs noise amplitude
epsilon_range = np.linspace(0.05, 0.5, 20)
escape_rates = []

for eps in epsilon_range:
    # Kramers formula: k ∝ exp(-ΔV/ε²)
    rate = np.exp(-barrier_height / eps**2)
    escape_rates.append(rate)

axes[1, 0].semilogy(epsilon_range, escape_rates, 'b-', linewidth=3)
axes[1, 0].set_xlabel('Noise amplitude ε')
axes[1, 0].set_ylabel('Escape rate (log scale)')
axes[1, 0].set_title('Kramers Escape Rate: exp(-ΔV/ε²)')
axes[1, 0].grid(True, alpha=0.3)

# Minimal action (quasi-potential)
# W(x_left, x_saddle) ≈ ΔV for this system
x_path = np.linspace(x_min_left, x_saddle, 100)
V_path = x_path**4/4 - x_path**2/2

axes[1, 1].plot(x_path, V_path, 'b-', linewidth=3, label='V(x) along path')
axes[1, 1].fill_between(x_path, V_min, V_path, alpha=0.3, color='red', 
                        label=f'Action I ≈ {barrier_height:.3f}')
axes[1, 1].plot([x_min_left], [V_min], 'go', markersize=10)
axes[1, 1].plot([x_saddle], [V_saddle], 'ro', markersize=10)
axes[1, 1].set_xlabel('x (along escape path)')
axes[1, 1].set_ylabel('V(x)')
axes[1, 1].set_title('Quasi-Potential (Action Functional)')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Large deviations quantify rare events")
print("✓ Connection to WKB and semiclassical analysis")
CONTEXT: Rare Events in Stochastic Systems

For a small-noise system:

dX_t^ε = b(X_t^ε)dt + ε·σ(X_t^ε)dW_t

as ε → 0, the probability of deviations from deterministic path φ:

P(X^ε ≈ φ) ≈ exp(-I(φ)/ε²)

where I(φ) is the rate function (action functional):

I(φ) = (1/2)∫₀ᵀ |φ̇ - b(φ)|² / σ²(φ) dt


Example: Escape from potential well with small noise

Potential V(x):
 4    2
x    x 
── - ──
4    2 

Drift b(x) = -V'(x):
   3    
- x  + x

Wentzell-Freidlin theory:
- Most probable escape path minimizes action I(φ)
- Quasi-potential W(x,y) = min I(φ) over paths x → y
- Escape rate: k ≈ exp(-W(x_min, x_saddle)/ε²)

This connects to:
- Pseudo-differential operators (generator in ε → 0 limit)
- WKB approximation (quasi-classical limit)
- Instanton theory in physics


----------------------------------------------------------------------
Visualization: Escape dynamics
----------------------------------------------------------------------

Barrier height ΔV = 0.250
No description has been provided for this image
✓ Large deviations quantify rare events
✓ Connection to WKB and semiclassical analysis

Branching Processes and Kolmogorov Equations¶

In [44]:
print("""
CONTEXT: Population Dynamics

A continuous-time branching process Z_t (population size) has generator:

L f(z) = λz[f(z+1) - f(z)] + μz[f(z-1) - f(z)]

where λ = birth rate, μ = death rate.

This is a discrete pseudo-differential operator!
""")

# In the continuous limit (diffusion approximation)
x = symbols('x', real=True, positive=True)
xi = symbols('xi', real=True)

lam_birth = symbols('lambda', real=True, positive=True)
mu_death = symbols('mu', real=True, positive=True)

print("\nDiffusion approximation (large population):")
print("dX_t = (λ - μ)X_t dt + √((λ + μ)X_t) dW_t")

# Generator of the diffusion
branching_drift = (lam_birth - mu_death) * x
branching_diff = (lam_birth + mu_death) * x

branching_generator = branching_drift * xi + (branching_diff / 2) * xi**2

print("\nGenerator symbol:")
pprint(branching_generator)

print("""
Special cases:
- λ > μ: Supercritical (population grows)
- λ = μ: Critical (random walk on √n scale)
- λ < μ: Subcritical (extinction certain)

Extinction probability q satisfies:
λq(2-q) = μ (quadratic equation)
""")

# Extinction probability
print("\n" + "-"*70)
print("Extinction probability")
print("-"*70)

q = symbols('q', real=True, positive=True)
extinction_eq = lam_birth * q * (2 - q) - mu_death

print("\nExtinction equation: λq(2-q) = μ")

solutions_ext = solve(extinction_eq, q)
print("\nSolutions:")
for sol in solutions_ext:
    pprint(sol)

# Numerical cases
lam_vals = [0.8, 1.0, 1.5]
mu_val = 1.0

print("\n" + "-"*70)
print("Numerical examples (μ = 1.0):")
print("-"*70)

for lam_val in lam_vals:
    if lam_val < mu_val:
        q_ext = 1.0  # Certain extinction
        regime = "Subcritical"
    elif lam_val == mu_val:
        q_ext = 1.0  # Certain but slow
        regime = "Critical"
    else:
        # Two solutions: q=1 (always) and q = μ/λ (from initial size 1)
        q_ext = mu_val / lam_val
        regime = "Supercritical"
    
    print(f"\nλ = {lam_val}, μ = {mu_val}: {regime}")
    print(f"  Extinction probability: q = {q_ext:.4f}")

# Simulation of branching processes
print("\n" + "-"*70)
print("Simulation: Branching process trajectories")
print("-"*70)

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Parameters
T_branch = 20.0
dt_branch = 0.01
N_branch = int(T_branch / dt_branch)
t_branch = np.linspace(0, T_branch, N_branch)

n_trajectories = 20

for ax_idx, (lam_val, regime_name) in enumerate([(0.8, 'Subcritical (λ<μ)'),
                                                   (1.0, 'Critical (λ=μ)'),
                                                   (1.5, 'Supercritical (λ>μ)')]):
    if ax_idx >= 3:
        break
    
    ax = axes.flat[ax_idx]
    
    extinct_count = 0
    
    for _ in range(n_trajectories):
        Z = np.zeros(N_branch)
        Z[0] = 10.0  # Initial population
        
        for i in range(1, N_branch):
            if Z[i-1] > 0:
                drift = (lam_val - mu_val) * Z[i-1]
                diffusion = np.sqrt((lam_val + mu_val) * Z[i-1])
                Z[i] = max(0, Z[i-1] + drift * dt_branch + diffusion * np.sqrt(dt_branch) * np.random.randn())
            else:
                Z[i] = 0
                
        if Z[-1] < 0.1:
            extinct_count += 1
            ax.plot(t_branch, Z, 'r-', alpha=0.3, linewidth=1)
        else:
            ax.plot(t_branch, Z, 'b-', alpha=0.5, linewidth=1)
    
    ax.set_xlabel('t')
    ax.set_ylabel('Population Z_t')
    ax.set_title(f'{regime_name}\nExtinct: {extinct_count}/{n_trajectories}')
    ax.grid(True, alpha=0.3)
    ax.set_ylim([0, max(50, ax.get_ylim()[1])])

# Theoretical distribution of extinction time
ax = axes[1, 1]

# For the subcritical case, mean extinction time
lam_sub = 0.8
mu_val = 1.0
r = mu_val - lam_sub  # Taux de déclin

# Simulation of extinction times
extinction_times = []
for _ in range(500):
    Z_ext = 10.0
    t_ext = 0
    while Z_ext > 0.5 and t_ext < 50:
        drift = (lam_sub - mu_val) * Z_ext
        diffusion = np.sqrt((lam_sub + mu_val) * Z_ext)
        Z_ext = max(0, Z_ext + drift * 0.01 + diffusion * np.sqrt(0.01) * np.random.randn())
        t_ext += 0.01
    if Z_ext < 0.5:
        extinction_times.append(t_ext)

ax.hist(extinction_times, bins=30, density=True, alpha=0.6, label='Simulated')
ax.set_xlabel('Extinction time T')
ax.set_ylabel('Density')
ax.set_title(f'Distribution of Extinction Time (λ={lam_sub})')
ax.legend()
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Branching processes model population growth/extinction")
print("✓ Generator determines survival/extinction probabilities")
CONTEXT: Population Dynamics

A continuous-time branching process Z_t (population size) has generator:

L f(z) = λz[f(z+1) - f(z)] + μz[f(z-1) - f(z)]

where λ = birth rate, μ = death rate.

This is a discrete pseudo-differential operator!


Diffusion approximation (large population):
dX_t = (λ - μ)X_t dt + √((λ + μ)X_t) dW_t

Generator symbol:
   2                      
x⋅ξ ⋅(λ + μ)              
──────────── + x⋅ξ⋅(λ - μ)
     2                    

Special cases:
- λ > μ: Supercritical (population grows)
- λ = μ: Critical (random walk on √n scale)
- λ < μ: Subcritical (extinction certain)

Extinction probability q satisfies:
λq(2-q) = μ (quadratic equation)


----------------------------------------------------------------------
Extinction probability
----------------------------------------------------------------------

Extinction equation: λq(2-q) = μ

Solutions:
      _______
    ╲╱ λ - μ 
1 - ─────────
       √λ    
      _______
    ╲╱ λ - μ 
1 + ─────────
       √λ    

----------------------------------------------------------------------
Numerical examples (μ = 1.0):
----------------------------------------------------------------------

λ = 0.8, μ = 1.0: Subcritical
  Extinction probability: q = 1.0000

λ = 1.0, μ = 1.0: Critical
  Extinction probability: q = 1.0000

λ = 1.5, μ = 1.0: Supercritical
  Extinction probability: q = 0.6667

----------------------------------------------------------------------
Simulation: Branching process trajectories
----------------------------------------------------------------------
No description has been provided for this image
✓ Branching processes model population growth/extinction
✓ Generator determines survival/extinction probabilities

Martingales and Harmonic Functions¶

In [45]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Martingale Theory

For a diffusion process X_t with generator L, a function h is:

- L-harmonic if Lh = 0
- L-superharmonic if Lh ≤ 0

Key result: h is L-harmonic ⟺ h(X_t) is a local martingale

This connects analysis (PDEs) to probability (martingales)!
""")

# Example: Brownian motion and harmonic functions
print("\nExample 1: Brownian Motion")
print("Generator: L = (1/2)∂²/∂x²")

# Harmonic functions: Lh = 0 ⟹ h''(x) = 0 ⟹ h(x) = ax + b

print("\nHarmonic functions: h(x) = ax + b (linear)")
print("⟹ h(W_t) is a martingale")

# Example 2: Brownian motion with drift
print("\n" + "-"*70)
print("Example 2: Brownian Motion with Drift")
print("-"*70)

mu = symbols('mu', real=True)
print(f"dX_t = μ dt + dW_t")
print(f"Generator: L = μ∂/∂x + (1/2)∂²/∂x²")

# Exponential harmonic function
# Lh = 0 with h(x) = e^{αx}
# ⟹ μα + (1/2)α² = 0
# ⟹ α = -2μ

alpha = symbols('alpha', real=True)
harmonic_exp = exp(alpha * x)

L_harmonic = mu * diff(harmonic_exp, x) + (1/2) * diff(harmonic_exp, x, 2)
L_harmonic_simplified = simplify(L_harmonic / harmonic_exp)

print("\nFor h(x) = e^{αx}:")
print("Lh / h =")
pprint(L_harmonic_simplified)

print("\nFor Lh = 0: α = -2μ")
print(f"⟹ h(x) = e^{{-2μx}} is harmonic")

# Application: Dirichlet problem
print("\n" + "-"*70)
print("Application: Dirichlet Problem via Martingales")
print("-"*70)

print("""
Dirichlet problem on interval [a,b]:
- Lh = 0 in (a,b)
- h(a) = A, h(b) = B

Probabilistic solution:
h(x) = E^x[h(X_τ)] where τ = hitting time of {a,b}

This expresses PDE solution in terms of stopped martingale!
""")

# Calculation for Brownian motion with drift
a_bound = symbols('a', real=True)
b_bound = symbols('b', real=True)
A_val = symbols('A', real=True)
B_val = symbols('B', real=True)

print("\nFor Brownian motion with drift μ on [a,b]:")
print("Solution: h(x) = A·φ(x) + B·ψ(x)")
print("where φ, ψ are harmonic basis (e^{-2μx}, e^{0·x})")

# Visualization
print("\n" + "-"*70)
print("Visualization: Harmonic functions and martingales")
print("-"*70)

# Standard Brownian motion
T_mart = 5.0
N_mart = 1000
dt_mart = T_mart / N_mart
t_mart = np.linspace(0, T_mart, N_mart)

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Brownian paths and h(W_t) = W_t (martingale)
for _ in range(10):
    W = np.zeros(N_mart)
    for i in range(1, N_mart):
        W[i] = W[i-1] + np.sqrt(dt_mart) * np.random.randn()
    axes[0, 0].plot(t_mart, W, alpha=0.5, linewidth=1)

axes[0, 0].set_xlabel('t')
axes[0, 0].set_ylabel('W_t')
axes[0, 0].set_title('Brownian Paths (h(x)=x is harmonic)')
axes[0, 0].grid(True, alpha=0.3)
axes[0, 0].axhline(0, color='k', linestyle='--', alpha=0.3)

# h(W_t) = W_t² - t (also a martingale!)
for _ in range(10):
    W = np.zeros(N_mart)
    for i in range(1, N_mart):
        W[i] = W[i-1] + np.sqrt(dt_mart) * np.random.randn()
    
    martingale = W**2 - t_mart
    axes[0, 1].plot(t_mart, martingale, alpha=0.5, linewidth=1)

axes[0, 1].set_xlabel('t')
axes[0, 1].set_ylabel('W_t² - t')
axes[0, 1].set_title('Quadratic Martingale (W² - t)')
axes[0, 1].grid(True, alpha=0.3)
axes[0, 1].axhline(0, color='k', linestyle='--', alpha=0.3)

# Dirichlet problem solution via simulation
a_val = -1.0
b_val = 1.0
A_val_num = 0.0
B_val_num = 1.0

x_init_vals = np.linspace(a_val, b_val, 50)
mean_exit_values = []

for x_init in x_init_vals:
    exit_values = []
    
    for _ in range(200):
        X = x_init
        t = 0
        max_time = 100
        
        while a_val < X < b_val and t < max_time:
            X = X + np.sqrt(0.01) * np.random.randn()
            t += 0.01
        
        # Value at exit
        if X <= a_val:
            exit_values.append(A_val_num)
        else:
            exit_values.append(B_val_num)
    
    mean_exit_values.append(np.mean(exit_values))

# Analytic solution for Brownian motion: h(x) = (x-a)/(b-a) * (B-A) + A
h_analytic = (x_init_vals - a_val) / (b_val - a_val) * (B_val_num - A_val_num) + A_val_num

axes[1, 0].plot(x_init_vals, mean_exit_values, 'bo', markersize=4, alpha=0.6, label='Monte Carlo')
axes[1, 0].plot(x_init_vals, h_analytic, 'r-', linewidth=3, label='Analytic')
axes[1, 0].plot([a_val, b_val], [A_val_num, B_val_num], 'go', markersize=10, label='Boundary')
axes[1, 0].set_xlabel('x (initial position)')
axes[1, 0].set_ylabel('h(x) = E^x[h(X_τ)]')
axes[1, 0].set_title('Dirichlet Problem: Probabilistic Solution')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)

# Mean stopping time
mean_stopping_times = []

for x_init in x_init_vals:
    stopping_times = []
    
    for _ in range(200):
        X = x_init
        t = 0
        max_time = 100
        
        while a_val < X < b_val and t < max_time:
            X = X + np.sqrt(0.01) * np.random.randn()
            t += 0.01
        
        if t < max_time:
            stopping_times.append(t)
    
    if stopping_times:
        mean_stopping_times.append(np.mean(stopping_times))
    else:
        mean_stopping_times.append(np.nan)

# Theoretical mean time: E^x[τ] = (b-x)(x-a) for standard Brownian motion
expected_time = (b_val - x_init_vals) * (x_init_vals - a_val)

axes[1, 1].plot(x_init_vals, mean_stopping_times, 'bo', markersize=4, alpha=0.6, label='Monte Carlo')
axes[1, 1].plot(x_init_vals, expected_time, 'r-', linewidth=3, label='Theory: (b-x)(x-a)')
axes[1, 1].set_xlabel('x (initial position)')
axes[1, 1].set_ylabel('E[τ] (mean exit time)')
axes[1, 1].set_title('Mean Exit Time from Interval')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Harmonic functions ↔ Martingales")
print("✓ PDE solutions via probabilistic representation")
CONTEXT: Martingale Theory

For a diffusion process X_t with generator L, a function h is:

- L-harmonic if Lh = 0
- L-superharmonic if Lh ≤ 0

Key result: h is L-harmonic ⟺ h(X_t) is a local martingale

This connects analysis (PDEs) to probability (martingales)!


Example 1: Brownian Motion
Generator: L = (1/2)∂²/∂x²

Harmonic functions: h(x) = ax + b (linear)
⟹ h(W_t) is a martingale

----------------------------------------------------------------------
Example 2: Brownian Motion with Drift
----------------------------------------------------------------------
dX_t = μ dt + dW_t
Generator: L = μ∂/∂x + (1/2)∂²/∂x²

For h(x) = e^{αx}:
Lh / h =
α⋅(0.5⋅α + μ)

For Lh = 0: α = -2μ
⟹ h(x) = e^{-2μx} is harmonic

----------------------------------------------------------------------
Application: Dirichlet Problem via Martingales
----------------------------------------------------------------------

Dirichlet problem on interval [a,b]:
- Lh = 0 in (a,b)
- h(a) = A, h(b) = B

Probabilistic solution:
h(x) = E^x[h(X_τ)] where τ = hitting time of {a,b}

This expresses PDE solution in terms of stopped martingale!


For Brownian motion with drift μ on [a,b]:
Solution: h(x) = A·φ(x) + B·ψ(x)
where φ, ψ are harmonic basis (e^{-2μx}, e^{0·x})

----------------------------------------------------------------------
Visualization: Harmonic functions and martingales
----------------------------------------------------------------------
No description has been provided for this image
✓ Harmonic functions ↔ Martingales
✓ PDE solutions via probabilistic representation

Invariant Measures and Ergodicity¶

In [46]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Ergodic Theory for Stochastic Processes

A measure μ is invariant for process X_t if:

∫ (Lf) dμ = 0 for all f

Equivalently: L*μ = 0 (in distributional sense)

When μ is unique and ergodic:

lim_{T→∞} (1/T)∫₀ᵀ f(X_t)dt = ∫ f dμ  (a.s.)
""")

# Example: Ornstein-Uhlenbeck
print("\nExample: Ornstein-Uhlenbeck Process")
print("dX_t = -θX_t dt + σ dW_t")

theta, sigma = symbols('theta sigma', real=True, positive=True)

print("\nGenerator: L = -θx∂/∂x + (σ²/2)∂²/∂x²")

# Invariant measure: Gibbs equilibrium
print("\nInvariant measure:")
print("μ_∞(x) = √(θ/πσ²) exp(-θx²/σ²)")

# Invariance condition: L*μ = 0
mu_invariant = sqrt(theta / (np.pi * sigma**2)) * exp(-theta * x**2 / sigma**2)

print("\nμ_∞(x) =")
pprint(mu_invariant)

# Spectrum of the generator and convergence rate
print("\n" + "-"*70)
print("Spectral gap and convergence rate")
print("-"*70)

print("""
The generator L has discrete spectrum:

λ_n = -nθ,  n = 0, 1, 2, ...

with eigenfunctions: Hermite polynomials H_n(√(θ/σ²) x)

Spectral gap: λ₁ - λ₀ = θ

Convergence to equilibrium:
||ρ(t,·) - μ_∞||_TV ≤ C e^{-θt}

Larger gap ⟹ faster mixing!
""")

# Visualization
print("\n" + "-"*70)
print("Visualization: Convergence to invariant measure")
print("-"*70)

theta_val = 1.0
sigma_val = 1.0

T_erg = 50.0
N_erg = 5000
dt_erg = T_erg / N_erg
t_erg = np.linspace(0, T_erg, N_erg)

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Multiple trajectories
n_traj = 50
all_trajectories = []

for _ in range(n_traj):
    X = np.zeros(N_erg)
    X[0] = 5.0  # Start far from equilibrium
    
    for i in range(1, N_erg):
        drift = -theta_val * X[i-1]
        X[i] = X[i-1] + drift * dt_erg + sigma_val * np.sqrt(dt_erg) * np.random.randn()
    
    all_trajectories.append(X)
    if _ < 10:
        axes[0, 0].plot(t_erg, X, alpha=0.5, linewidth=1)

# Equilibrium
equilibrium_std = sigma_val / np.sqrt(2 * theta_val)
axes[0, 0].axhline(0, color='r', linestyle='--', linewidth=2, label='Mean')
axes[0, 0].fill_between(t_erg, -equilibrium_std, equilibrium_std, 
                        alpha=0.2, color='red', label='±1 std')
axes[0, 0].set_xlabel('t')
axes[0, 0].set_ylabel('X_t')
axes[0, 0].set_title('Convergence to Equilibrium')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# Empirical mean vs theoretical
all_trajectories = np.array(all_trajectories)
time_average = np.mean(all_trajectories, axis=0)

axes[0, 1].plot(t_erg, time_average, 'b-', linewidth=2, label='Sample mean')
axes[0, 1].axhline(0, color='r', linestyle='--', linewidth=2, label='Theoretical mean')
axes[0, 1].fill_between(t_erg, -equilibrium_std/np.sqrt(n_traj), 
                        equilibrium_std/np.sqrt(n_traj),
                        alpha=0.3, color='red', label='95% CI')
axes[0, 1].set_xlabel('t')
axes[0, 1].set_ylabel('Sample mean')
axes[0, 1].set_title('Law of Large Numbers')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Distribution at different times
times_snapshot = [0, 5, 10, 30]
x_range_erg = np.linspace(-4, 6, 100)

for t_snap in times_snapshot:
    idx = int(t_snap / T_erg * N_erg)
    if idx < N_erg:
        snapshot = all_trajectories[:, idx]
        axes[1, 0].hist(snapshot, bins=30, density=True, alpha=0.4, 
                       label=f't={t_snap}')

# Theoretical invariant measure
mu_theory = (1/np.sqrt(2*np.pi*equilibrium_std**2)) * np.exp(-x_range_erg**2 / (2*equilibrium_std**2))
axes[1, 0].plot(x_range_erg, mu_theory, 'r-', linewidth=3, label='Invariant μ_∞')

axes[1, 0].set_xlabel('x')
axes[1, 0].set_ylabel('Density')
axes[1, 0].set_title('Evolution of Distribution to μ_∞')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)

# Temporal autocorrelation
# For OU: Corr(X_t, X_{t+s}) = exp(-θs)
max_lag = 100
lags = np.arange(0, max_lag)
autocorr_theory = np.exp(-theta_val * lags * dt_erg)

# Empirical autocorrelation
sample_traj = all_trajectories[0, 1000:]  # After equilibration
autocorr_empirical = []

for lag in lags:
    if lag < len(sample_traj) - 1:
        corr = np.corrcoef(sample_traj[:-lag-1], sample_traj[lag:-1])[0, 1]
        autocorr_empirical.append(corr)
    else:
        autocorr_empirical.append(0)

axes[1, 1].plot(lags * dt_erg, autocorr_theory, 'r-', linewidth=3, label='Theory: exp(-θs)')
axes[1, 1].plot(lags * dt_erg, autocorr_empirical, 'bo', markersize=4, 
               alpha=0.6, label='Empirical')
axes[1, 1].set_xlabel('Lag s')
axes[1, 1].set_ylabel('Autocorrelation')
axes[1, 1].set_title('Temporal Correlation: Corr(X_t, X_{t+s})')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Spectral gap determines mixing rate")
print("✓ Ergodic theorem: time average = space average")
CONTEXT: Ergodic Theory for Stochastic Processes

A measure μ is invariant for process X_t if:

∫ (Lf) dμ = 0 for all f

Equivalently: L*μ = 0 (in distributional sense)

When μ is unique and ergodic:

lim_{T→∞} (1/T)∫₀ᵀ f(X_t)dt = ∫ f dμ  (a.s.)


Example: Ornstein-Uhlenbeck Process
dX_t = -θX_t dt + σ dW_t

Generator: L = -θx∂/∂x + (σ²/2)∂²/∂x²

Invariant measure:
μ_∞(x) = √(θ/πσ²) exp(-θx²/σ²)

μ_∞(x) =
                          2 
                      -θ⋅x  
                      ──────
                         2  
                        σ   
0.564189583547756⋅√θ⋅ℯ      
────────────────────────────
             σ              

----------------------------------------------------------------------
Spectral gap and convergence rate
----------------------------------------------------------------------

The generator L has discrete spectrum:

λ_n = -nθ,  n = 0, 1, 2, ...

with eigenfunctions: Hermite polynomials H_n(√(θ/σ²) x)

Spectral gap: λ₁ - λ₀ = θ

Convergence to equilibrium:
||ρ(t,·) - μ_∞||_TV ≤ C e^{-θt}

Larger gap ⟹ faster mixing!


----------------------------------------------------------------------
Visualization: Convergence to invariant measure
----------------------------------------------------------------------
No description has been provided for this image
✓ Spectral gap determines mixing rate
✓ Ergodic theorem: time average = space average

Quantitative Finance - Black-Scholes Equation¶

In [47]:
x = symbols('x', real=True, positive=True)  # Asset price
xi = symbols('xi', real=True)

print("""
CONTEXT: Option Pricing

Asset price follows geometric Brownian motion:

dS_t = μS_t dt + σS_t dW_t

Option price V(t,S) satisfies Black-Scholes PDE:

∂V/∂t + (1/2)σ²S²∂²V/∂S² + rS∂V/∂S - rV = 0

This is backward heat equation with variable coefficients!
""")

r, sigma_bs, mu_bs = symbols('r sigma mu', real=True, positive=True)
t_bs, T_bs = symbols('t T', real=True, positive=True)

print("\nBlack-Scholes operator (in log-price x = log(S)):")
print("L = (r - σ²/2)∂/∂x + (σ²/2)∂²/∂x²")

# Change of variable: x = log(S)
# dX_t = (r - σ²/2)dt + σdW_t

BS_drift = r - sigma_bs**2 / 2
BS_diffusion = sigma_bs**2 / 2

BS_generator_symbol = BS_drift * xi + BS_diffusion * xi**2

print("\nGenerator symbol:")
pprint(BS_generator_symbol)

print("""
Martingale pricing formula:

V(t,S) = E^Q[e^{-r(T-t)} Payoff(S_T) | S_t = S]

where Q is risk-neutral measure (martingale measure).

This connects:
- PDEs (Black-Scholes equation)
- Stochastic calculus (Itô's lemma)
- Functional analysis (Feynman-Kac)
- Pseudo-differential operators (generator)
""")

# Option pricing via Monte Carlo
print("\n" + "-"*70)
print("Monte Carlo pricing: European Call Option")
print("-"*70)

# Parameters
S0 = 100.0      # Initial price
K = 100.0       # Strike
T_mat = 1.0     # Maturity
r_val = 0.05    # Risk-free rate
sigma_val = 0.2 # Volatility

# Closed-form Black-Scholes formula
from scipy.stats import norm as scipy_norm

def black_scholes_call(S, K, T, r, sigma):
    d1 = (np.log(S/K) + (r + sigma**2/2)*T) / (sigma*np.sqrt(T))
    d2 = d1 - sigma*np.sqrt(T)
    return S * scipy_norm.cdf(d1) - K * np.exp(-r*T) * scipy_norm.cdf(d2)

# Theoretical price
V_theory = black_scholes_call(S0, K, T_mat, r_val, sigma_val)

print(f"\nParameters: S0={S0}, K={K}, T={T_mat}, r={r_val}, σ={sigma_val}")
print(f"Black-Scholes price (closed form): {V_theory:.4f}")

# Monte Carlo
n_paths = 10000
dt_mc = 0.01
n_steps_mc = int(T_mat / dt_mc)

S_paths = np.zeros((n_paths, n_steps_mc + 1))
S_paths[:, 0] = S0

for i in range(1, n_steps_mc + 1):
    Z = np.random.randn(n_paths)
    S_paths[:, i] = S_paths[:, i-1] * np.exp((r_val - 0.5*sigma_val**2)*dt_mc + 
                                              sigma_val*np.sqrt(dt_mc)*Z)

# Payoff
payoffs = np.maximum(S_paths[:, -1] - K, 0)

# Discounted price
V_mc = np.exp(-r_val * T_mat) * np.mean(payoffs)
V_mc_std = np.exp(-r_val * T_mat) * np.std(payoffs) / np.sqrt(n_paths)

print(f"Monte Carlo price: {V_mc:.4f} ± {1.96*V_mc_std:.4f} (95% CI)")
print(f"Error: {abs(V_mc - V_theory):.4f}")

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Sample paths
t_mc = np.linspace(0, T_mat, n_steps_mc + 1)
for i in range(min(100, n_paths)):
    axes[0, 0].plot(t_mc, S_paths[i, :], 'b-', alpha=0.1, linewidth=0.5)

axes[0, 0].axhline(K, color='r', linestyle='--', linewidth=2, label=f'Strike K={K}')
axes[0, 0].set_xlabel('Time t')
axes[0, 0].set_ylabel('Stock price S_t')
axes[0, 0].set_title('Simulated Stock Price Paths')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# Terminal distribution
axes[0, 1].hist(S_paths[:, -1], bins=50, density=True, alpha=0.6, label='Simulated')

# Theoretical lognormal distribution
S_range = np.linspace(50, 150, 200)
mean_log = np.log(S0) + (r_val - 0.5*sigma_val**2)*T_mat
std_log = sigma_val*np.sqrt(T_mat)
pdf_theory = (1/(S_range*std_log*np.sqrt(2*np.pi))) * \
             np.exp(-(np.log(S_range) - mean_log)**2 / (2*std_log**2))

axes[0, 1].plot(S_range, pdf_theory, 'r-', linewidth=3, label='Lognormal theory')
axes[0, 1].axvline(K, color='g', linestyle='--', linewidth=2, label=f'Strike K={K}')
axes[0, 1].set_xlabel('Final price S_T')
axes[0, 1].set_ylabel('Density')
axes[0, 1].set_title('Terminal Distribution (Risk-Neutral)')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Payoff distribution
axes[1, 0].hist(payoffs, bins=50, density=True, alpha=0.6, edgecolor='black')
axes[1, 0].set_xlabel('Payoff max(S_T - K, 0)')
axes[1, 0].set_ylabel('Density')
axes[1, 0].set_title(f'Call Option Payoff Distribution\nMean = {np.mean(payoffs):.2f}')
axes[1, 0].grid(True, alpha=0.3)

# Option price as a function of S0
S0_range = np.linspace(70, 130, 50)
option_prices = [black_scholes_call(S, K, T_mat, r_val, sigma_val) for S in S0_range]
intrinsic_value = np.maximum(S0_range - K, 0)
time_value = np.array(option_prices) - intrinsic_value

axes[1, 1].plot(S0_range, option_prices, 'b-', linewidth=3, label='Option value V(S)')
axes[1, 1].plot(S0_range, intrinsic_value, 'r--', linewidth=2, label='Intrinsic value')
axes[1, 1].fill_between(S0_range, intrinsic_value, option_prices, 
                        alpha=0.3, color='green', label='Time value')
axes[1, 1].axvline(S0, color='purple', linestyle=':', linewidth=2, label=f'Current S₀={S0}')
axes[1, 1].set_xlabel('Stock price S')
axes[1, 1].set_ylabel('Option price')
axes[1, 1].set_title('Call Option Value')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Black-Scholes PDE solved via Feynman-Kac")
print("✓ Generator of GBM determines option prices")
CONTEXT: Option Pricing

Asset price follows geometric Brownian motion:

dS_t = μS_t dt + σS_t dW_t

Option price V(t,S) satisfies Black-Scholes PDE:

∂V/∂t + (1/2)σ²S²∂²V/∂S² + rS∂V/∂S - rV = 0

This is backward heat equation with variable coefficients!


Black-Scholes operator (in log-price x = log(S)):
L = (r - σ²/2)∂/∂x + (σ²/2)∂²/∂x²

Generator symbol:
 2  2     ⎛     2⎞
σ ⋅ξ      ⎜    σ ⎟
───── + ξ⋅⎜r - ──⎟
  2       ⎝    2 ⎠

Martingale pricing formula:

V(t,S) = E^Q[e^{-r(T-t)} Payoff(S_T) | S_t = S]

where Q is risk-neutral measure (martingale measure).

This connects:
- PDEs (Black-Scholes equation)
- Stochastic calculus (Itô's lemma)
- Functional analysis (Feynman-Kac)
- Pseudo-differential operators (generator)


----------------------------------------------------------------------
Monte Carlo pricing: European Call Option
----------------------------------------------------------------------

Parameters: S0=100.0, K=100.0, T=1.0, r=0.05, σ=0.2
Black-Scholes price (closed form): 10.4506
Monte Carlo price: 10.3177 ± 0.2859 (95% CI)
Error: 0.1329
No description has been provided for this image
✓ Black-Scholes PDE solved via Feynman-Kac
✓ Generator of GBM determines option prices

Mean-Field Processes and McKean-Vlasov Equations¶

In [48]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Interacting Particle Systems

System of N particles with mean-field interaction:

dX_t^i = -∇V(X_t^i)dt - (1/N)Σ_j ∇W(X_t^i - X_t^j)dt + σdW_t^i

As N → ∞, the empirical measure converges to ρ(t,x) satisfying
the McKean-Vlasov equation (nonlinear Fokker-Planck):

∂ρ/∂t = ∇·(ρ∇V) + ∇·(ρ∇(W*ρ)) + (σ²/2)Δρ

This is a nonlinear PDE coupling particles through their distribution!
""")

# Example: Cucker-Smale model (velocity alignment)
print("\nExample: Cucker-Smale Flocking Model")

print("""
Particles align their velocities through interaction:

dv_t^i = (1/N)Σ_j a(|x_t^i - x_t^j|)(v_t^j - v_t^i)dt + σdW_t^i

where a(r) is interaction kernel (e.g., a(r) = 1/(1+r²))

Mean-field limit: v(t,x) satisfies

∂v/∂t = ∫ a(|x-y|)(v(t,y) - v(t,x))ρ(t,y)dy + diffusion
""")

# Simulation
print("\n" + "-"*70)
print("Simulation: Mean-field particle system")
print("-"*70)

# Parameters
N_particles = 100
T_mf = 10.0
N_mf = 500
dt_mf = T_mf / N_mf
t_mf = np.linspace(0, T_mf, N_mf)

# Initial positions and velocities (random distribution)
np.random.seed(42)
X_particles = np.random.randn(N_particles, N_mf) * 2.0
V_particles = np.random.randn(N_particles, N_mf) * 0.5

# Interaction parameters
kappa = 1.0  # Alignment strength
sigma_noise = 0.1

def interaction_kernel(r):
    """Decreasing interaction kernel"""
    return 1.0 / (1.0 + r**2)

# Time evolution
for i in range(1, N_mf):
    for p in range(N_particles):
        # Mean interaction
        alignment = 0.0
        for q in range(N_particles):
            if p != q:
                distance = abs(X_particles[p, i-1] - X_particles[q, i-1])
                weight = interaction_kernel(distance)
                alignment += weight * (V_particles[q, i-1] - V_particles[p, i-1])
        
        alignment /= N_particles
        
        # Velocity update
        V_particles[p, i] = V_particles[p, i-1] + kappa * alignment * dt_mf + \
                           sigma_noise * np.sqrt(dt_mf) * np.random.randn()
        
        # Position update
        X_particles[p, i] = X_particles[p, i-1] + V_particles[p, i] * dt_mf

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Phase space trajectories
for p in range(min(30, N_particles)):
    axes[0, 0].plot(X_particles[p, :], V_particles[p, :], 
                   alpha=0.3, linewidth=1)

axes[0, 0].scatter(X_particles[:, 0], V_particles[:, 0], 
                  c='green', s=30, alpha=0.8, label='Initial', zorder=5)
axes[0, 0].scatter(X_particles[:, -1], V_particles[:, -1], 
                  c='red', s=30, alpha=0.8, label='Final', zorder=5)
axes[0, 0].set_xlabel('Position x')
axes[0, 0].set_ylabel('Velocity v')
axes[0, 0].set_title('Phase Space Trajectories')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# Temporal evolution of positions
for p in range(min(20, N_particles)):
    axes[0, 1].plot(t_mf, X_particles[p, :], alpha=0.5, linewidth=1)

axes[0, 1].set_xlabel('t')
axes[0, 1].set_ylabel('Position x')
axes[0, 1].set_title('Spatial Evolution')
axes[0, 1].grid(True, alpha=0.3)

# Velocity distribution over time
times_to_plot = [0, N_mf//4, N_mf//2, 3*N_mf//4, N_mf-1]
colors_time = plt.cm.viridis(np.linspace(0, 1, len(times_to_plot)))

for idx, t_idx in enumerate(times_to_plot):
    axes[1, 0].hist(V_particles[:, t_idx], bins=20, alpha=0.5, 
                   density=True, color=colors_time[idx],
                   label=f't={t_mf[t_idx]:.1f}')

axes[1, 0].set_xlabel('Velocity v')
axes[1, 0].set_ylabel('Density')
axes[1, 0].set_title('Velocity Distribution (Consensus)')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)

# Velocity variance (consensus measure)
velocity_variance = np.var(V_particles, axis=0)

axes[1, 1].semilogy(t_mf, velocity_variance, 'b-', linewidth=2)
axes[1, 1].set_xlabel('t')
axes[1, 1].set_ylabel('Var(v_t) (log scale)')
axes[1, 1].set_title('Velocity Variance (Flocking)')
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Mean-field limit: N → ∞ gives nonlinear PDE")
print("✓ McKean-Vlasov equations describe collective behavior")
print("✓ Applications: social dynamics, neuroscience, swarming")
CONTEXT: Interacting Particle Systems

System of N particles with mean-field interaction:

dX_t^i = -∇V(X_t^i)dt - (1/N)Σ_j ∇W(X_t^i - X_t^j)dt + σdW_t^i

As N → ∞, the empirical measure converges to ρ(t,x) satisfying
the McKean-Vlasov equation (nonlinear Fokker-Planck):

∂ρ/∂t = ∇·(ρ∇V) + ∇·(ρ∇(W*ρ)) + (σ²/2)Δρ

This is a nonlinear PDE coupling particles through their distribution!


Example: Cucker-Smale Flocking Model

Particles align their velocities through interaction:

dv_t^i = (1/N)Σ_j a(|x_t^i - x_t^j|)(v_t^j - v_t^i)dt + σdW_t^i

where a(r) is interaction kernel (e.g., a(r) = 1/(1+r²))

Mean-field limit: v(t,x) satisfies

∂v/∂t = ∫ a(|x-y|)(v(t,y) - v(t,x))ρ(t,y)dy + diffusion


----------------------------------------------------------------------
Simulation: Mean-field particle system
----------------------------------------------------------------------
No description has been provided for this image
✓ Mean-field limit: N → ∞ gives nonlinear PDE
✓ McKean-Vlasov equations describe collective behavior
✓ Applications: social dynamics, neuroscience, swarming

Sobolev Spaces and Elliptic Regularity¶

In [49]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Functional Analysis and Regularity Theory

The Sobolev space H^s(ℝ) consists of functions with:

||f||_{H^s}² = ∫ (1 + |ξ|²)^s |f̂(ξ)|² dξ < ∞

A pseudo-differential operator P with symbol p(ξ) of order m maps:

P: H^s → H^{s-m}

This is the foundation of elliptic regularity theory.
""")

# Derivative operator
print("\nExample: Derivative operator D = d/dx")
print("Symbol: σ(D) = iξ")

# Sobolev norm
s = symbols('s', real=True, positive=True)

print("\nSobolev norm with symbol (1 + ξ²)^{s/2}:")

Sobolev_symbol = (1 + xi**2)**(s/2)

print("\nH^s norm symbol:")
pprint(Sobolev_symbol)

Sobolev_op = PseudoDifferentialOperator(Sobolev_symbol, [x], mode='symbol')

print("""
Key results:

1. EMBEDDING THEOREM (Sobolev):
   H^s(ℝ) ⊂ C^k(ℝ) if s > k + 1/2

2. MULTIPLICATION:
   If s > 1/2, then H^s is an algebra (fg ∈ H^s if f,g ∈ H^s)

3. ELLIPTIC REGULARITY:
   If Pu = f with P elliptic of order m, then:
   f ∈ H^s ⟹ u ∈ H^{s+m}
   
   "Smooth data ⟹ smooth solution"

4. GÅRDING INEQUALITY:
   For elliptic P: Re⟨Pu,u⟩ ≥ c||u||_{H^{m/2}}² - C||u||²
""")

# Visualization of regularity
print("\n" + "-"*70)
print("Visualization: Sobolev regularity")
print("-"*70)

# Functions with different regularities
N = 512
L = 2*np.pi
x_vals = np.linspace(0, L, N, endpoint=False)
dx = L / N

# Frequencies
freqs = fftfreq(N, d=dx) * 2*np.pi

# Different functions
functions = {
    'Smooth (C^∞)': np.exp(-x_vals**2/2),  # Gaussian
    'H^2 (continuous 2nd deriv)': np.where(np.abs(x_vals - np.pi) < 1, 
                                           1 - (x_vals - np.pi)**2, 0),
    'H^1 (continuous, L^2 deriv)': np.abs(x_vals - np.pi),
    'H^{1/2} (boundary of continuity)': np.sign(x_vals - np.pi),
}

fig, axes = plt.subplots(3, 2, figsize=(14, 12))

colors = ['blue', 'green', 'orange', 'red']

for idx, (name, func) in enumerate(functions.items()):
    # Function in physical space
    ax_space = axes[idx, 0] if idx < 3 else axes[2, 0]
    ax_space.plot(x_vals, func, color=colors[idx], linewidth=2)
    ax_space.set_xlabel('x')
    ax_space.set_ylabel('f(x)')
    ax_space.set_title(name)
    ax_space.grid(True, alpha=0.3)
    
    # Fourier transform
    func_fft = fft(func)
    
    ax_freq = axes[idx, 1] if idx < 3 else axes[2, 1]
    ax_freq.semilogy(freqs[freqs > 0], np.abs(func_fft[freqs > 0])**2, 
                     'o', markersize=3, color=colors[idx], alpha=0.6)
    ax_freq.set_xlabel('Frequency ξ')
    ax_freq.set_ylabel('|f̂(ξ)|²')
    ax_freq.set_title(f'Fourier Spectrum: {name}')
    ax_freq.grid(True, alpha=0.3)
    
    # Reference decay lines for comparison
    xi_ref = np.logspace(0, 2, 50)
    for s_val in [0.5, 1, 2]:
        decay = xi_ref**(-2*s_val)
        ax_freq.plot(xi_ref, decay * np.max(np.abs(func_fft)**2), 
                    '--', alpha=0.3, label=f'ξ^{{-2·{s_val}}}')
    
    if idx == 0:
        ax_freq.legend(fontsize=8)

# Sobolev regularity plot
ax_sobolev = axes[0, 1]
ax_sobolev.clear()

s_values = np.linspace(0, 4, 100)
k_values = s_values - 0.5  # Sobolev embedding: s > k + 1/2

ax_sobolev.fill_between(s_values, 0, k_values, 
                        where=(k_values >= 0), alpha=0.3, 
                        color='lightblue', label='H^s ⊂ C^k')
ax_sobolev.plot(s_values, k_values, 'b-', linewidth=3)
ax_sobolev.axhline(0, color='k', linestyle='--', alpha=0.5)
ax_sobolev.axhline(1, color='r', linestyle=':', alpha=0.5, label='C^1')
ax_sobolev.axhline(2, color='g', linestyle=':', alpha=0.5, label='C^2')
ax_sobolev.set_xlabel('Sobolev exponent s')
ax_sobolev.set_ylabel('Continuity class k')
ax_sobolev.set_title('Sobolev Embedding: H^s(ℝ) ⊂ C^k(ℝ) for s > k+1/2')
ax_sobolev.legend()
ax_sobolev.grid(True, alpha=0.3)
ax_sobolev.set_xlim([0, 4])
ax_sobolev.set_ylim([-1, 3.5])

plt.tight_layout()
plt.show()

print("\n✓ Sobolev spaces quantify regularity via Fourier decay")
print("✓ Pseudo-differential operators shift regularity by their order")
CONTEXT: Functional Analysis and Regularity Theory

The Sobolev space H^s(ℝ) consists of functions with:

||f||_{H^s}² = ∫ (1 + |ξ|²)^s |f̂(ξ)|² dξ < ∞

A pseudo-differential operator P with symbol p(ξ) of order m maps:

P: H^s → H^{s-m}

This is the foundation of elliptic regularity theory.


Example: Derivative operator D = d/dx
Symbol: σ(D) = iξ

Sobolev norm with symbol (1 + ξ²)^{s/2}:

H^s norm symbol:
        s
        ─
        2
⎛ 2    ⎞ 
⎝ξ  + 1⎠ 

Key results:

1. EMBEDDING THEOREM (Sobolev):
   H^s(ℝ) ⊂ C^k(ℝ) if s > k + 1/2

2. MULTIPLICATION:
   If s > 1/2, then H^s is an algebra (fg ∈ H^s if f,g ∈ H^s)

3. ELLIPTIC REGULARITY:
   If Pu = f with P elliptic of order m, then:
   f ∈ H^s ⟹ u ∈ H^{s+m}
   
   "Smooth data ⟹ smooth solution"

4. GÅRDING INEQUALITY:
   For elliptic P: Re⟨Pu,u⟩ ≥ c||u||_{H^{m/2}}² - C||u||²


----------------------------------------------------------------------
Visualization: Sobolev regularity
----------------------------------------------------------------------
No description has been provided for this image
✓ Sobolev spaces quantify regularity via Fourier decay
✓ Pseudo-differential operators shift regularity by their order

Littlewood-Paley Theory¶

In [50]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Harmonic Analysis

Littlewood-Paley decomposition splits a function into frequency bands:

f = Σ_{j=-∞}^∞ Δ_j f

where Δ_j is a frequency cutoff at dyadic scale 2^j:

Δ_j f = φ(2^{-j}D) f,  φ supported in annulus

This is implemented via pseudo-differential operators!
""")

# Frequency localization operator
print("\nFrequency localization operator:")
print("Δ_j: project to frequencies |ξ| ≈ 2^j")

j = symbols('j', integer=True)

# Littlewood-Paley symbol (smooth characteristic function)
# φ_j(ξ) ≈ 1 for 2^{j-1} ≤ |ξ| ≤ 2^{j+1}, 0 elsewhere

print("""
Bernstein inequalities:

For u with spectrum in {|ξ| ≈ 2^j}:

||∂^α u||_{L^p} ≲ 2^{j|α|} ||u||_{L^p}

These are crucial for:
- Proving product estimates in Sobolev spaces
- Nonlinear PDE theory (Navier-Stokes, etc.)
- Paraproduct decomposition (Bony)
""")

# LP decomposition simulation
print("\n" + "-"*70)
print("Simulation: Littlewood-Paley decomposition")
print("-"*70)

# Test signal
N = 1024
L = 2*np.pi
x_vals = np.linspace(0, L, N, endpoint=False)

# Multi-scale signal
signal = (np.sin(x_vals) + 0.5*np.sin(5*x_vals) + 
          0.3*np.sin(20*x_vals) + 0.1*np.sin(50*x_vals))

# FFT
signal_fft = fft(signal)
freqs = fftfreq(N, d=L/N) * 2*np.pi

# Dyadic band decomposition
def smooth_cutoff(xi, j):
    """Smooth cutoff function for band j"""
    center = 2**j
    width = 2**j
    # Bump function
    return np.exp(-1/(1 - ((xi - center)/width)**2)) * (np.abs((xi - center)/width) < 1)

# Number of bands
j_min, j_max = -2, 5
bands = range(j_min, j_max + 1)

fig, axes = plt.subplots(3, 3, figsize=(15, 12))

# Original signal
axes[0, 0].plot(x_vals, signal, 'b-', linewidth=2)
axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('f(x)')
axes[0, 0].set_title('Original Signal')
axes[0, 0].grid(True, alpha=0.3)

# Spectrum
axes[0, 1].semilogy(np.abs(freqs), np.abs(signal_fft), 'b-', linewidth=1)
axes[0, 1].set_xlabel('|ξ|')
axes[0, 1].set_ylabel('|f̂(ξ)|')
axes[0, 1].set_title('Fourier Spectrum')
axes[0, 1].grid(True, alpha=0.3)
axes[0, 1].set_xlim([0, 60])

# Dyadic bands
colors_lp = plt.cm.rainbow(np.linspace(0, 1, len(bands)))

for idx, j_val in enumerate(bands):
    if idx >= 7:  # Limit to 7 bands for display
        break
    
    # Build filter
    filter_j = np.zeros_like(freqs)
    center_freq = 2**j_val
    
    # Simple band-pass filter
    mask = (np.abs(freqs) >= center_freq * 0.7) & (np.abs(freqs) <= center_freq * 1.4)
    filter_j[mask] = 1.0
    
    # Apply filter
    band_fft = signal_fft * filter_j
    band_signal = np.real(ifft(band_fft))
    
    # Plot
    row = 1 + idx // 3
    col = idx % 3
    
    if row < 3:
        axes[row, col].plot(x_vals, band_signal, color=colors_lp[idx], linewidth=1.5)
        axes[row, col].set_xlabel('x')
        axes[row, col].set_ylabel(f'Δ_{{{j_val}}} f')
        axes[row, col].set_title(f'Frequency Band j={j_val} (|ξ| ≈ {2**j_val:.1f})')
        axes[row, col].grid(True, alpha=0.3)
    
    # Highlight band in spectrum
    axes[0, 1].axvspan(center_freq * 0.7, center_freq * 1.4, 
                       alpha=0.2, color=colors_lp[idx])

# Reconstruction
axes[0, 2].set_visible(False)
axes[1, 2].set_visible(False)
axes[2, 2].set_visible(False)

plt.tight_layout()
plt.show()

print("\n✓ Littlewood-Paley: frequency localization tool")
print("✓ Essential for modern nonlinear PDE analysis")
CONTEXT: Harmonic Analysis

Littlewood-Paley decomposition splits a function into frequency bands:

f = Σ_{j=-∞}^∞ Δ_j f

where Δ_j is a frequency cutoff at dyadic scale 2^j:

Δ_j f = φ(2^{-j}D) f,  φ supported in annulus

This is implemented via pseudo-differential operators!


Frequency localization operator:
Δ_j: project to frequencies |ξ| ≈ 2^j

Bernstein inequalities:

For u with spectrum in {|ξ| ≈ 2^j}:

||∂^α u||_{L^p} ≲ 2^{j|α|} ||u||_{L^p}

These are crucial for:
- Proving product estimates in Sobolev spaces
- Nonlinear PDE theory (Navier-Stokes, etc.)
- Paraproduct decomposition (Bony)


----------------------------------------------------------------------
Simulation: Littlewood-Paley decomposition
----------------------------------------------------------------------
No description has been provided for this image
✓ Littlewood-Paley: frequency localization tool
✓ Essential for modern nonlinear PDE analysis

Paradifferential Calculus (Bony)¶

In [51]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Nonlinear Analysis

For products fg in Sobolev spaces, the paradifferential calculus gives:

fg = T_f g + T_g f + R(f,g)

where:
- T_f g: paradifferential operator (f acts on high freq of g)
- R(f,g): remainder (smoother than naive product)

Symbol of T_f:
σ(T_f)(x,ξ) = Σ_j χ_j(ξ) S_{j-1}f(x)

where S_j = low-pass filter, χ_j = frequency cutoff
""")

print("""
Key result (Bony, 1981):

If f ∈ H^s, g ∈ H^t with s + t > 0, s > 0, then:

fg ∈ H^r for any r < min(s, t, s+t)

This is OPTIMAL and shows products "lose derivatives".

Applications:
- Well-posedness of nonlinear PDEs (Navier-Stokes)
- Blow-up criteria
- Transport equations with rough coefficients
""")

# Illustration of the paradifferential calculus
print("\n" + "-"*70)
print("Paradifferential decomposition")
print("-"*70)

print("""
Example: Product of H^{1/2} functions

Let f, g ∈ H^{1/2}(ℝ).

Naive product: fg ∉ H^{1/2} (fails!)

Bony decomposition:
- T_f g ∈ H^{1/2} (paradifferential part)
- T_g f ∈ H^{1/2} (symmetric part)
- R(f,g) ∈ H^1 (remainder is smoother!)

Total: fg ∈ H^r for any r < 1/2
""")

# Simulation
N = 512
x_vals = np.linspace(0, 2*np.pi, N, endpoint=False)

# Two H^{1/2} functions (at the boundary of continuity)
f = np.sign(np.sin(x_vals))
g = np.sign(np.cos(2*x_vals))

# Naive product
fg_naive = f * g

# To simulate T_f g, we low-pass filter f and multiply by high-frequency part of g
def low_pass_filter(signal, cutoff_ratio=0.3):
    """Low-pass filter"""
    signal_fft = fft(signal)
    N = len(signal)
    cutoff = int(N * cutoff_ratio)
    signal_fft[cutoff:-cutoff] = 0
    return np.real(ifft(signal_fft))

def high_pass_filter(signal, cutoff_ratio=0.3):
    """High-pass filter"""
    signal_fft = fft(signal)
    N = len(signal)
    cutoff = int(N * cutoff_ratio)
    signal_fft[:cutoff] = 0
    signal_fft[-cutoff:] = 0
    return np.real(ifft(signal_fft))

# Bony decomposition (approximation)
f_low = low_pass_filter(f)
g_high = high_pass_filter(g)

g_low = low_pass_filter(g)
f_high = high_pass_filter(f)

T_f_g = f_low * g_high  # Paradifferential
T_g_f = g_low * f_high  # Symmetric part
R_fg = f_high * g_high   # Remainder (high × high = even higher frequencies)

fig, axes = plt.subplots(3, 2, figsize=(14, 12))

# Original functions
axes[0, 0].plot(x_vals, f, 'b-', linewidth=2, label='f (H^{1/2})')
axes[0, 0].plot(x_vals, g, 'r-', linewidth=2, alpha=0.7, label='g (H^{1/2})')
axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('Value')
axes[0, 0].set_title('Original Functions')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# Naive product
axes[0, 1].plot(x_vals, fg_naive, 'purple', linewidth=2)
axes[0, 1].set_xlabel('x')
axes[0, 1].set_ylabel('fg')
axes[0, 1].set_title('Naive Product fg')
axes[0, 1].grid(True, alpha=0.3)

# T_f g
axes[1, 0].plot(x_vals, f_low, 'b--', alpha=0.5, label='f_low')
axes[1, 0].plot(x_vals, T_f_g, 'g-', linewidth=2, label='T_f g')
axes[1, 0].set_xlabel('x')
axes[1, 0].set_ylabel('Value')
axes[1, 0].set_title('Paradifferential T_f g = f_low · g_high')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)

# T_g f
axes[1, 1].plot(x_vals, g_low, 'r--', alpha=0.5, label='g_low')
axes[1, 1].plot(x_vals, T_g_f, 'orange', linewidth=2, label='T_g f')
axes[1, 1].set_xlabel('x')
axes[1, 1].set_ylabel('Value')
axes[1, 1].set_title('Paradifferential T_g f = g_low · f_high')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

# Remainder R(f,g)
axes[2, 0].plot(x_vals, R_fg, 'brown', linewidth=2)
axes[2, 0].set_xlabel('x')
axes[2, 0].set_ylabel('R(f,g)')
axes[2, 0].set_title('Remainder R(f,g) = f_high · g_high (smoother!)')
axes[2, 0].grid(True, alpha=0.3)

# Verification of the decomposition
reconstruction = T_f_g + T_g_f + R_fg
axes[2, 1].plot(x_vals, fg_naive, 'purple', linewidth=2, label='fg (naive)')
axes[2, 1].plot(x_vals, reconstruction, 'g--', linewidth=2, alpha=0.7, 
               label='T_f g + T_g f + R')
axes[2, 1].set_xlabel('x')
axes[2, 1].set_ylabel('Value')
axes[2, 1].set_title('Bony Decomposition Check')
axes[2, 1].legend()
axes[2, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Paradifferential calculus handles rough products")
print("✓ Key tool for subcritical nonlinear PDEs")
CONTEXT: Nonlinear Analysis

For products fg in Sobolev spaces, the paradifferential calculus gives:

fg = T_f g + T_g f + R(f,g)

where:
- T_f g: paradifferential operator (f acts on high freq of g)
- R(f,g): remainder (smoother than naive product)

Symbol of T_f:
σ(T_f)(x,ξ) = Σ_j χ_j(ξ) S_{j-1}f(x)

where S_j = low-pass filter, χ_j = frequency cutoff


Key result (Bony, 1981):

If f ∈ H^s, g ∈ H^t with s + t > 0, s > 0, then:

fg ∈ H^r for any r < min(s, t, s+t)

This is OPTIMAL and shows products "lose derivatives".

Applications:
- Well-posedness of nonlinear PDEs (Navier-Stokes)
- Blow-up criteria
- Transport equations with rough coefficients


----------------------------------------------------------------------
Paradifferential decomposition
----------------------------------------------------------------------

Example: Product of H^{1/2} functions

Let f, g ∈ H^{1/2}(ℝ).

Naive product: fg ∉ H^{1/2} (fails!)

Bony decomposition:
- T_f g ∈ H^{1/2} (paradifferential part)
- T_g f ∈ H^{1/2} (symmetric part)
- R(f,g) ∈ H^1 (remainder is smoother!)

Total: fg ∈ H^r for any r < 1/2

No description has been provided for this image
✓ Paradifferential calculus handles rough products
✓ Key tool for subcritical nonlinear PDEs

Calderón-Zygmund Theory - Singular Integrals¶

In [52]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Singular Integral Operators

A Calderón-Zygmund operator has kernel K(x,y) with singularity:

|K(x,y)| ≲ |x-y|^{-n}  (dimension n)

Example: Hilbert transform on ℝ

(Hf)(x) = p.v. ∫ f(y)/(x-y) dy

Symbol: σ(H)(ξ) = -i·sgn(ξ)
""")

# Hilbert transform
print("\nHilbert Transform: H")
print("Symbol: σ(H) = -i·sgn(ξ)")

from sympy import sign as sgn

H_symbol = -I * sgn(xi)

print("\nSymbol:")
pprint(H_symbol)

print("""
Properties:

1. BOUNDEDNESS: H: L^p → L^p for 1 < p < ∞
   (fails for p=1 and p=∞!)

2. IDENTITY: H² = -Id

3. CAUCHY TRANSFORM: F + iHF is analytic (boundary values)

4. HARMONIC CONJUGATE: Δ(F + iHF) = 0

Applications:
- Analytic function theory
- Signal processing (analytic signal)
- Compensated compactness (PDE)
- BMO spaces (John-Nirenberg)
""")

# Simulation of the Hilbert transform
print("\n" + "-"*70)
print("Simulation: Hilbert transform")
print("-"*70)

N = 512
x_vals = np.linspace(-10, 10, N)
dx = x_vals[1] - x_vals[0]

# Test signal
signal_test = np.exp(-x_vals**2/2) * np.cos(2*x_vals)

# Hilbert transform via FFT
signal_fft = fft(signal_test)
freqs = fftfreq(N, d=dx) * 2*np.pi

# Multiply by -i·sgn(ξ)
H_multiplier = -1j * np.sign(freqs)
H_signal_fft = signal_fft * H_multiplier

H_signal = np.real(ifft(H_signal_fft))

# Analytic signal
analytic_signal = signal_test + 1j * H_signal

fig, axes = plt.subplots(2, 3, figsize=(15, 10))

# Original signal
axes[0, 0].plot(x_vals, signal_test, 'b-', linewidth=2)
axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('f(x)')
axes[0, 0].set_title('Original Signal f(x)')
axes[0, 0].grid(True, alpha=0.3)

# Hilbert transform
axes[0, 1].plot(x_vals, H_signal, 'r-', linewidth=2)
axes[0, 1].set_xlabel('x')
axes[0, 1].set_ylabel('(Hf)(x)')
axes[0, 1].set_title('Hilbert Transform Hf(x)')
axes[0, 1].grid(True, alpha=0.3)

# Signal and its transform
axes[0, 2].plot(x_vals, signal_test, 'b-', linewidth=2, label='f')
axes[0, 2].plot(x_vals, H_signal, 'r-', linewidth=2, label='Hf')
axes[0, 2].set_xlabel('x')
axes[0, 2].set_ylabel('Value')
axes[0, 2].set_title('f and Hf (90° phase shift)')
axes[0, 2].legend()
axes[0, 2].grid(True, alpha=0.3)

# Spectrum of the multiplier
axes[1, 0].plot(freqs, np.real(H_multiplier), 'b-', linewidth=2)
axes[1, 0].axhline(0, color='k', linestyle='--', alpha=0.5)
axes[1, 0].set_xlabel('ξ')
axes[1, 0].set_ylabel('Re[σ(H)(ξ)]')
axes[1, 0].set_title('Hilbert Symbol (real part = 0)')
axes[1, 0].grid(True, alpha=0.3)

axes[1, 1].plot(freqs, np.imag(H_multiplier), 'r-', linewidth=2)
axes[1, 1].set_xlabel('ξ')
axes[1, 1].set_ylabel('Im[σ(H)(ξ)] = -sgn(ξ)')
axes[1, 1].set_title('Hilbert Symbol (imaginary part)')
axes[1, 1].grid(True, alpha=0.3)

# Analytic signal in the complex plane
axes[1, 2].plot(signal_test, H_signal, 'purple', linewidth=2)
axes[1, 2].set_xlabel('Re (f)')
axes[1, 2].set_ylabel('Im (Hf)')
axes[1, 2].set_title('Analytic Signal: f + iHf')
axes[1, 2].grid(True, alpha=0.3)
axes[1, 2].set_aspect('equal')

plt.tight_layout()
plt.show()

print("\n✓ Hilbert transform: prototype singular integral")
print("✓ Pseudo-differential operator of order 0")
CONTEXT: Singular Integral Operators

A Calderón-Zygmund operator has kernel K(x,y) with singularity:

|K(x,y)| ≲ |x-y|^{-n}  (dimension n)

Example: Hilbert transform on ℝ

(Hf)(x) = p.v. ∫ f(y)/(x-y) dy

Symbol: σ(H)(ξ) = -i·sgn(ξ)


Hilbert Transform: H
Symbol: σ(H) = -i·sgn(ξ)

Symbol:
-ⅈ⋅sign(ξ)

Properties:

1. BOUNDEDNESS: H: L^p → L^p for 1 < p < ∞
   (fails for p=1 and p=∞!)

2. IDENTITY: H² = -Id

3. CAUCHY TRANSFORM: F + iHF is analytic (boundary values)

4. HARMONIC CONJUGATE: Δ(F + iHF) = 0

Applications:
- Analytic function theory
- Signal processing (analytic signal)
- Compensated compactness (PDE)
- BMO spaces (John-Nirenberg)


----------------------------------------------------------------------
Simulation: Hilbert transform
----------------------------------------------------------------------
No description has been provided for this image
✓ Hilbert transform: prototype singular integral
✓ Pseudo-differential operator of order 0

Fourier Multiplier Operators¶

In [53]:
x = symbols('x', real=True)
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Multiplier Theory

A Fourier multiplier operator has the form:

(T_m f)^(ξ) = m(ξ) f̂(ξ)

The multiplier m(ξ) is the symbol of T_m.

Mikhlin-Hörmander Theorem:
If |∂^α m(ξ)| ≲ |ξ|^{-|α|} for |α| ≤ [n/2] + 1, then:

T_m: L^p → L^p for 1 < p < ∞
""")

# Examples of multipliers
print("\nExamples of multipliers:")

multipliers = {
    'Derivative': I * xi,
    'Fractional Laplacian': (1 + xi**2)**(s/2),
    'Bessel potential': (1 + xi**2)**(-s/2),
    'Riesz transform': I * xi / abs(xi),
}

for name, mult in multipliers.items():
    print(f"\n{name}:")
    print(f"  m(ξ) = ", end="")
    pprint(mult)

print("""
Mihlin condition checks smoothness away from origin:

|ξ^k ∂_ξ^k m(ξ)| ≤ C for k = 0, 1, ..., ⌈n/2⌉+1

This ensures L^p boundedness!
""")

# Visualization of multipliers
print("\n" + "-"*70)
print("Visualization: Fourier multipliers")
print("-"*70)

xi_vals = np.linspace(-10, 10, 1000)
xi_vals_pos = xi_vals[xi_vals > 0.1]

fig, axes = plt.subplots(2, 3, figsize=(15, 10))

# Derivative
axes[0, 0].plot(xi_vals, xi_vals, 'b-', linewidth=2)
axes[0, 0].set_xlabel('ξ')
axes[0, 0].set_ylabel('m(ξ)')
axes[0, 0].set_title('Derivative: m(ξ) = iξ (real part)')
axes[0, 0].grid(True, alpha=0.3)

# Fractional Laplacian
for s_val in [0.5, 1, 1.5, 2]:
    mult_vals = (1 + xi_vals**2)**(s_val/2)
    axes[0, 1].plot(xi_vals, mult_vals, linewidth=2, label=f's={s_val}')

axes[0, 1].set_xlabel('ξ')
axes[0, 1].set_ylabel('m(ξ)')
axes[0, 1].set_title('Fractional Laplacian: (1+ξ²)^{s/2}')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)
axes[0, 1].set_yscale('log')

# Bessel potential
for s_val in [0.5, 1, 1.5, 2]:
    mult_vals = (1 + xi_vals**2)**(-s_val/2)
    axes[0, 2].plot(xi_vals, mult_vals, linewidth=2, label=f's={s_val}')

axes[0, 2].set_xlabel('ξ')
axes[0, 2].set_ylabel('m(ξ)')
axes[0, 2].set_title('Bessel Potential: (1+ξ²)^{-s/2}')
axes[0, 2].legend()
axes[0, 2].grid(True, alpha=0.3)

# Riesz transform
riesz_mult = xi_vals / (np.abs(xi_vals) + 1e-10)
axes[1, 0].plot(xi_vals, riesz_mult, 'r-', linewidth=2)
axes[1, 0].set_xlabel('ξ')
axes[1, 0].set_ylabel('m(ξ)')
axes[1, 0].set_title('Riesz Transform: ξ/|ξ| (sign function)')
axes[1, 0].grid(True, alpha=0.3)
axes[1, 0].axhline(0, color='k', linestyle='--', alpha=0.5)

# Verification of the Mikhlin condition
# |ξ^k ∂_ξ^k m(ξ)| bounded
s_val = 1.5
m_bessel = lambda xi: (1 + xi**2)**(-s_val/2)
dm_bessel = lambda xi: -s_val * xi * (1 + xi**2)**(-s_val/2 - 1)

mikhlin_0 = np.abs(m_bessel(xi_vals_pos))
mikhlin_1 = np.abs(xi_vals_pos * dm_bessel(xi_vals_pos))

axes[1, 1].plot(xi_vals_pos, mikhlin_0, 'b-', linewidth=2, label='|m(ξ)|')
axes[1, 1].plot(xi_vals_pos, mikhlin_1, 'r-', linewidth=2, label='|ξ m\'(ξ)|')
axes[1, 1].set_xlabel('ξ')
axes[1, 1].set_ylabel('Mikhlin condition')
axes[1, 1].set_title(f'Bessel s={s_val}: Mikhlin bounds')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)
axes[1, 1].set_xscale('log')

# Application: band-pass filtering
N_app = 512
x_app = np.linspace(0, 2*np.pi, N_app, endpoint=False)
signal_app = np.sin(x_app) + 0.5*np.sin(5*x_app) + 0.2*np.sin(20*x_app)

# Band-pass filter [2, 10]
signal_fft_app = fft(signal_app)
freqs_app = fftfreq(N_app, d=2*np.pi/N_app) * 2*np.pi

# Multiplier: indicator function of [2, 10]
multiplier_bandpass = ((np.abs(freqs_app) >= 2) & (np.abs(freqs_app) <= 10)).astype(float)

filtered_fft = signal_fft_app * multiplier_bandpass
filtered_signal = np.real(ifft(filtered_fft))

axes[1, 2].plot(x_app, signal_app, 'b-', linewidth=1, alpha=0.5, label='Original')
axes[1, 2].plot(x_app, filtered_signal, 'r-', linewidth=2, label='Band-pass [2,10]')
axes[1, 2].set_xlabel('x')
axes[1, 2].set_ylabel('Signal')
axes[1, 2].set_title('Fourier Multiplier Application')
axes[1, 2].legend()
axes[1, 2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Fourier multipliers = pseudo-differential operators with x-independent symbol")
print("✓ Mikhlin-Hörmander: sufficient condition for L^p boundedness")
CONTEXT: Multiplier Theory

A Fourier multiplier operator has the form:

(T_m f)^(ξ) = m(ξ) f̂(ξ)

The multiplier m(ξ) is the symbol of T_m.

Mikhlin-Hörmander Theorem:
If |∂^α m(ξ)| ≲ |ξ|^{-|α|} for |α| ≤ [n/2] + 1, then:

T_m: L^p → L^p for 1 < p < ∞


Examples of multipliers:

Derivative:
  m(ξ) = ⅈ⋅ξ

Fractional Laplacian:
  m(ξ) =         s
        ─
        2
⎛ 2    ⎞ 
⎝ξ  + 1⎠ 

Bessel potential:
  m(ξ) =         -s 
        ───
         2 
⎛ 2    ⎞   
⎝ξ  + 1⎠   

Riesz transform:
  m(ξ) = ⅈ⋅ξ
───
│ξ│

Mihlin condition checks smoothness away from origin:

|ξ^k ∂_ξ^k m(ξ)| ≤ C for k = 0, 1, ..., ⌈n/2⌉+1

This ensures L^p boundedness!


----------------------------------------------------------------------
Visualization: Fourier multipliers
----------------------------------------------------------------------
No description has been provided for this image
✓ Fourier multipliers = pseudo-differential operators with x-independent symbol
✓ Mikhlin-Hörmander: sufficient condition for L^p boundedness

Maximal Operators and Ergodic Theory¶

In [54]:
x = symbols('x', real=True)
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Maximal Functions

The Hardy-Littlewood maximal function:

(Mf)(x) = sup_{r>0} (1/|B_r|) ∫_{B_r(x)} |f(y)| dy

This is NOT a pseudo-differential operator, but related tools apply!

Maximal theorem:
- M: L^p → L^p for p > 1 (bounded)
- M: L^1 → L^{1,∞} (weak type)

Applications:
- Differentiation of integrals (Lebesgue points)
- Convergence of averages (ergodic theorem)
- Covering lemmas (Vitali, Besicovitch)
""")

# Local averaging operator
print("\nLocal averaging operator (approximation of maximal):")

# Average over ball of radius r
r = symbols('r', real=True, positive=True)

print("""
Averaging operator A_r:

(A_r f)(x) = (1/2r) ∫_{x-r}^{x+r} f(y) dy

Symbol (approximate): sinc(rξ) = sin(rξ)/(rξ)

As r → 0: A_r f → f (pointwise a.e.)
""")

# Simulation
print("\n" + "-"*70)
print("Simulation: Local averaging and maximal function")
print("-"*70)

N_max = 512
x_vals_max = np.linspace(0, 2*np.pi, N_max, endpoint=False)

# Test function with discontinuities
f_max = np.where((x_vals_max > np.pi/2) & (x_vals_max < 3*np.pi/2), 1.0, 0.0)
f_max += 0.3 * np.sin(5*x_vals_max)

# Local averages for different radii
radii = [0.1, 0.3, 0.5, 1.0]

def local_average(f, x, r, x_grid):
    """Local average over [x-r, x+r]"""
    dx = x_grid[1] - x_grid[0]
    mask = np.abs(x_grid - x) <= r
    if np.sum(mask) > 0:
        return np.mean(f[mask])
    return 0.0

# Maximal function (approximation)
maximal_f = np.zeros_like(x_vals_max)
for i, x_pt in enumerate(x_vals_max):
    max_avg = 0.0
    for r_test in np.linspace(0.01, 2.0, 50):
        avg = local_average(f_max, x_pt, r_test, x_vals_max)
        max_avg = max(max_avg, avg)
    maximal_f[i] = max_avg

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Original function
axes[0, 0].plot(x_vals_max, f_max, 'b-', linewidth=2)
axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('f(x)')
axes[0, 0].set_title('Original Function (with discontinuities)')
axes[0, 0].grid(True, alpha=0.3)

# Local averages
colors_avg = plt.cm.plasma(np.linspace(0.2, 0.9, len(radii)))

for idx, r_val in enumerate(radii):
    averaged = np.array([local_average(f_max, x_pt, r_val, x_vals_max) 
                        for x_pt in x_vals_max])
    axes[0, 1].plot(x_vals_max, averaged, linewidth=2, 
                   color=colors_avg[idx], label=f'r={r_val}')

axes[0, 1].plot(x_vals_max, f_max, 'k--', alpha=0.3, label='Original')
axes[0, 1].set_xlabel('x')
axes[0, 1].set_ylabel('(A_r f)(x)')
axes[0, 1].set_title('Local Averages')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Maximal function
axes[1, 0].plot(x_vals_max, f_max, 'b-', linewidth=1, alpha=0.5, label='f')
axes[1, 0].plot(x_vals_max, maximal_f, 'r-', linewidth=2, label='Mf')
axes[1, 0].set_xlabel('x')
axes[1, 0].set_ylabel('Value')
axes[1, 0].set_title('Hardy-Littlewood Maximal Function')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)

# Comparison of L^p norms
p_values = np.linspace(1, 4, 50)
norm_f = []
norm_Mf = []

for p_val in p_values:
    norm_f.append(np.mean(np.abs(f_max)**p_val)**(1/p_val))
    norm_Mf.append(np.mean(np.abs(maximal_f)**p_val)**(1/p_val))

axes[1, 1].plot(p_values, norm_f, 'b-', linewidth=2, label='||f||_p')
axes[1, 1].plot(p_values, norm_Mf, 'r-', linewidth=2, label='||Mf||_p')
axes[1, 1].axvline(1, color='k', linestyle='--', alpha=0.5, label='p=1 (critical)')
axes[1, 1].set_xlabel('p')
axes[1, 1].set_ylabel('L^p norm')
axes[1, 1].set_title('Norm Comparison: M bounded for p > 1')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Maximal function controls pointwise behavior")
print("✓ Key tool in harmonic analysis and differentiation theory")
CONTEXT: Maximal Functions

The Hardy-Littlewood maximal function:

(Mf)(x) = sup_{r>0} (1/|B_r|) ∫_{B_r(x)} |f(y)| dy

This is NOT a pseudo-differential operator, but related tools apply!

Maximal theorem:
- M: L^p → L^p for p > 1 (bounded)
- M: L^1 → L^{1,∞} (weak type)

Applications:
- Differentiation of integrals (Lebesgue points)
- Convergence of averages (ergodic theorem)
- Covering lemmas (Vitali, Besicovitch)


Local averaging operator (approximation of maximal):

Averaging operator A_r:

(A_r f)(x) = (1/2r) ∫_{x-r}^{x+r} f(y) dy

Symbol (approximate): sinc(rξ) = sin(rξ)/(rξ)

As r → 0: A_r f → f (pointwise a.e.)


----------------------------------------------------------------------
Simulation: Local averaging and maximal function
----------------------------------------------------------------------
No description has been provided for this image
✓ Maximal function controls pointwise behavior
✓ Key tool in harmonic analysis and differentiation theory

Restriction Operators - Harmonic Analysis¶

In [55]:
x = symbols('x', real=True)
x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)

print("""
CONTEXT: Restriction to Manifolds

For a surface S ⊂ ℝⁿ, the restriction operator:

R: f ↦ f|_S (restrict Fourier transform to S)

Question (Stein): When is R: L^p(ℝⁿ) → L^q(S) bounded?

Example: Sphere S^{n-1} ⊂ ℝⁿ

Stein-Tomas theorem:
If p < 2n/(n+1), then R: L^p → L^2(S^{n-1}) is bounded.

This is SHARP! Related to:
- Fourier restriction conjecture
- Kakeya conjecture  
- Bochner-Riesz problem
""")

print("""
For the circle S¹ ⊂ ℝ²:

Critical exponent: p = 4/3

Physical interpretation:
- Restriction = extracting oscillatory components
- Curvature of S helps (dispersion)
- Flat surfaces harder (Kakeya sets)

Applications:
- Dispersive PDEs (Schrödinger, wave)
- Strichartz estimates
- Nonlinear waves (global existence)
""")

# 2D simulation
print("\n" + "-"*70)
print("Visualization: Restriction to circle")
print("-"*70)

# Function in Fourier space
N_res = 256
xi_range = np.linspace(-10, 10, N_res)
eta_range = np.linspace(-10, 10, N_res)
XI, ETA = np.meshgrid(xi_range, eta_range)

# Test function in Fourier (Gaussian)
F_hat = np.exp(-(XI**2 + ETA**2)/4)

# Restriction to circle of radius R
R_circle = 3.0
theta_vals = np.linspace(0, 2*np.pi, 200)

xi_circle = R_circle * np.cos(theta_vals)
eta_circle = R_circle * np.sin(theta_vals)

# Interpolate F_hat onto the circle
interpolator = RegularGridInterpolator((xi_range, eta_range), F_hat.T, 
                                       bounds_error=False, fill_value=0)

F_restricted = interpolator(np.column_stack([xi_circle, eta_circle]))

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Function in Fourier space
im0 = axes[0, 0].contourf(XI, ETA, F_hat, levels=20, cmap='viridis')
axes[0, 0].plot(xi_circle, eta_circle, 'r-', linewidth=3, label=f'Circle R={R_circle}')
axes[0, 0].set_xlabel('ξ')
axes[0, 0].set_ylabel('η')
axes[0, 0].set_title('Function in Fourier Space F̂(ξ,η)')
axes[0, 0].legend()
axes[0, 0].set_aspect('equal')
plt.colorbar(im0, ax=axes[0, 0])

# Restriction to the circle
axes[0, 1].plot(theta_vals, F_restricted, 'b-', linewidth=2)
axes[0, 1].set_xlabel('θ (angle on circle)')
axes[0, 1].set_ylabel('F̂|_{S¹}(θ)')
axes[0, 1].set_title(f'Restricted to Circle: R={R_circle}')
axes[0, 1].grid(True, alpha=0.3)

# Norm comparison
radii = np.linspace(0.5, 8, 30)
L2_norms_full = []
L2_norms_restricted = []

for R_val in radii:
    # Full L² norm (approximation)
    L2_full = np.sqrt(np.sum(F_hat**2) * (xi_range[1]-xi_range[0])**2)
    L2_norms_full.append(L2_full)
    
    # L² norm on the circle
    xi_c = R_val * np.cos(theta_vals)
    eta_c = R_val * np.sin(theta_vals)
    F_c = interpolator(np.column_stack([xi_c, eta_c]))
    L2_circle = np.sqrt(np.trapezoid(F_c**2, theta_vals))
    L2_norms_restricted.append(L2_circle)

axes[1, 0].plot(radii, L2_norms_full, 'b-', linewidth=2, label='||F̂||_{L²(ℝ²)}')
axes[1, 0].plot(radii, L2_norms_restricted, 'r-', linewidth=2, label='||F̂|_S||_{L²(S¹)}')
axes[1, 0].set_xlabel('Circle radius R')
axes[1, 0].set_ylabel('L² norm')
axes[1, 0].set_title('Norm Comparison')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)

# Decay with radius (dispersion)
decay_rate = np.abs(np.gradient(L2_norms_restricted, radii))

axes[1, 1].semilogy(radii, L2_norms_restricted, 'b-', linewidth=2, label='Norm')
axes[1, 1].semilogy(radii, radii**(-1/2), 'r--', linewidth=2, label='R^{-1/2} (theory)')
axes[1, 1].set_xlabel('Circle radius R')
axes[1, 1].set_ylabel('||F̂|_S||_{L²} (log scale)')
axes[1, 1].set_title('Decay Rate (Dispersion)')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ Restriction operators measure concentration on manifolds")
print("✓ Curvature improves regularity (dispersion)")
CONTEXT: Restriction to Manifolds

For a surface S ⊂ ℝⁿ, the restriction operator:

R: f ↦ f|_S (restrict Fourier transform to S)

Question (Stein): When is R: L^p(ℝⁿ) → L^q(S) bounded?

Example: Sphere S^{n-1} ⊂ ℝⁿ

Stein-Tomas theorem:
If p < 2n/(n+1), then R: L^p → L^2(S^{n-1}) is bounded.

This is SHARP! Related to:
- Fourier restriction conjecture
- Kakeya conjecture  
- Bochner-Riesz problem


For the circle S¹ ⊂ ℝ²:

Critical exponent: p = 4/3

Physical interpretation:
- Restriction = extracting oscillatory components
- Curvature of S helps (dispersion)
- Flat surfaces harder (Kakeya sets)

Applications:
- Dispersive PDEs (Schrödinger, wave)
- Strichartz estimates
- Nonlinear waves (global existence)


----------------------------------------------------------------------
Visualization: Restriction to circle
----------------------------------------------------------------------
No description has been provided for this image
✓ Restriction operators measure concentration on manifolds
✓ Curvature improves regularity (dispersion)

Fredholm Theory and Analytic Index¶

In [56]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Fredholm Operators

An operator T: X → Y is Fredholm if:
- dim ker(T) < ∞
- dim coker(T) < ∞
- range(T) is closed

The Fredholm index: ind(T) = dim ker(T) - dim coker(T)

For elliptic pseudo-differential operators P of order m:

P: H^s → H^{s-m} is Fredholm (on compact manifolds)

The index is TOPOLOGICAL (depends only on symbol)!
""")

# Example: operator on the circle
print("\nExample: Operators on S¹")

# Derivative operator + zero-order term
a, b = symbols('a b', real=True)

print("""
Consider: P = d/dx + a(x)

on periodic functions (S¹ = ℝ/2πℤ)

Symbol: σ(P) = iξ + a(x)

Elliptic condition: σ(P) ≠ 0 everywhere

If a(x) = ic (constant), then:
- σ(P) = iξ + ic never zero for ξ ≠ -c
- P is Fredholm
- ind(P) = winding number of symbol around origin
""")

# Index computation
print("\n" + "-"*70)
print("Index computation via winding number")
print("-"*70)

# Symbol as a function of (x, ξ)
def symbol_on_circle(x_val, xi_val, a_func):
    """Symbol on the circle"""
    return 1j * xi_val + a_func(x_val)

# Different choices of a(x)
x_test = np.linspace(0, 2*np.pi, 100)

cases = {
    'a = 0 (pure derivative)': lambda x: 0,
    'a = i (constant shift)': lambda x: 1j,
    'a = 2i sin(x)': lambda x: 2j * np.sin(x),
    'a = exp(ix)': lambda x: np.exp(1j * x),
}

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

for idx, (name, a_func) in enumerate(cases.items()):
    ax = axes[idx // 2, idx % 2]
    
    # For different frequencies
    xi_values = [-3, -2, -1, 0, 1, 2, 3]
    
    for xi_val in xi_values:
        symbols_vals = symbol_on_circle(x_test, xi_val, a_func)
        ax.plot(symbols_vals.real, symbols_vals.imag, 
               linewidth=2, alpha=0.7, label=f'ξ={xi_val}')
    
    ax.plot(0, 0, 'ro', markersize=10, label='Origin')
    ax.set_xlabel('Re(σ)')
    ax.set_ylabel('Im(σ)')
    ax.set_title(name)
    ax.grid(True, alpha=0.3)
    ax.set_aspect('equal')
    
    if idx == 0:
        ax.legend(fontsize=8, loc='upper right')

plt.tight_layout()
plt.show()

print("""
Index formula (Atiyah-Singer on S¹):

ind(P) = (1/2πi) ∫_{S¹×ℝ} d log σ(P)

For P = d/dx + a(x):
- If a constant: ind = 0
- If a = e^{ikx}: ind = -k (topological charge)

This connects:
- Analysis (Fredholm operators)
- Topology (winding numbers, cohomology)
- Geometry (characteristic classes)
""")

print("\n✓ Fredholm index = topological invariant")
print("✓ Elliptic regularity + compactness → finite-dimensional kernel/cokernel")
CONTEXT: Fredholm Operators

An operator T: X → Y is Fredholm if:
- dim ker(T) < ∞
- dim coker(T) < ∞
- range(T) is closed

The Fredholm index: ind(T) = dim ker(T) - dim coker(T)

For elliptic pseudo-differential operators P of order m:

P: H^s → H^{s-m} is Fredholm (on compact manifolds)

The index is TOPOLOGICAL (depends only on symbol)!


Example: Operators on S¹

Consider: P = d/dx + a(x)

on periodic functions (S¹ = ℝ/2πℤ)

Symbol: σ(P) = iξ + a(x)

Elliptic condition: σ(P) ≠ 0 everywhere

If a(x) = ic (constant), then:
- σ(P) = iξ + ic never zero for ξ ≠ -c
- P is Fredholm
- ind(P) = winding number of symbol around origin


----------------------------------------------------------------------
Index computation via winding number
----------------------------------------------------------------------
No description has been provided for this image
Index formula (Atiyah-Singer on S¹):

ind(P) = (1/2πi) ∫_{S¹×ℝ} d log σ(P)

For P = d/dx + a(x):
- If a constant: ind = 0
- If a = e^{ikx}: ind = -k (topological charge)

This connects:
- Analysis (Fredholm operators)
- Topology (winding numbers, cohomology)
- Geometry (characteristic classes)


✓ Fredholm index = topological invariant
✓ Elliptic regularity + compactness → finite-dimensional kernel/cokernel

Microlocal Analysis - Wave Front Sets¶

In [57]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Microlocal Analysis

The wave front set WF(u) measures:
- WHERE singularities are (in x)
- IN WHICH DIRECTION they propagate (in ξ)

WF(u) ⊂ T*M \ {0} (cotangent bundle minus zero section)

Definition:
(x₀, ξ₀) ∉ WF(u) if ∃ φ with φ(x₀) ≠ 0 and

|̂(φu)(ξ)| = O(|ξ|^{-N}) for all N in cone around ξ₀

Physical meaning: u is smooth at x₀ in direction ξ₀
""")

print("""
Key results:

1. PROPAGATION OF SINGULARITIES:
   For hyperbolic PDE Pu = 0, singularities propagate
   along bicharacteristics (Hamilton flow of principal symbol)

2. COMPOSITION:
   WF(Pu) ⊂ WF(u) for pseudo-differential P

3. MICROLOCAL ELLIPTIC REGULARITY:
   If P elliptic at (x₀, ξ₀) and Pu smooth there,
   then u smooth at (x₀, ξ₀)

Applications:
- Geometrical optics (ray tracing)
- Uniqueness in Cauchy problem
- Scattering theory
""")

# Simulation: wave front set of a discontinuous function
print("\n" + "-"*70)
print("Simulation: Wave front set of step function")
print("-"*70)

N_wf = 512
x_wf = np.linspace(-2, 2, N_wf)
dx_wf = x_wf[1] - x_wf[0]

# Heaviside function (discontinuity at x=0)
u_heaviside = np.heaviside(x_wf, 0.5)

# Function with a corner at x=0 (|x|)
u_corner = np.abs(x_wf)

# Smooth function with bump
u_smooth = np.exp(-1/(1 - x_wf**2)) * (np.abs(x_wf) < 1)

functions_wf = {
    'Heaviside (jump at 0)': u_heaviside,
    '|x| (corner at 0)': u_corner,
    'Smooth bump': u_smooth,
}

fig, axes = plt.subplots(3, 3, figsize=(15, 12))

for idx, (name, u) in enumerate(functions_wf.items()):
    # Function
    axes[idx, 0].plot(x_wf, u, 'b-', linewidth=2)
    axes[idx, 0].axvline(0, color='r', linestyle='--', alpha=0.5, label='Singularity')
    axes[idx, 0].set_xlabel('x')
    axes[idx, 0].set_ylabel('u(x)')
    axes[idx, 0].set_title(name)
    axes[idx, 0].legend()
    axes[idx, 0].grid(True, alpha=0.3)
    
    # Fourier Transform
    u_fft = fft(u)
    freqs_wf = fftfreq(N_wf, d=dx_wf) * 2*np.pi
    
    axes[idx, 1].semilogy(np.abs(freqs_wf), np.abs(u_fft), 'b-', linewidth=1)
    axes[idx, 1].set_xlabel('|ξ|')
    axes[idx, 1].set_ylabel('|û(ξ)|')
    axes[idx, 1].set_title('Fourier Transform (decay rate)')
    axes[idx, 1].grid(True, alpha=0.3)
    
    # Time-frequency representation (wave front set approximation)
    # Use a sliding window
    window_size = 32
    n_windows = N_wf // window_size
    
    wvd = np.zeros((n_windows, N_wf // 2))
    
    for w_idx in range(n_windows):
        start = w_idx * window_size
        end = start + window_size
        if end <= N_wf:
            window = u[start:end] * np.hanning(window_size)
            window_fft = fft(window, n=N_wf)
            wvd[w_idx, :] = np.abs(window_fft[:N_wf//2])**2
    
    x_centers = x_wf[::window_size][:n_windows]
    freq_range = freqs_wf[:N_wf//2]
    
    im = axes[idx, 2].contourf(x_centers, freq_range, wvd.T, levels=20, cmap='hot')
    axes[idx, 2].set_xlabel('x (position)')
    axes[idx, 2].set_ylabel('ξ (frequency)')
    axes[idx, 2].set_title('Time-Frequency (Wave Front approx)')
    plt.colorbar(im, ax=axes[idx, 2])

plt.tight_layout()
plt.show()

print("\n✓ Wave front set: microlocal refinement of singular support")
print("✓ Tracks propagation direction of singularities")
CONTEXT: Microlocal Analysis

The wave front set WF(u) measures:
- WHERE singularities are (in x)
- IN WHICH DIRECTION they propagate (in ξ)

WF(u) ⊂ T*M \ {0} (cotangent bundle minus zero section)

Definition:
(x₀, ξ₀) ∉ WF(u) if ∃ φ with φ(x₀) ≠ 0 and

|̂(φu)(ξ)| = O(|ξ|^{-N}) for all N in cone around ξ₀

Physical meaning: u is smooth at x₀ in direction ξ₀


Key results:

1. PROPAGATION OF SINGULARITIES:
   For hyperbolic PDE Pu = 0, singularities propagate
   along bicharacteristics (Hamilton flow of principal symbol)

2. COMPOSITION:
   WF(Pu) ⊂ WF(u) for pseudo-differential P

3. MICROLOCAL ELLIPTIC REGULARITY:
   If P elliptic at (x₀, ξ₀) and Pu smooth there,
   then u smooth at (x₀, ξ₀)

Applications:
- Geometrical optics (ray tracing)
- Uniqueness in Cauchy problem
- Scattering theory


----------------------------------------------------------------------
Simulation: Wave front set of step function
----------------------------------------------------------------------
No description has been provided for this image
✓ Wave front set: microlocal refinement of singular support
✓ Tracks propagation direction of singularities

Kernel Methods and Reproducing Kernel Hilbert Spaces¶

In [58]:
from sklearn.datasets import make_moons, make_circles, make_classification
from sklearn.svm import SVC

x, y = symbols('x y', real=True)
xi, eta = symbols('xi eta', real=True)

print("""
CONTEXT: Kernels as Pseudo-Differential Operators

A kernel k(x,y) defines an integral operator:

(K_k f)(x) = ∫ k(x,y) f(y) dy

In Fourier space (for translation-invariant kernels):

k(x,y) = k(x-y) ⟹ K̂_k(ξ) = k̂(ξ)

This is a Fourier multiplier operator!
""")

# Classical kernels in ML
print("\nClassical kernels in Machine Learning:")

kernels_ml = {
    'Gaussian (RBF)': 'k(x,y) = exp(-||x-y||²/2σ²)',
    'Laplacian': 'k(x,y) = exp(-||x-y||/σ)',
    'Matérn': 'k(x,y) ~ (1 + √(2ν)||x-y||/σ)^ν K_ν(...)',
    'Polynomial': 'k(x,y) = (x·y + c)^d',
    'Spectral Mixture': 'k(x,y) = Σ exp(-||x-y||²/2ℓ²)cos(2π||x-y||μ)',
}

for name, formula in kernels_ml.items():
    print(f"\n{name}:")
    print(f"  {formula}")

print("""
Connection to ΨDOs:

1. GAUSSIAN KERNEL ↔ HEAT OPERATOR
   k_t(x,y) = exp(-||x-y||²/4t) / (4πt)^{n/2}
   
   This is the fundamental solution of heat equation!
   Operator: exp(tΔ) (Laplacian semigroup)

2. MATÉRN KERNEL ↔ BESSEL POTENTIAL
   Symbol: (1 + |ξ|²)^{-ν}
   
   Inverse of fractional Sobolev operator
   Controls smoothness of GP samples

3. NEURAL TANGENT KERNEL (NTK)
   Infinite-width neural networks → kernel machines
   Connection to pseudo-differential calculus of compositions
""")

# Simulation: SVM with different kernels
print("\n" + "-"*70)
print("Simulation: SVM with different kernels (ΨDO perspective)")
print("-"*70)

# Generate data
np.random.seed(42)
X, y = make_moons(n_samples=200, noise=0.15)

# Different kernels (= different operators)
kernels_svm = {
    'RBF (Heat)': ('rbf', 0.5),
    'Linear': ('linear', None),
    'Polynomial (degree 3)': ('poly', 3),
}

fig, axes = plt.subplots(2, 3, figsize=(15, 10))

for idx, (name, (kernel, param)) in enumerate(kernels_svm.items()):
    ax = axes[idx // 3, idx % 3]
    
    # Train SVM
    if kernel == 'rbf':
        clf = SVC(kernel=kernel, gamma=1/(2*param**2))
    elif kernel == 'poly':
        clf = SVC(kernel=kernel, degree=param)
    else:
        clf = SVC(kernel=kernel)
    
    clf.fit(X, y)
    
    # Decision grid
    x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
    y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
                         np.linspace(y_min, y_max, 100))
    
    Z = clf.decision_function(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    
    # Plot
    ax.contourf(xx, yy, Z, levels=20, cmap='RdBu', alpha=0.6)
    ax.contour(xx, yy, Z, levels=[0], colors='black', linewidths=2)
    ax.scatter(X[:, 0], X[:, 1], c=y, cmap='RdBu', edgecolors='k', s=50)
    
    # Support vectors
    ax.scatter(clf.support_vectors_[:, 0], clf.support_vectors_[:, 1],
              s=200, linewidth=2, facecolors='none', edgecolors='k',
              label='Support Vectors')
    
    ax.set_title(f'{name}\nAccuracy: {clf.score(X, y):.3f}')
    ax.legend()
    ax.set_xlabel('x₁')
    ax.set_ylabel('x₂')

# Visualization of Gaussian kernel as an operator
ax_kernel = axes[1, 0]
x_kern = np.linspace(-3, 3, 100)
sigma_vals = [0.3, 0.7, 1.5]

for sigma in sigma_vals:
    kernel_vals = np.exp(-x_kern**2 / (2*sigma**2))
    ax_kernel.plot(x_kern, kernel_vals, linewidth=2, label=f'σ={sigma}')

ax_kernel.set_xlabel('||x - y||')
ax_kernel.set_ylabel('k(x, y)')
ax_kernel.set_title('Gaussian Kernel (Heat Kernel)')
ax_kernel.legend()
ax_kernel.grid(True, alpha=0.3)

# Spectrum of the kernel (integral operator)
ax_spectrum = axes[1, 1]
xi_spec = np.linspace(0, 10, 200)

for sigma in sigma_vals:
    # Fourier transform of Gaussian: also Gaussian
    spectrum = np.exp(-sigma**2 * xi_spec**2 / 2)
    ax_spectrum.semilogy(xi_spec, spectrum, linewidth=2, label=f'σ={sigma}')

ax_spectrum.set_xlabel('Frequency ξ')
ax_spectrum.set_ylabel('|k̂(ξ)|')
ax_spectrum.set_title('Kernel Spectrum (Fourier Multiplier)')
ax_spectrum.legend()
ax_spectrum.grid(True, alpha=0.3)

# Gram matrix
ax_gram = axes[1, 2]

# Small sample for visualization
X_sample = X[:30]
K_matrix = np.exp(-cdist(X_sample, X_sample, 'sqeuclidean') / (2*0.5**2))

im = ax_gram.imshow(K_matrix, cmap='viridis', interpolation='nearest')
ax_gram.set_title('Gram Matrix K (Kernel as Operator)')
ax_gram.set_xlabel('Sample i')
ax_gram.set_ylabel('Sample j')
plt.colorbar(im, ax=ax_gram)

plt.tight_layout()
plt.show()

print("\n✓ Kernels = integral operators with specific symbols")
print("✓ Choice of kernel ↔ choice of function space (RKHS)")
CONTEXT: Kernels as Pseudo-Differential Operators

A kernel k(x,y) defines an integral operator:

(K_k f)(x) = ∫ k(x,y) f(y) dy

In Fourier space (for translation-invariant kernels):

k(x,y) = k(x-y) ⟹ K̂_k(ξ) = k̂(ξ)

This is a Fourier multiplier operator!


Classical kernels in Machine Learning:

Gaussian (RBF):
  k(x,y) = exp(-||x-y||²/2σ²)

Laplacian:
  k(x,y) = exp(-||x-y||/σ)

Matérn:
  k(x,y) ~ (1 + √(2ν)||x-y||/σ)^ν K_ν(...)

Polynomial:
  k(x,y) = (x·y + c)^d

Spectral Mixture:
  k(x,y) = Σ exp(-||x-y||²/2ℓ²)cos(2π||x-y||μ)

Connection to ΨDOs:

1. GAUSSIAN KERNEL ↔ HEAT OPERATOR
   k_t(x,y) = exp(-||x-y||²/4t) / (4πt)^{n/2}
   
   This is the fundamental solution of heat equation!
   Operator: exp(tΔ) (Laplacian semigroup)

2. MATÉRN KERNEL ↔ BESSEL POTENTIAL
   Symbol: (1 + |ξ|²)^{-ν}
   
   Inverse of fractional Sobolev operator
   Controls smoothness of GP samples

3. NEURAL TANGENT KERNEL (NTK)
   Infinite-width neural networks → kernel machines
   Connection to pseudo-differential calculus of compositions


----------------------------------------------------------------------
Simulation: SVM with different kernels (ΨDO perspective)
----------------------------------------------------------------------
No description has been provided for this image
✓ Kernels = integral operators with specific symbols
✓ Choice of kernel ↔ choice of function space (RKHS)

Gaussian Processes as Pseudo-Differential Operators¶

In [59]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Gaussian Processes and Covariance Operators

A Gaussian Process GP(μ, k) is determined by:
- Mean function μ(x)
- Covariance kernel k(x,y)

The covariance operator:
(C_k f)(x) = ∫ k(x,y) f(y) dy

For stationary k(x,y) = k(x-y), this is a pseudo-differential operator
with symbol k̂(ξ).

Key result (Whittle-Matérn):
k_ν(r) ~ (1 + √(2ν)r/ℓ)^ν K_ν(√(2ν)r/ℓ)

Symbol: (1 + ℓ²|ξ|²)^{-ν-n/2}

Controls smoothness: ν > k ⟹ k-times differentiable samples
""")

# Matérn kernel
print("\nMatérn Kernel Family:")
from scipy.special import  gamma

nu_values = [0.5, 1.5, 2.5, np.inf]

def matern_kernel(r, nu, ell):
    """Matérn kernel"""
    if nu == np.inf:
        # RBF (limit ν → ∞)
        return np.exp(-r**2 / (2*ell**2))
    elif nu == 0.5:
        # Exponential
        return np.exp(-r / ell)
    else:
        factor = 2**(1-nu) / gamma(nu)
        arg = np.sqrt(2*nu) * r / ell
        return factor * arg**nu * kv(nu, arg)

# GP simulation
print("\n" + "-"*70)
print("Simulation: Gaussian Process samples with different kernels")
print("-"*70)

x_gp = np.linspace(0, 10, 200)
n_samples = 5

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

for idx, nu in enumerate(nu_values):
    ax = axes[idx // 2, idx % 2]
    
    # Covariance matrix
    X_grid = x_gp[:, np.newaxis]
    ell = 1.0
    
    if nu == np.inf:
        K = np.exp(-cdist(X_grid, X_grid, 'sqeuclidean') / (2*ell**2))
        label = 'ν = ∞ (RBF, C^∞)'
    else:
        distances = cdist(X_grid, X_grid, 'euclidean')
        K = matern_kernel(distances, nu, ell)
        label = f'ν = {nu} (C^{{{int(np.ceil(nu))-1}}})'
    
    # Regularization
    K += 1e-8 * np.eye(len(x_gp))
    
    # Sampling
    L = np.linalg.cholesky(K)
    
    for _ in range(n_samples):
        sample = L @ np.random.randn(len(x_gp))
        ax.plot(x_gp, sample, alpha=0.7, linewidth=1.5)
    
    ax.set_xlabel('x')
    ax.set_ylabel('f(x)')
    ax.set_title(f'Matérn GP: {label}')
    ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("""
Interpretation via ΨDO:

1. SMOOTHNESS CONTROL
   ν = 1/2: Brownian paths (nowhere differentiable)
   ν = 3/2: Once differentiable
   ν = 5/2: Twice differentiable
   ν → ∞: Infinitely differentiable (RBF)

2. SPECTRAL DENSITY
   Power spectrum S(ω) = |k̂(ω)|²
   
   For Matérn: S(ω) ~ (1 + ω²)^{-ν-1/2}
   
   Polynomial decay ↔ finite smoothness
   Exponential decay ↔ infinite smoothness

3. STOCHASTIC PDE REPRESENTATION
   Matérn GP with ν = n + 1/2 solves:
   
   (κ² - Δ)^{n+1} u = W (white noise)
   
   This is a fractional operator!
""")

# Spectral visualization
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Spectra of Matérn kernels
omega = np.logspace(-2, 2, 200)

for nu in [0.5, 1.5, 2.5, 5.5]:
    ell = 1.0
    if nu == np.inf:
        spectrum = np.exp(-ell**2 * omega**2 / 2)
        axes[0].loglog(omega, spectrum, linewidth=2, label='ν = ∞')
    else:
        spectrum = (1 + ell**2 * omega**2)**(-nu - 0.5)
        axes[0].loglog(omega, spectrum, linewidth=2, label=f'ν = {nu}')

axes[0].set_xlabel('Frequency ω')
axes[0].set_ylabel('Spectral density S(ω)')
axes[0].set_title('Power Spectra of Matérn Kernels')
axes[0].legend()
axes[0].grid(True, alpha=0.3, which='both')

# Application: GP regression
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import Matern

# Noisy data
np.random.seed(0)
X_train = np.random.uniform(0, 10, 20)[:, np.newaxis]
y_train = np.sin(X_train).ravel() + 0.1 * np.random.randn(20)

X_test = np.linspace(0, 10, 200)[:, np.newaxis]

for nu in [0.5, 1.5, 2.5]:
    kernel = Matern(length_scale=1.0, nu=nu)
    gpr = GaussianProcessRegressor(kernel=kernel, alpha=0.1**2)
    gpr.fit(X_train, y_train)
    
    y_pred, y_std = gpr.predict(X_test, return_std=True)
    
    axes[1].plot(X_test, y_pred, linewidth=2, label=f'ν={nu}')
    axes[1].fill_between(X_test.ravel(), 
                        y_pred - 1.96*y_std,
                        y_pred + 1.96*y_std,
                        alpha=0.2)

axes[1].scatter(X_train, y_train, c='red', s=50, zorder=10, label='Data')
axes[1].plot(X_test, np.sin(X_test), 'k--', label='True function')
axes[1].set_xlabel('x')
axes[1].set_ylabel('y')
axes[1].set_title('GP Regression with Different Smoothness')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n✓ GP covariance = pseudo-differential operator")
print("✓ Matérn family parameterizes smoothness via operator order")
CONTEXT: Gaussian Processes and Covariance Operators

A Gaussian Process GP(μ, k) is determined by:
- Mean function μ(x)
- Covariance kernel k(x,y)

The covariance operator:
(C_k f)(x) = ∫ k(x,y) f(y) dy

For stationary k(x,y) = k(x-y), this is a pseudo-differential operator
with symbol k̂(ξ).

Key result (Whittle-Matérn):
k_ν(r) ~ (1 + √(2ν)r/ℓ)^ν K_ν(√(2ν)r/ℓ)

Symbol: (1 + ℓ²|ξ|²)^{-ν-n/2}

Controls smoothness: ν > k ⟹ k-times differentiable samples


Matérn Kernel Family:

----------------------------------------------------------------------
Simulation: Gaussian Process samples with different kernels
----------------------------------------------------------------------
/tmp/ipykernel_13044/1688173878.py:42: RuntimeWarning: invalid value encountered in multiply
  return factor * arg**nu * kv(nu, arg)
No description has been provided for this image
Interpretation via ΨDO:

1. SMOOTHNESS CONTROL
   ν = 1/2: Brownian paths (nowhere differentiable)
   ν = 3/2: Once differentiable
   ν = 5/2: Twice differentiable
   ν → ∞: Infinitely differentiable (RBF)

2. SPECTRAL DENSITY
   Power spectrum S(ω) = |k̂(ω)|²
   
   For Matérn: S(ω) ~ (1 + ω²)^{-ν-1/2}
   
   Polynomial decay ↔ finite smoothness
   Exponential decay ↔ infinite smoothness

3. STOCHASTIC PDE REPRESENTATION
   Matérn GP with ν = n + 1/2 solves:
   
   (κ² - Δ)^{n+1} u = W (white noise)
   
   This is a fractional operator!

No description has been provided for this image
✓ GP covariance = pseudo-differential operator
✓ Matérn family parameterizes smoothness via operator order

Neural Tangent Kernel (NTK) - Infinite Width Limit¶

In [60]:
print("""
CONTEXT: Deep Learning meets Kernel Methods

For a neural network f(x; θ) with parameters θ, the Neural Tangent Kernel:

K_NTK(x, x') = ⟨∇_θ f(x; θ), ∇_θ f(x'; θ)⟩

In the infinite width limit, K_NTK remains constant during training!

Key result (Jacot et al., 2018):
Training dynamics of infinite-width NN = kernel regression with K_NTK

The NTK can be computed recursively for deep networks, and relates to
composition of pseudo-differential operators!
""")

print("""
NTK for shallow network (one hidden layer, width m → ∞):

f(x) = (1/√m) Σᵢ aᵢ σ(wᵢ·x + bᵢ)

where σ is activation function.

K_NTK^{(1)}(x,x') = E[σ'(w·x) σ'(w·x') (x·x')] + E[σ'(w·x) σ'(w·x')]

For ReLU: this has a closed form!

For deep networks (L layers):
K_NTK^{(L)} computed recursively via:
- K^{(ℓ+1)} = Σ^{(ℓ)} · K^{(ℓ)} + Σ^{(ℓ)}

where Σ^{(ℓ)} depends on activation covariance at layer ℓ.

This is analogous to COMPOSITION of operators!
""")

# Compute NTK for shallow ReLU network
def ntk_relu_shallow(X1, X2):
    """NTK for shallow ReLU network"""
    # Normalize
    X1_norm = X1 / np.linalg.norm(X1, axis=1, keepdims=True)
    X2_norm = X2 / np.linalg.norm(X2, axis=1, keepdims=True)
    
    # Dot product
    dot_product = X1_norm @ X2_norm.T
    dot_product = np.clip(dot_product, -1, 1)
    
    # Angle
    theta = np.arccos(dot_product)
    
    # NTK for ReLU (exact formula)
    ntk = (1/(2*np.pi)) * (dot_product * (np.pi - theta) + np.sin(theta))
    
    return ntk

# Simulation
print("\n" + "-"*70)
print("Simulation: NTK vs finite-width network")
print("-"*70)

# Toy data
np.random.seed(42)
X_train_ntk = np.random.randn(50, 2)
y_train_ntk = (X_train_ntk[:, 0]**2 + X_train_ntk[:, 1]**2 > 1).astype(float)

X_test_ntk = np.random.randn(200, 2)

# Compute NTK
K_train = ntk_relu_shallow(X_train_ntk, X_train_ntk)
K_test_train = ntk_relu_shallow(X_test_ntk, X_train_ntk)

# Regression with NTK (kernel ridge regression)
lambda_reg = 0.01
alpha = np.linalg.solve(K_train + lambda_reg * np.eye(len(K_train)), y_train_ntk)
y_pred_ntk = K_test_train @ alpha

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Visualize K_NTK
im0 = axes[0, 0].imshow(K_train, cmap='viridis', interpolation='nearest')
axes[0, 0].set_title('NTK Gram Matrix K_NTK')
axes[0, 0].set_xlabel('Sample i')
axes[0, 0].set_ylabel('Sample j')
plt.colorbar(im0, ax=axes[0, 0])

# Eigenvalues of NTK
eigenvalues = np.linalg.eigvalsh(K_train)
axes[0, 1].plot(np.sort(eigenvalues)[::-1], 'bo-', linewidth=2, markersize=6)
axes[0, 1].set_xlabel('Index')
axes[0, 1].set_ylabel('Eigenvalue')
axes[0, 1].set_title('Spectrum of K_NTK (determines learning)')
axes[0, 1].set_yscale('log')
axes[0, 1].grid(True, alpha=0.3)

# NTK predictions
axes[1, 0].scatter(X_train_ntk[:, 0], X_train_ntk[:, 1], 
                  c=y_train_ntk, cmap='RdBu', s=100, edgecolors='k',
                  label='Train', zorder=5)
axes[1, 0].scatter(X_test_ntk[:, 0], X_test_ntk[:, 1], 
                  c=y_pred_ntk, cmap='RdBu', s=30, alpha=0.5,
                  label='Predictions (NTK)')
axes[1, 0].set_xlabel('x₁')
axes[1, 0].set_ylabel('x₂')
axes[1, 0].set_title('NTK Predictions (Infinite Width Limit)')
axes[1, 0].legend()

# Comparison with finite network
from sklearn.neural_network import MLPClassifier

mlp = MLPClassifier(hidden_layer_sizes=(100,), activation='relu', 
                   max_iter=1000, random_state=42)
mlp.fit(X_train_ntk, y_train_ntk)
y_pred_mlp = mlp.predict(X_test_ntk)

axes[1, 1].scatter(X_train_ntk[:, 0], X_train_ntk[:, 1], 
                  c=y_train_ntk, cmap='RdBu', s=100, edgecolors='k',
                  label='Train', zorder=5)
axes[1, 1].scatter(X_test_ntk[:, 0], X_test_ntk[:, 1], 
                  c=y_pred_mlp, cmap='RdBu', s=30, alpha=0.5,
                  label='Predictions (Finite NN)')
axes[1, 1].set_xlabel('x₁')
axes[1, 1].set_ylabel('x₂')
axes[1, 1].set_title(f'Finite Width NN (width=100)')
axes[1, 1].legend()

plt.tight_layout()
plt.show()

print("\n✓ NTK: infinite-width limit connects NNs to kernel methods")
print("✓ Composition of layers ↔ composition of operators")
CONTEXT: Deep Learning meets Kernel Methods

For a neural network f(x; θ) with parameters θ, the Neural Tangent Kernel:

K_NTK(x, x') = ⟨∇_θ f(x; θ), ∇_θ f(x'; θ)⟩

In the infinite width limit, K_NTK remains constant during training!

Key result (Jacot et al., 2018):
Training dynamics of infinite-width NN = kernel regression with K_NTK

The NTK can be computed recursively for deep networks, and relates to
composition of pseudo-differential operators!


NTK for shallow network (one hidden layer, width m → ∞):

f(x) = (1/√m) Σᵢ aᵢ σ(wᵢ·x + bᵢ)

where σ is activation function.

K_NTK^{(1)}(x,x') = E[σ'(w·x) σ'(w·x') (x·x')] + E[σ'(w·x) σ'(w·x')]

For ReLU: this has a closed form!

For deep networks (L layers):
K_NTK^{(L)} computed recursively via:
- K^{(ℓ+1)} = Σ^{(ℓ)} · K^{(ℓ)} + Σ^{(ℓ)}

where Σ^{(ℓ)} depends on activation covariance at layer ℓ.

This is analogous to COMPOSITION of operators!


----------------------------------------------------------------------
Simulation: NTK vs finite-width network
----------------------------------------------------------------------
No description has been provided for this image
✓ NTK: infinite-width limit connects NNs to kernel methods
✓ Composition of layers ↔ composition of operators

Graph Operators and Graph Neural Networks¶

In [61]:
print("""
CONTEXT: Spectral Methods on Graphs

For a graph G = (V, E), the graph Laplacian:

L = D - A

where D = degree matrix, A = adjacency matrix.

The normalized Laplacian:

L_norm = I - D^{-1/2} A D^{-1/2}

Spectral decomposition: L = UΛU^T

Graph convolution (spectral):
f ⋆_G h = U ((U^T h) ⊙ ĥ)

where ĥ are "frequencies" on the graph!

This is a discrete pseudo-differential operator.
""")

# Build an example graph
import networkx as nx

print("\n" + "-"*70)
print("Simulation: Graph Laplacian and spectral filtering")
print("-"*70)

# Community graph
n_nodes = 50
G = nx.karate_club_graph()  # Classic graph

# Adjacency matrix and Laplacian
A = nx.adjacency_matrix(G).todense()
D = np.diag(np.array(A.sum(axis=1)).flatten())
L = D - A

# Normalized Laplacian
D_sqrt_inv = np.diag(1 / np.sqrt(np.diag(D) + 1e-10))
L_norm = np.eye(len(G)) - D_sqrt_inv @ A @ D_sqrt_inv

# Spectral decomposition
eigenvalues_graph, eigenvectors_graph = np.linalg.eigh(L_norm)

fig, axes = plt.subplots(2, 3, figsize=(15, 10))

# Graph
pos = nx.spring_layout(G, seed=42)
nx.draw_networkx(G, pos, ax=axes[0, 0], node_size=50, with_labels=False,
                node_color='lightblue', edge_color='gray', alpha=0.7)
axes[0, 0].set_title('Karate Club Graph')
axes[0, 0].axis('off')

# Spectrum
axes[0, 1].plot(eigenvalues_graph, 'bo-', markersize=6)
axes[0, 1].set_xlabel('Index')
axes[0, 1].set_ylabel('Eigenvalue λ')
axes[0, 1].set_title('Spectrum of Graph Laplacian')
axes[0, 1].grid(True, alpha=0.3)
axes[0, 1].axhline(0, color='r', linestyle='--', alpha=0.5)

# Eigenvectors (Fourier modes on the graph)
for idx in [1, 2, 5]:  # Skip 0 (constant)
    if idx < len(eigenvectors_graph[0]):
        eigenmode = eigenvectors_graph[:, idx]
        
        ax_mode = axes[(idx)//3, (idx)%3]
        
        node_colors = eigenmode
        nx.draw_networkx(G, pos, ax=ax_mode, node_size=200, with_labels=False,
                        node_color=node_colors, cmap='RdBu', edge_color='gray',
                        alpha=0.8, vmin=-np.max(np.abs(eigenmode)),
                        vmax=np.max(np.abs(eigenmode)))
        ax_mode.set_title(f'Eigenmode {idx} (λ={eigenvalues_graph[idx]:.3f})')
        ax_mode.axis('off')

# Spectral filtering
# Signal on the graph
signal_graph = np.random.randn(len(G))

# Transform into spectral basis
signal_freq = eigenvectors_graph.T @ signal_graph

# Low-pass filter (keep only low frequencies)
cutoff = 5
filter_spectrum = (eigenvalues_graph < eigenvalues_graph[cutoff]).astype(float)

# Apply the filter
filtered_freq = signal_freq * filter_spectrum

# Return to spatial domain
filtered_signal = eigenvectors_graph @ filtered_freq

ax_filter = axes[1, 0]
nx.draw_networkx(G, pos, ax=ax_filter, node_size=200, with_labels=False,
                node_color=signal_graph, cmap='viridis', edge_color='gray',
                alpha=0.8)
ax_filter.set_title('Original Signal')
ax_filter.axis('off')

ax_filtered = axes[1, 1]
nx.draw_networkx(G, pos, ax=ax_filtered, node_size=200, with_labels=False,
                node_color=filtered_signal, cmap='viridis', edge_color='gray',
                alpha=0.8)
ax_filtered.set_title(f'Low-Pass Filtered (cutoff λ<{eigenvalues_graph[cutoff]:.2f})')
ax_filtered.axis('off')

# Signal spectrum
ax_spectrum = axes[1, 2]
ax_spectrum.stem(eigenvalues_graph, np.abs(signal_freq), basefmt=' ')
ax_spectrum.axvline(eigenvalues_graph[cutoff], color='r', linestyle='--', 
                   linewidth=2, label='Cutoff')
ax_spectrum.set_xlabel('Eigenvalue λ')
ax_spectrum.set_ylabel('|Signal Frequency|')
ax_spectrum.set_title('Signal in Spectral Domain')
ax_spectrum.legend()
ax_spectrum.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("""
Graph Neural Networks (GNNs):

Graph convolution layer:
H^{(ℓ+1)} = σ(D̃^{-1/2} Ã D̃^{-1/2} H^{(ℓ)} W^{(ℓ)})

where à = A + I (with self-loops).

This is:
1. A graph filter (pseudo-differential on graph)
2. Followed by linear transformation W
3. Followed by nonlinearity σ

Spectral GNNs use explicit eigendecomposition:
h_out = σ(U g_θ(Λ) U^T h_in)

where g_θ(λ) is a learnable filter function!

Connection to ΨDOs:
- Graph Laplacian = discrete differential operator
- Spectral filtering = Fourier multiplier on graph
- Multi-scale graphs → wavelet transforms
""")

print("\n✓ Graph Laplacian: discrete analog of Laplace-Beltrami")
print("✓ GNNs = learnable graph filters (pseudo-differential operators)")
CONTEXT: Spectral Methods on Graphs

For a graph G = (V, E), the graph Laplacian:

L = D - A

where D = degree matrix, A = adjacency matrix.

The normalized Laplacian:

L_norm = I - D^{-1/2} A D^{-1/2}

Spectral decomposition: L = UΛU^T

Graph convolution (spectral):
f ⋆_G h = U ((U^T h) ⊙ ĥ)

where ĥ are "frequencies" on the graph!

This is a discrete pseudo-differential operator.

----------------------------------------------------------------------
Simulation: Graph Laplacian and spectral filtering
----------------------------------------------------------------------
No description has been provided for this image
Graph Neural Networks (GNNs):

Graph convolution layer:
H^{(ℓ+1)} = σ(D̃^{-1/2} Ã D̃^{-1/2} H^{(ℓ)} W^{(ℓ)})

where à = A + I (with self-loops).

This is:
1. A graph filter (pseudo-differential on graph)
2. Followed by linear transformation W
3. Followed by nonlinearity σ

Spectral GNNs use explicit eigendecomposition:
h_out = σ(U g_θ(Λ) U^T h_in)

where g_θ(λ) is a learnable filter function!

Connection to ΨDOs:
- Graph Laplacian = discrete differential operator
- Spectral filtering = Fourier multiplier on graph
- Multi-scale graphs → wavelet transforms


✓ Graph Laplacian: discrete analog of Laplace-Beltrami
✓ GNNs = learnable graph filters (pseudo-differential operators)

Attention Mechanisms as Integral Operators¶

In [62]:
print("""
CONTEXT: Transformers and Attention

The attention mechanism in Transformers:

Attention(Q, K, V) = softmax(QK^T/√d) V

Can be viewed as a data-dependent integral operator:

(A f)(x) = ∫ a(x, y) f(y) dy

where a(x, y) = softmax kernel (non-stationary!)

Key insight:
- Attention = adaptive kernel
- Self-attention = graph operator with learned adjacency
- Multi-head attention = sum of operators

Relation to ΨDOs:
- Positional encoding = frequency information
- Layer norm = operator normalization
- Residual connections = operator splitting
""")

# Simple attention simulation
print("\n" + "-"*70)
print("Simulation: Attention as an operator")
print("-"*70)

def attention_operator(Q, K, V):
    """Standard attention mechanism"""
    d_k = Q.shape[-1]
    scores = (Q @ K.T) / np.sqrt(d_k)
    weights = np.exp(scores) / np.exp(scores).sum(axis=-1, keepdims=True)
    output = weights @ V
    return output, weights

# Input sequence (embeddings)
seq_length = 20
d_model = 8

np.random.seed(42)
X = np.random.randn(seq_length, d_model)

# Projection matrices (simplified)
W_Q = np.random.randn(d_model, d_model) * 0.1
W_K = np.random.randn(d_model, d_model) * 0.1
W_V = np.random.randn(d_model, d_model) * 0.1

# Compute Q, K, V
Q = X @ W_Q
K = X @ W_K
V = X @ W_V

# Apply attention
output, attention_weights = attention_operator(Q, K, V)

fig, axes = plt.subplots(2, 3, figsize=(15, 10))

# Input sequence
axes[0, 0].imshow(X.T, cmap='RdBu', aspect='auto', interpolation='nearest')
axes[0, 0].set_xlabel('Position in sequence')
axes[0, 0].set_ylabel('Feature dimension')
axes[0, 0].set_title('Input Sequence X')
plt.colorbar(axes[0, 0].images[0], ax=axes[0, 0])

# Attention matrix (operator kernel)
im1 = axes[0, 1].imshow(attention_weights, cmap='hot', interpolation='nearest')
axes[0, 1].set_xlabel('Key position j')
axes[0, 1].set_ylabel('Query position i')
axes[0, 1].set_title('Attention Weights A(i,j)\n(Adaptive Kernel)')
plt.colorbar(im1, ax=axes[0, 1])

# Output
axes[0, 2].imshow(output.T, cmap='RdBu', aspect='auto', interpolation='nearest')
axes[0, 2].set_xlabel('Position in sequence')
axes[0, 2].set_ylabel('Feature dimension')
axes[0, 2].set_title('Output = A·V')
plt.colorbar(axes[0, 2].images[0], ax=axes[0, 2])

# Spectral analysis of the attention matrix
eigenvalues_attn, eigenvectors_attn = np.linalg.eigh(attention_weights)

axes[1, 0].plot(np.sort(eigenvalues_attn)[::-1], 'bo-', markersize=6, linewidth=2)
axes[1, 0].set_xlabel('Index')
axes[1, 0].set_ylabel('Eigenvalue')
axes[1, 0].set_title('Spectrum of Attention Matrix')
axes[1, 0].grid(True, alpha=0.3)

# Effective rank (participation ratio)
eigenvalues_pos = eigenvalues_attn[eigenvalues_attn > 0]
effective_rank = (np.sum(eigenvalues_pos)**2) / np.sum(eigenvalues_pos**2)

axes[1, 0].text(0.5, 0.9, f'Effective rank: {effective_rank:.1f}/{seq_length}',
               transform=axes[1, 0].transAxes, fontsize=10,
               bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))

# Eigenvectors (attention modes)
for idx in [0, 1]:
    eigenmode_attn = eigenvectors_attn[:, -(idx+1)]  # Largest eigenvalues
    
    axes[1, 1+idx].plot(eigenmode_attn, 'o-', linewidth=2, markersize=6)
    axes[1, 1+idx].set_xlabel('Position')
    axes[1, 1+idx].set_ylabel('Amplitude')
    axes[1, 1+idx].set_title(f'Attention Mode {idx+1}\n(λ={eigenvalues_attn[-(idx+1)]:.3f})')
    axes[1, 1+idx].grid(True, alpha=0.3)
    axes[1, 1+idx].axhline(0, color='k', linestyle='--', alpha=0.3)

plt.tight_layout()
plt.show()

print("""
Interpretation as ΨDO:

1. ATTENTION WEIGHTS = ADAPTIVE KERNEL
   A(i,j) = exp(q_i·k_j/√d) / Σ_k exp(q_i·k_k/√d)
   
   This is a data-dependent integral operator kernel

2. POSITIONAL ENCODING
   PE(pos, 2i) = sin(pos/10000^{2i/d})
   PE(pos, 2i+1) = cos(pos/10000^{2i/d})
   
   Injects frequency information (like Fourier basis)

3. MULTI-HEAD ATTENTION
   MultiHead = Concat(head₁, ..., head_h)W^O
   
   Each head = different operator
   Concatenation = direct sum of operators

4. SELF-ATTENTION AS GRAPH OPERATOR
   Fully connected graph with learned edge weights
   Similar to spectral graph convolution
""")

print("\n✓ Attention = data-dependent integral operator")
print("✓ Transformers = composition of adaptive operators")
CONTEXT: Transformers and Attention

The attention mechanism in Transformers:

Attention(Q, K, V) = softmax(QK^T/√d) V

Can be viewed as a data-dependent integral operator:

(A f)(x) = ∫ a(x, y) f(y) dy

where a(x, y) = softmax kernel (non-stationary!)

Key insight:
- Attention = adaptive kernel
- Self-attention = graph operator with learned adjacency
- Multi-head attention = sum of operators

Relation to ΨDOs:
- Positional encoding = frequency information
- Layer norm = operator normalization
- Residual connections = operator splitting


----------------------------------------------------------------------
Simulation: Attention as an operator
----------------------------------------------------------------------
No description has been provided for this image
Interpretation as ΨDO:

1. ATTENTION WEIGHTS = ADAPTIVE KERNEL
   A(i,j) = exp(q_i·k_j/√d) / Σ_k exp(q_i·k_k/√d)
   
   This is a data-dependent integral operator kernel

2. POSITIONAL ENCODING
   PE(pos, 2i) = sin(pos/10000^{2i/d})
   PE(pos, 2i+1) = cos(pos/10000^{2i/d})
   
   Injects frequency information (like Fourier basis)

3. MULTI-HEAD ATTENTION
   MultiHead = Concat(head₁, ..., head_h)W^O
   
   Each head = different operator
   Concatenation = direct sum of operators

4. SELF-ATTENTION AS GRAPH OPERATOR
   Fully connected graph with learned edge weights
   Similar to spectral graph convolution


✓ Attention = data-dependent integral operator
✓ Transformers = composition of adaptive operators

Diffusion Models and Score Matching¶

In [63]:
x = symbols('x', real=True)
xi = symbols('xi', real=True)

print("""
CONTEXT: Generative Models via Stochastic PDEs

Diffusion models (DDPM, Score-based models) use:

Forward process (adding noise):
dX_t = √(dβ_t/dt) dW_t

Reverse process (denoising):
dX_t = -β_t ∇_x log p_t(x) dt + √(β_t) dW̃_t

where ∇ log p_t is the SCORE FUNCTION.

The reverse SDE is governed by the Fokker-Planck equation:

∂p/∂t = -∇·(β∇ log p · p) + (β/2)Δp

This is a NONLINEAR pseudo-differential operator!
""")

print("""
Connection to ΨDOs:

1. FORWARD DIFFUSION = HEAT OPERATOR
   p_t(x|x_0) = N(x; √(1-β̄_t)x_0, β̄_t I)
   
   This is exp(tΔ) applied to δ(x - x_0)

2. SCORE FUNCTION = LOGARITHMIC DERIVATIVE
   s_θ(x,t) ≈ ∇_x log p_t(x)
   
   Learned via denoising score matching

3. REVERSE PROCESS = NONLINEAR PARABOLIC PDE
   Operator: -β∇·(∇ log p · p) + (β/2)Δ
   
   Composition of differential operators

4. SAMPLING = SOLVING BACKWARD SDE
   Numerical integration of operator flow
""")

# 1D diffusion simulation
print("\n" + "-"*70)
print("Simulation: Forward and reverse diffusion")
print("-"*70)

# Target distribution (Gaussian mixture)
def target_distribution(x):
    """Target distribution (mixture)"""
    return 0.5 * np.exp(-(x+2)**2/0.5) + 0.5 * np.exp(-(x-2)**2/0.5)

# Target samples
x_range = np.linspace(-6, 6, 1000)
p_target = target_distribution(x_range)
p_target /= np.trapezoid(p_target, x_range)

# Sample from target
samples_target = np.concatenate([
    np.random.randn(500) * np.sqrt(0.5) - 2,
    np.random.randn(500) * np.sqrt(0.5) + 2
])

# Forward diffusion
T = 5.0
n_steps = 50
t_vals = np.linspace(0, T, n_steps)
beta_t = 0.1 + 0.9 * t_vals / T  # Variance schedule

# Diffuse samples
samples_diffused = [samples_target.copy()]

for i in range(1, n_steps):
    dt = t_vals[i] - t_vals[i-1]
    noise = np.sqrt(beta_t[i] * dt) * np.random.randn(len(samples_target))
    samples_diffused.append(samples_diffused[-1] + noise)

fig, axes = plt.subplots(2, 3, figsize=(15, 10))

# Initial distribution
axes[0, 0].hist(samples_target, bins=50, density=True, alpha=0.6, label='Samples')
axes[0, 0].plot(x_range, p_target, 'r-', linewidth=2, label='True density')
axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('Density')
axes[0, 0].set_title('Target Distribution p₀(x)')
axes[0, 0].legend()
axes[0, 0].set_xlim([-6, 6])

# Intermediate diffusion
for idx, step in enumerate([n_steps//4, n_steps//2, 3*n_steps//4]):
    ax = axes[0, 1] if idx == 0 else axes[0, 2] if idx == 1 else axes[1, 0]
    
    ax.hist(samples_diffused[step], bins=50, density=True, alpha=0.6)
    
    # Theoretical density (convolution with Gaussian)
    sigma_t = np.sqrt(np.sum(beta_t[:step] * np.diff(t_vals[:step+1])))
    p_diffused = np.convolve(p_target, 
                             np.exp(-x_range**2/(2*sigma_t**2))/np.sqrt(2*np.pi*sigma_t**2),
                             mode='same')
    p_diffused /= np.trapezoid(p_diffused, x_range)
    
    ax.plot(x_range, p_diffused, 'r-', linewidth=2, label='Theory')
    ax.set_xlabel('x')
    ax.set_ylabel('Density')
    ax.set_title(f'Diffused at t={t_vals[step]:.2f}\n(σ²={sigma_t**2:.2f})')
    ax.legend()
    ax.set_xlim([-6, 6])

# Final distribution (Gaussian)
axes[1, 1].hist(samples_diffused[-1], bins=50, density=True, alpha=0.6, label='Samples')
gaussian_final = np.exp(-x_range**2/(2*np.sum(beta_t * np.diff(t_vals, prepend=0))))/np.sqrt(2*np.pi*np.sum(beta_t * np.diff(t_vals, prepend=0)))
axes[1, 1].plot(x_range, gaussian_final, 'r-', linewidth=2, label='Gaussian')
axes[1, 1].set_xlabel('x')
axes[1, 1].set_ylabel('Density')
axes[1, 1].set_title(f'Final Distribution p_T(x)\n(Nearly Gaussian)')
axes[1, 1].legend()
axes[1, 1].set_xlim([-6, 6])

# Variance evolution
variances = [np.var(samples_diffused[i]) for i in range(len(samples_diffused))]

axes[1, 2].plot(t_vals, variances, 'bo-', linewidth=2, markersize=4)
axes[1, 2].set_xlabel('Time t')
axes[1, 2].set_ylabel('Variance σ²(t)')
axes[1, 2].set_title('Diffusion Process: Variance Growth')
axes[1, 2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("""
Score-based Generative Models:

1. TRAINING
   Learn s_θ(x,t) ≈ ∇_x log p_t(x) via denoising
   
   Loss: E_t,x,ε[||s_θ(x_t, t) - ε||²]
   where x_t = √(ᾱ_t)x + √(1-ᾱ_t)ε

2. SAMPLING (Reverse SDE)
   x_{t-Δt} = x_t + [-½β_t ∇_x log p_t(x_t)]Δt + √(β_tΔt)·z
   
   where ∇_x log p_t ≈ s_θ(x_t, t)

3. CONNECTION TO OPERATORS
   - Score function = symbol of logarithmic operator
   - Denoising = inverse of heat operator
   - Sampling = ODE/SDE integration

Applications:
- Image generation (DALL-E 2, Stable Diffusion)
- Video generation
- Protein structure prediction (AlphaFold 2)
""")

print("\n✓ Diffusion models = solving reverse heat equation")
print("✓ Score matching = learning operator symbols")
CONTEXT: Generative Models via Stochastic PDEs

Diffusion models (DDPM, Score-based models) use:

Forward process (adding noise):
dX_t = √(dβ_t/dt) dW_t

Reverse process (denoising):
dX_t = -β_t ∇_x log p_t(x) dt + √(β_t) dW̃_t

where ∇ log p_t is the SCORE FUNCTION.

The reverse SDE is governed by the Fokker-Planck equation:

∂p/∂t = -∇·(β∇ log p · p) + (β/2)Δp

This is a NONLINEAR pseudo-differential operator!


Connection to ΨDOs:

1. FORWARD DIFFUSION = HEAT OPERATOR
   p_t(x|x_0) = N(x; √(1-β̄_t)x_0, β̄_t I)
   
   This is exp(tΔ) applied to δ(x - x_0)

2. SCORE FUNCTION = LOGARITHMIC DERIVATIVE
   s_θ(x,t) ≈ ∇_x log p_t(x)
   
   Learned via denoising score matching

3. REVERSE PROCESS = NONLINEAR PARABOLIC PDE
   Operator: -β∇·(∇ log p · p) + (β/2)Δ
   
   Composition of differential operators

4. SAMPLING = SOLVING BACKWARD SDE
   Numerical integration of operator flow


----------------------------------------------------------------------
Simulation: Forward and reverse diffusion
----------------------------------------------------------------------
No description has been provided for this image
Score-based Generative Models:

1. TRAINING
   Learn s_θ(x,t) ≈ ∇_x log p_t(x) via denoising
   
   Loss: E_t,x,ε[||s_θ(x_t, t) - ε||²]
   where x_t = √(ᾱ_t)x + √(1-ᾱ_t)ε

2. SAMPLING (Reverse SDE)
   x_{t-Δt} = x_t + [-½β_t ∇_x log p_t(x_t)]Δt + √(β_tΔt)·z
   
   where ∇_x log p_t ≈ s_θ(x_t, t)

3. CONNECTION TO OPERATORS
   - Score function = symbol of logarithmic operator
   - Denoising = inverse of heat operator
   - Sampling = ODE/SDE integration

Applications:
- Image generation (DALL-E 2, Stable Diffusion)
- Video generation
- Protein structure prediction (AlphaFold 2)


✓ Diffusion models = solving reverse heat equation
✓ Score matching = learning operator symbols

Optimal Transport and Wasserstein Distances in ML¶

In [64]:
print("""
CONTEXT: Optimal Transport Theory

The Wasserstein distance W_2(μ, ν) between distributions:

W_2²(μ, ν) = inf_γ ∫∫ ||x-y||² dγ(x,y)

where γ is a transport plan (coupling).

For Gaussian distributions:
W_2²(N(m₁,Σ₁), N(m₂,Σ₂)) = ||m₁-m₂||² + Tr(Σ₁ + Σ₂ - 2(Σ₂^{1/2}Σ₁Σ₂^{1/2})^{1/2})

Connection to ΨDOs:
- Transport map T: x ↦ ∇φ(x) where φ is Monge-Ampère solution
- φ satisfies: det(∇²φ) = ρ₀/ρ₁ (ρ₀∘T)
- This is a nonlinear PDE!
""")

print("""
Applications in ML:

1. DOMAIN ADAPTATION
   Align source and target distributions via OT
   
2. GANS (Wasserstein GANs)
   Use W_1 distance instead of KL divergence
   More stable training

3. BARYCENTER PROBLEMS
   Average of distributions in Wasserstein space
   
4. SLICED-WASSERSTEIN
   W̃(μ,ν) = ∫_{S^{d-1}} W_1(θ#μ, θ#ν) dθ
   
   Projection onto lines (Radon transform!)
   This is a pseudo-differential operator

5. NEURAL OPTIMAL TRANSPORT
   Learn transport map T via neural networks
""")

# 1D optimal transport simulation
print("\n" + "-"*70)
print("Simulation: Optimal transport between distributions")
print("-"*70)

# Two 1D distributions
x_ot = np.linspace(0, 10, 1000)

# Source distribution (mixture)
rho_source = 0.6 * np.exp(-(x_ot-2)**2/0.5) + 0.4 * np.exp(-(x_ot-7)**2/0.8)
rho_source /= np.trapezoid(rho_source, x_ot)

# Target distribution (unimodal)
rho_target = np.exp(-(x_ot-5)**2/2.0)
rho_target /= np.trapezoid(rho_target, x_ot)

# Samples
n_samples_ot = 500
samples_source = np.random.choice(x_ot, size=n_samples_ot, p=rho_source/rho_source.sum())
samples_target = np.random.choice(x_ot, size=n_samples_ot, p=rho_target/rho_target.sum())

# Compute Wasserstein distance
W1_distance = wasserstein_distance(samples_source, samples_target)

# CDF for transport
cdf_source = np.cumsum(rho_source)
cdf_source /= cdf_source[-1]

cdf_target = np.cumsum(rho_target)
cdf_target /= cdf_target[-1]

# Transport map (quantile transform)
def transport_map(x):
    """Optimal map in 1D"""
    # Find CDF(x) in source
    idx_source = np.searchsorted(x_ot, x)
    if idx_source >= len(cdf_source):
        idx_source = len(cdf_source) - 1
    cdf_val = cdf_source[idx_source]
    
    # Find x' such that CDF_target(x') = cdf_val
    idx_target = np.searchsorted(cdf_target, cdf_val)
    if idx_target >= len(x_ot):
        idx_target = len(x_ot) - 1
    
    return x_ot[idx_target]

# Apply transport
samples_transported = np.array([transport_map(s) for s in samples_source])

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Source and target distributions
axes[0, 0].plot(x_ot, rho_source, 'b-', linewidth=2, label='Source ρ₀')
axes[0, 0].plot(x_ot, rho_target, 'r-', linewidth=2, label='Target ρ₁')
axes[0, 0].fill_between(x_ot, rho_source, alpha=0.3, color='blue')
axes[0, 0].fill_between(x_ot, rho_target, alpha=0.3, color='red')
axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('Density')
axes[0, 0].set_title(f'Source and Target Distributions\nW₁ = {W1_distance:.3f}')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# CDFs
axes[0, 1].plot(x_ot, cdf_source, 'b-', linewidth=2, label='CDF source')
axes[0, 1].plot(x_ot, cdf_target, 'r-', linewidth=2, label='CDF target')
axes[0, 1].set_xlabel('x')
axes[0, 1].set_ylabel('Cumulative probability')
axes[0, 1].set_title('Cumulative Distribution Functions')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Transport map
T_map = np.array([transport_map(xi) for xi in x_ot])
axes[1, 0].plot(x_ot, T_map, 'g-', linewidth=2)
axes[1, 0].plot(x_ot, x_ot, 'k--', alpha=0.5, label='Identity')
axes[1, 0].set_xlabel('x (source)')
axes[1, 0].set_ylabel('T(x) (target)')
axes[1, 0].set_title('Optimal Transport Map T')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)

# Transported samples
axes[1, 1].hist(samples_source, bins=30, density=True, alpha=0.5, 
               color='blue', label='Source samples')
axes[1, 1].hist(samples_transported, bins=30, density=True, alpha=0.5,
               color='green', label='Transported')
axes[1, 1].hist(samples_target, bins=30, density=True, alpha=0.5,
               color='red', label='Target samples')
axes[1, 1].plot(x_ot, rho_target, 'r-', linewidth=2)
axes[1, 1].set_xlabel('x')
axes[1, 1].set_ylabel('Density')
axes[1, 1].set_title('Transport of Samples: T#ρ₀ → ρ₁')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("""
Computational OT in ML:

1. SINKHORN ALGORITHM
   Entropic regularization: W_ε(μ,ν) = min_γ ⟨γ,C⟩ - εH(γ)
   
   Fast iterative algorithm (matrix scaling)
   
2. NEURAL OT
   Parametrize T_θ: x ↦ x + f_θ(x)
   
   Minimize: E_x~ρ₀[||x - T_θ(x)||²] + W₂(T_θ#ρ₀, ρ₁)

3. OT FOR GENERATIVE MODELS
   Learn generator G: Z → X minimizing W(G#p_z, p_data)
   
4. CONTINUOUS NORMALIZING FLOWS
   ODE: dx/dt = v_t(x) where v_t is learned
   
   This is a time-dependent vector field (operator flow)
""")

print("\n✓ Optimal transport = geometric way to compare distributions")
print("✓ Transport maps = nonlinear pseudo-differential operators")
CONTEXT: Optimal Transport Theory

The Wasserstein distance W_2(μ, ν) between distributions:

W_2²(μ, ν) = inf_γ ∫∫ ||x-y||² dγ(x,y)

where γ is a transport plan (coupling).

For Gaussian distributions:
W_2²(N(m₁,Σ₁), N(m₂,Σ₂)) = ||m₁-m₂||² + Tr(Σ₁ + Σ₂ - 2(Σ₂^{1/2}Σ₁Σ₂^{1/2})^{1/2})

Connection to ΨDOs:
- Transport map T: x ↦ ∇φ(x) where φ is Monge-Ampère solution
- φ satisfies: det(∇²φ) = ρ₀/ρ₁ (ρ₀∘T)
- This is a nonlinear PDE!


Applications in ML:

1. DOMAIN ADAPTATION
   Align source and target distributions via OT
   
2. GANS (Wasserstein GANs)
   Use W_1 distance instead of KL divergence
   More stable training

3. BARYCENTER PROBLEMS
   Average of distributions in Wasserstein space
   
4. SLICED-WASSERSTEIN
   W̃(μ,ν) = ∫_{S^{d-1}} W_1(θ#μ, θ#ν) dθ
   
   Projection onto lines (Radon transform!)
   This is a pseudo-differential operator

5. NEURAL OPTIMAL TRANSPORT
   Learn transport map T via neural networks


----------------------------------------------------------------------
Simulation: Optimal transport between distributions
----------------------------------------------------------------------
No description has been provided for this image
Computational OT in ML:

1. SINKHORN ALGORITHM
   Entropic regularization: W_ε(μ,ν) = min_γ ⟨γ,C⟩ - εH(γ)
   
   Fast iterative algorithm (matrix scaling)
   
2. NEURAL OT
   Parametrize T_θ: x ↦ x + f_θ(x)
   
   Minimize: E_x~ρ₀[||x - T_θ(x)||²] + W₂(T_θ#ρ₀, ρ₁)

3. OT FOR GENERATIVE MODELS
   Learn generator G: Z → X minimizing W(G#p_z, p_data)
   
4. CONTINUOUS NORMALIZING FLOWS
   ODE: dx/dt = v_t(x) where v_t is learned
   
   This is a time-dependent vector field (operator flow)


✓ Optimal transport = geometric way to compare distributions
✓ Transport maps = nonlinear pseudo-differential operators
In [ ]: