Skip to content

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's Paused state 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 above 2^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, the limit, 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-compat and optuna-rdb, and what each pulls in.
  • Storage — the seam import writes through.