Algorithms and States
ProcessAlgorithm and ProcessState are the two main building blocks you compose into a process.
- Use
ProcessAlgorithmfor something that actively participates in the loop by implementingProcesses.step!. - Use
ProcessStatefor data that should be initialized into a subcontext and then shared or read by algorithms.
Both are process entities: values that the package knows how to place inside a process. When a process is prepared, each entity gets a named part of the process context. That named part is its subcontext.
User methods receive a view of the current subcontext, plus any routed or shared values that were made visible to it. In normal code, treat that view like a read-only input object. Return a NamedTuple to write values back.
Lifecycle Hooks
The framework lifecycle is:
initphase- looped
step!phase cleanupphase
Order matters here: states are initialized first, then algorithms. Cleanup follows the same order. This means an algorithm can read values prepared by an earlier state if those values are routed or shared to it.
Two details from the implementation are worth knowing:
cleanupruns on natural finite completion.- If a process is interrupted, paused, or runs under
Indefinite(), the current context is stored without automatic cleanup.
There is no built-in prepare! hook in the current pipeline. If you want a one-time preparation step, fold it into init or guard the first step! with a flag in state.
Full Definitions
struct MyAlgo <: ProcessAlgorithm
gain::Float64
end
function Processes.init(a::MyAlgo, context)
x = 0.0
return (; x)
end
function Processes.step!(a::MyAlgo, context)
(; x) = context
x = x + a.gain
return (; x)
end
function Processes.cleanup(::MyAlgo, context)
return (;)
endstruct MyState <: ProcessState end
function Processes.init(::MyState, context)
return (; shared_buffer = Float64[])
endMacro Shortcuts
@ProcessAlgorithm
@ProcessAlgorithm creates the struct and the needed step! methods from a function signature.
@ProcessAlgorithm function Accumulate(x, gain)
x = x + gain
return (; x)
endYou can still define init and cleanup manually for the generated type.
Macro-Generated Algorithm Semantics
@ProcessAlgorithm also supports managed local state and configuration fields. Managed local state is data that belongs to one algorithm and is created during init, then read again during each step!.
The signature is split into:
- plain positional arguments: values read from context during
Processes.step! @managed(...)positional arguments: algorithm-owned values created duringProcesses.init- normal keyword arguments: values read from context during
Processes.step!, using the declared default when absent - optional trailing
@input((; ...))/@inputs((; ...))/@init((; ...)): values read only while constructing managed state - optional
@config ...declarations before the function: fields stored on the generated algorithm object
Example:
@ProcessAlgorithm begin
@config n::Int = 8
@config damp = 1.0
function MyAlgo(
signal,
@managed(buffer = zeros(n)),
@managed(scale),
@managed(metric = 0.0);
@inputs((; scale = 2.0))
)
@inbounds for i in eachindex(signal)
buffer[i] = damp * scale * signal[i]
end
metric = sum(buffer)
return (; buffer, scale, metric)
end
endRules worth knowing:
@managed(name)capturesnamefrom the init context into local managed state.@managed(name = expr)evaluatesexprduringProcesses.init.@managed(a, b = expr, c = expr2)expands to multiple managed locals in order.@input/@inputs/@initmay only appear once and must be the last keyword-like item.@configfields must have defaults and become fields on the generated struct. You can write them in a surrounding block or as a prelude like@ProcessAlgorithm @config seed = 1 function MyAlgo(...) ... end.- inside the algorithm body, config fields are available directly by name. Use
seed, notconfig.seed. - plain positional arguments are runtime-only and are not available while constructing managed state.
- Julia
wheresignatures are supported.
For a macro-generated algorithm MyAlgo, the main entrypoints are:
- direct/bootstrap call:
step!(MyAlgo(), args...; @inputs((; ...))) - init hook:
Processes.init(MyAlgo(), context) - step hook:
Processes.step!(MyAlgo(), context)
Internally the macro defines:
struct MyAlgo <: ProcessAlgorithm endorBase.@kwdef struct MyAlgo ... end- a hidden implementation function containing the user body
- public
Processes.step!(algo::MyAlgo, ...)methods for direct calls - generated
Processes.initandProcesses.step!methods that feed the implementation
Type annotations and where clauses are preserved on the generated public call signatures and the hidden implementation function. The runtime context extraction methods currently bind simple locals before forwarding into that typed implementation.
Analysis-Friendly Forms
If you want ContextAnalyser to discover dependencies more reliably:
- use
@inputs((; ...))to make init-time requirements explicit - use
context.nameor(; name) = contextfor required reads - use
get(context, :name, default)for optional reads - return plain
NamedTuples frominitandstep!
See Init Analysis for the analyzer workflow and limitations.
@ProcessState
@ProcessState creates a ProcessState and an init method.
@ProcessState function SharedParams(dt)
return (; dt)
endComposition
Compose entities with loop algorithms:
CompositeAlgorithm(...)for interleaved stepping with intervals.Routine(...)for sequential blocks with repeats.
Both can include ProcessStates and user options such as Route and Share.
Changing a Loop Algorithm Schedule
Process lifetime controls how long the outer process loop runs. Inside a loop algorithm, the per-child schedule is controlled by the loop algorithm itself:
CompositeAlgorithmandThreadedCompositeAlgorithmuse intervals.Routineuses repeats.
For composite algorithms, use the edit helpers before resolving the algorithm:
algo = CompositeAlgorithm(FastStep, SlowStep, (1, 10))
algo = changeinterval(algo, 2, 20)
interval(algo, 2) == Processes.Interval(20)
algo = changeintervals(algo, (1, 5))
intervals(algo) == (Processes.Interval(1), Processes.Interval(5))These helpers return a new loop algorithm with the updated schedule. They are intended for unresolved loop algorithms; if you already called resolve, edit the original algorithm and resolve it again.
When adding a child, pass the new child's schedule as the last argument:
algo = addalgo(algo, :logger => Logger, 100)For Routine, the constructor and addalgo schedule argument are repeat counts:
routine = Routine(Prepare, Train, (1, 50))
routine = addalgo(routine, :validate => Validate, 1)