PDEStudioCore.jl is a headless Julia backend for running, storing and analysing PDE simulations. You write the solver; PDEStudioCore runs it over parameter sweeps, caches every result on disk under a hash of its parameters, and computes statistics (norms, errors against a reference, conservation) for you.
It is the engine behind the interactive frontend PDEStudio.jl, but works on its own, for example on a cluster without a display.
- One data type for any dimension: Eulerian grids (
ESimData) and Lagrangian particles (LSimData) in 1D, 2D, 3D or more, with or without time, in any floating point precision. Conversions between both representations are built in and cached. - Declarative sweeps: shared parameters, per-method overrides and swept values (
SimulationConfig) expand into the Cartesian product of runs; duplicate runs are removed. - Reproducible caching: results are stored as JLD2 files named by a deterministic SHA-256 hash of the parameters, written atomically. Re-running a configuration only computes what is missing.
- Statistics pipeline: register a statistic once (
register_stat!) with the dimensions it keeps, implementcalc_stat, and it is evaluated (multithreaded) for every dataset, including errors against an analytical reference.data.stats[:Solution]always is the solution. - Safe serialization: parameters are written and read with a literal parser (
val2str/str2val) that never evaluates code.
pkg> add PDEStudioCoreusing PDEStudioCore
using StaticArrays
# 1. A solver: the resolved parameters of one run in, a dataset out
function advection_1d(params::ParamDict)
N, c, T, cfl = params[:N], params[:c], params[:T], params[:cfl]
x = collect(range(0.0, 1.0, length = N + 1))[1:N] # periodic grid
dt = cfl * (x[2] - x[1]) / c
t = collect(0.0:dt:T)
u = zeros(SVector{1, Float64}, N, length(t))
u[:, 1] .= SVector.(sin.(2π .* x))
for n in 1:length(t) - 1, i in 1:N
u_l, u_c, u_r = u[mod1(i - 1, N), n][1], u[i, n][1], u[mod1(i + 1, N), n][1]
u[i, n + 1] = SVector(params[:scheme] == "upwind" ? u_c - cfl * (u_c - u_l) :
0.5 * (u_r + u_l) - 0.5 * cfl * (u_r - u_l))
end
return create_sim_data(x, u, t, params) # dims (:x, :t)
end
# 2. An exact solution (optional): a factory params -> ((x, t) -> u), enables error statistics
exact_advection(params) = xt -> SVector(sin(2π * (xt[1] - params[:c] * xt[2])))
# 3. The configuration
shared = create_param_dict(:N => 100, :c => 1.0, :T => 2.0, :cfl => 0.5, :sim_func_name => :advection_1d)
methods = create_method_dict(:upwind => create_param_dict(:scheme => "upwind"),
:lax_friedrichs => create_param_dict(:scheme => "lax_friedrichs"))
config = SimulationConfig(shared, methods, [:upwind, :lax_friedrichs]; # active methods (`defaults`)
varied = create_varied_dict(:cfl => [0.3, 0.5, 0.8]),
reference = :exact_advection)
# 4. Run: 2 methods × 3 CFL numbers = 6 runs, with statistics
set_stat_preset!("hyperbolic") # mass, L1/L2 norms and errors, wave position, …
set_save_path!("results")
run_all_simulations(config; parallel = true, calculate_stats = true)
# 5. Use the results
p = assemble_params(config.shared, config.methods, :upwind) # parameters of one run…
p[:cfl] = 0.5
sim = load_sim_data(p, :raw)
sim.stats[:l2error] # one value per time stepMore complete scripts are in examples/: advection_1d.jl and the minimal dummy.jl.
SimulationConfig field |
Meaning |
|---|---|
shared |
Parameters of every run, including :sim_func_name (the solver). |
methods |
Per-method overrides, e.g. the scheme. |
defaults |
The methods that are run. |
varied |
The sweep; a swept key must also exist in shared or a method. |
reference |
Name of a reference factory (exact solution) for error statistics. |
postprocess |
Name of a function data -> Bool deciding whether a result is saved. |
sources |
Files included before running (e.g. the file defining the solver). |
Functions are referenced by name and resolved in the module set with set_target_module! (default: Main).
The documentation covers the data structures, the SimulationConfig in depth, storage and conversions, custom statistics and the full API.
If you use PDEStudioCore.jl in your research, please cite it via its Zenodo DOI: 10.5281/zenodo.22893504.
Portions of this codebase and documentation were drafted with the assistance of large language models (LLMs). All code has been human-reviewed, verified, and tested. If you notice any inaccuracies or unexpected behavior, please open an issue.