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 operators

Batch 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 succeeds

Compiled Evaluation

For evaluating the same tree many times, compile it to a function:

f = compile_tree(tree, [:x])
f(5.0)  # 6.0

Context-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:

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.evaluateFunction
evaluate(tree::AbstractNode, ctx::EvalContext) -> Any
evaluate(tree::AbstractNode, grammar::Grammar, ctx::EvalContext) -> Any
evaluate(tree::AbstractNode; kwargs...) -> Any

Evaluate 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.0
source
SymbolicOptimization.evaluate_batchFunction
evaluate_batch(tree::AbstractNode, data::AbstractMatrix, var_names::Vector{Symbol}) -> Vector
evaluate_batch(tree::AbstractNode, grammar::Grammar, data::AbstractMatrix, var_names::Vector{Symbol}) -> Vector

Evaluate a tree over multiple data points.

Arguments

  • tree: The expression tree to evaluate
  • data: Matrix where each row is a data point, columns correspond to variables
  • var_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]
source
evaluate_batch(tree::AbstractNode, data::Vector{<:Dict}) -> Vector

Evaluate a tree over a vector of binding dictionaries.

source
SymbolicOptimization.compile_treeFunction
compile_tree(tree::AbstractNode, var_names::Vector{Symbol}) -> Function

Compile 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.0
source
SymbolicOptimization.EvalContextType
EvalContext

A 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)  # true

Advanced 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
source
SymbolicOptimization.with_bindingsFunction
with_bindings(ctx::EvalContext, bindings::Dict) -> EvalContext
with_bindings(ctx::EvalContext; kwargs...) -> EvalContext

Create a new context with additional/overridden bindings.

source
SymbolicOptimization.AbstractEvalContextType
AbstractEvalContext

Abstract 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...) -> Any
  • has_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)
source
SymbolicOptimization.SimpleContextType
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)
source
SymbolicOptimization.VectorAggregatorContextType
VectorAggregatorContext

Pre-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)
source
SymbolicOptimization.resolve_variableFunction
resolve_variable(ctx::AbstractEvalContext, name::Symbol) -> Any

Resolve a variable name to its value in the given context. Must be implemented by subtypes.

source
resolve_variable(ctx::EvalContext, name::Symbol)

Resolve variable for the standard EvalContext.

source
SymbolicOptimization.apply_operatorFunction
apply_operator(ctx::AbstractEvalContext, op::Symbol, args...) -> Any

Apply 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.

source
SymbolicOptimization.has_variableFunction
has_variable(ctx::AbstractEvalContext, name::Symbol) -> Bool

Check if a variable exists in the context. Default implementation tries to resolve and catches errors.

source
SymbolicOptimization.evaluate_with_contextFunction
evaluate_with_context(tree::AbstractNode, ctx) -> Any

Evaluate 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.0
source
SymbolicOptimization.evaluate_sequentialFunction
evaluate_sequential(tree::AbstractNode, make_context, update_context!, n_steps;
                    init_state=nothing) -> Vector

Evaluate a tree sequentially over multiple steps.

Arguments

  • tree: The expression tree to evaluate
  • make_context: Function (state, step) -> context that creates evaluation context
  • update_context!: Function (state, result, step) -> nothing that updates state after evaluation
  • n_steps: Number of steps to run
  • init_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(...))
source