Study and Trials
A study is one optimization session; a trial is one evaluation of your objective. Together
they implement Optuna's define-by-run model: the objective suggests hyperparameters as it runs,
and the sampler draws them from the search space it has inferred so far. OptunaSharp ports
optuna.study and optuna.trial; the entry points live in the root OptunaSharp namespace.
Your first study
using OptunaSharp;
var study = Optuna.CreateStudy(direction: StudyDirection.Minimize);
study.Optimize(trial =>
{
double x = trial.SuggestFloat("x", -10, 10);
return (x - 2) * (x - 2);
}, nTrials: 100);
Console.WriteLine(study.BestValue); // ~0
Console.WriteLine(study.BestParams["x"]); // ~2
The objective is a Func<Trial, double>. Its return value is the number to minimize (or maximize).
Creating a study
public static Study CreateStudy(
BaseStorage? storage = null, // default: InMemoryStorage
BaseSampler? sampler = null, // default: TPESampler (single-obj) / NSGAIISampler (multi-obj)
BasePruner? pruner = null, // default: MedianPruner
string? studyName = null, // default: an auto-generated "no-name-…"
StudyDirection? direction = null, // Minimize (default) or Maximize
bool loadIfExists = false, // resume instead of throwing if the name exists
IEnumerable<StudyDirection>? directions = null); // multi-objective (see below)
Related module functions on Optuna: LoadStudy, DeleteStudy, CopyStudy,
GetAllStudySummaries, and GetAllStudyNames. Pass a persistent storage plus
studyName and loadIfExists: true to make a run resumable. GetAllStudySummaries returns
StudySummary records — a study's name, direction(s), best trial, trial count, timestamps, and
attributes — a lightweight snapshot that never loads every trial.
Suggesting hyperparameters
Inside the objective, ask the trial for each hyperparameter. The sampler decides the value; you get a concrete number or choice back immediately.
double lr = trial.SuggestFloat("lr", 1e-5, 1e-1, log: true); // log-uniform
double drop = trial.SuggestFloat("dropout", 0.0, 0.5, step: 0.1); // stepped
long units = trial.SuggestInt("units", 16, 256, log: true);
long layers = trial.SuggestInt("layers", 1, 5); // step defaults to 1
// Categorical — a typed overload returns the element type directly:
string optimizer = trial.SuggestCategorical("optimizer", new[] { "adam", "sgd", "rmsprop" });
bool useBn = trial.SuggestCategorical("batch_norm", new[] { true, false });
A C# enum drives a categorical directly — its members become the string choices and the value comes
back typed (see Pythonic search space):
enum Optimizer { Adam, Sgd, RmsProp }
// inside the objective — over every declared member:
Optimizer optimizer = trial.SuggestCategorical<Optimizer>("optimizer");
The full signatures (on the shared BaseTrial base):
double SuggestFloat(string name, double low, double high, double? step = null, bool log = false);
long SuggestInt(string name, long low, long high, long step = 1, bool log = false);
CategoricalChoice SuggestCategorical(string name, IEnumerable<CategoricalChoice> choices);
// plus typed convenience overloads for string / bool / int / long / double sequences,
// and SuggestCategorical<TEnum>(name) / (name, subset) for enums
step and log are mutually exclusive for floats, exactly as in Optuna. Categorical values are
modeled with CategoricalChoice, which implements Python equality semantics
(true == 1, 1.0 == 1, NaN != NaN).
Ask-and-tell
For loops you control yourself — a training framework's own loop, batched evaluation, or
human-in-the-loop tuning — use ask/tell instead of Optimize:
for (int i = 0; i < 100; i++)
{
Trial trial = study.Ask();
double x = trial.SuggestFloat("x", -10, 10);
study.Tell(trial, (x - 2) * (x - 2));
}
Tell has overloads taking a Trial or a trial number, a single double or an
IReadOnlyList<double> (multi-objective), and an explicit TrialState (e.g. to record a failure).
Ask optionally takes fixedDistributions to pin part of the search space.
Reading results
double best = study.BestValue; // single-objective
var bestParams = study.BestParams; // IReadOnlyDictionary<string, object?>
FrozenTrial bestTrial = study.BestTrial;
List<FrozenTrial> all = study.Trials;
Filter by state with GetTrials(deepcopy: true, states: new[] { TrialState.Complete }). TrialState
is Running, Complete, Pruned, Fail, or Waiting; state.IsFinished() is a handy extension.
Pruning unpromising trials
Report intermediate values during training and let the pruner stop hopeless trials early:
study.Optimize(trial =>
{
for (int step = 0; step < 100; step++)
{
double loss = TrainOneEpoch(trial, step);
trial.Report(loss, step);
if (trial.ShouldPrune())
throw new TrialPruned();
}
return FinalScore(trial);
}, nTrials: 50);
Optimize catches TrialPruned and records the trial as Pruned.
Attributes, enqueueing, and manual trials
study.SetUserAttr("dataset", "cifar10"); // arbitrary metadata on the study
trial.SetUserAttr("fold", 3); // …or on a trial
study.EnqueueTrial(new() { ["x"] = 0.0 }); // evaluate specific params next
study.AddTrial(FrozenTrial.CreateTrial( // inject an already-evaluated trial
value: 0.5, @params: new() { ["x"] = 1.5 }));
Callbacks and trial limits
Optimize takes a callbacks list of Action<Study, FrozenTrial> run after every trial — for logging,
early exit, or bookkeeping. MaxTrialsCallback caps a study at a total number of trials in chosen
states across every worker, independent of how many nTrials any single Optimize call requested
(useful when several processes share one storage):
using OptunaSharp;
study.Optimize(Objective, nTrials: 1000, callbacks: new[]
{
new MaxTrialsCallback(100, new[] { TrialState.Complete }).Invoke, // stop once 100 have completed
});
Trial types
Trial— the live handle passed to your objective; itsSuggest*calls draw from the sampler.FrozenTrial— an immutable snapshot instudy.Trials; exposesNumber,State,Value/Values,Params,UserAttrs,Duration,IntermediateValues, andDeepCopy(). Build one with the staticFrozenTrial.CreateTrial(...)for tests orAddTrial.FixedTrial— returns preset parameters, for unit-testing an objective without a study:new FixedTrial(new() { ["x"] = 2.0 }).
Exporting trials
study.TrialsDataframe() returns a TrialsTable (the analog of Optuna's pandas
trials_dataframe()) with a column per parameter, value, attribute, and timestamp.
Multi-objective studies
Pass directions instead of direction to optimize several objectives at once; the objective then
returns an IReadOnlyList<double>. See Multi-objective Optimization.
Relationship to Optuna
| Optuna (Python) | OptunaSharp |
|---|---|
optuna.create_study(direction="minimize") |
Optuna.CreateStudy(direction: StudyDirection.Minimize) |
study.optimize(objective, n_trials=100) |
study.Optimize(objective, nTrials: 100) |
trial.suggest_float("x", -10, 10, log=True) |
trial.SuggestFloat("x", -10, 10, log: true) |
trial.suggest_int("n", 1, 5) |
trial.SuggestInt("n", 1, 5) |
trial.suggest_categorical("k", ["a","b"]) |
trial.SuggestCategorical("k", new[] { "a", "b" }) |
study.best_params / study.best_value |
study.BestParams / study.BestValue |
study.ask() / study.tell(t, v) |
study.Ask() / study.Tell(t, v) |
raise optuna.TrialPruned() |
throw new TrialPruned() |
Under a fixed seed, OptunaSharp reproduces Optuna's trial sequence bit-for-bit wherever the path is deterministic — the guarantee the port is built around.
See also
Distributions · Samplers · Pruners · Storage · Multi-objective Optimization · the API reference.