Evaluation
The evaluation engine computes the value of expression trees given variable bindings.
Basic Evaluation
tree = FunctionNode(:+, Variable(:x), Constant(1.0))
# Multiple calling conventions
evaluate(tree, x=5.0) # 6.0
evaluate(tree, EvalContext(x=5.0)) # 6.0
evaluate(tree, Dict(:x => 5.0)) # 6.0
evaluate(tree, grammar, EvalContext(x=5.0)) # Uses grammar's safe operatorsBatch Evaluation
data = [1.0; 2.0; 3.0;;] # 3x1 matrix
results = evaluate_batch(tree, data, [:x]) # [2.0, 3.0, 4.0]Safe Evaluation
safe_evaluate(tree, EvalContext(x=5.0)) # Returns NaN on any error
is_valid_on(tree, EvalContext(x=5.0)) # true if evaluation succeedsCompiled Evaluation
For evaluating the same tree many times, compile it to a function:
f = compile_tree(tree, [:x])
f(5.0) # 6.0Context-Aware Evaluation
For problems where operators need access to external state (belief updating, aggregation, time-series), use context-aware evaluation.
EvalContext with Custom Operators
operators = Dict{Symbol, Function}(
:add_bonus => (v, bonus, ctx) -> begin
dm = ctx[:data_mean]
result = copy(v)
idx = argmin(abs.(v .- dm))
result[idx] += bonus
return result
end,
:choose_by_obs => (a, b, ctx) -> ctx[:is_heads] ? a : b,
)
ctx = EvalContext(
Dict{Symbol, Any}(:probs => probs, :data_mean => 0.6, :is_heads => true),
operators
)
result = evaluate(tree, ctx)Helper functions:
with_operators— add operators to existing contextwith_bindings— add bindings to existing contexthas_operator— check if operator existsget_operator— get operator function
AbstractEvalContext (Type-Safe)
For production code, define a typed context:
struct BeliefContext <: AbstractEvalContext
probs::Vector{Float64}
data_mean::Float64
n_tosses::Int
is_heads::Bool
end
function SymbolicOptimization.resolve_variable(ctx::BeliefContext, name::Symbol)
name == :probs && return ctx.probs
name == :data_mean && return ctx.data_mean
error("Unknown variable: \$name")
end
function SymbolicOptimization.apply_operator(ctx::BeliefContext, op::Symbol, args...)
if op == :add_bonus
v, bonus = args
result = copy(v)
result[argmin(abs.(v .- ctx.data_mean))] += bonus
return result
end
return nothing # Fall back to defaults
end
function SymbolicOptimization.has_custom_operator(ctx::BeliefContext, op::Symbol)
op in (:add_bonus,)
end
ctx = BeliefContext([0.1, 0.2, 0.7], 0.6, 100, true)
result = evaluate_with_context(tree, ctx)Sequential Evaluation
For iterative problems (belief updating, time-series):
results = evaluate_sequential(tree, make_ctx, update!, n_steps; init_state=state)Evaluation Reference
SymbolicOptimization.evaluate — Function
evaluate(tree::AbstractNode, ctx::EvalContext) -> Any
evaluate(tree::AbstractNode, grammar::Grammar, ctx::EvalContext) -> Any
evaluate(tree::AbstractNode; kwargs...) -> AnyEvaluate an expression tree with given variable bindings.
Examples
tree = FunctionNode(:+, Variable(:x), Constant(1.0))
# With EvalContext
ctx = EvalContext(x=5.0)
evaluate(tree, ctx) # 6.0
# With keyword arguments
evaluate(tree, x=5.0) # 6.0
# With Grammar (uses safe operators)
g = Grammar(binary_operators=[+])
evaluate(tree, g, ctx) # 6.0SymbolicOptimization.evaluate_batch — Function
evaluate_batch(tree::AbstractNode, data::AbstractMatrix, var_names::Vector{Symbol}) -> Vector
evaluate_batch(tree::AbstractNode, grammar::Grammar, data::AbstractMatrix, var_names::Vector{Symbol}) -> VectorEvaluate a tree over multiple data points.
Arguments
tree: The expression tree to evaluatedata: Matrix where each row is a data point, columns correspond to variablesvar_names: Names of variables corresponding to columns
Returns
Vector of results, one per row of data.
Example
tree = FunctionNode(:+, Variable(:x), Variable(:y))
data = [1.0 2.0; 3.0 4.0; 5.0 6.0] # 3 rows, 2 columns
results = evaluate_batch(tree, data, [:x, :y]) # [3.0, 7.0, 11.0]evaluate_batch(tree::AbstractNode, data::Vector{<:Dict}) -> VectorEvaluate a tree over a vector of binding dictionaries.
SymbolicOptimization.safe_evaluate — Function
safe_evaluate(tree::AbstractNode, ctx::EvalContext) -> Float64Evaluate a tree, returning NaN for any errors.
SymbolicOptimization.is_valid_on — Function
is_valid_on(tree::AbstractNode, ctx::EvalContext) -> BoolCheck if a tree evaluates to a finite value for the given context.
SymbolicOptimization.compile_tree — Function
compile_tree(tree::AbstractNode, var_names::Vector{Symbol}) -> FunctionCompile an expression tree into a Julia function for faster repeated evaluation.
Example
tree = FunctionNode(:*,
FunctionNode(:+, Variable(:x), Constant(1.0)),
Variable(:y)
)
f = compile_tree(tree, [:x, :y])
f(2.0, 3.0) # (2 + 1) * 3 = 9.0SymbolicOptimization.EvalContext — Type
EvalContextA context for evaluating expression trees, binding variable names to values and optionally providing custom operator implementations.
Basic Usage (Variable Bindings)
# From keyword arguments
ctx = EvalContext(x=1.0, y=2.0)
# From a Dict
ctx = EvalContext(Dict(:x => 1.0, :y => 2.0))
# From pairs
ctx = EvalContext(:x => 1.0, :y => 2.0)
ctx[:x] # 1.0
ctx[:z] # throws KeyError
haskey(ctx, :x) # trueAdvanced Usage (Context-Aware Operators)
For problems like belief updating where operators need access to context (e.g., add_ibe_bonus needs data_mean), you can register custom operators that receive the full context:
# Define context-aware operators
# Signature: (args..., ctx::EvalContext) -> result
custom_ops = Dict{Symbol, Function}(
:add_ibe_bonus => (v, bonus, ctx) -> begin
data_mean = ctx[:data_mean]
# Find hypothesis closest to data_mean, add bonus to it
biases = ctx[:biases]
idx = argmin(abs.(biases .- data_mean))
result = copy(v)
result[idx] += bonus
return result
end,
:select_by_data => (a, b, ctx) -> ctx[:is_heads] ? a : b
)
# Create context with operators
ctx = EvalContext(
Dict(:probs => probs, :data_mean => 0.6, :is_heads => true, :biases => 0:0.1:1),
custom_ops
)
# Evaluate - custom operators automatically receive context
result = evaluate(tree, ctx)This allows the same tree structure to be used for:
- Aggregator discovery: Simple context with just
ps(predictions vector) - Belief updating: Rich context with running statistics, custom operators
SymbolicOptimization.with_operators — Function
with_operators(ctx::EvalContext, ops::Dict{Symbol, Function}) -> EvalContextCreate a new context with additional/overridden operators.
SymbolicOptimization.with_bindings — Function
with_bindings(ctx::EvalContext, bindings::Dict) -> EvalContext
with_bindings(ctx::EvalContext; kwargs...) -> EvalContextCreate a new context with additional/overridden bindings.
SymbolicOptimization.has_operator — Function
Check if an operator exists.
has_operator(ctx::EvalContext, op::Symbol) -> BoolCheck if the context has a custom implementation for the given operator.
SymbolicOptimization.get_operator — Function
get_operator(ctx::EvalContext, op::Symbol) -> Union{Function, Nothing}Get the custom operator implementation, or nothing if not defined.
SymbolicOptimization.AbstractEvalContext — Type
AbstractEvalContextAbstract base type for evaluation contexts. Subtype this to create domain-specific evaluation contexts.
Required Interface
Subtypes should implement:
resolve_variable(ctx::MyContext, name::Symbol) -> Any
Optional Interface
Subtypes may implement:
apply_operator(ctx::MyContext, op::Symbol, args...) -> Anyhas_custom_operator(ctx::MyContext, op::Symbol) -> Bool
Example: Aggregator Context
struct AggregatorContext <: AbstractEvalContext
ps::Vector{Float64} # Forecaster predictions
end
resolve_variable(ctx::AggregatorContext, name::Symbol) =
name == :ps ? ctx.ps : error("Unknown variable: $name")Example: Belief Updating Context
mutable struct BeliefContext <: AbstractEvalContext
probs::Vector{Float64}
data_mean::Float64
is_heads::Bool
# ... more fields
end
resolve_variable(ctx::BeliefContext, name::Symbol) = begin
name == :probs && return ctx.probs
name == :data_mean && return ctx.data_mean
# ...
end
# Custom operator that uses context
apply_operator(ctx::BeliefContext, op::Symbol, args...) = begin
if op == :add_ibe_bonus
return add_ibe_bonus_impl(args[1], args[2], ctx.data_mean)
elseif op == :select_by_data
return ctx.is_heads ? args[1] : args[2]
else
return nothing # Fall back to default
end
end
has_custom_operator(ctx::BeliefContext, op::Symbol) =
op in (:add_ibe_bonus, :select_by_data)SymbolicOptimization.SimpleContext — Type
SimpleContext(; kwargs...)
SimpleContext(bindings::Dict)A simple context that just holds variable bindings. This is the context-system equivalent of EvalContext.
Example
ctx = SimpleContext(x=1.0, y=2.0)
result = evaluate_with_context(tree, ctx)SymbolicOptimization.VectorAggregatorContext — Type
VectorAggregatorContextPre-built context for aggregator discovery problems. Provides a prediction vector ps and common vector operations.
Example
ctx = VectorAggregatorContext(predictions)
result = evaluate_with_context(aggregator_tree, ctx)SymbolicOptimization.resolve_variable — Function
resolve_variable(ctx::AbstractEvalContext, name::Symbol) -> AnyResolve a variable name to its value in the given context. Must be implemented by subtypes.
resolve_variable(ctx::EvalContext, name::Symbol)Resolve variable for the standard EvalContext.
SymbolicOptimization.apply_operator — Function
apply_operator(ctx::AbstractEvalContext, op::Symbol, args...) -> AnyApply an operator with the given arguments in the context. Return nothing to use the default implementation.
Default implementation returns nothing, causing fallback to standard evaluation.
SymbolicOptimization.has_custom_operator — Function
has_custom_operator(ctx::AbstractEvalContext, op::Symbol) -> BoolCheck if the context provides a custom implementation for the operator. Default returns false.
SymbolicOptimization.has_variable — Function
has_variable(ctx::AbstractEvalContext, name::Symbol) -> BoolCheck if a variable exists in the context. Default implementation tries to resolve and catches errors.
SymbolicOptimization.evaluate_with_context — Function
evaluate_with_context(tree::AbstractNode, ctx) -> AnyEvaluate a tree using a custom evaluation context.
This is the main entry point for domain-specific evaluation. The context controls:
- How variables are resolved
- How operators are applied (with optional custom implementations)
Example
# Define your context
struct MyContext <: AbstractEvalContext
x::Float64
y::Float64
end
resolve_variable(ctx::MyContext, name::Symbol) =
name == :x ? ctx.x : name == :y ? ctx.y : error("Unknown: $name")
# Evaluate
ctx = MyContext(1.0, 2.0)
tree = FunctionNode(:+, [Variable(:x), Variable(:y)])
result = evaluate_with_context(tree, ctx) # 3.0SymbolicOptimization.safe_evaluate_with_context — Function
safe_evaluate_with_context(tree::AbstractNode, ctx; default=NaN) -> AnySafely evaluate a tree, returning default on any error.
SymbolicOptimization.evaluate_sequential — Function
evaluate_sequential(tree::AbstractNode, make_context, update_context!, n_steps;
init_state=nothing) -> VectorEvaluate a tree sequentially over multiple steps.
Arguments
tree: The expression tree to evaluatemake_context: Function(state, step) -> contextthat creates evaluation contextupdate_context!: Function(state, result, step) -> nothingthat updates state after evaluationn_steps: Number of steps to runinit_state: Initial state passed to make_context
Returns
Vector of results from each step.
Example: Belief Updating
# State tracks current beliefs and data
mutable struct State
probs::Vector{Float64}
data::Vector{Bool}
end
make_ctx = (state, t) -> BeliefContext(state.probs, state.data, t, ...)
update_state! = (state, result, t) -> begin
state.probs = normalize(result)
end
results = evaluate_sequential(tree, make_ctx, update_state!, 250, init_state=State(...))