Skip to content

Building on the core

photonics_helper.core is meant to be built on. This page walks through a complete, runnable external tool — a dispersive-broadening calculator — written using only the core primitives, and validates it against a closed-form result. The full script is examples/33_build_on_core.py.

python examples/33_build_on_core.py

A test runs the same script in a fresh interpreter and asserts that it imports no solver/plotting module (tests/test_build_on_core.py), so the example is an executable guarantee that the foundation stands alone.

What the tool does

Given a material's n(λ), it computes the group-velocity dispersion β₂ and propagates a Gaussian pulse through a length z of glass, then checks the pulse broadening against the textbook result

T(z) / T₀ = sqrt(1 + (z / L_D)²),    L_D = T₀² / |β₂|.

Step 1 — define a material

The core does not force you to use the bundled database. OpticalMaterial is a runtime-checkable protocol, so any object with the right shape works — that is the intended extension point:

from dataclasses import dataclass
import numpy as np
from photonics_helper.core import units
from photonics_helper.core.materials import OpticalMaterial


@dataclass
class CauchyGlass:
    name: str = "Cauchy glass"
    A: float = 1.4437
    B_um2: float = 0.00354
    source: str | None = "two-term Cauchy fit (illustrative)"
    license: str | None = None

    def n_func(self, wavelength_um: float) -> float:
        return self.A + self.B_um2 / wavelength_um**2

    def k_func(self, wavelength_um: float) -> float:
        return 0.0

    @property
    def wl(self) -> units.WavelengthArray:
        return units.WavelengthArray(np.linspace(1.0, 2.0, 11), "um")

    @property
    def n(self) -> np.ndarray:
        return np.array([self.n_func(w) for w in self.wl.as_um])

    @property
    def k(self) -> np.ndarray:
        return np.zeros_like(self.n)


assert isinstance(CauchyGlass(), OpticalMaterial)

Alternatively, load a bundled material with photonics_helper.core.materials.material("Silica") — note that the bundled database is read through the Raman SQLite backend today (a naming holdover), so the protocol path above is what keeps an external tool free of that dependency.

Step 2 — dispersion from units and constants

units handles the conversions (wavelength ↔ angular frequency) and constants supplies c:

import math
from photonics_helper.core import constants, units


def group_velocity_dispersion(material, wavelength) -> float:
    omega0 = wavelength.to_freq().to_omega().as_rad_s

    def beta(omega):  # beta(omega) = n(omega) * omega / c
        wavelength_um = (2 * math.pi * constants.C_MS / omega) * 1e6
        return material.n_func(wavelength_um) * omega / constants.C_MS

    step = omega0 * 1e-3  # central finite difference
    return (beta(omega0 + step) - 2 * beta(omega0) + beta(omega0 - step)) / step**2


beta2 = group_velocity_dispersion(CauchyGlass(), units.Wavelength(1550, "nm"))
print(f"beta2 = {beta2 * 1e27:.2f} ps^2/km")  # -> 24.27 ps^2/km

Step 3 — propagate on a core grid

TemporalGrid provides the time/frequency sampling and a documented FFT convention (FFT(A)·dt forward, /dt inverse, both fftshifted), and Wavelength/Time keep the units honest:

import numpy as np
from photonics_helper.core import grids, units

grid = grids.TemporalGrid(N=1 << 14, Tmax=units.Time(20, "ps"))
t0 = units.Time(50, "fs").as_s
field = np.exp(-(grid.t**2) / (2 * t0**2)).astype(complex)

dispersion_length = t0**2 / abs(beta2)


def propagate(field, beta2, length_m):
    spectral = grid.fft(field)
    phase = np.exp(1j * beta2 * grid.w**2 * length_m / 2.0)
    return grid.ifft(spectral * phase)


final = propagate(field, beta2, dispersion_length)

Step 4 — validate

Measure the RMS width before and after; the ratio must reproduce the closed form. The script prints:

  z/L_D    numeric    analytic   |error|
   0.5    1.118034   1.118034   2.22e-16
   1.0    1.414214   1.414214   8.88e-16
   2.0    2.236068   2.236068   1.78e-15
   4.0    4.123106   4.123106   2.66e-15

What you get, and what you don't

From the core Not from the core
Typed units and constants The GNLSE / solver stack
TemporalGrid + FFT conventions Plotting, dashboards
The OpticalMaterial protocol and the bundled database The specific physics of your problem — that is yours to write

That is the point: the core removes the unit/numerics/data plumbing so your code is about your physics.

Next