Interoperate with Optuna¶
Goal: keep your existing Optuna study and your existing dashboard.
Optuna is where most hyperparameter tuning happens, and "rewrite everything" is not a migration plan. atune reads and writes Optuna's own storage formats, so a study can cross in either direction without either tool knowing about the other.
This page is about moving the study. If what you are moving is the source code
— an objective, a sampler argument, a trial.report call — that is
Migrate from Optuna, which maps the two Python APIs onto
each other and names the places where the same line means something else.
Two formats, and the difference matters:
| Format | What it is | atune direction |
|---|---|---|
| Journal file | Optuna's append-only op-log, one JSON object per line | read and write |
| RDB-sqlite | Optuna's relational on-disk database | write only |
Export an atune study to Optuna¶
In Python: an in-memory reading snapshot¶
Install the optional extra atune[optuna] (Optuna 5), then call
atune.optuna.to_optuna_study on an atune study. The result is an
independent optuna.Study backed by InMemoryStorage, suitable for Optuna's
reading tools. atune storage stays the source of truth. Nothing written to
the copy flows back. Importing atune or atune.optuna does not import Optuna;
calling the adapter without it raises an ImportError naming the extra.
from atune.optuna import to_optuna_study
study.optimize(lambda trial: trial.suggest_float("x", 0.0, 1.0), n_trials=3)
snapshot = to_optuna_study(study)
require(isinstance(snapshot, optuna.Study), "snapshot is not an Optuna Study")
require(len(snapshot.trials) == len(study.trials), "snapshot lost trials")
require(snapshot.trials[0].state.name == "PRUNED", "snapshot lost pruning")
snapshot.set_user_attr("copy_only", True)
require("copy_only" not in study.user_attrs, "snapshot wrote back")
Optuna's plot_optimization_history, plot_param_importances, and
plot_pareto_front accept the copy. Install Plotly for the default plotting
backend and scikit-learn for the default importance evaluator. The
callback examples save temporary HTML history plots.
The pinned adapter gate serializes all three figures.
Trials retain their order, numbers, per-trial distributions, directions,
values, user attributes and constraints. Categorical choices use string
labels, matching the file converters: an imported numeric choice such as 32
becomes the label "32", and an export writes labels as strings. Boolean
distributions keep False and True. Paused trials map to RUNNING;
multi-objective intermediate reports retain objective zero. The copy has fresh timestamps and omits worker, retry,
checkpoint, lineage and per-seed payloads. NaN objectives map to FAIL with
atune:original_state and atune:objective_not_finite attributes.
Stop concurrent writers when attributes and trials must reflect one instant. This snapshot promises Optuna's reading interface only. The file converters below remain the route for persistent Optuna storage.
File converters¶
The command is atune export, and the flags name the format. --optuna PATH writes
a journal file; --optuna-rdb PATH writes the sqlite database. With neither, the
journal goes to stdout. Both may be given at once, each naming its own output — see
the CLI reference for the exact surface, which is generated
from the binary and therefore cannot drift from it.
The RDB output is the one to reach for if the goal is the dashboard:
optuna-dashboard sqlite:///PATH opens it, as does
optuna.load_study(storage="sqlite:///PATH"). Nothing atune-specific is needed on
the Optuna side; the schema is Optuna's own, verified against Optuna 5.0.0
(schema version 12, Alembic head v3.2.0.a, library stamp 5.0.0).
Journal exports use UTC-aware timestamps with six fractional digits and
+00:00; Optuna 5 loads them as local naive datetimes representing the same
instant. RDB timestamps remain naive UTC in the database; Optuna 5 also converts
them to local naive datetimes when loading trials.
Constraints¶
A trial's canonical constraint vector exports as the Optuna system attr
constraints, a list of floats, in both formats. Optuna 5's trial.constraints
exposes these entries as keys "0", "1", and so on. Per-replicate constraints
stay in the existing fan payloads.
Import reads this list and Optuna 5's named constraints:<key> system attrs.
Named entries override matching list indices. ASCII unsigned decimal keys sort
by numeric value (arbitrary length, lexical ties), followed by other keys in
lexical order: "2" precedes "10", which precedes "c0". Each non-empty
constraint declaration in a study must have the same key set; inconsistent
keys or widths return Error::Incompatible before any destination mutation.
Empty declarations remain unconstrained. Names are mapped into vector positions;
atune does not retain the source constraint names. Legacy atune user attr
atune:constraints remains a read alias when system constraints are absent.
Other system attrs are skipped and counted.
Import an Optuna study into atune¶
atune import <journal> --storage <spec> reads an Optuna journal and reconstructs
the study through atune's storage seam: distributions parsed by parameter name,
categoricals resolved by label, non-finite values decoded from Optuna's string
tokens. It reports what it did as counts, which is how you find out that something
was skipped rather than discovering it later.
The importer also reads rustuna 0.1.1 journals. Its user_attr_str values
remain strings; system_attr_str constraint values parse as decimal numbers.
When these maps are present, their companion {"rustuna": null} dummy maps are
ignored. DISCARD_TRIALS (op 10) is validated, skipped and counted with a report
note; discarded trials remain visible, matching Optuna 5 and rustuna's default
replay. Malformed operations abort. Empty parameter names are explicitly refused
with Error::Incompatible, because atune's search space requires non-empty names.
The reverse of the RDB path does not exist: atune writes Optuna's database but does not read it. If your study is in an RDB, export it from Optuna to a journal first.
What does not survive the round trip¶
This is the part worth reading before you rely on it. The mapping is faithful where the two models agree and lossy where they do not, and the losses are counted rather than silent:
- Timestamps become import-time. Optuna's own timestamps are not carried across.
- A paused trial exports as
RUNNING. atune'sPausedstate has no Optuna equivalent, so it degrades to the nearest one rather than being dropped. - Worker attribution, retry counts, and multi-seed parent/fan structure are dropped. They have nowhere to go in Optuna's model.
- Wide integer parameters may lose precision. Optuna represents parameter
values internally as
f64, so it cannot preserve every distinct integer above2^53. Round-tripping such values can change an integer; keep Optuna integer ranges within the exact-integer range when exact fidelity matters. - An open search space exports its trials, not its policy. Every trial of
a grown study carries the range it was actually drawn from — the export
writes each trial with its own recorded distribution, so nothing about
what happened is lost — but the declaration itself (
open, the sides, thelimit, the growth knobs) has no Optuna counterpart and is dropped. A re-imported study is therefore a closed one; declare the range open again and you have made a new declaration on a new study, exactly as the write-once rule intends. - A NaN objective becomes a failed trial, which is Optuna's own semantics for it, with a note attribute recording what happened. A NaN intermediate uses Optuna's native encoding and is lossless.
That last pair is not a detail. A NaN written the naive way produces a database Optuna's loader refuses to open — the whole study, not the one trial. Both cases are regression-tested against a real Optuna load.
What verifies this page¶
The command surface is generated in the CLI reference.
CLI integration tests cover import/export, including SHA-256-pinned Optuna 5.0.0
and rustuna 0.1.1 fixtures.
Real round trips are an explicit ignored gate, run in CI using CPython 3.10 and
.github/release/python-interop-requirements.txt. Set ATUNE_OPTUNA_PYTHON to
that environment's interpreter and run the interop gate.
The tests fail if Optuna is missing or its version differs from 5.0.0; they
verify both constrained exports and warning-free NSGA-II/TPE continuation.
The Python snapshot is exercised by the executable migration example and
crates/atune_py/tests/test_optuna_adapter.py. The Rust workflow builds an
installed wheel, requires Optuna 5.0.0, and runs that suite plus all three CPU
callbacks with hash-pinned inputs.
Next¶
- Migrate from Optuna — the other half: the Python API mapping, and the six places a migrated line means something else.
- Inspect a study — atune's own viewers, if the dashboard was the only reason for the round trip.
- Feature reference —
optuna-compatandoptuna-rdb, and what each pulls in. - Storage — the seam import writes through.