Plugins
Plugins let you hook into the search loop without modifying SymbolicRegression.jl itself. A plugin is a small struct that opts into lifecycle hooks: observing mutations, biasing selection, injecting initial population members, or tracking statistics across generations.
How the search works
The search maintains multiple populations of candidate expressions, evolved in parallel. Each population runs on a worker (a thread or process); a single head node coordinates them.
A cycle is one round of evolution on a single population. Within a cycle, the engine runs many steps: each step picks a random member via tournament selection (sample a few members, keep the one with the lowest cost), mutates it, and decides whether to accept the result. The cost used in tournament selection combines the raw loss with a complexity penalty.
After a cycle finishes, the worker sends its updated population back to the head node. The head node merges results, updates the hall of fame, and dispatches the next cycle. Plugins can hook into any of these stages. Note: on_generation_end! fires on the head when a completed cycle is received (not per inner step), while on_cycle_end! fires on the worker at the end of its cycle.
Using a plugin
Pass plugin instances to Options via the plugins keyword:
using SymbolicRegression
options = Options(;
binary_operators=[+, -, *, /],
unary_operators=[cos],
plugins=(AdaptiveParsimonyPlugin(; tournament=true, mutation_acceptance=true),),
)Multiple plugins compose. The engine iterates the tuple at each lifecycle point and dispatches the appropriate hook on each plugin type.
AdaptiveParsimonyPlugin ships with the package and is enabled by default. It biases tournament selection and mutation acceptance away from over-represented complexities, using a sliding window of recent equation frequencies.
AdaptiveMutationWeightsPlugin is also enabled by default. It learns relative mutation weights from successful search moves, with the learned multipliers regularized halfway toward the configured weights in log space. Pass plugins=(AdaptiveMutationWeightsPlugin(adaptation_strength=0),) to disable only mutation-weight adaptation, or default_plugins=() to disable all automatic plugins.
Writing a custom plugin
Define a struct that subtypes AbstractPlugin, then override whichever hooks you need. The struct holds immutable configuration; mutable runtime state lives in a separate object returned by init_plugin_state.
See the Writing a Custom Plugin tutorial for a complete walkthrough that builds a plugin from scratch.
Lifecycle hooks
Every hook dispatches on your plugin type. Default implementations are no-ops (or return 1.0 for multipliers, nothing for factories). Override only what you need.
Hooks fall into four categories:
| Category | Name shape | Contract |
|---|---|---|
| Observer | on_X_start!, on_X_end! | Engine fires, plugin reacts. Return value ignored. |
| Multiplier | X_multiplier | Returns a Real. Plugins compose multiplicatively. |
| Conditioner | condition_X! | Mutates a passed struct in place. |
| Factory | init_X | Called once per (plugin, output) at startup. |
Initialization and teardown
SymbolicRegression.CoreModule.PluginModule.init_plugin_state Function
init_plugin_state(plugin::AbstractPlugin, options, dataset) -> stateCreate the mutable per-output state for plugin. Called once per (plugin, output) pair at search start.
Override by dispatching on your plugin type:
SymbolicRegression.init_plugin_state(p::MyPlugin, options, dataset) =
MyPluginState(p.config)Default returns nothing.
Experimental
SymbolicRegression.CoreModule.PluginModule.fork_plugin_state Function
fork_plugin_state(head_state, plugin, dataset) -> stateBuild the worker-side plugin state for one population, given the head node's current plugin state for this output and the dataset the worker will operate on. The returned state persists across that population's worker dispatches.
Default returns deepcopy(head_state) (full snapshot).
Experimental
SymbolicRegression.CoreModule.PluginModule.refresh_worker_plugin_state Function
refresh_worker_plugin_state(worker_state, latest_head_state, plugin, dataset) -> stateRefresh worker_state from latest_head_state before the worker's next dispatch. The default preserves worker_state. Plugins whose head-side hooks update worker data can return a merged or replaced worker state.
Experimental
SymbolicRegression.CoreModule.PluginModule.on_search_start! Function
on_search_start!(state, plugin, dataset, options, ropt)Lifecycle hook called on the head node after initialization, before warmup and the main search loop. Called once per (plugin, output) pair.
Override by dispatching on your plugin type:
SymbolicRegression.on_search_start!(s::MyPluginState, p::MyPlugin, dataset, options, ropt) = ...Default is a no-op.
Experimental
SymbolicRegression.CoreModule.PluginModule.on_search_end! Function
on_search_end!(state, plugin, search_state, dataset, options, ropt)Lifecycle hook called on the head node after the main search loop exits and before tearing down processes/threads. Multiprocessing cycles may still be running when this hook is called. Called once per (plugin, output) pair.
Override by dispatching on your plugin type. Default is a no-op.
Experimental
Per-generation and per-cycle
SymbolicRegression.CoreModule.PluginModule.on_generation_end! Function
on_generation_end!(state, plugin, search_state, dataset, options, ropt, returned_pop)Lifecycle hook called on the head node after each cycle's result has been received from a worker. Runs serially; safe to mutate plugin state, update concept databases, drain feedback channels, etc. Called once per (plugin, output) pair per cycle. state is this output's state; returned_pop is the population the worker produced.
Override by dispatching on your plugin type. Default is a no-op.
Experimental
SymbolicRegression.CoreModule.PluginModule.on_cycle_start! Function
on_cycle_start!(state, plugin, cycle_idx, ncycles, options)Observer hook fired at the start of each evolution cycle (1-based cycle_idx, ncycles total). Plugins update their own mutable state here based on the cycle position. For example, SimulatedAnnealingPlugin recomputes its temperature once per cycle and consumes it later in mutation_acceptance_multiplier and condition_mutation!.
Default is a no-op.
Experimental
SymbolicRegression.CoreModule.PluginModule.on_cycle_end! Function
on_cycle_end!(state, plugin, pop, dataset, hof, options)Lifecycle hook called on the worker at the end of each evolution cycle, paired with on_cycle_start!. May run concurrently across workers. Use only worker-local state, or use Channel / RemoteChannel for cross-worker communication.
Override by dispatching on your plugin type. Default is a no-op.
Experimental
SymbolicRegression.CoreModule.PluginModule.on_mutation_end! Function
on_mutation_end!(state, plugin, mutation::AbstractMutation, event::MutationEvent, dataset, options)Lifecycle hook called on the worker immediately before each return from next_generation, after the final accept/reject decision for a mutation. Called once per plugin per mutation. Plugins can dispatch on the mutation type (e.g. ::ConstantMutation) for type-specific handling, or ::AbstractMutation for a generic catch-all.
Default is a no-op.
Experimental
SymbolicRegression.CoreModule.PluginModule.MutationEvent Type
MutationEventBundle of per-mutation observations passed to on_mutation_end! once the accept/reject decision has been made inside next_generation.
Fields
accepted::Bool:trueif the mutation was accepted (viareturn_immediatelyor annealing/fitness acceptance);falseif rejected (constraint failure, NaN loss, or annealing/frequency rejection).before_cost::C: search cost of the parent member before mutation.after_cost::Union{C,Nothing}: search cost after mutation.nothingif no valid evaluation occurred.before_loss::L: raw loss of the parent member before mutation.after_loss::Union{L,Nothing}: raw loss after mutation.nothingif no valid evaluation occurred.mutation_idx::Int: index of the sampled mutation intooptions.mutations(and into the conditioned weights vector, which shares its order).
C and L are the cost and loss types.
The mutation kind itself is passed as a separate dispatch arg to on_mutation_end!, not stored on the event — that way plugin authors can write type-specific methods.
Experimental
Selection and acceptance biases
SymbolicRegression.CoreModule.PluginModule.tournament_cost_multiplier Function
tournament_cost_multiplier(state, plugin, member, options) -> RealPer-plugin multiplier applied to a candidate's member.cost during tournament selection in _best_of_sample. Plugins compose multiplicatively: the adjusted cost is member.cost * ∏ tournament_cost_multiplier(s, p, ...) across all plugins in tuple order. Default returns 1.0 (no adjustment).
The shipped AdaptiveParsimonyPlugin is the canonical example; it reads frequency statistics from its own state, not from an engine-passed arg.
Experimental
SymbolicRegression.CoreModule.PluginModule.mutation_acceptance_multiplier Function
mutation_acceptance_multiplier(state, plugin, ctx::MutationAcceptanceContext, options) -> RealPer-plugin multiplicative contribution to the engine's per-mutation accept probability. The engine takes the product across all plugins and draws one rand against it — so multiple plugins compose without introducing independent rand draws. Default returns 1.0.
Plugins that need cycle-progress / temperature in their multiplier should maintain their own mutable state and update it in on_cycle_start!.
Experimental
Mutation conditioning
SymbolicRegression.CoreModule.PluginModule.prepare_mutation_context Function
prepare_mutation_context(mutation::AbstractMutation)Build a fresh per-call mutable context for mutation, called inside next_generation immediately after mutation sampling — so only the selected mutation pays the construction cost.
Default returns nothing (no context; the mutation runs directly from its immutable fields). Override for any mutation type that wants plugins to layer per-call configuration on top of its base values via condition_mutation!:
mutable struct MyMutationContext
strength::Float64
end
SymbolicRegression.prepare_mutation_context(m::MyMutation) = MyMutationContext(m.strength)Experimental
SymbolicRegression.CoreModule.PluginModule.condition_mutation! Function
condition_mutation!(context, state, plugin, mutation, options)In-place plugin modification of the per-call context produced by prepare_mutation_context. Fired only for the mutation selected this call, and only when its context is not nothing. Composes by sequential in-place mutation in plugin tuple order.
Default is a no-op.
Experimental
condition_mutation_weights! is a related hook that modifies the mutation weight vector before sampling. See the Customization page for its full docstring.
Operation defaults
Plugins may contribute weighted mutation and crossover defaults. Explicit entries in Options(; mutations=..., crossovers=...) take precedence.
SymbolicRegression.CoreModule.PluginModule.plugin_mutations Function
plugin_mutations(plugin::AbstractPlugin) -> collectionWeighted custom mutations contributed by plugin, as AbstractMutation() => weight pairs. These are treated as plugin defaults: an explicit entry in Options(; mutations=...) of the same mutation type overrides the plugin contribution, while a plugin contribution overrides a built-in default. Passing default_mutations overrides all automatic defaults, including plugin contributions. The default is empty.
Experimental
SymbolicRegression.CoreModule.PluginModule.plugin_crossovers Function
plugin_crossovers(plugin::AbstractPlugin) -> collectionWeighted custom crossovers contributed by plugin, as AbstractCrossover() => weight pairs. These are treated as plugin defaults: an explicit entry in Options(; crossovers=...) of the same crossover type overrides the plugin contribution, while a plugin contribution overrides a built-in default. Passing default_crossovers overrides all automatic defaults, including plugin contributions. The default is empty.
Experimental
Population seeding
SymbolicRegression.CoreModule.PluginModule.init_member Function
init_member(state, plugin, dataset, options)Called when initializing each population member's tree during initial population creation only. Every plugin is asked; at most one may return a non-nothing value, and two or more providers is an error. If all plugins return nothing, the engine falls through to gen_random_tree.
Override by dispatching on your plugin type. Default returns nothing.
State used
init_member is called with the head node's state instance, not a per-worker copy. In :multithreading mode, multiple population-creation tasks may call it concurrently — ensure your implementation is thread-safe or limit it to read-only access of the state.
Experimental
Thread and process safety
on_generation_end!runs serially on the head node. Safe to mutate state.on_cycle_end!andon_mutation_end!run on workers against per-dispatch copies built byfork_plugin_state. Cross-worker communication requiresChannel/RemoteChannel.init_memberreads head-node state. In multithreading mode, multiple population-creation tasks may call it concurrently, so keep it read-only or thread-safe.
Dispatching on mutation type
on_mutation_end! receives the mutation as a typed argument. You can write specific methods for individual mutation types:
function SymbolicRegression.on_mutation_end!(
state::MyState,
::MyPlugin,
::ConstantMutation,
event::MutationEvent,
dataset,
options,
)
# handle constant mutations specifically
endAvailable mutation types: ConstantMutation, OperatorMutation, FeatureMutation, SwapOperandsMutation, AddNodeMutation, InsertNodeMutation, DeleteNodeMutation, FormConnectionMutation, BreakConnectionMutation, RotateTreeMutation, BacksolveMutation, SimplifyMutation, RandomizeMutation, OptimizeMutation, DoNothingMutation.
Built-in plugins
SymbolicRegression.AdaptiveParsimonyModule.AdaptiveParsimonyPlugin Type
AdaptiveParsimonyPlugin <: AbstractPluginFrequency-weighted parsimony adjustments at two engine decision points:
tournament: whentrue, multiplies tournament-selection cost byexp(adaptive_parsimony_scaling * f), wherefis the recent relative frequency of equations at this complexity. Biases selection against over-represented complexities.mutation_acceptance: whentrue, multiplies the mutation acceptance probability byold_freq / new_freq. Biases mutation acceptance away from over-represented complexities.
Both default to true. Frequency statistics are tracked per output (per dataset in multi-target regression), so different outputs don't interfere with each other's complexity distributions. Equivalent to the legacy Options(; use_frequency_in_tournament=true, use_frequency=true) flags, which are auto-translated into this plugin during Options construction.
Experimental
Part of the experimental plugin interface.
SymbolicRegression.AdaptiveMutationWeightsModule.AdaptiveMutationWeightsPlugin Type
AdaptiveMutationWeightsPlugin <: AbstractPluginOnline-adapt per-mutation weights from the search's own success statistics. For each mutation kind, the plugin tracks attempts and strictly improving successes in the configured reward metric and adjusts a multiplicative factor applied to that mutation's base weight, updated each mutation via an EMA over the smoothed success-ratio with a floor clamp. Only the sampled mutation's multiplier is updated, then active multipliers are normalized to unit mean.
Statistics persist independently for each population.
Fields
smoothing::Float64 = 0.02: EMA factor for the multiplier update.floor::Float64 = 0.05: clamp range for the sampled mutation's target ratio ([floor, 1/floor]) before the EMA update and mean normalization.reward::Symbol = :cost: objective used to count improvements. Supported values are:costand:loss.adaptation_strength::Float64 = 0.5: strength of the learned multiplier in log space. Zero preserves the original mutation weights, while one applies the learned multipliers without regularization.
Mutation kinds excluded from accounting are declared by dispatch on skip_in_adaptive_weights; by default SimplifyMutation and DoNothingMutation are skipped. To add your own:
SymbolicRegression.AdaptiveMutationWeightsModule.skip_in_adaptive_weights(::MyMutation) = trueExperimental
SymbolicRegression.SimulatedAnnealingModule.SimulatedAnnealingPlugin Type
SimulatedAnnealingPlugin(; alpha=0.1)Couple the search's mutation pipeline to a temperature schedule that sweeps linearly from 1.0 at the first cycle of an iteration to 0.0 at the last. The plugin uses its current temperature for two things:
Constant-perturbation magnitude: via
condition_mutation!, the plugin multipliesConstantMutationContext.scaleby the current temperature.Mutation acceptance: via
mutation_acceptance_multiplier, the plugin contributesexp(-(after_cost - before_cost) / (T * alpha))to the engine's combined accept probability — multiple plugins' multipliers compose against a single rand draw so the legacyannealing × frequency_parsimonysemantics are preserved.
The "temperature" concept is entirely local to this plugin's mutable state; nothing else in the engine references it.
Experimental
SymbolicRegression.MutationBurstModule.MutationBurstPlugin Type
MutationBurstPlugin(; retry_attempts=4, compound_probability=0.25, compound_max_steps=2)Per-cycle local-search extensions to the basic single-mutation loop:
Retry (outer): if the engine rejects a mutation, re-run
next_generationagainst the original parent up toretry_attemptstotal times. Break on the first accepted result.Compound burst (inner): after an accepted mutation, with probability
compound_probabilitychain another mutation step on the result, up tocompound_max_stepstotal accepted mutations.
retry_attempts = 1 disables retry; compound_probability = 0 disables compound bursts; the combination reproduces the upstream single-mutation loop.
Like all hooks, wrap_mutation_step composes across plugins in options.plugins tuple order: earlier plugins wrap outside later ones. The retry-around-compound nesting above is internal to this plugin's own wrap_mutation_step implementation and is not affected by tuple order.
Extra experimental
The retry/compound mechanisms and their composition were validated on a single benchmark suite — they may change behavior, defaults, or config-knob names in minor releases until exercised more broadly.
Abstract type
SymbolicRegression.CoreModule.PluginModule.AbstractPlugin Type
AbstractPluginAbstract type for a plugin's configuration. A plugin instance is an immutable struct whose fields are the user-tunable settings of the plugin. Mutable runtime data is held separately in a state object returned by init_plugin_state.
A search may have any number of plugins active simultaneously, supplied via the plugins = (Plugin1(), Plugin2(), ...) keyword on Options. The engine iterates the tuple at each lifecycle point and dispatches the appropriate hook on the plugin type.
Plugin state
State is duck-typed: it can be any object, including a NamedTuple, a Dict, or a mutable struct of your own. Every hook dispatches on the plugin type, so the state needs no particular supertype.
Thread / Multiprocessing Safety:
on_generation_end!runs serially on the head node — safe to mutate.on_cycle_end!andon_mutation_end!run on workers, against per-population states built byfork_plugin_statebefore the first dispatch and retained across later dispatches. Cross-worker communication must useChannel/RemoteChannel.init_memberreads the head node's per-output state during initial population creation. In multithreading mode, multiple population-creation tasks may call it concurrently — keep it read-only or thread-safe.In multiprocessing mode, plugin config is serialized to workers via
options.plugins, and state is forked on the head byfork_plugin_stateand shipped with the dispatch. Workers never construct their own state.
Experimental
The plugin interface is experimental. Hook signatures may change in minor releases until validated by multiple in-tree plugins.