AI context for solving user optimization problems with fcmaes-rust
This file is operational context for an AI that must design and implement an optimization solution with this repository. It is not a claim that one optimizer is universally best. Use the problem structure, evaluation cost, and the user’s desired output to choose a small set of defensible candidates, then compare them under the same declared objective-call and compute-resource protocol.
The repository is a Rust 2024 Cargo workspace. fcmaes-core is a 100% native
Rust optimizer implementation; it does not wrap, link, load, or invoke the
original fast-cma-es C++ backend. examples contains native Rust objective
functions and executable applications; fcmaes-py is an optional low-level
PyO3 extension exposing the Rust core, not an alternative implementation.
All optimizers minimize.
The public registry surfaces are deliberately smaller than the GitHub repository:
- Rust users depend on the published
fcmaes-corecrate. Rust 1.88 is the tested minimum supported toolchain. - Python users install the
fcmaes-rustdistribution and importfcmaes_rust. Published wheels support CPython 3.11 through 3.13. examples,tutorials, and the internal nativefcmaes-gtopsource are GitHub-only and are not separate published packages.
Keep distribution and API names distinct:
cargo add fcmaes-core -> use fcmaes_core
python -m pip install fcmaes-rust -> import fcmaes_rust
Installing a compatible CPython wheel requires neither a Rust toolchain nor a
C/C++ compiler. Building the Python package from its source distribution does
require Rust. Do not tell a Python user to install the historical fcmaes
package when they asked specifically for this Rust implementation.
Required workflow for the AI
Before selecting an algorithm, create a problem card containing the following facts. Inspect the user’s code and data where possible. Ask only for facts that cannot be inferred and would materially change the solution.
| Question | Why it matters |
|---|---|
| What is the decision dimension? | Population size, covariance cost, and useful budgets depend strongly on dimension. |
| What is each variable’s lower and upper bound? | Most global algorithms need a finite, meaningful search box. |
| Which variables are continuous, integer, ordinal, categorical, or structured? | DE and MODE have integer mutation masks, but categorical and structured variables still need decoding or repair. |
| Is there one objective, several objectives, or a quality-diversity archive? | This selects scalar optimization, MODE/weighted retry, or MAP-Elites. |
| Which directions are optimized? | Convert maximization to minimization by negating the value. |
| What are the constraints and feasibility convention? | MODE and moretry require constraints after objectives, feasible at g(x) <= 0. |
| Is a good initial point known? | CMA-ES, CR-FM-NES, PGPE, and local refinement benefit from it. |
| Is the objective smooth, discontinuous, noisy, stochastic, or multimodal? | Distribution search, ranking, restart strategy, and stopping tolerances differ. |
| For a stochastic objective, can simulations accept explicit seeds? | Fixed common-random-number seed sets make candidate comparisons fair; disjoint seeds are needed for validation. |
| How long does one evaluation take, and is it thread-safe? | This determines inner batch parallelism versus outer retry parallelism. |
| What is the evaluation, wall-time, memory, and core budget? | Equal calls alone can favor a sequential method or hide surrogate overhead; declare both call and resource accounting. |
| Is a target value known? | Set stop_fitness; do not confuse it with value_limit. |
| Does the user need one solution, a Pareto front, or diverse behavior niches? | The output requirement is part of the algorithm choice. |
Do not proceed with arbitrary placeholder bounds. If bounds are genuinely unknown, derive physically meaningful limits, transform the variables, or use an explicitly supported unbounded path. An unnecessarily wide box can cost orders of magnitude more evaluations.
For every completed user solution, state the resulting problem card, chosen algorithm and rejected alternatives, objective/constraint convention, non-default parameters, evaluation and worker budgets, seeds, validation method, and reported quality statistics. Code without this configuration record is not a reproducible optimization solution.
First decide whether fcmaes is appropriate
Use the human-facing
Choosing an optimizer guide to route
structured, differentiable, evaluation-scarce, real-time, and
structure-evolution problems to more suitable tools before selecting an
fcmaes algorithm.
Do not select a gradient-free optimizer merely because the model is a simulation. Prefer a gradient-based optimizer when all decision variables are continuous, the end-to-end objective is differentiable, reliable analytical, automatic-differentiation, adjoint, or sensitivity gradients are available, and the required result is one optimum or a smooth Pareto trade-off. Those gradients usually carry much more information per evaluation.
fcmaes is a strong candidate when one or more of these conditions holds:
- gradients are unavailable, unreliable, or dominated by simulation noise;
- event appearance/disappearance, contact, failure, clipping, repair, routing, integer choices, or categorical decoding makes the objective nonsmooth;
- the landscape is multimodal and needs global exploration or restarts;
- the result must be a repertoire of behaviorally diverse solutions rather than one optimum; or
- a robust aggregate such as a worst case, quantile, or failure rate creates nonsmooth outer logic.
Events alone do not invalidate gradients. Diffsol, for example, provides
forward and adjoint sensitivities, so its smooth parameter-fitting tutorials
are naturally paired with L-BFGS or another gradient-based optimizer. Consider
fcmaes around such a model only when discrete policies, resets, robust maxima,
solver failures, or other end-to-end discontinuities make those sensitivities
misleading or unavailable. See
tutorials/README.md#23-diffsol-why-gradients-are-the-better-default.
The core boundary is intentional. Standalone local derivative-free methods, Bayesian optimization, and gradient solvers remain external and can be driven through retry’s optimizer closure. The corrected 20-seed optimizer-boundary experiment found no general DE→Nelder–Mead advantage and only a narrow, small-budget BO regime. Do not add an optimizer dependency to core merely because one application needs it.
Fast algorithm-selection decision tree
- If valid, economical gradients exist for the complete decision-to-objective map, start with a gradient-based method and use fcmaes only as a justified global, discrete, robust, or QD outer layer.
- If the user wants the best solution in many behavior niches rather than
one global optimum, use
Archiveplus MAP-Elites. Add Diversifier after the archive has useful coverage. - If there are two or more competing objectives:
- Use MODE when the user wants a Pareto population from one coordinated run, especially with explicit constraints or integer-decoded variables.
- Use
moretrywhen many independent scalar optimizer runs are desirable, when outer parallelism is important, or when a DE/CMA/BiteOpt pipeline is already effective for weighted scalar objectives.
- For one scalar objective:
- Start with BiteOpt for a difficult bounded, nonsmooth, multimodal, or poorly characterized black box.
- Start with DE for robust bounded exploration, mixed continuous/integer decoding, and DE-to-CMA pipelines.
- Use active CMA-ES when a useful guess exists, variables are continuous, and correlations or ill-conditioning are important.
- Use CR-FM-NES for higher-dimensional continuous distribution search where a full CMA covariance update is unattractive.
- Use PGPE for high-throughput mirrored batches, diagonal distribution search, or noisy objectives where rank-based updates help. See the fixed-topology neural-controller tutorial for an equal-protocol PGPE/CR-FM-NES comparison.
- Use Dual Annealing for low-to-moderate-dimensional global exploration, optionally followed by its bounded local search.
- If global structure is uncertain or local optima are likely, use independent retry. Use coordinated advanced retry when retained elites can usefully generate local crossover boxes and increasing budgets.
- Benchmark at least two plausible choices with identical bounds, objective, seeds, and a declared call/wall-resource protocol. A good general comparison is BiteOpt versus DE followed by CMA-ES. When algorithms have materially different sequential overhead or parallelism, equal objective calls are not an equal wall-time comparison.
Choose the integration surface
Prefer a native Rust objective when evaluation throughput matters. Put
read-only model data behind shared references or Arc, give each evaluation
isolated mutable state, and let fcmaes distribute candidates. This avoids
Python callback and serialization overhead and is the design demonstrated by
the application examples and native application tutorials.
The Buckingham–Pi example is also a useful pattern for optimized feature
discovery. It parameterizes every dimensionally valid exponent matrix as
E = Ns * C, scores regressions on a disjoint holdout set, rejects overflowing
log-features, and penalizes nearly dependent groups. Use this pattern when
algebraic structure can make all optimizer trials valid by construction.
Do not call an in-sample regression score “cross-validation,” and distinguish
the coefficient of variation from cross-validation in output and reports. See
docs/buckingham-pi.md.
Python users can still use the Rust optimizers through fcmaes_rust.
One-shot functions include optimize_de, optimize_acma,
optimize_crfmnes, optimize_pgpe, optimize_da, and optimize_bite;
stateful DE, ACMA, CRFMNES, PGPE, Bite, MODE, and Archive
classes expose the corresponding ask/tell or archive workflows. Retry is
available through minimize_retry, minimize_advanced_retry, and
minimize_moretry. Treat this as a low-level NumPy-oriented API and consult
docs/python-bindings.md for the authoritative signatures and result layouts.
The native optimizer loop releases the GIL, but every Python objective callback
must reacquire it. Cheap objectives written in Python therefore rarely scale
across threads. Python callbacks dominated by NumPy, another native extension,
or external work may scale only if that work releases the GIL. When the
objective itself can be implemented efficiently in Rust, prefer the direct
fcmaes-core path.
Algorithm comparison
| Algorithm | Best fit | Main limitation | Practical initial configuration |
|---|---|---|---|
Differential Evolution (De) | Bounded global search, discontinuities, mixed decoded variables, restarts | One-shot optimize is serial; convergence near a smooth optimum can be slower than CMA-ES | Default population 31; uniform initialization when no guess; give roughly 40% of a DE-to-CMA budget to DE; use ask/tell for external batching |
Active CMA-ES (Cmaes) | Continuous correlated or ill-conditioned variables, refinement from a guess | Full covariance work grows roughly quadratically with dimension; requires a guess and step size | Default population 31; normalized coordinates; initial sigma commonly 0.05–0.3 in normalized optimizer units |
CR-FM-NES (Crfmnes) | Medium/high-dimensional continuous search with complete batch evaluation | Needs a meaningful guess and scalar sigma; not a discrete optimizer | Even population, default 32; use optimize_batch and parallelize expensive batches |
PGPE (Pgpe) | High-dimensional mirrored sampling, noisy objectives, parallel batches | Diagonal search distribution does not learn a full covariance | Even population, default 32; ranking on; tune center and standard-deviation learning only after establishing a baseline |
Dual Annealing (optimize_da) | Low/moderate-dimensional global exploration, optional local refinement | No built-in population batching; often less attractive in very high dimensions | Keep local search enabled for deterministic objectives; disable it for very noisy or discontinuous objectives |
BiteOpt (BiteOpt, DeepBiteOpt) | Hard bounded black boxes, nonsmoothness, multimodality, minimal tuning | No native general constraint or categorical model; batched ask/tell changes feedback timing | Automatic population; depth 1 first, then try small deep values if the budget is large |
MODE (Mode) | Constrained multi-objective Pareto search | Requires an ask/tell driver and objective-plus-constraint rows | Population 64–256 initially; NSGA-II update; parallelize each asked population |
Weighted MO retry (moretry) | Parallel multi-objective scalarizations using any scalar optimizer | Quality depends on objective scaling and sampled weights; the retained set is not one evolving Pareto population | At least tens of retries; normalize objective magnitudes before selecting weight ranges |
MAP-Elites (map_elites) | Quality-diversity coverage over chosen descriptors | Requires meaningful behavior descriptors and fixed descriptor bounds | Capacity based on desired resolution; chunk at least the worker count; use the 2-D grid fast path when applicable |
Diversifier (diversify) | CMA-ME-style improvement of/filling an existing archive | More expensive than simple emitters and needs an initialized archive | Run MAP-Elites first; default CMA population 31 and stall criterion 20 |
These are starting hypotheses. Objective landscapes dominate generic rules.
Scalar optimizer parameters
All scalar objective values must be minimized and finite when possible. The
fitness layer replaces non-finite scalar results with NAN_REPLACEMENT = 1e99,
but an explicit, scaled penalty is easier to diagnose.
Fitness::set_normalize(true) normalizes decision coordinates to [-1,1];
it does not normalize objective or constraint values. Scale objective and
constraint terms explicitly when their magnitudes differ materially.
Differential Evolution
DeParams defaults are:
| Field | Default | Tuning guidance |
|---|---|---|
popsize | 31 | Increase for broad multimodal exploration or large dimensions; ensure the budget still permits many generations. |
max_evaluations | 100,000 | Set from the user’s real budget. |
keep | 200 | Usually leave unchanged; it controls temporal/history behavior. |
stop_fitness | negative infinity | Set only when a genuine target is known. |
f | 0.5 | Higher explores farther; lower takes smaller differential steps. Tune after population and budget. |
cr | 0.9 | Lower can help nearly separable problems; high values allow correlated coordinate changes. |
min_mutate / max_mutate | 0.1 / 0.5 | Controls the fraction used by optional integer mutation. |
min_sigma | 0 | Minimum adaptive sampling spread. Usually leave at zero. |
seed / runid | 0 / 0 | Set from the experiment or RetryContext. |
De::new(fitness, guess, sigma, integer_mask, params) uses uniform box
initialization when guess and sigma are empty. With a guess, supply one
sigma per dimension. Keep Fitness normalization disabled for the standard
DE path and provide the guess and sigma in physical variable units; the
canonical runner follows this convention. If normalized fitness is required
by custom ask/tell integration, encode every working point consistently. The
integer mask improves integer-coordinate mutation, but other DE operations
remain continuous: the objective must still perform
authoritative rounding, truncation, lookup, and repair.
Active CMA-ES
CmaesParams defaults are:
| Field | Default | Tuning guidance |
|---|---|---|
popsize | 31 | Increase for noise or multimodality; decrease cautiously for very expensive objectives. |
mu | 0 | Zero selects half the population. Usually leave automatic. |
max_evaluations | 100,000 | Give enough budget for multiple covariance updates. |
accuracy | 1.0 | Leave at default unless stopping behavior has been validated. |
stop_fitness | negative infinity | Use a known target. |
stop_tol_hist_fun | -1 | Negative selects the automatic history tolerance. Avoid tight tolerances on noise. |
update_gap | -1 | Negative selects the automatic covariance update interval. |
seed / runid | 0 / 0 | Use distinct values for independent runs. |
The guess must match the dimension. input_sigma has length one (broadcast)
or one value per coordinate. With Fitness::set_normalize(true), the mean is
encoded to [-1,1]; provide sigma in that optimizer space. A sigma of 0.1 is
roughly 5% of the full physical span. With normalization off, sigma is in raw
variable units. Cmaes::optimize(objective, workers) supports population
parallelism.
CR-FM-NES
CrfmnesParams defaults are population 32, 100,000 evaluations,
stop_fitness = -infinity, penalty_coef = 1e5, bound-violation handling on,
and seed/run ID zero. The population is forced to at least two. Supply a guess
and scalar sigma. With normalized Fitness, supply the guess in physical
coordinates and sigma in normalized optimizer coordinates. The “constraint
violation” option concerns box-bound violations; arbitrary user constraints
still require a penalty or another
constrained method. Use optimize_batch for expensive objectives.
PGPE
PgpeParams defaults are:
| Field | Default |
|---|---|
popsize | 32, rounded up to even |
max_evaluations | 100,000 |
stop_fitness | negative infinity |
lr_decay_steps | 1,000 |
use_ranking | true |
center_learning_rate | 0.15 |
stdev_learning_rate | 0.1 |
stdev_max_change | 0.2 |
b1 / b2 / eps | 0.9 / 0.999 / 1e-8 |
decay_coef | 1.0 |
seed / runid | 0 / 0 |
Supply a guess and either one standard deviation or one per coordinate. Keep
the guess in physical coordinates; when Fitness normalization is enabled,
the standard deviation is in normalized optimizer coordinates. Keep ranking
enabled for a robust first experiment. Increase the even population for noisy
objectives or to expose more parallel work. Change learning rates
one at a time and validate across multiple seeds.
Dual Annealing
DaParams contains max_evaluations = 100_000, use_local_search = true,
and seed/run ID zero. It supports finite bounds and an explicitly unbounded
path when both bound vectors are empty. Local search uses bounded projected
L-BFGS with finite-difference gradients; objective noise makes those
differences unreliable.
BiteOpt
BiteParams defaults are automatic population (0, resolving to
9 + 3*dimension), 100,000 evaluations, no finite stop target, automatic
stall criterion, and seed/run ID zero. The deep-mode argument to
optimize_bite is separate from BiteParams: values at most one select a
plain run; the validated maximum is 36.
Start with depth 1. Test depths 2–6 only when the problem is demonstrably multimodal and the evaluation budget is large enough to feed multiple internal populations. Ask/tell batching enables parallel evaluation but delays selector feedback; compare its convergence with the one-at-a-time path on cheap test versions.
Recommended scalar recipes
Unknown bounded black box
Run two fair baselines:
- BiteOpt with automatic population and depth 1.
- DE for approximately 40% of each run budget, then normalized CMA-ES from the DE result for the remaining approximately 60%.
The repository’s canonical DE-to-CMA restart implementation is
examples/src/runner.rs. It consumes every RetryContext field, converts
advanced-retry fractional standard deviations to DE’s physical units, and
uses normalized standard deviations for CMA-ES.
Known good continuous guess
Use normalized CMA-ES first. Compare against CR-FM-NES for larger dimensions. Choose initial per-coordinate sigma from uncertainty in the guess, not from a fixed constant. If the guess is known within about 10% of each range, a normalized sigma around 0.1–0.2 is a reasonable first test.
Expensive objective
Prefer one population run with inner batch evaluation when every generation is expensive and useful. Choose a population at least as large as the worker count, normally a small multiple of it. If global restarts are more important, use outer retry workers and keep inner optimizer workers at one.
If the simulator also owns a thread pool, benchmark two explicit topologies: many serial simulator evaluations under fcmaes versus fewer evaluations using the simulator’s internal parallelism. Keep the total worker allowance equal and do not enable both pools at full size.
Noisy objective
Use PGPE ranking, larger populations, or repeated evaluations. Do not use a
tight stop_fitness or convergence tolerance based on one noisy observation.
If the objective averages replications internally, report both optimizer calls
and the true number of simulations; fcmaes can count only calls it makes.
Use a fixed, named training seed set for every candidate so that optimizer comparisons use common random numbers. Re-evaluate finalists, Pareto points, and QD elites with a disjoint holdout seed set. For noisy QD, validate both quality and descriptor location because an elite can migrate to another niche under new stochastic paths.
Constraints and variable encoding
Use these conventions consistently:
- All objectives are minimized. Negate quantities that must be maximized.
- MODE and
moretryrows are[objective_0, ..., objective_m, constraint_0, ..., constraint_k]. - A constraint is feasible when
constraint <= 0. - Convert an equality
h(x) = 0toabs(h(x)) - tolerance <= 0when a tolerance is meaningful. - For scalar optimizers, use decoding/repair for hard structural rules and a
scaled penalty for residual violations. A common starting form is
objective + rho * sum(max(0, g_i(x))^2)after normalizing terms. - Return a large finite rejection value for invalid simulations. Reserve NaN for truly exceptional paths and test those paths explicitly.
- Encode a bounded integer as a real coordinate, round or truncate it in the objective, clamp it, and optionally provide DE/MODE’s integer mask.
- Encode a categorical variable as an index into a fixed list. Do not treat the numerical distance between unrelated categories as physically real.
- Repair permutations, schedules, and mutually exclusive choices
deterministically. See the job-shop, scheduling, harvesting, multi-UAV, and
Mazda examples for decoding patterns. Use
docs/combinatorial-encodings.mdfor the tested recipes, representation trade-offs, and repair-versus-constraint decision.
If a scalar penalty dominates everywhere, normalize the base objective and
constraint residuals before tuning rho. If it is too small, infeasible points
win; if it is too large, the landscape becomes nearly flat away from the
feasible boundary.
Multi-objective optimization
MODE
Use MODE for a coordinated population. Mode::try_new receives a bounded
Fitness, number of objectives, number of trailing constraints, an optional
integer mask, and ModeParams. Evaluate every row returned by ask, preserve
row order, then call tell once.
ModeParams defaults are population 64, f = 0.5, cr = 0.9, crossover
probability/index pro_c = 0.5 / dis_c = 15, mutation probability/index
pro_m = 0.9 / dis_m = 20, NSGA-II update enabled,
pareto_update = 0, integer mutation range 0.1–0.5, and seed/run ID zero.
Parameter guidance:
- Start with NSGA-II update.
- Use at least population 64. Try 128–512 for difficult fronts, more objectives, or cheap objectives; keep enough budget for many generations.
- The DE update requires at least four members.
- Scale objective magnitudes before judging front quality. Crowding distance itself is normalized across objectives, but simulation and reporting scales still matter.
- Filter feasible rows before calling
pareto_indicesfor the objective prefix. - Use
parallel_batchto evaluate each asked population. Ordered collection makes one- and multi-worker seeded runs identical for deterministic objectives.
Weighted multi-objective retry
MoRetryConfig::new(weight_lower, weight_upper) defaults to ordinary retry,
zero constraints, p-norm exponent 2, and no value limits. It samples a weight
vector for every retry and retains the unscalarized row and weights.
Use it when scalar runs are effective and independent weight vectors can run
in parallel. Set ncon to the number of trailing constraints. A positive
constraint adds its weight as a violation penalty. value_limits, when used,
contains one strict upper limit for every objective and constraint.
Normalize or shift objectives to comparable, preferably nonnegative scales
before selecting weight bounds. With a non-integer value_exp, negative
weighted values can make the p-norm undefined. Use pareto_indices on retained
unscalarized rows; do not report the scalarized retry value as a Pareto
objective.
Quality diversity
Use MAP-Elites only when behavior descriptors express diversity the user actually values. A descriptor is not another minimized objective.
- Define decision bounds and descriptor bounds.
- Construct
Archive::try_new. - Seed parent candidates with
seed_uniform. - Evaluate that initial population and update the archive.
- Run
map_elitesormap_elites_batch. - Optionally run
diversifyordiversify_batchto improve/fill niches. - Report occupancy, coverage, best fitness,
qd_score, and representative elites—not only the single best point.
MapElitesParams defaults are 100 generations, chunk 20, SBX enabled,
dis_c = 20, dis_m = 20, Iso+LineDD sigmas 0.02/0.2, and zero CMA-emitter
generations. DiversifierParams defaults are 100,000 evaluations, population
31, and stall criterion 20.
Archive guidance:
- In two descriptor dimensions,
samples_per_niche = 0selects the fast regular grid with O(1) lookup. Non-factorable capacities have ragged rows; useArchive::grid_layout()for rendering andArchive::capacity()for the coverage denominator rather than multiplyinggrid_shape()dimensions. - Positive
samples_per_nicheselects k-means CVT centers and nearest-center lookup. Use it for non-grid or higher-dimensional descriptor spaces. - Capacity controls resolution and memory. More niches also require more evaluations to obtain useful coverage.
- Set
chunk_size >= workers; 4–16 times the worker count is a useful throughput range for expensive objectives, subject to evaluation cost. - SBX is a good bounded default. Try Iso+LineDD when local variation along elite differences is more appropriate.
map_elites_batchanddiversify_batchevaluate concurrently, then mutate the archive serially in candidate order.QdBatchFitnessmust return exactly one(fitness, descriptor)pair per input in the same order.qd_scoreis higher-is-better but depends on the fitness convention: the implementation sums reciprocal fitness for an all-positive archive and negated negative elites otherwise. Compare it only between archives with the same quality definition, descriptor bounds, and niche geometry.
Lessons from the application tutorials
The twenty-two standalone tutorials are implementation references for expensive native objectives. They keep application dependencies outside the root workspace and demonstrate these transferable choices:
| Tutorial | Main problem property | Recommended lesson |
|---|---|---|
| NeXosim production line | Stochastic discrete events and mixed controls | Compare outer candidate parallelism with simulator-internal parallelism; use common random numbers and holdout seeds. |
| Rapier trebuchet | Contact and release discontinuities | Use BiteOpt retry for one target, MODE for engineering trade-offs, and QD only for meaningful trajectory behaviors. |
| ReBop oscillator | Intrinsic stochastic simulation noise | Fix candidate-comparison seeds, report true simulation counts, and validate Pareto/QD results on disjoint paths. |
| Oscillator topology search | Discrete signed graphs around variable-dimensional stochastic inner problems | Keep the agent or evolutionary proposer outside deterministic grammar, optimization, persistence, and held-out motif scoring; match random and evolutionary controls. |
| Brahe constellation | Access-window discontinuities and worst-gap aggregation | Keep feasibility explicit and assign either fcmaes or the simulator ownership of parallelism. |
| RustPower voltage control | Mixed-integer controls, contingencies, and solver failures | Return calibrated constraint violations for failed power flows; reject a QD formulation when descriptors do not produce an informative archive. |
| Atmospheric source localization | Censored inverse inference, model mismatch, and non-identifiability | Use robust residuals and disjoint sensors/weather; keep MODE for error/emission trade-offs and interpret a source-centroid QD map as alternative hypotheses, not a confidence region. |
| Room ventilation | Custom numerical backend, variable geometry, and grid sensitivity | A purpose-built backend can keep objective state isolated and fast, but then solver verification, held-out scenarios, constraint margins, and resolution sensitivity are part of the optimization evidence. |
| SmartCore hyperparameter tuning | Mixed variables, nested stochastic fitting, and validation overfitting | Use probability-aware objectives, common folds and model seeds, disjoint candidate selection, and a frozen final test; report model-fit cost as well as optimizer calls. |
| Neural controller policy search | 118-dimensional fixed-topology policy and randomized rollouts | Use PGPE or CR-FM-NES when full covariance is unattractive; use common per-population scenarios, rotate them deterministically, validate disjoint plants, and reserve a frozen final test. |
| GTOC1 “Save the Earth” | Narrow low-thrust closure constraints and model-fidelity differences | Use a staged fidelity ladder, repropagate the final continuous-thrust solution independently, and keep ephemeris/model qualifications attached to every score claim. |
| GTOC1 route search | Agent-proposed variable planet orders with unequal fidelity costs | Let deterministic Rust own grammar and equal-budget controls; archive failures and promote candidates through predeclared L0/L1/L2 gates before making trajectory claims. |
| sindr circuit design | Smooth circuit features plus discrete catalogue realization | Interpolate peaks and crossings instead of optimizing sampled arg-max staircases; validate the final catalogue under component tolerances. |
| thevenin gate driver | Transient thresholds, ringing, and simulator disagreement | Interpolate time-domain metrics and require timestep refinement plus an independent ngspice comparison before accepting a front. |
| Optical lens design | Multimodal ray tracing with hard ray-loss regions | Verify the native ray tracer independently, retain typed invalid designs, and benchmark global search rather than assuming a local simplex can cross penalty cliffs. |
| Rapier quadruped gait | Contact-driven behavior repertoires and terrain overfitting | Treat QD as the primary output only when descriptors remain meaningful and elites survive disjoint terrain replay. |
| Phased-array codebook | Quantized phase/attenuation registers and element failures | Cross-check direct and FFT kernels, optimize hardware codes rather than ideal phases, and apply the descriptor gate on the archive’s exact native layout. |
| Bilevel energy hub | Discontinuous sizing around a convex dispatch problem | Solve the inner LP to optimality, optimize only the outer nonconvex choices, and report candidate calls, LP solves, and pivots separately. |
| Field-service routing | Assignment/permutation plateaus and disrupted task sets | Prove decoder invariants, validate hard feasibility on disrupted scenarios, and skip QD when coverage or retention fails. |
| Water-network scheduling | Quantized controls with hydraulic state memory | Separate DDA optimization from PDA validation, record hydraulic failures, and reject repertoires that migrate under unseen demand. |
| Truss topology and sizing | Exact-cardinality topology plus mechanisms and conditioning | Decode exact-k structures, classify FEM failures, and test removal robustness instead of replacing mechanisms with arbitrary large stresses. |
| Network coverage | Large binary submodular structure with available certificates | Run exact tiny oracles and specialist greedy/certificate baselines before generic search; publish the specialist win when MODE is dominated. |
MODE and MAP-Elites are complementary, not substitutes. Keep MODE when the user needs objective trade-offs, and add MAP-Elites only when descriptor-space coverage is itself useful. A QD pilot that runs successfully but has poor or misleading coverage should remain a documented negative result rather than be promoted into the main formulation.
Do not treat a custom simulator as independently validated merely because its optimizer integration is tested. Separate three questions: whether the numerical kernel satisfies reference properties, whether selected designs survive held-out scenarios and resolution changes, and whether the physical model is adequate for the user’s decision. The room-ventilation tutorial demonstrates the first two while explicitly declining engineering claims about the third.
Retry selection and parameters
Basic retry
Use retry for independent restarts. RetryConfig defaults are 1,024 retries,
available parallelism (workers = 0), retained capacity 500, no value filter,
no finite stop target, 50,000 evaluations per retry, root seed zero, and no
improvement-history samples.
Set these fields explicitly in production:
num_retries: number of independent runs, at least the worker count.workers: zero for available cores or a specific outer worker count.max_evaluations: budget for each run, not the global total.capacity: number of distinct results to retain; one is enough if only the best point matters, but advanced retry and diagnostics benefit from more.value_limit: store only completed results strictly better than this filter.stop_fitness: stop claiming new runs after the stored best reaches this target.seed: root of independently spawned persistent worker RNG streams.statistic_num: maximum retained best-improvement samples.
Total configured work is approximately num_retries * max_evaluations, unless
early stopping or algorithm termination reduces it. Always return the actual
evaluation count in RetryRunResult.
Coordinated advanced retry
Use advanced_retry when elite crossover and adaptive run budgets are likely
to help. Defaults are 5,000 retries, 1,500 starting evaluations, maximum budget
factor 50, checkpoint interval 100, crossover probability 0.5, and normalized
diversity threshold 0.15.
The optimizer closure must use:
context.bounds, which may be a local crossover box;context.guess, when present;context.sdev, as per-coordinate fractional step information;context.max_evaluations, which grows with retry progress;context.run_seedwhen a run must reproduce independently of worker scheduling;context.seedretains the older worker-stream behavior;context.run_id;context.value_limit, which for crossover may require beating a parent.
Ignoring these fields defeats coordinated retry. Start with basic retry for a new objective, then compare advanced retry under equal total evaluations.
Parallelism rules
There are two distinct levels:
- Outer parallelism:
retry,advanced_retry, ormoretryruns independent optimizers on worker threads. - Inner parallelism:
Cmaes::optimize,Fitness::eval_population*,parallel_batch,map_elites_batch, ordiversify_batchevaluates one population concurrently.
Worker semantics for inner batches are 1 = serial, positive values = exactly
that many cached Rayon threads, and non-positive values = the global Rayon
pool. Retry uses workers = 0 for available parallelism and caps active
workers by the number of retries.
Do not normally enable full outer and full inner parallelism simultaneously. For 16 cores choose one of these:
- 16 retry workers and one inner worker for many independent searches;
- one optimizer with 16 inner workers for an expensive population batch;
- a deliberate split such as four retries with four inner workers when both levels have enough work.
The objective must be Sync. Keep read-only model data shared, avoid a mutex
around the expensive computation, and give each stochastic evaluation an
independent deterministic seed. Python callbacks must reacquire the GIL and
may not scale for cheap Python objective bodies.
Single-worker retry is exactly repeatable. Multi-worker retry owns independent
PCG streams, so the compatibility context.seed depends on which logical
worker claims a run. context.run_seed, by contrast, depends only on the root
seed and run_id; use it for worker-count-independent objective randomness.
Timing-dependent early stopping can still change which run IDs are started.
Ordered parallel population batches are deterministic for a deterministic
objective.
Budget and parameter-setting procedure
Tune in this order:
- Validate objective values, signs, constraints, decoding, and bounds.
- Transform variables with extreme scale differences. Use log coordinates for positive variables spanning orders of magnitude.
- Choose the algorithm family from the desired result and problem structure.
- Set the evaluation budget and worker topology.
- Set population/chunk size so several generations fit in the budget and all workers receive work.
- Set initialization and sigma from real uncertainty.
- Only then tune algorithm-specific mutation, crossover, or learning rates.
Use a staged budget:
- Smoke test: a few populations, enough to exercise decoding and reporting.
- Pilot: several seeds for two or three candidate algorithms.
- Production: allocate the measured wall-time/evaluation budget to the best robust configuration, retaining independent validation seeds.
Population algorithms check limits at generation boundaries, so reported evaluations can exceed the configured limit by part of a population. Use the reported evaluation count for comparisons.
Minimal implementation patterns
Bounded scalar DE
#![allow(unused)]
fn main() {
use fcmaes_core::{De, DeParams, Fitness};
let dim = 8;
let lower = vec![-5.0; dim];
let upper = vec![5.0; dim];
let objective = |x: &[f64]| x.iter().map(|v| v * v).sum::<f64>();
let fitness = Fitness::bounded(dim, 1, &lower, &upper);
let parameters = DeParams {
max_evaluations: 50_000,
seed: 1,
..Default::default()
};
let result = De::new(fitness, &[], &[], None, ¶meters).optimize(&objective);
assert!(result.y.is_finite());
}
Parallel MODE population
#![allow(unused)]
fn main() {
use fcmaes_core::{parallel_batch, Fitness, Mode, ModeParams};
let dim = 6;
let fitness = Fitness::bounded(dim, 2, &[0.0; 6], &[1.0; 6]);
let mut mode = Mode::try_new(
fitness,
2, // minimized objectives
0, // trailing constraints
None,
&ModeParams { popsize: 128, seed: 1, ..Default::default() },
)?;
let xs = mode.ask();
let ys = parallel_batch(&xs, 16, |x| {
vec![x.iter().sum(), x.iter().map(|v| (v - 0.5).powi(2)).sum()]
});
mode.tell(&ys);
Ok::<(), &'static str>(())
}
Batch MAP-Elites
#![allow(unused)]
fn main() {
use fcmaes_core::{
map_elites_batch, parallel_batch, Archive, MapElitesParams, Rng,
};
let mut rng = Rng::new(1);
let lower = vec![-1.0; 4];
let upper = vec![1.0; 4];
let mut archive = Archive::try_new(4, &[-1.0; 2], &[1.0; 2], 256, 0, &mut rng)?;
archive.seed_uniform(&lower, &upper, &mut rng);
let mut batch = |xs: &[Vec<f64>]| {
parallel_batch(xs, 16, |x| {
let fitness = x.iter().map(|v| v * v).sum();
(fitness, vec![x[0], x[1]])
})
};
let initial = archive.xs().to_vec();
archive.update_batch(&initial, &mut batch)?;
archive.argsort();
map_elites_batch(
&mut archive,
&mut batch,
&lower,
&upper,
&MapElitesParams { generations: 1_000, chunk_size: 128, ..Default::default() },
&mut rng,
)?;
Ok::<(), &'static str>(())
}
For retry and the DE-to-CMA sequence, copy the maintained patterns from
docs/retry.md and examples/src/runner.rs rather than rebuilding seed and
context handling ad hoc.
Validation and reporting checklist
Before declaring success, the AI should:
- Unit-test decoding, objective signs, constraint signs, bounds, and known reference points.
- Confirm every optimizer result is decoded and re-evaluated independently.
- Run
cargo test --workspaceand use--releasefor timings. - Record algorithm, all non-default parameters, bounds, seed, workers, configured budget, actual evaluations, and wall time.
- Compare algorithms using the same declared objective-call and resource limits; use equal wall deadlines when sequential overhead or parallel slot utilization differs materially.
- Use multiple seeds and report at least best, median or mean, standard deviation, and success/feasibility rate.
- For multi-objective runs, report feasible Pareto points and a suitable front quality measure; do not compare only one selected point.
- For QD runs, report capacity, occupied niches, coverage, best fitness,
qd_score, descriptor ranges, and—when noisy—holdout niche migration. - Check scaling by testing workers 1 and N. Deterministic ordered batches should produce the same values; parallel retry may be statistically rather than bitwise reproducible.
- Preserve a small deterministic smoke configuration in tests.
Failure diagnosis
| Symptom | Likely cause | First action |
|---|---|---|
| No improvement from the initial population | Wrong sign, invalid decoding, huge flat penalty, or bounds too broad | Print and test several decoded points and individual objective terms. |
| All results are infeasible | Constraint sign error or penalty scale too weak/strong | Verify feasible means <= 0 and test a known feasible point. |
| CMA-ES immediately hits bounds | Guess or sigma is in the wrong coordinate scale | Check whether Fitness normalization is enabled and rescale sigma. |
| DE consumes budget without refinement | Population too large for the budget or smooth local convergence is needed | Reduce population or hand the best point to CMA-ES. |
| Parallel run is slower | Objective is too cheap, batches are too small, or worker pools are nested | Use one worker level and increase work per batch. |
| Multi-objective front covers only one extreme | Objective scaling/weight ranges are poor or population/retries are too small | Normalize objectives and increase MODE population or scalarization diversity. |
| MAP-Elites coverage stays low | Descriptor bounds are wrong, capacity is too high, or invalid descriptors are returned | Inspect descriptor distributions and reduce capacity for the pilot. |
| Noisy runs stop inconsistently | Target/tolerance is tighter than noise | Increase replication/population and use robust aggregate reporting. |
Repository references
docs/getting-started.md: building and basic Rust use.docs/choosing-an-optimizer.md: deciding whether fcmaes-rust or a structured, gradient-based, surrogate-based, real-time, or genotype-aware alternative fits the complete problem.docs/optimizers.md: public optimizer interfaces and defaults.docs/retry.md: basic, advanced, and weighted retry.docs/architecture.md: objective flow, normalization, and concurrency.docs/examples.md: native application and benchmark commands.docs/combinatorial-encodings.md: bounded integer, categorical, Boolean, random-key permutation, exact-cardinality subset, partition, ordering, and deterministic repair patterns.docs/buckingham-pi.md: dimensionally valid continuous feature search, holdout validation, conditioning safeguards, and MODE objectives.docs/python-bindings.md: CPython 3.11–3.13 package, callable signatures, result layouts, GIL behavior, and Python examples.examples/src/runner.rs: canonical DE-to-CMA retry integration.examples/src/bin/mazda_mo.rs: parallel constrained MODE driver.examples/src/bin/mazda_qd.rs: parallel MAP-Elites/Diversifier driver.examples/src/uav.rs: random-key decoding for mixed assignment, ordering, scalar, and multi-objective optimization.examples/src/encoding.rs: dependency-free reference decoders and invariant tests supporting the combinatorial encoding cookbook.examples/src/buckingham.rs: nullspace parameterization that makes every continuous optimizer trial dimensionally valid.tutorials/README.md: nine native application-optimization tutorials, MODE/MAP-Elites selection, stochastic validation, parallelism ownership, validation-aware hyperparameter tuning, fixed-topology neural policy search, and the Diffsol gradient-based counterexample.tutorials/ml-hyperparameter-tuning/: native probability forests, mixed-variable decoding, fixed-fold tuning, disjoint selection, frozen final evaluation, fair baselines, constrained MODE, and a MAP-Elites pilot.tutorials/cfd-room-ventilation/: custom native simulation state, worst-case training releases, held-out validation, three-grid sensitivity, MODE, and MAP-Elites.tutorials/neural-controller-policy-search/: PGPE, CR-FM-NES, active CMA-ES, and BiteOpt under an equal direct-policy-search protocol with common scenarios, disjoint validation, a frozen test, and scaling evidence.- Generated rustdoc:
cargo doc --workspace --no-deps --open.
When code and this guide disagree, treat the current public Rust API and tests as authoritative, update the guide with the implementation, and document the reason for the change.