Current section
Files
Jump to
Current section
Files
lib/fixed_trial.ex
defmodule Scout.FixedTrial do
@moduledoc """
Fixed trial for testing objective functions with predefined parameters.
Equivalent to Optuna's FixedTrial, allows testing objective functions
without running a full optimization.
## Example
def objective(trial) do
x = Scout.Trial.suggest_float(trial, "x", -1.0, 1.0)
y = Scout.Trial.suggest_int(trial, "y", -5, 5)
x + y
end
# Test with fixed values
trial = Scout.FixedTrial.new(%{"x" => 1.0, "y" => -1})
assert objective(trial) == 0.0
trial = Scout.FixedTrial.new(%{"x" => -1.0, "y" => -4})
assert objective(trial) == -5.0
"""
defstruct [
:params,
:user_attrs,
:system_attrs,
:intermediate_values,
:id,
:study_id
]
@doc """
Creates a new FixedTrial with predefined parameter values.
"""
def new(params, opts \\ []) do
%__MODULE__{
params: params,
user_attrs: Keyword.get(opts, :user_attrs, %{}),
system_attrs: Keyword.get(opts, :system_attrs, %{}),
intermediate_values: Keyword.get(opts, :intermediate_values, %{}),
id: Keyword.get(opts, :id, "fixed_trial_#{System.unique_integer([:positive])}"),
study_id: Keyword.get(opts, :study_id, "test_study")
}
end
@doc """
Suggests a float value. Returns the fixed value if available.
"""
def suggest_float(%__MODULE__{params: params}, name, low, high, opts \\ []) do
case Map.get(params, name) do
nil ->
# Generate default value if not provided
if Keyword.get(opts, :log, false) do
:math.sqrt(low * high) # Geometric mean for log scale
else
(low + high) / 2 # Arithmetic mean
end
value ->
# Validate bounds
if value < low or value > high do
raise ArgumentError,
"Fixed value #{value} for '#{name}' is outside bounds [#{low}, #{high}]"
end
value
end
end
@doc """
Suggests an integer value. Returns the fixed value if available.
"""
def suggest_int(%__MODULE__{params: params}, name, low, high, opts \\ []) do
case Map.get(params, name) do
nil ->
# Generate default value if not provided
if Keyword.get(opts, :log, false) do
round(:math.sqrt(low * high))
else
div(low + high, 2)
end
value ->
# Validate bounds
if value < low or value > high do
raise ArgumentError,
"Fixed value #{value} for '#{name}' is outside bounds [#{low}, #{high}]"
end
round(value)
end
end
@doc """
Suggests a categorical value. Returns the fixed value if available.
"""
def suggest_categorical(%__MODULE__{params: params}, name, choices) do
case Map.get(params, name) do
nil ->
# Return first choice as default
hd(choices)
value ->
# Validate choice
if value not in choices do
raise ArgumentError,
"Fixed value #{inspect(value)} for '#{name}' is not in choices #{inspect(choices)}"
end
value
end
end
@doc """
Reports an intermediate value. For testing, just stores it.
"""
def report(%__MODULE__{} = trial, value, step) do
intermediate_values = Map.put(trial.intermediate_values, step, value)
%{trial | intermediate_values: intermediate_values}
end
@doc """
Checks if trial should be pruned. Always returns false for fixed trials.
"""
def should_prune?(%__MODULE__{}), do: false
@doc """
Sets a user attribute.
"""
def set_user_attr(%__MODULE__{} = trial, key, value) do
user_attrs = Map.put(trial.user_attrs, key, value)
%{trial | user_attrs: user_attrs}
end
@doc """
Gets a user attribute.
"""
def get_user_attr(%__MODULE__{user_attrs: attrs}, key, default \\ nil) do
Map.get(attrs, key, default)
end
@doc """
Sets a system attribute.
"""
def set_system_attr(%__MODULE__{} = trial, key, value) do
system_attrs = Map.put(trial.system_attrs, key, value)
%{trial | system_attrs: system_attrs}
end
@doc """
Gets a system attribute.
"""
def get_system_attr(%__MODULE__{system_attrs: attrs}, key, default \\ nil) do
Map.get(attrs, key, default)
end
@doc """
Creates a test helper for property-based testing.
Generates random fixed trials within the search space.
"""
def generate(search_space) do
params = for {name, spec} <- search_space, into: %{} do
value = case spec do
{:uniform, min, max} ->
min + :rand.uniform() * (max - min)
{:log_uniform, min, max} ->
log_min = :math.log(min)
log_max = :math.log(max)
:math.exp(log_min + :rand.uniform() * (log_max - log_min))
{:int, min, max} ->
min + :rand.uniform(max - min + 1) - 1
{:choice, choices} ->
Enum.random(choices)
end
{name, value}
end
new(params)
end
@doc """
Validates an objective function with multiple test cases.
## Example
test_cases = [
{%{"x" => 0, "y" => 0}, 0},
{%{"x" => 1, "y" => 1}, 2},
{%{"x" => -1, "y" => 2}, 1}
]
Scout.FixedTrial.validate_objective(objective, test_cases)
"""
def validate_objective(objective_fn, test_cases) do
results = Enum.map(test_cases, fn {params, expected} ->
trial = new(params)
actual = objective_fn.(trial)
%{
params: params,
expected: expected,
actual: actual,
passed: abs(actual - expected) < 1.0e-6
}
end)
all_passed = Enum.all?(results, & &1.passed)
{all_passed, results}
end
@doc """
Runs an objective function with a fixed trial and returns detailed results.
"""
def run(objective_fn, params, opts \\ []) do
trial = new(params, opts)
start_time = System.monotonic_time(:microsecond)
result = try do
{:ok, objective_fn.(trial)}
rescue
e -> {:error, e}
end
end_time = System.monotonic_time(:microsecond)
duration = (end_time - start_time) / 1_000_000 # Convert to seconds
%{
params: params,
result: result,
duration: duration,
trial: trial
}
end
@doc """
Creates a mock study for testing with fixed trials.
"""
def mock_study(trials_data) do
trials = Enum.map(trials_data, fn {params, value} ->
%Scout.Trial{
id: "trial_#{System.unique_integer([:positive])}",
study_id: "mock_study", # Add required field
bracket: 0, # Add required field
params: params,
score: value,
status: :completed
}
end)
best_trial = Enum.min_by(trials, & &1.score)
%{
trials: trials,
best_trial: best_trial,
best_params: best_trial.params,
best_value: best_trial.score,
n_trials: length(trials)
}
end
end