Options
The simulate function accepts a SimOptions value that controls output paths, logging, the continuous-time solver, hooks, and the time label used in plots.
history = simulate(
user_data;
t = (0, 10),
init_fcn,
rates_fcn,
updates_fcn,
options = SimOptions(;
outdir = "out",
solver = Solvers.DormandPrince54Options(),
hooks = [
Hooks.ProgressBarOptions(),
],
log = Logs.BasicLogOptions(),
time_dimension = "Time" => "s",
),
)Every field has a default. Most simulations can begin with SimOptions() or omit the options keyword entirely.
SystemsOfSystems.SimOptions — Type
A container for the options supplied to simulate, with fields for:
outdir: A directory to save any outputs to (such asResources.OutputFile)log: Log options to use (e.g.,Logs.BasicLogOptions())solver: Solver to use (e.g.,Solvers.DormandPrince54Options())hooks: A vector of hooks (e.g.,[Hooks.ProgressBarOptions(),])time_dimension: ADimensionfor the time unit (e.g.,"time" => "s").
Solvers
The solver advances continuous states between event times. The default is the adaptive Dormand-Prince 5(4) solver.
DormandPrince54Options is appropriate for most simulations. initial_dt is its first proposed step, max_dt limits later proposals, and abs_tol and rel_tol control local error. Accepted steps will be shortened when necessary to stop exactly at a user-requested time, schedule occurrence, or model t_next.
options = SimOptions(;
solver = Solvers.DormandPrince54Options(;
initial_dt = 0.01,
max_dt = 0.1,
abs_tol = 1e-6,
rel_tol = 1e-5,
),
)When it's unnecessary for the solver to adapt the step, then an explicit Runge-Kutta solver like RungeKutta4Options is faster. Its dt is essentially the maximum time step it will take, but it can be cut short in order to step exactly to scheduled times, model-requested t_next values, etc.
options = SimOptions(;
solver = Solvers.RungeKutta4Options(;
dt = 1//100,
),
)The times passed as t to simulate are required sample times, not a general fixed-step setting. For example, t = 0:1:10 makes the solver stop at every whole second, but an adaptive solver can take as many smaller steps as necessary between those times.
SystemsOfSystems.Solvers.DormandPrince54Options — Type
DormandPrince54Options(; initial_dt, max_dt, abs_tol, rel_tol)A container for the embedded Dormand-Prince 5(4) solver options. The fifth-order solution advances the model, while the fourth-order solution estimates local error for adaptive step control.
SystemsOfSystems.Solvers.RungeKutta4Options — Type
RungeKutta4Options(; dt)A container for the classical fourth-order Runge-Kutta solver options, where dt is the fixed official step spacing. Scheduled user and model times remain hard bounds and may shorten an individual step.
SystemsOfSystems.Solvers.Ralston2Options — Type
Ralston2Options(; dt)A container for the second-order Ralston Runge-Kutta solver options, where dt is the requested fixed official step spacing in seconds. Scheduled, model-requested, and user-requested times remain hard bounds and may shorten an individual step.
Structured-State Errors
The adaptive solver must reduce the error in each state variable to one normalized scalar. The default SystemsOfSystems.normalized_variable_error implementation compares the components returned by Dimensions.eachdim and returns the largest error. This works automatically for scalars, arrays, static arrays, and user types that implement the Dimensions interface.
A state type that does not use Dimensions can specialize the function directly. For example:
struct PositionVelocity
position::Float64
velocity::Float64
end
function SystemsOfSystems.normalized_variable_error(
value::PositionVelocity,
embedded_value::PositionVelocity,
absolute_tolerance,
relative_tolerance,
)
return max(
SystemsOfSystems.normalized_scalar_error(
value.position,
embedded_value.position,
absolute_tolerance,
relative_tolerance,
),
SystemsOfSystems.normalized_scalar_error(
value.velocity,
embedded_value.velocity,
absolute_tolerance,
relative_tolerance,
),
)
endThese functions are public but intentionally qualified because most users never call them directly.
SystemsOfSystems.normalized_variable_error — Function
normalized_variable_error(value, embedded_value, absolute_tolerance, relative_tolerance)Return the maximum normalized scalar error between two values. The default method compares the scalar components returned by Dimensions.eachdim. Types that do not support eachdim can define a specialized method.
SystemsOfSystems.normalized_scalar_error — Function
normalized_scalar_error(value, embedded_value, absolute_tolerance, relative_tolerance)Return the normalized error between two scalar values. The allowable error is the larger of the absolute tolerance and the relative tolerance times abs(value). A result no greater than one satisfies the tolerances.
Custom Solvers
Solver extensions define an immutable Solvers.AbstractSolverOptions, a per-simulation Solvers.AbstractIntegrator, and methods for Solvers.create_integrator and Solvers.step!. A wrapper can add behavior to an existing solver without reimplementing its numerical method:
struct CountingSolverOptions{O} <: Solvers.AbstractSolverOptions
solver::O
count::Base.RefValue{Int}
end
struct CountingIntegrator{I} <: Solvers.AbstractIntegrator
integrator::I
count::Base.RefValue{Int}
end
function Solvers.create_integrator(options::CountingSolverOptions, problem, initial_state)
integrator = Solvers.create_integrator(options.solver, problem, initial_state)
return CountingIntegrator(integrator, options.count)
end
function Solvers.step!(integrator::CountingIntegrator, problem, request)
integrator.count[] += 1
return Solvers.step!(integrator.integrator, problem, request)
endA solver implemented from scratch receives one Solvers.StepRequest at a time and returns either Solvers.AcceptedStep or Solvers.SolverFailure. When request.t_start == request.t_next_crv_draw, the solver chooses its proposed interval and uses Solvers.draw_continuous_random_variables to draw the interval's continuous random variables. Rejected attempts reuse that state, and an accepted result carries the proposed interval endpoint in t_next_crv_draw, even if the accepted numerical step is shorter. These protocol functions and types are public but remain qualified under Solvers.
SystemsOfSystems.Solvers.AbstractSolverOptions — Type
The common supertype for immutable, user-facing solver configuration.
Options may be reused across simulations. Runtime state such as the next adaptive step size belongs to an AbstractIntegrator created from the options for one simulation.
SystemsOfSystems.Solvers.AbstractIntegrator — Type
The common supertype for a runtime continuous-time integrator.
An integrator may retain controller history and numerical caches. It is owned by one simulation and receives the potentially discontinuously updated model state on every call to step!.
SystemsOfSystems.Solvers.StepRequest — Type
StepRequest(t_start, t_bound, state, t_next_crv_draw)A container for one accepted continuous-time step request beginning at the official rational time t_start. The integrator may choose any rational endpoint no later than t_bound.
t_bound is selected by the simulation scheduler from user-requested times, model-requested times, and the overall end time. It is a hard boundary: no numerical stage may cause the accepted state to be labeled with a time beyond it.
t_next_crv_draw is the endpoint of the currently committed continuous-random interval. Equality with t_start means the solver should begin a new interval. Otherwise, the solver holds the current draws until it reaches that endpoint.
SystemsOfSystems.Solvers.AcceptedStep — Type
AcceptedStepThe result of exactly one accepted numerical step.
state_at_start includes the continuous random draws belonging to the accepted interval, and rates_at_start is the authoritative rates evaluation for that accepted sample. The simulation loop logs those values and considers their model stop requests. Intermediate stage outputs and stop requests never cross this boundary.
next_dt is a floating-point controller suggestion. It is deliberately a duration rather than an absolute time; the scheduler converts it into an official rational endpoint for the next attempt.
t_next_crv_draw carries the committed continuous-random interval endpoint into the next step request.
SystemsOfSystems.Solvers.SolverFailure — Type
SolverFailure(time, reason)A result indicating that no acceptable numerical step could be produced from time.
A solver failure is not a model stop request. Keeping it as a distinct result prevents the normal accepted-step path from carrying an abstract stop field and makes it impossible for the simulation loop to run hooks or updates for a step that was never accepted.
SystemsOfSystems.ContinuousProblems.draw_continuous_random_variables — Function
draw_continuous_random_variables(problem, t_start, dt_f, state)Draws every continuous random variable for a newly committed interval beginning at the official t_start with floating-point duration dt_f. Rejected numerical attempts and accepted substeps within that interval reuse the returned state.
SystemsOfSystems.Solvers.create_integrator — Function
create_integrator(options, problem, initial_state)Creates runtime solver state for one simulation. problem and initial_state are accepted by the interface even when a particular method does not yet require initialization caches.
SystemsOfSystems.Solvers.step! — Function
step!(integrator, problem, request)Attempts and returns exactly one accepted numerical step without crossing request.t_bound. The fixed and adaptive overloads below form the complete simulation-facing solver protocol.
Hooks
Hooks allow other processes to interact with the simulation loop, and the sim can have any number of hooks in the hooks vector of SimOptions.
options = SimOptions(;
hooks = [
Hooks.ProgressBarOptions(),
Hooks.SimTimeoutOptions(;
max_run_time = 60.,
),
],
)ProgressBarOptions displays command-line progress. Its update interval is wall-clock time, not simulation time.
SimTimeoutOptions requests a clean stop after the simulation has run for the specified wall-clock duration. The timeout is checked by the simulation loop; it is not a hard operating-system deadline that interrupts arbitrary user code.
ClockSyncOptions prevents the simulation from advancing faster than wall-clock time. It is useful for soft real-time demonstrations and hardware- or software-in-the-loop setups. It cannot make a simulation run in real time when one simulation step requires more computation than the corresponding wall-clock interval.
These are the built-in hooks.
SystemsOfSystems.Hooks.ProgressBarOptions — Type
ProgressBarOptionsA container for command-line progress bar options, including update_interval for how often the progress bar updates (seconds) and description for the progress bar's text.
SystemsOfSystems.Hooks.SimTimeoutOptions — Type
SimTimeoutOptionsA container for SimTimeout options, which can end a simulation that takes too long.
Fields:
max_run_time: The maximum run time before the hook should terminate the sim (s)
SystemsOfSystems.Hooks.ClockSyncOptions — Type
ClockSyncOptionsA container for ClockSync options, which keep the simulation loop from running faster than real time. If the amount of desired stall time is larger than sleep_margin (s), it will sleep until sleep_margin before the next trigger time. After that, it enters a tight loop using time_ns() to determine when it's time to continue with the simulation.
Since time_ns() is used for timing, it is unaffected by system clock updates, and it updates continuously. This value ultimately comes from the operating system, the computer's oscillator, and time synchronization sources, which are only used to determine how many oscillations occur per externally-referenced unit of time. On Linux and macOS, the external corrections prevent most drift. Its timing performance can vary by target platform, especially for long-running simulations with high precision requirements.
Julia's sleep function has a minimum duration of 1ms. The default sleep_margin is 2ms to allow the model to enter the "tight timing loop" after sleeping.
This type uses UInt64 to store the start and current times in nanoseconds. This means that real-time synchronization can be sustained for approximately 584 years and is unlikely to limit the duration of the simulation.
Further hooks can be developed using the hooks interface.
A custom hook normally defines separate option and runtime types. The creation method receives the requested simulation times and initial model; update methods receive each accepted time and the corresponding pre-update model. The default Hooks.close_hook! does nothing, so a hook only needs to specialize it when cleanup is necessary.
struct CallbackHookOptions{F} <: Hooks.AbstractHookOptions
callback::F
end
struct CallbackHook{F} <: Hooks.AbstractHook
callback::F
end
Hooks.create_hook(options::CallbackHookOptions, t, model) =
CallbackHook(options.callback)
function Hooks.update_hook!(hook::CallbackHook, t, model)
hook.callback(t, model)
return Hooks.HookOutputs()
endSystemsOfSystems.Hooks.AbstractHookOptions — Type
AbstractHookOptionsAn abstract type for a set of options used to construct a subtype of AbstractHook.
SystemsOfSystems.Hooks.AbstractHook — Type
AbstractHookAn abstract type for functionality that "hooks into" the sim loop.
All subtypes are expected to provide the following interface:
create_hook: Turns the hook's options (AbstractHookOptions) into the hook itself.update_hook!: Called at the beginning of each sim step, this allows a hook to update its internal state.close_hook!: Called at the end of the sim (whether the sim ended for nominal reasons or caught an error), allowing the hook to close any resources it's using, such as i/o.
SystemsOfSystems.Hooks.HookOutputs — Type
HookOutputsA container for the output of update_hook!, allowing the hook to communicate with the simulation loop.
Fields:
stop::Bool: Set to true to stop the sim (default: false)
SystemsOfSystems.Hooks.create_hook — Function
create_hook(options::AbstractHookOptions, t, model)Returns a subtype of AbstractHook built from the provided options, where t is an array of exact simulation times corresponding to the set of times passed to simulate (i.e., first(t) is when the sim will start, last(t) is when it will end, and anything in between is a desired output time for the sim, and model is the initial model.
SystemsOfSystems.Hooks.update_hook! — Function
update_hook!(hook::AbstractHook, t, model)Allows the hook to update its internal state at time t using the model. The model will correspond with continuous-time updates up to t, and it will not yet have performed its discrete update at t.
SystemsOfSystems.Hooks.close_hook! — Function
close_hook!(hook::AbstractHook, t_end, model)Called at the end of the simulation (whether the sim completed nominally or had an error), allowing the hook to close i/o resources, summarize, etc., where t_end is the final sim time and model is the final model.
Logs
The log option selects where, and whether, simulation histories are stored.
BasicLogOptions is the default. It stores selected histories in ordinary Julia arrays in memory and is normally the fastest choice.
options = SimOptions(;
log = Logs.BasicLogOptions(),
)NullLogOptions turns time-series logging off. It is useful when only fields such as history.t_stop and history.model are needed.
options = SimOptions(;
log = Logs.NullLogOptions(),
)HDF5LogOptions writes time-series data directly to disk. This supports histories that are too large for RAM, at the cost of slower simulation. Constants that cannot be represented by HDF5Vectors are omitted with a warning that identifies the constant, its type, and the underlying error. HDF5 logging becomes available after importing HDF5Vectors.
using HDF5Vectors
options = SimOptions(;
log = Logs.HDF5LogOptions(;
filename = "out/history.h5",
),
)If the history fits in memory and only the final artifact needs to be HDF5, a BasicLogOptions simulation followed by Logs.save_log_to_hdf5 is faster than logging directly to HDF5. The same unsupported-constant behavior applies when saving an existing log.
The HDF5 representation retains model order and types; constants and their VariableDescription metadata; and each time series' title, dimensions, signal path, continuous/discrete designation, groups, and interpolator. Model types and interpolators use Julia serialization. Files should therefore come only from trusted sources, and custom serialized types must be available when loading.
Logs.load_hdf5_log returns (log, root_model_history). The returned time series remain backed by the open file, so the log should be closed when it is no longer needed:
log, root = Logs.load_hdf5_log("out/history.h5")
try
position = root["position"]
# Use the loaded history.
finally
Logs.close_log(log)
endSystemsOfSystems.Logs.BasicLogOptions — Type
BasicLogOptions(; logging_policy = AllPassLoggingPolicy())A container for in-memory BasicLog options.
logging_policy assigns a model logging policy to every model, by path. The default AllPassLoggingPolicy logs all variables of all models on all samples.
SystemsOfSystems.Logs.NullLogOptions — Type
NullLogOptions()An empty container for NullLog options, which disable history logging.
SystemsOfSystems.Logs.HDF5LogOptions — Type
HDF5LogOptions(; filename, logging_policy)
HDF5LogOptions(filename)A container for HDF5-backed log options, where filename is the output file.
logging_policy assigns a model logging policy to every model, by path. The default AllPassLoggingPolicy logs all variables of all models on all samples.
An HDF5 log records the same selected continuous and discrete states, outputs, and metadata as a BasicLog, but stores time-series data on disk. Constants that cannot be represented by HDF5Vectors are omitted with a warning. This supports histories that would not fit in RAM, at the cost of slower logging.
If the selected history fits in memory and only the final artifact needs to be HDF5, it is faster to use a BasicLog and call save_log_to_hdf5 after simulation.
SystemsOfSystems.Logs.load_hdf5_log — Function
load_hdf5_log(filename)Loads a log from an HDF5 file and returns (log, root_model_history). The log owns the open file and should be closed with close_log when it is no longer needed.
Interpolators and model types are restored using Julia serialization, so files should come only from trusted sources. Custom interpolator types must be available in the loading environment. An unavailable model type produces a warning and is represented by Missing without preventing the remaining history from loading.
SystemsOfSystems.Logs.save_log_to_hdf5 — Function
save_log_to_hdf5(filename, log)Saves a log to an HDF5 file in the same format used by the HDF5Log.
Constants that cannot be represented by HDF5Vectors are omitted with a warning.
Standalone Time Series
Individual time series can use the same HDF5 representation without constructing a complete log. These functions operate on an open HDF5 file, and loaded vectors remain usable only while that file is open.
using HDF5
using HDF5Vectors
HDF5.h5open("signal.h5", "w") do file
Logs.save_time_series_to_hdf5(file, "signals/position", position)
end
HDF5.h5open("signal.h5", "r") do file
loaded_position = Logs.load_time_series_from_hdf5(file, "signals/position")
# Use loaded_position before this block closes the file.
endSystemsOfSystems.Logs.save_time_series_to_hdf5 — Function
save_time_series_to_hdf5(fid, path, time_series; kwargs...)Saves a TimeSeries beneath path in an open HDF5 file. Additional keyword arguments are passed to HDF5Vectors when storing the time and data vectors.
SystemsOfSystems.Logs.load_time_series_from_hdf5 — Function
load_time_series_from_hdf5(fid, path)Loads the TimeSeries stored beneath path in an open HDF5 file. Its time and data remain backed by that file, which must remain open while the returned series is in use. The interpolator is restored using Julia serialization, so files should come only from trusted sources and custom interpolator types must be available in the loading environment.
SystemsOfSystems.Logs.close_log — Function
close_log(::AbstractLog)If there are any resources open for the given log, this closes them (which may make some logs non-operational).
Logging Policies
Both BasicLogOptions and HDF5LogOptions accept a logging_policy. A logging policy assigns two choices to each model:
- A variable set that selects which constants, states, and outputs are stored
- A sampler that selects when the states and outputs are recorded
The default AllPassLoggingPolicy stores every variable from every model and samples at every simulation time.
SystemsOfSystems.LoggingPolicies.AllPassLoggingPolicy — Type
AllPassLoggingPolicy()A logging policy that assigns an AllPassModelLoggingPolicy to every model. This is the default policy for BasicLog and HDF5Log.
One Policy for Every Model
UniformLoggingPolicy applies the same ModelLoggingPolicy to all models. For example, the following stores all variables but records states and outputs only on times that align with a 0.1-second grid:
using SystemsOfSystems: LoggingPolicies, Samplers
logging_policy = LoggingPolicies.UniformLoggingPolicy(;
policy = LoggingPolicies.ModelLoggingPolicy(;
sampler = Samplers.RegularSampler(1//10),
),
)
options = SimOptions(;
log = Logs.BasicLogOptions(; logging_policy),
)A logging sampler does not force the simulation to take steps. (Changing the log never affects the result of the simulation.) It only selects from times that already exist.
SystemsOfSystems.LoggingPolicies.UniformLoggingPolicy — Type
UniformLoggingPolicy(policy::AbstractModelLoggingPolicy)
UniformLoggingPolicy(; policy::AbstractModelLoggingPolicy)A logging policy that assigns the same model logging policy to every model.
SystemsOfSystems.LoggingPolicies.ModelLoggingPolicy — Type
ModelLoggingPolicy(; sampler::AbstractSampler, variable_set::AbstractVariableSet)A model logging policy that explicitly sets which variables are stored with variable_set and when stored states and outputs are recorded with sampler.
SystemsOfSystems.Samplers.CompleteSampler — Type
CompleteSampler()A sampler that logs the model's states and outputs at every simulation-loop sample. Discrete states retain their normal sparse change-event representation: only fields present in an update result are appended.
SystemsOfSystems.Samplers.NullSampler — Type
NullSampler()A sampler that skips the model's states and outputs at every simulation-loop sample.
The model history and any time-series containers selected by its model logging policy are still created during initialization. A model's sampler has no effect on the samplers assigned independently to its submodels.
SystemsOfSystems.Samplers.RegularSampler — Type
RegularSampler(; period, offset = 0)
RegularSampler(period, offset = 0)A sampler that logs the model's states and outputs at times in the sequence offset + n * period, for nonnegative integer n. Every selected state is snapshotted at those times, including discrete states absent from the current update result. Discrete snapshots reflect the post-update model state. Discrete outputs remain event-like and are recorded only when the current update result supplies them.
period and offset are converted to exact simulation times. period must be finite and strictly positive, and offset must be finite. A sampler does not add times to the simulation scheduler: it only selects from accepted simulation times that already exist.
Policies by Model Path
RegexLoggingPolicy allows the user to provide different model logging policies to different models according to the models' "paths". The root model has path "/", while descendants have paths such as "/plant" and "/vehicle/sensor". The first matching rule wins.
The following policy selects plant samples on a 100 Hz grid, omits two variables from the controller, and stores every variable from all remaining models:
logging_policy = LoggingPolicies.RegexLoggingPolicy(;
rules = [
r"^/plant$" => LoggingPolicies.ModelLoggingPolicy(;
sampler = Samplers.RegularSampler(1//100),
),
r"^/controller$" => LoggingPolicies.ModelLoggingPolicy(;
variable_set = LoggingPolicies.VariableExclusionList([
:large_cache,
:debug_state,
]),
),
],
default = LoggingPolicies.AllPassModelLoggingPolicy(),
)
options = SimOptions(;
log = Logs.BasicLogOptions(; logging_policy),
)Again, for the sake of clarity, note that asking for sampling at every 1/100 period does not force the simulation to take steps that align with that sampling period. (Again, we do not want the log type to influence the results of the simulation.) The selected sampler therefore needs to align with the discrete steps requested by schedules, t_next, etc.
If no default is specified for RegexLoggingPolicy, models that don't match the regular expressions are omitted from the log (they receive a NullModelLoggingPolicy).
SystemsOfSystems.LoggingPolicies.RegexLoggingPolicy — Type
RegexLoggingPolicy(; rules, default)
RegexLoggingPolicy(rules, default)A logging policy containing rules, a vector of RegexLoggingPolicyRule. Each model is given the model logging policy from the first entry whose regular expression occurs in the model path. Rules can be provided as RegexLoggingPolicyRule, expression => policy, or expression => sampler pairs. A model that matches no rule receives default, which is a NullModelLoggingPolicy by default. The first matching rule wins.
Example:
using SystemsOfSystems: LoggingPolicies, Samplers
LoggingPolicies.RegexLoggingPolicy(;
rules = [
r"^/my_model$" => Samplers.RegularSampler(1//10),
r"^/my_other_model/" => Samplers.RegularSampler(1//1),
],
default = LoggingPolicies.AllPassModelLoggingPolicy(),
)Here, exactly /my_model will be logged on any steps that align with a 0.1s grid, while descendants of /my_other_model will be logged on any steps that align with a 1s grid, and all other models will be logged completely.
SystemsOfSystems.LoggingPolicies.AllPassModelLoggingPolicy — Type
AllPassModelLoggingPolicy()A model logging policy that stores all of the model's variables and records them at every accepted sample.
SystemsOfSystems.LoggingPolicies.NullModelLoggingPolicy — Type
NullModelLoggingPolicy()A model logging policy that stores none of the model's variables. The model history itself is still present as a structural node in the log, and submodels use their independently assigned policies.
Selecting Variables
ModelLoggingPolicy.variable_set controls which variables are present in that model's history:
AllVariables()selects every variable.NoVariables()selects no variables.VariableList(names)selects only the listed variables.VariableExclusionList(names)selects everything except the listed variables.
This can be useful for models that have "weird" states. E.g., a discrete state could be a function, but we might not want to log a time history of functions (though we could).
Names can be strings or symbols. A model's variable set does not control its submodels; each submodel receives its own model logging policy.
SystemsOfSystems.LoggingPolicies.AllVariables — Type
AllVariables()A variable set that selects all of the model's constants, states, and outputs.
SystemsOfSystems.LoggingPolicies.NoVariables — Type
NoVariables()A variable set that selects none of the model's constants, states, or outputs.
SystemsOfSystems.LoggingPolicies.VariableList — Type
VariableList(list)
VariableList(; list)A variable set that selects the model variables whose names are in list. Names may be strings or symbols.
SystemsOfSystems.LoggingPolicies.VariableExclusionList — Type
VariableExclusionList(list)
VariableExclusionList(; list)A variable set that selects all model variables except those whose names are in list. Names may be strings or symbols.