Skip to main content

Crate legendre

Crate legendre 

Source
Expand description

§legendre — a PDE simulation framework

legendre solves systems of time-dependent partial differential equations — deterministic or stochastic, in any spatial dimension — on block-decomposed structured grids. Heat transport, reaction–diffusion, and phase-field solidification are models here, not the framework: they are all expressed against the same small trait surface.

The architecture follows four principles:

  1. Mathematical objects own no execution. A physics::model::Model cannot spawn threads, a geometry::grid::Grid cannot write files, a discretization::stencil::Stencil cannot allocate.
  2. Execution is scheduler-driven. Everything runs because the core::scheduler::Scheduler requests it. No trait in this crate mentions Rayon; only concrete schedulers do.
  3. Storage is separate from views. Fields are typed views into a core::storage::StorageBackend; allocation happens once, up front, through an core::storage::Allocator.
  4. Numerical methods are policies. A grid plus a discretization policy yields operator realizations (discretization::stencil::Stencils); models state what operators they need and never learn how they were realized.

The fundamental unit of computation is the block, not the grid. Even a uniform Cartesian grid is a collection of fixed-size blocks: this gives cache locality, natural parallel work units, localized halo exchange, and an execution model that is unchanged when adaptive refinement arrives.

§Ownership graph

Simulation
├── Scheduler        (how blocks are dispatched)
├── State            (Storage + field views, ghost-inclusive slabs)
├── Grid             (topology: blocks, indices, views)
├── Discretization   (policy: builds Stencils for Operators)
├── Model            (mathematics: rhs, never mutates state)
├── Integrator       (advances State using Model::rhs)
└── Observers        (async output; never block the solver)

§A complete model

The dimension-generic heat equation ∂u/∂t = κ∇²u with no-flux boundaries, wired to a grid, integrator, and scheduler:

use legendre::core::{
    scheduler::SerialScheduler,
    scratch::Scratch,
    simulation::Simulation,
    state::{BlockStateMut, FieldHandle, State, StateBuilder},
    storage::{StorageBackend, SystemAllocator},
};
use legendre::discretization::{
    finite_difference::FiniteDifference,
    operators::{Discretizes, Laplacian},
    stencil::Stencil,
};
use legendre::geometry::cartesian::{CartesianGrid, fill_ghosts_mirror, for_each_interior};
use legendre::integrators::ForwardEuler;
use legendre::physics::model::{Model, RhsContext};

struct Heat<const D: usize> {
    kappa: f64,
    u: Option<FieldHandle<f64>>,
}

impl<const D: usize, P> Model<CartesianGrid<D>, P> for Heat<D>
where
    P: Discretizes<CartesianGrid<D>, Laplacian>,
{
    type Scalar = f64;

    fn register_fields(&mut self, builder: &mut StateBuilder<f64>) {
        self.u = Some(builder.register("u", 1)); // name + ghost width
    }

    fn fill_ghosts<S: StorageBackend<f64>>(
        &self,
        grid: &CartesianGrid<D>,
        state: &mut State<f64, S>,
        _t: f64,
    ) {
        fill_ghosts_mirror(grid, state, self.u.unwrap());
    }

    fn rhs_block<S: StorageBackend<f64>>(
        &self,
        ctx: &RhsContext<'_, CartesianGrid<D>, P>,
        state: &State<f64, S>,
        out: &mut BlockStateMut<'_, f64, S>,
        _scratch: &mut Scratch<f64, S>,
    ) {
        let u = self.u.unwrap();
        let lap = ctx.disc.build(ctx.grid, Laplacian);
        let input = state.view(ctx.grid, ctx.block, u);
        let mut output = out.view_mut(ctx.grid, ctx.block, u);
        lap.apply(ctx.grid, ctx.block, input, &mut output);
        for_each_interior(input.interior(), |i| {
            output.set(i, output.get(i) * self.kappa);
        });
    }
}

let grid = CartesianGrid::new([32; 2], [16; 2], [0.0; 2], [0.1; 2])?;
let heat = Heat::<2> { kappa: 0.7, u: None };
let mut sim = Simulation::new(
    grid,
    FiniteDifference,
    heat,
    ForwardEuler,
    SerialScheduler,
    SystemAllocator,
);
for _ in 0..10 {
    sim.step(1e-3);
}

Swap core::scheduler::SerialScheduler for core::scheduler::RayonScheduler and the run parallelizes over blocks with bitwise-identical results; the examples/ directory adds the async observation pipeline (Parquet snapshots, live statistics) on top.

Modules§

core
Framework core: state, storage, scheduling, scratch, observation, and the simulation owner. Everything here is grid- and physics-agnostic.
discretization
Numerical schemes as policies.
geometry
Grids and topology: the Grid trait and concrete grid families. Quadtree/octree AMR grids land here later behind the same trait.
integrators
Time integration.
io
Output backends.
physics
Models: pure mathematics over grids and discretization policies. Phase-fields, diffusion, elasticity etc. are peer modules here.
util
Small shared utilities. Nothing here may depend on other framework modules.