Skip to content

anypinn.catalog

Catalog of ready-made ODE/PDE problem building blocks.

AC_U_KEY = 'u' module-attribute

ALPHA_KEY = 'alpha' module-attribute

BETA_KEY = 'beta' module-attribute

BURGERS_NU_KEY = 'nu' module-attribute

BURGERS_U_KEY = 'u' module-attribute

DELTA_KEY = 'delta' module-attribute

DIFFUSIVITY_D_KEY = 'D' module-attribute

DIFFUSIVITY_U_KEY = 'u' module-attribute

E_KEY = 'E' module-attribute

FHN_A_KEY = 'a' module-attribute

FHN_EPSILON_KEY = 'epsilon' module-attribute

FHN_V_KEY = 'v' module-attribute

FHN_W_KEY = 'w' module-attribute

GAMMA_KEY = 'gamma' module-attribute

GS_DU_KEY = 'D_u' module-attribute

GS_DV_KEY = 'D_v' module-attribute

GS_F_KEY = 'F' module-attribute

GS_K_KEY = 'k' module-attribute

GS_U_KEY = 'u' module-attribute

GS_V_KEY = 'v' module-attribute

HEAT_ALPHA_KEY = 'alpha' module-attribute

HEAT_U_KEY = 'u' module-attribute

LORENZ_BETA_KEY = 'beta' module-attribute

LORENZ_RHO_KEY = 'rho' module-attribute

LORENZ_SIGMA_KEY = 'sigma' module-attribute

LORENZ_X_KEY = 'x' module-attribute

LORENZ_Y_KEY = 'y' module-attribute

LV_X_KEY = 'x' module-attribute

MU_KEY = 'mu' module-attribute

N_KEY = 'N' module-attribute

OMEGA_KEY = 'omega0' module-attribute

POISSON_U_KEY = 'u' module-attribute

Rt_KEY = 'Rt' module-attribute

SEIR_BETA_KEY = 'beta' module-attribute

SEIR_GAMMA_KEY = 'gamma' module-attribute

SEIR_I_KEY = 'I' module-attribute

SEIR_S_KEY = 'S' module-attribute

SIGMA_KEY = 'sigma' module-attribute

SIR_BETA_KEY = 'beta' module-attribute

SIR_DELTA_KEY = 'delta' module-attribute

SIR_I_KEY = 'I' module-attribute

SIR_S_KEY = 'S' module-attribute

U_KEY = 'u' module-attribute

V_KEY = 'v' module-attribute

WAVE_C_KEY = 'c' module-attribute

WAVE_U_KEY = 'u' module-attribute

X_KEY = 'x' module-attribute

Y_KEY = 'y' module-attribute

ZETA_KEY = 'zeta' module-attribute

Z_KEY = 'z' module-attribute

__all__ = ['AC_U_KEY', 'ALPHA_KEY', 'BETA_KEY', 'BURGERS_NU_KEY', 'BURGERS_U_KEY', 'DELTA_KEY', 'DIFFUSIVITY_D_KEY', 'DIFFUSIVITY_U_KEY', 'E_KEY', 'FHN_A_KEY', 'FHN_EPSILON_KEY', 'FHN_V_KEY', 'FHN_W_KEY', 'GAMMA_KEY', 'GS_DU_KEY', 'GS_DV_KEY', 'GS_F_KEY', 'GS_K_KEY', 'GS_U_KEY', 'GS_V_KEY', 'HEAT_ALPHA_KEY', 'HEAT_U_KEY', 'LORENZ_BETA_KEY', 'LORENZ_RHO_KEY', 'LORENZ_SIGMA_KEY', 'LORENZ_X_KEY', 'LORENZ_Y_KEY', 'LV_X_KEY', 'MU_KEY', 'N_KEY', 'OMEGA_KEY', 'POISSON_U_KEY', 'SEIR_BETA_KEY', 'SEIR_GAMMA_KEY', 'SEIR_I_KEY', 'SEIR_S_KEY', 'SIGMA_KEY', 'SIR', 'SIR_BETA_KEY', 'SIR_DELTA_KEY', 'SIR_I_KEY', 'SIR_S_KEY', 'U_KEY', 'V_KEY', 'WAVE_C_KEY', 'WAVE_U_KEY', 'X_KEY', 'Y_KEY', 'ZETA_KEY', 'Z_KEY', 'AllenCahnDataModule', 'Burgers1DDataModule', 'DampedOscillatorDataModule', 'FitzHughNagumoDataModule', 'GrayScott2DDataModule', 'Heat1DDataModule', 'InverseDiffusivityDataModule', 'LorenzDataModule', 'LotkaVolterraDataModule', 'Poisson2DDataModule', 'Rt_KEY', 'SEIRDataModule', 'SIRInvDataModule', 'VanDerPolDataModule', 'Wave1DDataModule', 'rSIR'] module-attribute

AllenCahnDataModule

Bases: PINNDataModule

DataModule for 1D Allen-Cahn equation.

gen_data produces ground-truth u(x,t) via scipy method-of-lines (central differences for d2u/dx2 with periodic ghost cells + ODE integration). The data is used for prediction/validation only — training uses PDEResidualConstraint + PeriodicBCConstraint + IC (no DataConstraint).

Source code in src/anypinn/catalog/allen_cahn.py
class AllenCahnDataModule(PINNDataModule):
    """DataModule for 1D Allen-Cahn equation.

    gen_data produces ground-truth u(x,t) via scipy method-of-lines
    (central differences for d2u/dx2 with periodic ghost cells + ODE integration).
    The data is used for prediction/validation only — training uses
    PDEResidualConstraint + PeriodicBCConstraint + IC (no DataConstraint).
    """

    def __init__(
        self,
        hp: PINNHyperparameters,
        true_epsilon: float = TRUE_EPSILON,
        n_x: int = 256,
        n_t: int = 200,
        grid_size: int = 50,
        residual_scorer: ResidualScorer | None = None,
        validation: ValidationRegistry | None = None,
        callbacks: list[DataCallback] | None = None,
    ):
        self.true_epsilon = true_epsilon
        self.n_x = n_x
        self.n_t = n_t
        self.grid_size = grid_size
        super().__init__(hp, validation, callbacks, residual_scorer=residual_scorer)

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate numerical solution on a 2D meshgrid via method of lines."""
        import numpy as np
        from scipy.integrate import solve_ivp
        from scipy.interpolate import RegularGridInterpolator

        n_x = self.n_x
        eps = self.true_epsilon

        # Periodic domain [-1, 1): n_x interior points, dx = 2/n_x
        x_fd = np.linspace(-1, 1, n_x, endpoint=False)
        dx = x_fd[1] - x_fd[0]

        # IC: u(x,0) = -tanh(x / (sqrt(2*eps)))
        scale = math.sqrt(2 * eps)
        u0 = -np.tanh(x_fd / scale)

        t_span = (0.0, 1.0)
        t_eval = np.linspace(0, 1, self.n_t)

        def rhs(_t: float, u: np.ndarray) -> np.ndarray:
            """RHS: du/dt = eps*d2u/dx2 + u - u^3 with periodic BCs."""
            # Periodic padding
            u_pad = np.empty(n_x + 2)
            u_pad[1:-1] = u
            u_pad[0] = u[-1]  # left ghost = rightmost interior
            u_pad[-1] = u[0]  # right ghost = leftmost interior

            # Central differences for d2u/dx2
            d2u = (u_pad[2:] - 2 * u_pad[1:-1] + u_pad[:-2]) / dx**2

            return eps * d2u + u - u**3

        sol = solve_ivp(rhs, t_span, u0, t_eval=t_eval, method="RK45", max_step=0.001)

        # sol.y has shape (n_x, n_t)
        x_sol = x_fd
        t_sol = sol.t

        interp = RegularGridInterpolator(
            (x_sol, t_sol), sol.y, method="linear", bounds_error=False, fill_value=None
        )

        # Output meshgrid
        xs = torch.linspace(-1, 1, self.grid_size)
        ts = torch.linspace(0, 1, self.grid_size)
        grid_x, grid_t = torch.meshgrid(xs, ts, indexing="ij")

        x_grid = torch.stack([grid_x.reshape(-1), grid_t.reshape(-1)], dim=1)  # (N, 2)

        pts = np.stack([x_grid[:, 0].numpy(), x_grid[:, 1].numpy()], axis=1)
        u_ref = torch.tensor(interp(pts), dtype=torch.float32)

        # Shape: (N, 1, 1) to match codebase convention
        y_data = u_ref.unsqueeze(-1).unsqueeze(1)

        return x_grid, y_data

grid_size = grid_size instance-attribute

n_t = n_t instance-attribute

n_x = n_x instance-attribute

true_epsilon = true_epsilon instance-attribute

__init__(hp: PINNHyperparameters, true_epsilon: float = TRUE_EPSILON, n_x: int = 256, n_t: int = 200, grid_size: int = 50, residual_scorer: ResidualScorer | None = None, validation: ValidationRegistry | None = None, callbacks: list[DataCallback] | None = None)

Source code in src/anypinn/catalog/allen_cahn.py
def __init__(
    self,
    hp: PINNHyperparameters,
    true_epsilon: float = TRUE_EPSILON,
    n_x: int = 256,
    n_t: int = 200,
    grid_size: int = 50,
    residual_scorer: ResidualScorer | None = None,
    validation: ValidationRegistry | None = None,
    callbacks: list[DataCallback] | None = None,
):
    self.true_epsilon = true_epsilon
    self.n_x = n_x
    self.n_t = n_t
    self.grid_size = grid_size
    super().__init__(hp, validation, callbacks, residual_scorer=residual_scorer)

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate numerical solution on a 2D meshgrid via method of lines.

Source code in src/anypinn/catalog/allen_cahn.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate numerical solution on a 2D meshgrid via method of lines."""
    import numpy as np
    from scipy.integrate import solve_ivp
    from scipy.interpolate import RegularGridInterpolator

    n_x = self.n_x
    eps = self.true_epsilon

    # Periodic domain [-1, 1): n_x interior points, dx = 2/n_x
    x_fd = np.linspace(-1, 1, n_x, endpoint=False)
    dx = x_fd[1] - x_fd[0]

    # IC: u(x,0) = -tanh(x / (sqrt(2*eps)))
    scale = math.sqrt(2 * eps)
    u0 = -np.tanh(x_fd / scale)

    t_span = (0.0, 1.0)
    t_eval = np.linspace(0, 1, self.n_t)

    def rhs(_t: float, u: np.ndarray) -> np.ndarray:
        """RHS: du/dt = eps*d2u/dx2 + u - u^3 with periodic BCs."""
        # Periodic padding
        u_pad = np.empty(n_x + 2)
        u_pad[1:-1] = u
        u_pad[0] = u[-1]  # left ghost = rightmost interior
        u_pad[-1] = u[0]  # right ghost = leftmost interior

        # Central differences for d2u/dx2
        d2u = (u_pad[2:] - 2 * u_pad[1:-1] + u_pad[:-2]) / dx**2

        return eps * d2u + u - u**3

    sol = solve_ivp(rhs, t_span, u0, t_eval=t_eval, method="RK45", max_step=0.001)

    # sol.y has shape (n_x, n_t)
    x_sol = x_fd
    t_sol = sol.t

    interp = RegularGridInterpolator(
        (x_sol, t_sol), sol.y, method="linear", bounds_error=False, fill_value=None
    )

    # Output meshgrid
    xs = torch.linspace(-1, 1, self.grid_size)
    ts = torch.linspace(0, 1, self.grid_size)
    grid_x, grid_t = torch.meshgrid(xs, ts, indexing="ij")

    x_grid = torch.stack([grid_x.reshape(-1), grid_t.reshape(-1)], dim=1)  # (N, 2)

    pts = np.stack([x_grid[:, 0].numpy(), x_grid[:, 1].numpy()], axis=1)
    u_ref = torch.tensor(interp(pts), dtype=torch.float32)

    # Shape: (N, 1, 1) to match codebase convention
    y_data = u_ref.unsqueeze(-1).unsqueeze(1)

    return x_grid, y_data

Burgers1DDataModule

Bases: PINNDataModule

DataModule for 1D Burgers equation inverse problem.

gen_data produces ground-truth u(x,t) via scipy method-of-lines (finite-difference spatial discretization + ODE integration), with optional measurement noise.

Source code in src/anypinn/catalog/burgers_1d.py
class Burgers1DDataModule(PINNDataModule):
    """DataModule for 1D Burgers equation inverse problem.

    gen_data produces ground-truth u(x,t) via scipy method-of-lines
    (finite-difference spatial discretization + ODE integration),
    with optional measurement noise.
    """

    def __init__(
        self,
        hp: PINNHyperparameters,
        true_nu: float = TRUE_NU,
        noise_std: float = 0.01,
        grid_size: int = 50,
        residual_scorer: ResidualScorer | None = None,
        validation: ValidationRegistry | None = None,
        callbacks: list[DataCallback] | None = None,
    ):
        self.true_nu = true_nu
        self.noise_std = noise_std
        self.grid_size = grid_size
        super().__init__(hp, validation, callbacks, residual_scorer=residual_scorer)

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate numerical solution on a 2D meshgrid via method of lines."""
        import numpy as np
        from scipy.integrate import solve_ivp
        from scipy.interpolate import RegularGridInterpolator

        n_x = 256  # interior spatial points for FD discretization
        x_fd = torch.linspace(-1, 1, n_x + 2).numpy()  # includes boundaries
        dx = x_fd[1] - x_fd[0]

        # IC: u(x,0) = -sin(pi*x)
        u0 = -torch.sin(math.pi * torch.tensor(x_fd[1:-1])).numpy()

        t_span = (0.0, 1.0)
        t_eval = torch.linspace(0, 1, self.grid_size).numpy()

        nu = self.true_nu

        def rhs(_t, u):
            """RHS: du/dt = nu*d2u/dx2 - u*du/dx with homogeneous Dirichlet BCs."""
            # Pad with boundary values (u=0 at x=-1 and x=1)
            u_pad = torch.zeros(len(u) + 2).numpy()
            u_pad[1:-1] = u

            # Central differences for d2u/dx2
            d2u = (u_pad[2:] - 2 * u_pad[1:-1] + u_pad[:-2]) / dx**2

            # Central differences for du/dx
            du = (u_pad[2:] - u_pad[:-2]) / (2 * dx)

            return nu * d2u - u * du

        sol = solve_ivp(rhs, t_span, u0, t_eval=t_eval, method="RK45", max_step=0.001)

        # sol.y has shape (n_x, len(t_eval))
        # Interpolate onto output meshgrid
        x_interior = x_fd[1:-1]
        t_sol = sol.t

        interp = RegularGridInterpolator(
            (x_interior, t_sol), sol.y, method="linear", bounds_error=False, fill_value=None
        )

        # Output meshgrid
        xs = torch.linspace(-1, 1, self.grid_size)
        ts = torch.linspace(0, 1, self.grid_size)
        grid_x, grid_t = torch.meshgrid(xs, ts, indexing="ij")

        x_grid = torch.stack([grid_x.reshape(-1), grid_t.reshape(-1)], dim=1)  # (N, 2)

        pts = np.stack([x_grid[:, 0].numpy(), x_grid[:, 1].numpy()], axis=1)
        u_ref = torch.tensor(interp(pts), dtype=torch.float32)

        # Add measurement noise
        u_noisy = u_ref + self.noise_std * torch.randn_like(u_ref)

        # Shape: (N, 1, 1) to match codebase convention
        y_data = u_noisy.unsqueeze(-1).unsqueeze(1)

        return x_grid, y_data

grid_size = grid_size instance-attribute

noise_std = noise_std instance-attribute

true_nu = true_nu instance-attribute

__init__(hp: PINNHyperparameters, true_nu: float = TRUE_NU, noise_std: float = 0.01, grid_size: int = 50, residual_scorer: ResidualScorer | None = None, validation: ValidationRegistry | None = None, callbacks: list[DataCallback] | None = None)

Source code in src/anypinn/catalog/burgers_1d.py
def __init__(
    self,
    hp: PINNHyperparameters,
    true_nu: float = TRUE_NU,
    noise_std: float = 0.01,
    grid_size: int = 50,
    residual_scorer: ResidualScorer | None = None,
    validation: ValidationRegistry | None = None,
    callbacks: list[DataCallback] | None = None,
):
    self.true_nu = true_nu
    self.noise_std = noise_std
    self.grid_size = grid_size
    super().__init__(hp, validation, callbacks, residual_scorer=residual_scorer)

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate numerical solution on a 2D meshgrid via method of lines.

Source code in src/anypinn/catalog/burgers_1d.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate numerical solution on a 2D meshgrid via method of lines."""
    import numpy as np
    from scipy.integrate import solve_ivp
    from scipy.interpolate import RegularGridInterpolator

    n_x = 256  # interior spatial points for FD discretization
    x_fd = torch.linspace(-1, 1, n_x + 2).numpy()  # includes boundaries
    dx = x_fd[1] - x_fd[0]

    # IC: u(x,0) = -sin(pi*x)
    u0 = -torch.sin(math.pi * torch.tensor(x_fd[1:-1])).numpy()

    t_span = (0.0, 1.0)
    t_eval = torch.linspace(0, 1, self.grid_size).numpy()

    nu = self.true_nu

    def rhs(_t, u):
        """RHS: du/dt = nu*d2u/dx2 - u*du/dx with homogeneous Dirichlet BCs."""
        # Pad with boundary values (u=0 at x=-1 and x=1)
        u_pad = torch.zeros(len(u) + 2).numpy()
        u_pad[1:-1] = u

        # Central differences for d2u/dx2
        d2u = (u_pad[2:] - 2 * u_pad[1:-1] + u_pad[:-2]) / dx**2

        # Central differences for du/dx
        du = (u_pad[2:] - u_pad[:-2]) / (2 * dx)

        return nu * d2u - u * du

    sol = solve_ivp(rhs, t_span, u0, t_eval=t_eval, method="RK45", max_step=0.001)

    # sol.y has shape (n_x, len(t_eval))
    # Interpolate onto output meshgrid
    x_interior = x_fd[1:-1]
    t_sol = sol.t

    interp = RegularGridInterpolator(
        (x_interior, t_sol), sol.y, method="linear", bounds_error=False, fill_value=None
    )

    # Output meshgrid
    xs = torch.linspace(-1, 1, self.grid_size)
    ts = torch.linspace(0, 1, self.grid_size)
    grid_x, grid_t = torch.meshgrid(xs, ts, indexing="ij")

    x_grid = torch.stack([grid_x.reshape(-1), grid_t.reshape(-1)], dim=1)  # (N, 2)

    pts = np.stack([x_grid[:, 0].numpy(), x_grid[:, 1].numpy()], axis=1)
    u_ref = torch.tensor(interp(pts), dtype=torch.float32)

    # Add measurement noise
    u_noisy = u_ref + self.noise_std * torch.randn_like(u_ref)

    # Shape: (N, 1, 1) to match codebase convention
    y_data = u_noisy.unsqueeze(-1).unsqueeze(1)

    return x_grid, y_data

DampedOscillatorDataModule

Bases: PINNDataModule

DataModule for damped oscillator inverse problem. Generates synthetic data via odeint.

Source code in src/anypinn/catalog/damped_oscillator.py
class DampedOscillatorDataModule(PINNDataModule):
    """DataModule for damped oscillator inverse problem. Generates synthetic data via odeint."""

    def __init__(
        self,
        hp: ODEHyperparameters,
        gen_props: ODEProperties,
        noise_std: float = 0.0,
        validation: ValidationRegistry | None = None,
        callbacks: Sequence[DataCallback] | None = None,
    ):
        super().__init__(hp, validation, callbacks)
        self.gen_props = gen_props
        self.noise_std = noise_std

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate synthetic damped oscillator data using odeint + Gaussian noise."""

        def oscillator_ode(t: Tensor, y: Tensor) -> Tensor:
            return self.gen_props.ode(t, y, self.gen_props.args)

        t = config.x

        sol = odeint(oscillator_ode, self.gen_props.y0, t)  # [T, 2]
        x_true = sol[:, 0]

        x_obs = x_true + self.noise_std * torch.randn_like(x_true)

        return t.unsqueeze(-1), x_obs.unsqueeze(-1).unsqueeze(1)

gen_props = gen_props instance-attribute

noise_std = noise_std instance-attribute

__init__(hp: ODEHyperparameters, gen_props: ODEProperties, noise_std: float = 0.0, validation: ValidationRegistry | None = None, callbacks: Sequence[DataCallback] | None = None)

Source code in src/anypinn/catalog/damped_oscillator.py
def __init__(
    self,
    hp: ODEHyperparameters,
    gen_props: ODEProperties,
    noise_std: float = 0.0,
    validation: ValidationRegistry | None = None,
    callbacks: Sequence[DataCallback] | None = None,
):
    super().__init__(hp, validation, callbacks)
    self.gen_props = gen_props
    self.noise_std = noise_std

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate synthetic damped oscillator data using odeint + Gaussian noise.

Source code in src/anypinn/catalog/damped_oscillator.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate synthetic damped oscillator data using odeint + Gaussian noise."""

    def oscillator_ode(t: Tensor, y: Tensor) -> Tensor:
        return self.gen_props.ode(t, y, self.gen_props.args)

    t = config.x

    sol = odeint(oscillator_ode, self.gen_props.y0, t)  # [T, 2]
    x_true = sol[:, 0]

    x_obs = x_true + self.noise_std * torch.randn_like(x_true)

    return t.unsqueeze(-1), x_obs.unsqueeze(-1).unsqueeze(1)

FitzHughNagumoDataModule

Bases: PINNDataModule

DataModule for FitzHugh-Nagumo inverse problem.

Generates synthetic data via odeint. Only the voltage v is observed; the recovery variable w is inferred through ODE residuals alone.

Source code in src/anypinn/catalog/fitzhugh_nagumo.py
class FitzHughNagumoDataModule(PINNDataModule):
    """DataModule for FitzHugh-Nagumo inverse problem.

    Generates synthetic data via odeint. Only the voltage v is observed;
    the recovery variable w is inferred through ODE residuals alone.
    """

    def __init__(
        self,
        hp: ODEHyperparameters,
        gen_props: ODEProperties,
        noise_std: float = 0.0,
        validation: ValidationRegistry | None = None,
        callbacks: Sequence[DataCallback] | None = None,
    ):
        super().__init__(hp, validation, callbacks)
        self.gen_props = gen_props
        self.noise_std = noise_std

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate synthetic FHN data. Returns only v (partially observed)."""

        def fhn_ode(t: Tensor, y: Tensor) -> Tensor:
            return self.gen_props.ode(t, y, self.gen_props.args)

        t = config.x

        sol = odeint(fhn_ode, self.gen_props.y0, t)  # [T, 2]
        v_true = sol[:, 0]

        v_obs = v_true + self.noise_std * torch.randn_like(v_true)

        # (N, 1, 1) — single observed field
        y_data = v_obs.unsqueeze(1).unsqueeze(-1)

        return t.unsqueeze(-1), y_data

gen_props = gen_props instance-attribute

noise_std = noise_std instance-attribute

__init__(hp: ODEHyperparameters, gen_props: ODEProperties, noise_std: float = 0.0, validation: ValidationRegistry | None = None, callbacks: Sequence[DataCallback] | None = None)

Source code in src/anypinn/catalog/fitzhugh_nagumo.py
def __init__(
    self,
    hp: ODEHyperparameters,
    gen_props: ODEProperties,
    noise_std: float = 0.0,
    validation: ValidationRegistry | None = None,
    callbacks: Sequence[DataCallback] | None = None,
):
    super().__init__(hp, validation, callbacks)
    self.gen_props = gen_props
    self.noise_std = noise_std

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate synthetic FHN data. Returns only v (partially observed).

Source code in src/anypinn/catalog/fitzhugh_nagumo.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate synthetic FHN data. Returns only v (partially observed)."""

    def fhn_ode(t: Tensor, y: Tensor) -> Tensor:
        return self.gen_props.ode(t, y, self.gen_props.args)

    t = config.x

    sol = odeint(fhn_ode, self.gen_props.y0, t)  # [T, 2]
    v_true = sol[:, 0]

    v_obs = v_true + self.noise_std * torch.randn_like(v_true)

    # (N, 1, 1) — single observed field
    y_data = v_obs.unsqueeze(1).unsqueeze(-1)

    return t.unsqueeze(-1), y_data

GrayScott2DDataModule

Bases: PINNDataModule

DataModule for 2D Gray-Scott reaction-diffusion inverse problem.

gen_data produces ground-truth u(x,y,t) and v(x,y,t) via scipy method-of-lines (finite-difference spatial discretization + ODE integration), with optional measurement noise.

Source code in src/anypinn/catalog/gray_scott_2d.py
class GrayScott2DDataModule(PINNDataModule):
    """DataModule for 2D Gray-Scott reaction-diffusion inverse problem.

    gen_data produces ground-truth u(x,y,t) and v(x,y,t) via scipy
    method-of-lines (finite-difference spatial discretization + ODE
    integration), with optional measurement noise.
    """

    def __init__(
        self,
        hp: PINNHyperparameters,
        true_du: float = TRUE_DU,
        true_dv: float = TRUE_DV,
        true_f: float = TRUE_F,
        true_k: float = TRUE_K,
        noise_std: float = 0.01,
        sim_size: int = 64,
        residual_scorer: ResidualScorer | None = None,
        validation: ValidationRegistry | None = None,
        callbacks: list[DataCallback] | None = None,
    ):
        self.true_du = true_du
        self.true_dv = true_dv
        self.true_f = true_f
        self.true_k = true_k
        self.noise_std = noise_std
        self.sim_size = sim_size
        super().__init__(hp, validation, callbacks, residual_scorer=residual_scorer)

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate numerical solution on a 3D meshgrid via method of lines."""
        import numpy as np
        from scipy.integrate import solve_ivp
        from scipy.interpolate import RegularGridInterpolator

        n = self.sim_size
        dx = 1.0 / (n - 1)
        x_fd = np.linspace(0, 1, n)

        # ICs: u=1, v=0 everywhere; center square u=0.5, v=0.25
        u0 = np.ones((n, n))
        v0 = np.zeros((n, n))
        lo = int(0.4 * n)
        hi = int(0.6 * n)
        u0[lo:hi, lo:hi] = 0.5
        v0[lo:hi, lo:hi] = 0.25

        y0 = np.concatenate([u0.ravel(), v0.ravel()])

        du_val = self.true_du
        dv_val = self.true_dv
        f_val = self.true_f
        k_val = self.true_k

        def rhs(_t: float, y: np.ndarray) -> np.ndarray:
            u = y[: n * n].reshape(n, n)
            v = y[n * n :].reshape(n, n)

            # 5-point FD Laplacian with Neumann (zero-flux) BCs via padding
            u_pad = np.pad(u, 1, mode="edge")
            v_pad = np.pad(v, 1, mode="edge")

            lap_u = (
                u_pad[2:, 1:-1]
                + u_pad[:-2, 1:-1]
                + u_pad[1:-1, 2:]
                + u_pad[1:-1, :-2]
                - 4 * u_pad[1:-1, 1:-1]
            ) / dx**2
            lap_v = (
                v_pad[2:, 1:-1]
                + v_pad[:-2, 1:-1]
                + v_pad[1:-1, 2:]
                + v_pad[1:-1, :-2]
                - 4 * v_pad[1:-1, 1:-1]
            ) / dx**2

            uv2 = u * v**2
            du_dt = du_val * lap_u - uv2 + f_val * (1 - u)
            dv_dt = dv_val * lap_v + uv2 - (f_val + k_val) * v

            return np.concatenate([du_dt.ravel(), dv_dt.ravel()])

        t_span = (0.0, T_TOTAL)
        n_t_sim = 50
        t_eval = np.linspace(0, T_TOTAL, n_t_sim)

        sol = solve_ivp(rhs, t_span, y0, t_eval=t_eval, method="Radau", max_step=5.0)

        # sol.y shape: (2*n*n, n_t_sim)
        u_sol = sol.y[: n * n, :].reshape(n, n, -1)  # (n, n, n_t)
        v_sol = sol.y[n * n :, :].reshape(n, n, -1)

        # Build interpolators for u and v
        t_norm_sim = sol.t / T_TOTAL  # normalize to [0, 1]
        interp_u = RegularGridInterpolator(
            (x_fd, x_fd, t_norm_sim),
            u_sol,
            method="linear",
            bounds_error=False,
            fill_value=None,
        )
        interp_v = RegularGridInterpolator(
            (x_fd, x_fd, t_norm_sim),
            v_sol,
            method="linear",
            bounds_error=False,
            fill_value=None,
        )

        # Output measurement grid in [0,1]^3
        n_xy = 20
        n_t = 10
        xs = torch.linspace(0, 1, n_xy)
        ys = torch.linspace(0, 1, n_xy)
        ts = torch.linspace(0, 1, n_t)
        gx, gy, gt = torch.meshgrid(xs, ys, ts, indexing="ij")
        x_grid = torch.stack([gx.reshape(-1), gy.reshape(-1), gt.reshape(-1)], dim=1)

        pts = x_grid.numpy()
        u_ref = torch.tensor(interp_u(pts), dtype=torch.float32)
        v_ref = torch.tensor(interp_v(pts), dtype=torch.float32)

        # Add measurement noise
        u_noisy = u_ref + self.noise_std * torch.randn_like(u_ref)
        v_noisy = v_ref + self.noise_std * torch.randn_like(v_ref)

        # Shape: (N, 2, 1)
        y_data = torch.stack([u_noisy, v_noisy], dim=1).unsqueeze(-1)

        return x_grid, y_data

noise_std = noise_std instance-attribute

sim_size = sim_size instance-attribute

true_du = true_du instance-attribute

true_dv = true_dv instance-attribute

true_f = true_f instance-attribute

true_k = true_k instance-attribute

__init__(hp: PINNHyperparameters, true_du: float = TRUE_DU, true_dv: float = TRUE_DV, true_f: float = TRUE_F, true_k: float = TRUE_K, noise_std: float = 0.01, sim_size: int = 64, residual_scorer: ResidualScorer | None = None, validation: ValidationRegistry | None = None, callbacks: list[DataCallback] | None = None)

Source code in src/anypinn/catalog/gray_scott_2d.py
def __init__(
    self,
    hp: PINNHyperparameters,
    true_du: float = TRUE_DU,
    true_dv: float = TRUE_DV,
    true_f: float = TRUE_F,
    true_k: float = TRUE_K,
    noise_std: float = 0.01,
    sim_size: int = 64,
    residual_scorer: ResidualScorer | None = None,
    validation: ValidationRegistry | None = None,
    callbacks: list[DataCallback] | None = None,
):
    self.true_du = true_du
    self.true_dv = true_dv
    self.true_f = true_f
    self.true_k = true_k
    self.noise_std = noise_std
    self.sim_size = sim_size
    super().__init__(hp, validation, callbacks, residual_scorer=residual_scorer)

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate numerical solution on a 3D meshgrid via method of lines.

Source code in src/anypinn/catalog/gray_scott_2d.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate numerical solution on a 3D meshgrid via method of lines."""
    import numpy as np
    from scipy.integrate import solve_ivp
    from scipy.interpolate import RegularGridInterpolator

    n = self.sim_size
    dx = 1.0 / (n - 1)
    x_fd = np.linspace(0, 1, n)

    # ICs: u=1, v=0 everywhere; center square u=0.5, v=0.25
    u0 = np.ones((n, n))
    v0 = np.zeros((n, n))
    lo = int(0.4 * n)
    hi = int(0.6 * n)
    u0[lo:hi, lo:hi] = 0.5
    v0[lo:hi, lo:hi] = 0.25

    y0 = np.concatenate([u0.ravel(), v0.ravel()])

    du_val = self.true_du
    dv_val = self.true_dv
    f_val = self.true_f
    k_val = self.true_k

    def rhs(_t: float, y: np.ndarray) -> np.ndarray:
        u = y[: n * n].reshape(n, n)
        v = y[n * n :].reshape(n, n)

        # 5-point FD Laplacian with Neumann (zero-flux) BCs via padding
        u_pad = np.pad(u, 1, mode="edge")
        v_pad = np.pad(v, 1, mode="edge")

        lap_u = (
            u_pad[2:, 1:-1]
            + u_pad[:-2, 1:-1]
            + u_pad[1:-1, 2:]
            + u_pad[1:-1, :-2]
            - 4 * u_pad[1:-1, 1:-1]
        ) / dx**2
        lap_v = (
            v_pad[2:, 1:-1]
            + v_pad[:-2, 1:-1]
            + v_pad[1:-1, 2:]
            + v_pad[1:-1, :-2]
            - 4 * v_pad[1:-1, 1:-1]
        ) / dx**2

        uv2 = u * v**2
        du_dt = du_val * lap_u - uv2 + f_val * (1 - u)
        dv_dt = dv_val * lap_v + uv2 - (f_val + k_val) * v

        return np.concatenate([du_dt.ravel(), dv_dt.ravel()])

    t_span = (0.0, T_TOTAL)
    n_t_sim = 50
    t_eval = np.linspace(0, T_TOTAL, n_t_sim)

    sol = solve_ivp(rhs, t_span, y0, t_eval=t_eval, method="Radau", max_step=5.0)

    # sol.y shape: (2*n*n, n_t_sim)
    u_sol = sol.y[: n * n, :].reshape(n, n, -1)  # (n, n, n_t)
    v_sol = sol.y[n * n :, :].reshape(n, n, -1)

    # Build interpolators for u and v
    t_norm_sim = sol.t / T_TOTAL  # normalize to [0, 1]
    interp_u = RegularGridInterpolator(
        (x_fd, x_fd, t_norm_sim),
        u_sol,
        method="linear",
        bounds_error=False,
        fill_value=None,
    )
    interp_v = RegularGridInterpolator(
        (x_fd, x_fd, t_norm_sim),
        v_sol,
        method="linear",
        bounds_error=False,
        fill_value=None,
    )

    # Output measurement grid in [0,1]^3
    n_xy = 20
    n_t = 10
    xs = torch.linspace(0, 1, n_xy)
    ys = torch.linspace(0, 1, n_xy)
    ts = torch.linspace(0, 1, n_t)
    gx, gy, gt = torch.meshgrid(xs, ys, ts, indexing="ij")
    x_grid = torch.stack([gx.reshape(-1), gy.reshape(-1), gt.reshape(-1)], dim=1)

    pts = x_grid.numpy()
    u_ref = torch.tensor(interp_u(pts), dtype=torch.float32)
    v_ref = torch.tensor(interp_v(pts), dtype=torch.float32)

    # Add measurement noise
    u_noisy = u_ref + self.noise_std * torch.randn_like(u_ref)
    v_noisy = v_ref + self.noise_std * torch.randn_like(v_ref)

    # Shape: (N, 2, 1)
    y_data = torch.stack([u_noisy, v_noisy], dim=1).unsqueeze(-1)

    return x_grid, y_data

Heat1DDataModule

Bases: PINNDataModule

DataModule for 1D heat equation inverse problem.

gen_data produces sparse interior measurements from the analytic solution u(x,t) = exp(-alpha pi^2 t) sin(pi x), with optional noise. These measurements are used by DataConstraint during training to recover alpha.

Source code in src/anypinn/catalog/heat_1d.py
class Heat1DDataModule(PINNDataModule):
    """DataModule for 1D heat equation inverse problem.

    gen_data produces sparse interior measurements from the analytic
    solution u(x,t) = exp(-alpha pi^2 t) sin(pi x), with optional noise.
    These measurements are used by DataConstraint during training
    to recover alpha.
    """

    def __init__(
        self,
        hp: PINNHyperparameters,
        true_alpha: float = TRUE_ALPHA,
        n_measurements: int = 200,
        noise_std: float = 0.01,
        grid_size: int = 50,
        validation: ValidationRegistry | None = None,
        callbacks: list[DataCallback] | None = None,
    ):
        self.true_alpha = true_alpha
        self.n_measurements = n_measurements
        self.noise_std = noise_std
        self.grid_size = grid_size
        super().__init__(hp, validation, callbacks)

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate analytic solution on a 2D meshgrid for training + prediction."""
        xs = torch.linspace(0, 1, self.grid_size)
        ts = torch.linspace(0, 1, self.grid_size)
        grid_x, grid_t = torch.meshgrid(xs, ts, indexing="ij")

        x_grid = torch.stack([grid_x.reshape(-1), grid_t.reshape(-1)], dim=1)  # (N, 2)

        u_analytic = torch.exp(-self.true_alpha * math.pi**2 * x_grid[:, 1]) * torch.sin(
            math.pi * x_grid[:, 0]
        )

        # Add measurement noise
        u_noisy = u_analytic + self.noise_std * torch.randn_like(u_analytic)

        # Shape: (N, 1, 1) to match codebase convention
        y_data = u_noisy.unsqueeze(-1).unsqueeze(1)

        return x_grid, y_data

grid_size = grid_size instance-attribute

n_measurements = n_measurements instance-attribute

noise_std = noise_std instance-attribute

true_alpha = true_alpha instance-attribute

__init__(hp: PINNHyperparameters, true_alpha: float = TRUE_ALPHA, n_measurements: int = 200, noise_std: float = 0.01, grid_size: int = 50, validation: ValidationRegistry | None = None, callbacks: list[DataCallback] | None = None)

Source code in src/anypinn/catalog/heat_1d.py
def __init__(
    self,
    hp: PINNHyperparameters,
    true_alpha: float = TRUE_ALPHA,
    n_measurements: int = 200,
    noise_std: float = 0.01,
    grid_size: int = 50,
    validation: ValidationRegistry | None = None,
    callbacks: list[DataCallback] | None = None,
):
    self.true_alpha = true_alpha
    self.n_measurements = n_measurements
    self.noise_std = noise_std
    self.grid_size = grid_size
    super().__init__(hp, validation, callbacks)

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate analytic solution on a 2D meshgrid for training + prediction.

Source code in src/anypinn/catalog/heat_1d.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate analytic solution on a 2D meshgrid for training + prediction."""
    xs = torch.linspace(0, 1, self.grid_size)
    ts = torch.linspace(0, 1, self.grid_size)
    grid_x, grid_t = torch.meshgrid(xs, ts, indexing="ij")

    x_grid = torch.stack([grid_x.reshape(-1), grid_t.reshape(-1)], dim=1)  # (N, 2)

    u_analytic = torch.exp(-self.true_alpha * math.pi**2 * x_grid[:, 1]) * torch.sin(
        math.pi * x_grid[:, 0]
    )

    # Add measurement noise
    u_noisy = u_analytic + self.noise_std * torch.randn_like(u_analytic)

    # Shape: (N, 1, 1) to match codebase convention
    y_data = u_noisy.unsqueeze(-1).unsqueeze(1)

    return x_grid, y_data

InverseDiffusivityDataModule

Bases: PINNDataModule

DataModule for 1D inverse diffusivity problem.

gen_data produces ground-truth u(x,t) via scipy method-of-lines (central differences with variable D(x), integrated with solve_ivp), with optional measurement noise.

Source code in src/anypinn/catalog/inverse_diffusivity.py
class InverseDiffusivityDataModule(PINNDataModule):
    """DataModule for 1D inverse diffusivity problem.

    gen_data produces ground-truth u(x,t) via scipy method-of-lines
    (central differences with variable D(x), integrated with solve_ivp),
    with optional measurement noise.
    """

    def __init__(
        self,
        hp: PINNHyperparameters,
        n_x: int = 80,
        n_t: int = 80,
        noise_std: float = 0.01,
        grid_size: int = 50,
        validation: ValidationRegistry | None = None,
        callbacks: list[DataCallback] | None = None,
    ):
        self.n_x = n_x
        self.n_t = n_t
        self.noise_std = noise_std
        self.grid_size = grid_size
        super().__init__(hp, validation, callbacks)

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate numerical solution on a 2D meshgrid via method of lines."""
        import numpy as np
        from scipy.integrate import solve_ivp
        from scipy.interpolate import RegularGridInterpolator

        n_x = self.n_x
        dx = 1.0 / (n_x - 1)
        x_fd = np.linspace(0, 1, n_x)

        # True D(x) at grid points
        d_vals = 0.1 + 0.05 * np.sin(2 * np.pi * x_fd)

        # IC: u(x,0) = sin(pi*x)
        u0 = np.sin(np.pi * x_fd)
        # Enforce Dirichlet BCs
        u0[0] = 0.0
        u0[-1] = 0.0

        def rhs(_t: float, u: np.ndarray) -> np.ndarray:
            du_dt = np.zeros_like(u)
            # Interior points: d/dx(D(x) du/dx) via central differences
            for i in range(1, n_x - 1):
                # D at half-points
                d_right = 0.5 * (d_vals[i] + d_vals[i + 1])
                d_left = 0.5 * (d_vals[i - 1] + d_vals[i])
                du_dt[i] = (d_right * (u[i + 1] - u[i]) - d_left * (u[i] - u[i - 1])) / dx**2
            # BCs: u(0,t) = u(1,t) = 0 => du_dt = 0 at boundaries
            return du_dt

        t_span = (0.0, 1.0)
        t_eval = np.linspace(0, 1, self.n_t)

        sol = solve_ivp(rhs, t_span, u0, t_eval=t_eval, method="Radau", max_step=0.05)

        # sol.y shape: (n_x, n_t_actual)
        u_sol = sol.y  # (n_x, n_t)

        # Build interpolator
        interp_u = RegularGridInterpolator(
            (x_fd, sol.t),
            u_sol,
            method="linear",
            bounds_error=False,
            fill_value=None,
        )

        # Output measurement grid
        xs = torch.linspace(0, 1, self.grid_size)
        ts = torch.linspace(0, 1, self.grid_size)
        grid_x, grid_t = torch.meshgrid(xs, ts, indexing="ij")
        x_grid = torch.stack([grid_x.reshape(-1), grid_t.reshape(-1)], dim=1)  # (N, 2)

        pts = x_grid.numpy()
        u_ref = torch.tensor(interp_u(pts), dtype=torch.float32)

        # Add measurement noise
        u_noisy = u_ref + self.noise_std * torch.randn_like(u_ref)

        # Shape: (N, 1, 1) to match codebase convention
        y_data = u_noisy.unsqueeze(-1).unsqueeze(1)

        return x_grid, y_data

grid_size = grid_size instance-attribute

n_t = n_t instance-attribute

n_x = n_x instance-attribute

noise_std = noise_std instance-attribute

__init__(hp: PINNHyperparameters, n_x: int = 80, n_t: int = 80, noise_std: float = 0.01, grid_size: int = 50, validation: ValidationRegistry | None = None, callbacks: list[DataCallback] | None = None)

Source code in src/anypinn/catalog/inverse_diffusivity.py
def __init__(
    self,
    hp: PINNHyperparameters,
    n_x: int = 80,
    n_t: int = 80,
    noise_std: float = 0.01,
    grid_size: int = 50,
    validation: ValidationRegistry | None = None,
    callbacks: list[DataCallback] | None = None,
):
    self.n_x = n_x
    self.n_t = n_t
    self.noise_std = noise_std
    self.grid_size = grid_size
    super().__init__(hp, validation, callbacks)

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate numerical solution on a 2D meshgrid via method of lines.

Source code in src/anypinn/catalog/inverse_diffusivity.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate numerical solution on a 2D meshgrid via method of lines."""
    import numpy as np
    from scipy.integrate import solve_ivp
    from scipy.interpolate import RegularGridInterpolator

    n_x = self.n_x
    dx = 1.0 / (n_x - 1)
    x_fd = np.linspace(0, 1, n_x)

    # True D(x) at grid points
    d_vals = 0.1 + 0.05 * np.sin(2 * np.pi * x_fd)

    # IC: u(x,0) = sin(pi*x)
    u0 = np.sin(np.pi * x_fd)
    # Enforce Dirichlet BCs
    u0[0] = 0.0
    u0[-1] = 0.0

    def rhs(_t: float, u: np.ndarray) -> np.ndarray:
        du_dt = np.zeros_like(u)
        # Interior points: d/dx(D(x) du/dx) via central differences
        for i in range(1, n_x - 1):
            # D at half-points
            d_right = 0.5 * (d_vals[i] + d_vals[i + 1])
            d_left = 0.5 * (d_vals[i - 1] + d_vals[i])
            du_dt[i] = (d_right * (u[i + 1] - u[i]) - d_left * (u[i] - u[i - 1])) / dx**2
        # BCs: u(0,t) = u(1,t) = 0 => du_dt = 0 at boundaries
        return du_dt

    t_span = (0.0, 1.0)
    t_eval = np.linspace(0, 1, self.n_t)

    sol = solve_ivp(rhs, t_span, u0, t_eval=t_eval, method="Radau", max_step=0.05)

    # sol.y shape: (n_x, n_t_actual)
    u_sol = sol.y  # (n_x, n_t)

    # Build interpolator
    interp_u = RegularGridInterpolator(
        (x_fd, sol.t),
        u_sol,
        method="linear",
        bounds_error=False,
        fill_value=None,
    )

    # Output measurement grid
    xs = torch.linspace(0, 1, self.grid_size)
    ts = torch.linspace(0, 1, self.grid_size)
    grid_x, grid_t = torch.meshgrid(xs, ts, indexing="ij")
    x_grid = torch.stack([grid_x.reshape(-1), grid_t.reshape(-1)], dim=1)  # (N, 2)

    pts = x_grid.numpy()
    u_ref = torch.tensor(interp_u(pts), dtype=torch.float32)

    # Add measurement noise
    u_noisy = u_ref + self.noise_std * torch.randn_like(u_ref)

    # Shape: (N, 1, 1) to match codebase convention
    y_data = u_noisy.unsqueeze(-1).unsqueeze(1)

    return x_grid, y_data

LorenzDataModule

Bases: PINNDataModule

DataModule for Lorenz system inverse problem. Generates synthetic data via odeint.

Source code in src/anypinn/catalog/lorenz.py
class LorenzDataModule(PINNDataModule):
    """DataModule for Lorenz system inverse problem. Generates synthetic data via odeint."""

    def __init__(
        self,
        hp: ODEHyperparameters,
        gen_props: ODEProperties,
        noise_std: float = 0.0,
        validation: ValidationRegistry | None = None,
        callbacks: Sequence[DataCallback] | None = None,
    ):
        super().__init__(hp, validation, callbacks)
        self.gen_props = gen_props
        self.noise_std = noise_std

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate synthetic Lorenz data using odeint + additive Gaussian noise."""

        def lorenz_ode(t: Tensor, y: Tensor) -> Tensor:
            return self.gen_props.ode(t, y, self.gen_props.args)

        t = config.x

        sol = odeint(lorenz_ode, self.gen_props.y0, t)  # [T, 3]
        x_true = sol[:, 0]
        y_true = sol[:, 1]
        z_true = sol[:, 2]

        x_obs = x_true + self.noise_std * torch.randn_like(x_true)
        y_obs = y_true + self.noise_std * torch.randn_like(y_true)
        z_obs = z_true + self.noise_std * torch.randn_like(z_true)

        y_data = torch.stack([x_obs, y_obs, z_obs], dim=1).unsqueeze(-1)

        return t.unsqueeze(-1), y_data

gen_props = gen_props instance-attribute

noise_std = noise_std instance-attribute

__init__(hp: ODEHyperparameters, gen_props: ODEProperties, noise_std: float = 0.0, validation: ValidationRegistry | None = None, callbacks: Sequence[DataCallback] | None = None)

Source code in src/anypinn/catalog/lorenz.py
def __init__(
    self,
    hp: ODEHyperparameters,
    gen_props: ODEProperties,
    noise_std: float = 0.0,
    validation: ValidationRegistry | None = None,
    callbacks: Sequence[DataCallback] | None = None,
):
    super().__init__(hp, validation, callbacks)
    self.gen_props = gen_props
    self.noise_std = noise_std

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate synthetic Lorenz data using odeint + additive Gaussian noise.

Source code in src/anypinn/catalog/lorenz.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate synthetic Lorenz data using odeint + additive Gaussian noise."""

    def lorenz_ode(t: Tensor, y: Tensor) -> Tensor:
        return self.gen_props.ode(t, y, self.gen_props.args)

    t = config.x

    sol = odeint(lorenz_ode, self.gen_props.y0, t)  # [T, 3]
    x_true = sol[:, 0]
    y_true = sol[:, 1]
    z_true = sol[:, 2]

    x_obs = x_true + self.noise_std * torch.randn_like(x_true)
    y_obs = y_true + self.noise_std * torch.randn_like(y_true)
    z_obs = z_true + self.noise_std * torch.randn_like(z_true)

    y_data = torch.stack([x_obs, y_obs, z_obs], dim=1).unsqueeze(-1)

    return t.unsqueeze(-1), y_data

LotkaVolterraDataModule

Bases: PINNDataModule

DataModule for Lotka-Volterra inverse problem. Generates synthetic data via odeint.

Source code in src/anypinn/catalog/lotka_volterra.py
class LotkaVolterraDataModule(PINNDataModule):
    """DataModule for Lotka-Volterra inverse problem. Generates synthetic data via odeint."""

    def __init__(
        self,
        hp: ODEHyperparameters,
        gen_props: ODEProperties,
        noise_frac: float = 0.0,
        validation: ValidationRegistry | None = None,
        callbacks: Sequence[DataCallback] | None = None,
    ):
        super().__init__(hp, validation, callbacks)
        self.gen_props = gen_props
        self.noise_frac = noise_frac

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate synthetic Lotka-Volterra data using odeint + Gaussian noise."""

        def lv_ode(t: Tensor, y: Tensor) -> Tensor:
            return self.gen_props.ode(t, y, self.gen_props.args)

        t = config.x

        sol = odeint(lv_ode, self.gen_props.y0, t)  # [T, 2]
        x_true = sol[:, 0].clamp_min(0.0)
        y_true = sol[:, 1].clamp_min(0.0)

        x_obs = x_true + self.noise_frac * x_true.abs().mean() * torch.randn_like(x_true)
        y_obs = y_true + self.noise_frac * y_true.abs().mean() * torch.randn_like(y_true)
        x_obs = x_obs.clamp_min(0.0)
        y_obs = y_obs.clamp_min(0.0)

        y_data = torch.stack([x_obs, y_obs], dim=1).unsqueeze(-1)

        return t.unsqueeze(-1), y_data

gen_props = gen_props instance-attribute

noise_frac = noise_frac instance-attribute

__init__(hp: ODEHyperparameters, gen_props: ODEProperties, noise_frac: float = 0.0, validation: ValidationRegistry | None = None, callbacks: Sequence[DataCallback] | None = None)

Source code in src/anypinn/catalog/lotka_volterra.py
def __init__(
    self,
    hp: ODEHyperparameters,
    gen_props: ODEProperties,
    noise_frac: float = 0.0,
    validation: ValidationRegistry | None = None,
    callbacks: Sequence[DataCallback] | None = None,
):
    super().__init__(hp, validation, callbacks)
    self.gen_props = gen_props
    self.noise_frac = noise_frac

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate synthetic Lotka-Volterra data using odeint + Gaussian noise.

Source code in src/anypinn/catalog/lotka_volterra.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate synthetic Lotka-Volterra data using odeint + Gaussian noise."""

    def lv_ode(t: Tensor, y: Tensor) -> Tensor:
        return self.gen_props.ode(t, y, self.gen_props.args)

    t = config.x

    sol = odeint(lv_ode, self.gen_props.y0, t)  # [T, 2]
    x_true = sol[:, 0].clamp_min(0.0)
    y_true = sol[:, 1].clamp_min(0.0)

    x_obs = x_true + self.noise_frac * x_true.abs().mean() * torch.randn_like(x_true)
    y_obs = y_true + self.noise_frac * y_true.abs().mean() * torch.randn_like(y_true)
    x_obs = x_obs.clamp_min(0.0)
    y_obs = y_obs.clamp_min(0.0)

    y_data = torch.stack([x_obs, y_obs], dim=1).unsqueeze(-1)

    return t.unsqueeze(-1), y_data

Poisson2DDataModule

Bases: PINNDataModule

DataModule for 2D Poisson equation on [0,1]^2.

Generates a meshgrid with the analytic solution u(x,y) = sin(pix)sin(pi*y). The data is used for prediction/validation only -- training uses PDEResidualConstraint + DirichletBCConstraints (no DataConstraint).

Source code in src/anypinn/catalog/poisson_2d.py
class Poisson2DDataModule(PINNDataModule):
    """DataModule for 2D Poisson equation on [0,1]^2.

    Generates a meshgrid with the analytic solution u(x,y) = sin(pi*x)*sin(pi*y).
    The data is used for prediction/validation only -- training uses
    PDEResidualConstraint + DirichletBCConstraints (no DataConstraint).
    """

    def __init__(
        self,
        hp: PINNHyperparameters,
        grid_size: int = 30,
        validation: ValidationRegistry | None = None,
        callbacks: list[DataCallback] | None = None,
    ):
        self.grid_size = grid_size
        super().__init__(hp, validation, callbacks)

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate analytic solution on a 2D meshgrid for prediction."""
        xs = torch.linspace(0, 1, self.grid_size)
        ys = torch.linspace(0, 1, self.grid_size)
        grid_x, grid_y = torch.meshgrid(xs, ys, indexing="ij")

        x_grid = torch.stack([grid_x.reshape(-1), grid_y.reshape(-1)], dim=1)  # (N, 2)

        u_analytic = torch.sin(math.pi * x_grid[:, 0]) * torch.sin(math.pi * x_grid[:, 1])

        # Shape: (N, 1, 1) to match codebase convention
        y_data = u_analytic.unsqueeze(-1).unsqueeze(1)

        return x_grid, y_data

grid_size = grid_size instance-attribute

__init__(hp: PINNHyperparameters, grid_size: int = 30, validation: ValidationRegistry | None = None, callbacks: list[DataCallback] | None = None)

Source code in src/anypinn/catalog/poisson_2d.py
def __init__(
    self,
    hp: PINNHyperparameters,
    grid_size: int = 30,
    validation: ValidationRegistry | None = None,
    callbacks: list[DataCallback] | None = None,
):
    self.grid_size = grid_size
    super().__init__(hp, validation, callbacks)

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate analytic solution on a 2D meshgrid for prediction.

Source code in src/anypinn/catalog/poisson_2d.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate analytic solution on a 2D meshgrid for prediction."""
    xs = torch.linspace(0, 1, self.grid_size)
    ys = torch.linspace(0, 1, self.grid_size)
    grid_x, grid_y = torch.meshgrid(xs, ys, indexing="ij")

    x_grid = torch.stack([grid_x.reshape(-1), grid_y.reshape(-1)], dim=1)  # (N, 2)

    u_analytic = torch.sin(math.pi * x_grid[:, 0]) * torch.sin(math.pi * x_grid[:, 1])

    # Shape: (N, 1, 1) to match codebase convention
    y_data = u_analytic.unsqueeze(-1).unsqueeze(1)

    return x_grid, y_data

SEIRDataModule

Bases: PINNDataModule

DataModule for SEIR inverse problem. Generates synthetic data via odeint.

Source code in src/anypinn/catalog/seir.py
class SEIRDataModule(PINNDataModule):
    """DataModule for SEIR inverse problem. Generates synthetic data via odeint."""

    def __init__(
        self,
        hp: ODEHyperparameters,
        gen_props: ODEProperties,
        noise_std: float = 0.0,
        validation: ValidationRegistry | None = None,
        callbacks: Sequence[DataCallback] | None = None,
    ):
        super().__init__(hp, validation, callbacks)
        self.gen_props = gen_props
        self.noise_std = noise_std

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate synthetic SEIR data using odeint + Gaussian noise."""

        def seir_ode(t: Tensor, y: Tensor) -> Tensor:
            return self.gen_props.ode(t, y, self.gen_props.args)

        t = config.x

        sol = odeint(seir_ode, self.gen_props.y0, t)  # [T, 3]
        I_true = sol[:, 2].clamp_min(0.0)

        I_obs = I_true + self.noise_std * torch.randn_like(I_true)
        I_obs = I_obs.clamp_min(0.0)

        return t.unsqueeze(-1), I_obs.unsqueeze(-1).unsqueeze(1)

gen_props = gen_props instance-attribute

noise_std = noise_std instance-attribute

__init__(hp: ODEHyperparameters, gen_props: ODEProperties, noise_std: float = 0.0, validation: ValidationRegistry | None = None, callbacks: Sequence[DataCallback] | None = None)

Source code in src/anypinn/catalog/seir.py
def __init__(
    self,
    hp: ODEHyperparameters,
    gen_props: ODEProperties,
    noise_std: float = 0.0,
    validation: ValidationRegistry | None = None,
    callbacks: Sequence[DataCallback] | None = None,
):
    super().__init__(hp, validation, callbacks)
    self.gen_props = gen_props
    self.noise_std = noise_std

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate synthetic SEIR data using odeint + Gaussian noise.

Source code in src/anypinn/catalog/seir.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate synthetic SEIR data using odeint + Gaussian noise."""

    def seir_ode(t: Tensor, y: Tensor) -> Tensor:
        return self.gen_props.ode(t, y, self.gen_props.args)

    t = config.x

    sol = odeint(seir_ode, self.gen_props.y0, t)  # [T, 3]
    I_true = sol[:, 2].clamp_min(0.0)

    I_obs = I_true + self.noise_std * torch.randn_like(I_true)
    I_obs = I_obs.clamp_min(0.0)

    return t.unsqueeze(-1), I_obs.unsqueeze(-1).unsqueeze(1)

SIRInvDataModule

Bases: PINNDataModule

DataModule for SIR Inverse problem.

Source code in src/anypinn/catalog/sir.py
class SIRInvDataModule(PINNDataModule):
    """
    DataModule for SIR Inverse problem.
    """

    def __init__(
        self,
        hp: ODEHyperparameters,
        gen_props: ODEProperties | None = None,
        validation: ValidationRegistry | None = None,
        callbacks: Sequence[DataCallback] | None = None,
    ):
        super().__init__(hp, validation, callbacks)
        self.gen_props = gen_props

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate synthetic data."""
        assert self.gen_props is not None, "SIR properties are required to generate data"
        gen_props = self.gen_props

        args = gen_props.args.copy()
        args.update(config.args_to_train)

        data = odeint(
            lambda x, y: gen_props.ode(x, y, args),
            gen_props.y0,
            config.x,
        )

        I_true = data[:, 1].clamp_min(0.0)

        I_obs = self._noise(I_true, config.noise_level)

        return config.x.unsqueeze(-1), I_obs.unsqueeze(-1).unsqueeze(1)

    def _noise(self, I_true: Tensor, noise_level: float) -> Tensor:
        if noise_level < 1.0:
            return I_true
        else:
            return torch.poisson(I_true / noise_level) * noise_level

gen_props = gen_props instance-attribute

__init__(hp: ODEHyperparameters, gen_props: ODEProperties | None = None, validation: ValidationRegistry | None = None, callbacks: Sequence[DataCallback] | None = None)

Source code in src/anypinn/catalog/sir.py
def __init__(
    self,
    hp: ODEHyperparameters,
    gen_props: ODEProperties | None = None,
    validation: ValidationRegistry | None = None,
    callbacks: Sequence[DataCallback] | None = None,
):
    super().__init__(hp, validation, callbacks)
    self.gen_props = gen_props

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate synthetic data.

Source code in src/anypinn/catalog/sir.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate synthetic data."""
    assert self.gen_props is not None, "SIR properties are required to generate data"
    gen_props = self.gen_props

    args = gen_props.args.copy()
    args.update(config.args_to_train)

    data = odeint(
        lambda x, y: gen_props.ode(x, y, args),
        gen_props.y0,
        config.x,
    )

    I_true = data[:, 1].clamp_min(0.0)

    I_obs = self._noise(I_true, config.noise_level)

    return config.x.unsqueeze(-1), I_obs.unsqueeze(-1).unsqueeze(1)

VanDerPolDataModule

Bases: PINNDataModule

DataModule for Van der Pol oscillator inverse problem.

Generates synthetic data via odeint.

Source code in src/anypinn/catalog/van_der_pol.py
class VanDerPolDataModule(PINNDataModule):
    """DataModule for Van der Pol oscillator inverse problem.

    Generates synthetic data via odeint.
    """

    def __init__(
        self,
        hp: ODEHyperparameters,
        gen_props: ODEProperties,
        noise_std: float = 0.0,
        validation: ValidationRegistry | None = None,
        callbacks: Sequence[DataCallback] | None = None,
    ):
        super().__init__(hp, validation, callbacks)
        self.gen_props = gen_props
        self.noise_std = noise_std

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate synthetic Van der Pol data using odeint + Gaussian noise."""

        def vdp_ode(t: Tensor, y: Tensor) -> Tensor:
            return self.gen_props.ode(t, y, self.gen_props.args)

        t = config.x

        sol = odeint(vdp_ode, self.gen_props.y0, t)  # [T, 2]
        u_true = sol[:, 0]

        u_obs = u_true + self.noise_std * torch.randn_like(u_true)

        return t.unsqueeze(-1), u_obs.unsqueeze(-1).unsqueeze(1)

gen_props = gen_props instance-attribute

noise_std = noise_std instance-attribute

__init__(hp: ODEHyperparameters, gen_props: ODEProperties, noise_std: float = 0.0, validation: ValidationRegistry | None = None, callbacks: Sequence[DataCallback] | None = None)

Source code in src/anypinn/catalog/van_der_pol.py
def __init__(
    self,
    hp: ODEHyperparameters,
    gen_props: ODEProperties,
    noise_std: float = 0.0,
    validation: ValidationRegistry | None = None,
    callbacks: Sequence[DataCallback] | None = None,
):
    super().__init__(hp, validation, callbacks)
    self.gen_props = gen_props
    self.noise_std = noise_std

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate synthetic Van der Pol data using odeint + Gaussian noise.

Source code in src/anypinn/catalog/van_der_pol.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate synthetic Van der Pol data using odeint + Gaussian noise."""

    def vdp_ode(t: Tensor, y: Tensor) -> Tensor:
        return self.gen_props.ode(t, y, self.gen_props.args)

    t = config.x

    sol = odeint(vdp_ode, self.gen_props.y0, t)  # [T, 2]
    u_true = sol[:, 0]

    u_obs = u_true + self.noise_std * torch.randn_like(u_true)

    return t.unsqueeze(-1), u_obs.unsqueeze(-1).unsqueeze(1)

Wave1DDataModule

Bases: PINNDataModule

DataModule for 1D wave equation inverse problem.

gen_data produces sparse interior measurements from the analytic solution u(x,t) = sin(pi x) cos(c pi t), with optional noise. These measurements are used by DataConstraint during training to recover c.

Source code in src/anypinn/catalog/wave_1d.py
class Wave1DDataModule(PINNDataModule):
    """DataModule for 1D wave equation inverse problem.

    gen_data produces sparse interior measurements from the analytic
    solution u(x,t) = sin(pi x) cos(c pi t), with optional noise.
    These measurements are used by DataConstraint during training
    to recover c.
    """

    def __init__(
        self,
        hp: PINNHyperparameters,
        true_c: float = TRUE_C,
        n_measurements: int = 200,
        noise_std: float = 0.01,
        grid_size: int = 50,
        validation: ValidationRegistry | None = None,
        callbacks: list[DataCallback] | None = None,
    ):
        self.true_c = true_c
        self.n_measurements = n_measurements
        self.noise_std = noise_std
        self.grid_size = grid_size
        super().__init__(hp, validation, callbacks)

    @override
    def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
        """Generate analytic solution on a 2D meshgrid for training + prediction."""
        xs = torch.linspace(0, 1, self.grid_size)
        ts = torch.linspace(0, 1, self.grid_size)
        grid_x, grid_t = torch.meshgrid(xs, ts, indexing="ij")

        x_grid = torch.stack([grid_x.reshape(-1), grid_t.reshape(-1)], dim=1)  # (N, 2)

        u_analytic = torch.sin(math.pi * x_grid[:, 0]) * torch.cos(
            self.true_c * math.pi * x_grid[:, 1]
        )

        # Add measurement noise
        u_noisy = u_analytic + self.noise_std * torch.randn_like(u_analytic)

        # Shape: (N, 1, 1) to match codebase convention
        y_data = u_noisy.unsqueeze(-1).unsqueeze(1)

        return x_grid, y_data

grid_size = grid_size instance-attribute

n_measurements = n_measurements instance-attribute

noise_std = noise_std instance-attribute

true_c = true_c instance-attribute

__init__(hp: PINNHyperparameters, true_c: float = TRUE_C, n_measurements: int = 200, noise_std: float = 0.01, grid_size: int = 50, validation: ValidationRegistry | None = None, callbacks: list[DataCallback] | None = None)

Source code in src/anypinn/catalog/wave_1d.py
def __init__(
    self,
    hp: PINNHyperparameters,
    true_c: float = TRUE_C,
    n_measurements: int = 200,
    noise_std: float = 0.01,
    grid_size: int = 50,
    validation: ValidationRegistry | None = None,
    callbacks: list[DataCallback] | None = None,
):
    self.true_c = true_c
    self.n_measurements = n_measurements
    self.noise_std = noise_std
    self.grid_size = grid_size
    super().__init__(hp, validation, callbacks)

gen_data(config: GenerationConfig) -> tuple[Tensor, Tensor]

Generate analytic solution on a 2D meshgrid for training + prediction.

Source code in src/anypinn/catalog/wave_1d.py
@override
def gen_data(self, config: GenerationConfig) -> tuple[Tensor, Tensor]:
    """Generate analytic solution on a 2D meshgrid for training + prediction."""
    xs = torch.linspace(0, 1, self.grid_size)
    ts = torch.linspace(0, 1, self.grid_size)
    grid_x, grid_t = torch.meshgrid(xs, ts, indexing="ij")

    x_grid = torch.stack([grid_x.reshape(-1), grid_t.reshape(-1)], dim=1)  # (N, 2)

    u_analytic = torch.sin(math.pi * x_grid[:, 0]) * torch.cos(
        self.true_c * math.pi * x_grid[:, 1]
    )

    # Add measurement noise
    u_noisy = u_analytic + self.noise_std * torch.randn_like(u_analytic)

    # Shape: (N, 1, 1) to match codebase convention
    y_data = u_noisy.unsqueeze(-1).unsqueeze(1)

    return x_grid, y_data

SIR(x: Tensor, y: Tensor, args: ArgsRegistry) -> Tensor

The SIR ODE system. $$ \begin{align} \frac{dS}{dt} &= -\beta \frac{SI}{N} \ \frac{dI}{dt} &= \beta \frac{SI}{N} - \delta I \ \frac{dR}{dt} &= \delta I \ \end{align} $$

Parameters:

Name Type Description Default
x Tensor

Time variable.

required
y Tensor

State variables [S, I].

required
args ArgsRegistry

Arguments dictionary (beta, delta, N).

required

Returns:

Type Description
Tensor

Derivatives [dS/dt, dI/dt].

Source code in src/anypinn/catalog/sir.py
def SIR(x: Tensor, y: Tensor, args: ArgsRegistry) -> Tensor:
    """
    The SIR ODE system.
    $$
    \\begin{align}
    \\frac{dS}{dt} &= -\\beta \\frac{SI}{N} \\\\
    \\frac{dI}{dt} &= \\beta \\frac{SI}{N} - \\delta I \\\\
    \\frac{dR}{dt} &= \\delta I \\\\
    \\end{align}
    $$

    Args:
        x: Time variable.
        y: State variables [S, I].
        args: Arguments dictionary (beta, delta, N).

    Returns:
        Derivatives [dS/dt, dI/dt].
    """
    S, I = y
    b, d, N = args[BETA_KEY], args[DELTA_KEY], args[N_KEY]

    dS = -b(x) * S * I / N(x)
    dI = b(x) * S * I / N(x) - d(x) * I
    return torch.stack([dS, dI])

rSIR(x: Tensor, y: Tensor, args: ArgsRegistry) -> Tensor

The reduced SIR ODE system. $$ \begin{align} \frac{dI}{dt} &= \delta (R_t - 1) I \end{align} $$

Parameters:

Name Type Description Default
x Tensor

Time variable.

required
y Tensor

State variables [I].

required
args ArgsRegistry

Arguments dictionary (delta, Rt).

required

Returns:

Type Description
Tensor

Derivatives [dI/dt].

Source code in src/anypinn/catalog/sir.py
def rSIR(x: Tensor, y: Tensor, args: ArgsRegistry) -> Tensor:
    """
    The reduced SIR ODE system.
    $$
    \\begin{align}
    \\frac{dI}{dt} &= \\delta (R_t - 1) I
    \\end{align}
    $$

    Args:
        x: Time variable.
        y: State variables [I].
        args: Arguments dictionary (delta, Rt).

    Returns:
        Derivatives [dI/dt].
    """
    I = y
    d, Rt = args[DELTA_KEY], args[Rt_KEY]

    dI = d(x) * (Rt(x) - 1) * I
    return dI