Table of Contents

pycma

OptunaSharp.Interop.PyCma provides PyCmaSampler, a C# sampler that calls the Python cma package's CMAEvolutionStrategy and BoundTransform. The numerical CMA-ES implementation runs in Python; OptunaSharp owns the study, trial history and parameter conversion.

This is the compatibility bridge for Python's optuna_integration.PyCmaSampler. Its constructor emits the upstream deprecation warning introduced in 4.9.0 with removal scheduled for 6.0.0. The managed OptunaSharp.Samplers.CmaEsSampler instead ports the separate cmaes implementation and is the core alternative. The two backends should not be expected to return identical suggestions.

Install and optimize

dotnet add package OptunaSharp.Interop.PyCma
python -m pip install cma

The package targets .NET 8 and .NET 10, brings the shared Python.NET runtime transitively, and needs a discoverable CPython containing cma. It does not require the Python optuna_integration package at application runtime.

using System;
using OptunaSharp;
using OptunaSharp.Interop.PyCma;

var study = Optuna.CreateStudy(sampler: new PyCmaSampler(seed: 42));
study.Optimize(trial =>
{
    double x = trial.SuggestFloat("x", -5, 5);
    double y = trial.SuggestFloat("y", -5, 5);
    return x * x + y * y;
}, nTrials: 50, nJobs: 1);

Console.WriteLine(study.BestValue);

Use at least two varying parameters. A one-dimensional relative space falls back to the independent sampler; a multi-objective study raises an error. Construction imports cma immediately.

Configure the optimizer

Argument Behavior
x0 Initial values keyed by parameter name, in the objective's external representation
sigma0 Initial global step size in CMA coordinates
cmaStds Per-parameter multipliers of the global step size; unspecified entries use 1
seed Seeds CMA-ES and the default independent sampler
cmaOpts Additional Python CMAEvolutionStrategy options, such as popsize or verbose
nStartupTrials Completed-trial threshold before CMA sampling; default 1
independentSampler Startup and out-of-intersection sampler; seeded RandomSampler by default
warnIndependentSampling Controls the fallback warning; default true
using System.Collections.Generic;
using OptunaSharp.Interop.PyCma;

var sampler = new PyCmaSampler(
    x0: new Dictionary<string, object?> { ["x"] = 0.0, ["y"] = 0.0 },
    sigma0: 0.5,
    cmaStds: new Dictionary<string, double> { ["x"] = 1.0, ["y"] = 2.0 },
    cmaOpts: new Dictionary<string, object?> { ["popsize"] = 8, ["verbose"] = -2 },
    seed: 42);

Without x0, numeric distributions use their midpoint, with log distributions using a midpoint in log space; categoricals start from a middle choice. Without sigma0, the sampler uses the smallest transformed parameter range divided by six, with a small positive floor to avoid division by zero. Provide x0 for every parameter in the relative space when overriding it.

The sampler supplies seed, BoundaryHandler and bounds to cmaOpts itself; these values replace conflicting dictionary entries. Use the typed cmaStds argument for per-parameter scaling, which overrides CMA_stds when supplied. Additional option values should be Python.NET-convertible primitives. verbose defaults to -2 when absent. There is no generic Python callable option API.

Parameter mapping

Unlike the native CmaEsSampler, this bridge keeps categoricals in the relative search space and maps each choice to an ordinal coordinate. A category is recovered by rounding to its choice index; the ordering of choices therefore participates in the search geometry.

Log distributions are optimized in log space. Stepped floats use an offset coordinate, while integer and stepped values are rounded back onto their grids. Single-value distributions are skipped, and conditional or changing parameters outside the intersection use the independent sampler. The preserved upstream fallback warning mentions categorical distributions broadly, but the actual relative-sampling implementation supports ordinal categoricals and has a dedicated parity test.

Replay, persistence and concurrency

On each relative suggestion the sampler creates a fresh Python optimizer with its seed, reconstructs completed generations from compatible stored trials, performs ask/tell for those generations, and asks for the next candidate. Maximizing objective values are negated before tell, because CMA-ES minimizes. Only full completed generations update the optimizer; an unfinished generation determines which candidate is returned next.

The Python optimizer is disposed within the same GIL scope. No live CMA optimizer is retained between trials, and PyCmaSampler does not implement IDisposable. Its managed seed, initial mean, intersection cache and independent sampler are still stateful: use a separate instance per study.

The replay code accounts for compatible trials in other states when choosing a candidate index and can advance extra ask calls when that index exceeds a population. This is not a promise of an identical trajectory under asynchronous completion. Use fixed configuration, a fixed seed and nJobs: 1 for reference comparisons. Persisted study history is used for replay; arbitrary changes to the configuration or independent sampler state on restart do not reproduce an uninterrupted run.

Validation and scope

The live parity tests compare against Python optuna_integration.PyCmaSampler: two- and three-dimensional spheres, mixed float/integer spaces, categoricals, logarithmic and stepped parameters, maximization, startup counts, explicit means/scales, population sizes and reproducibility. They also check the one-dimensional fallback and multi-objective rejection. The fixture reports inconclusive when cma or the Python reference integration is unavailable.

These seeded comparisons validate the tested configurations on the same backend. They do not claim that pycma equals cmaes, that Python version upgrades preserve every trajectory, or that the sampler provides native AOT support. There is no constraint callback or multi-objective extension on this sampler's public API.

Source: sampler and generation replay, Python crossing.