Orchestrating with SimulationConfig

SimulationConfig is the central orchestration structure of PDEStudioCore.jl. Instead of writing nested loops over parameters, numerical methods and resolutions, you describe the batch once. The backend expands it into the exact Cartesian product of your parameter space, hashes every parameter set and runs only what is not on disk yet.


Step 1: Setting the Target Module

PDEStudioCore is a serialised, headless backend: functions are referenced by name (a Symbol or String), never as closures, so that every run is fully described by plain data. First, tell the backend in which module these names are looked up (usually Main or your own module):

using PDEStudioCore
using StaticArrays

PDEStudioCore.set_target_module!(@__MODULE__)
set_save_path!(joinpath(@__DIR__, "results"))   # data goes to results/data/

Step 2: Defining the Solver

The solver receives one argument, the ParamDict of a run, and returns a dataset built with create_sim_data (or NoSimData() if it has nothing to store). It receives a copy of the parameters; the result is always stored under the parameters of the task.

function my_wave_solver(params::ParamDict)
    N, cfl, scheme = params[:N], params[:cfl], params[:scheme]
    x = collect(range(0.0, 1.0, length = N))
    t = collect(range(0.0, params[:T], length = 50))

    u = zeros(N, length(t))   # ... run your numerical scheme here ...

    return create_sim_data(x, u, t, params)   # numeric arrays are scalar fields (M = 1)
end

Step 3: The Shared Parameters

The first layer of the hierarchy holds the values that every run shares. The solver is selected with the :sim_func_name parameter; since it is a parameter, it is part of the hash, and methods may override it to use different solvers.

shared = Dict(
    :sim_func_name => :my_wave_solver,
    :L => 1.0,     # domain length
    :T => 5.0,     # final time
    :N => 100,     # default resolution (swept below)
)

Step 4: Method Overrides

The second layer maps method names to parameter overrides; they replace matching shared keys. Use :ignore => [:key1, :key2] to drop shared keys for a single method.

methods = Dict(
    :upwind       => Dict(:scheme => "Upwind",       :cfl => 0.5),
    :lax_wendroff => Dict(:scheme => "Lax-Wendroff", :cfl => 0.8),
)

Step 5: The Sweep

varied lists the values to sweep. A varied key is applied to every method whose parameters contain that key (here: through shared).

varied = Dict(:N => [50, 100, 200])

Step 6: Assembly and Execution

config = SimulationConfig(shared, methods, [:upwind, :lax_wendroff]; varied = varied)

config                    # prints a summary of the configuration
generate_tasks(config)    # the 6 parameter sets (2 methods × 3 resolutions)

run_all_simulations(config)                               # runs what is missing
results = [load_sim_data(p) for p in generate_tasks(config)]

The third positional argument (defaults, the methods to run) is optional and defaults to all methods of methods. Without any methods, the shared parameters are run on their own: SimulationConfig(shared; varied = varied).


Reference: Fields and Arguments

FieldConstructor argumentMeaning
shared1st positionalBaseline parameters shared by all runs.
methods2nd positionalPer-method overrides (MethodDict).
defaults3rd positionalMethods executed by run_all_simulations (default: all).
variedkeyword variedThe sweep (VariedDict).
referencekeyword referenceName of the reference factory, see Advanced Config.
postprocesskeyword postprocessName of the post-processing function.
sourceskeyword sourcesFiles included into the target module before running.

All dictionaries may use String or Symbol keys, and assigning a field (e.g. config.reference = "exact_factory") accepts the same loose input as the constructor.

Unknown keywords and field names raise an error.