Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-core crate. Rust 1.88 is the tested minimum supported toolchain.
  • Python users install the fcmaes-rust distribution and import fcmaes_rust. Published wheels support CPython 3.11 through 3.13.
  • examples, tutorials, and the internal native fcmaes-gtop source 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.

QuestionWhy 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

  1. 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.
  2. If the user wants the best solution in many behavior niches rather than one global optimum, use Archive plus MAP-Elites. Add Diversifier after the archive has useful coverage.
  3. 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 moretry when 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.
  4. 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.
  5. 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.
  6. 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

AlgorithmBest fitMain limitationPractical initial configuration
Differential Evolution (De)Bounded global search, discontinuities, mixed decoded variables, restartsOne-shot optimize is serial; convergence near a smooth optimum can be slower than CMA-ESDefault 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 guessFull covariance work grows roughly quadratically with dimension; requires a guess and step sizeDefault 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 evaluationNeeds a meaningful guess and scalar sigma; not a discrete optimizerEven population, default 32; use optimize_batch and parallelize expensive batches
PGPE (Pgpe)High-dimensional mirrored sampling, noisy objectives, parallel batchesDiagonal search distribution does not learn a full covarianceEven 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 refinementNo built-in population batching; often less attractive in very high dimensionsKeep local search enabled for deterministic objectives; disable it for very noisy or discontinuous objectives
BiteOpt (BiteOpt, DeepBiteOpt)Hard bounded black boxes, nonsmoothness, multimodality, minimal tuningNo native general constraint or categorical model; batched ask/tell changes feedback timingAutomatic population; depth 1 first, then try small deep values if the budget is large
MODE (Mode)Constrained multi-objective Pareto searchRequires an ask/tell driver and objective-plus-constraint rowsPopulation 64–256 initially; NSGA-II update; parallelize each asked population
Weighted MO retry (moretry)Parallel multi-objective scalarizations using any scalar optimizerQuality depends on objective scaling and sampled weights; the retained set is not one evolving Pareto populationAt least tens of retries; normalize objective magnitudes before selecting weight ranges
MAP-Elites (map_elites)Quality-diversity coverage over chosen descriptorsRequires meaningful behavior descriptors and fixed descriptor boundsCapacity 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 archiveMore expensive than simple emitters and needs an initialized archiveRun 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:

FieldDefaultTuning guidance
popsize31Increase for broad multimodal exploration or large dimensions; ensure the budget still permits many generations.
max_evaluations100,000Set from the user’s real budget.
keep200Usually leave unchanged; it controls temporal/history behavior.
stop_fitnessnegative infinitySet only when a genuine target is known.
f0.5Higher explores farther; lower takes smaller differential steps. Tune after population and budget.
cr0.9Lower can help nearly separable problems; high values allow correlated coordinate changes.
min_mutate / max_mutate0.1 / 0.5Controls the fraction used by optional integer mutation.
min_sigma0Minimum adaptive sampling spread. Usually leave at zero.
seed / runid0 / 0Set 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:

FieldDefaultTuning guidance
popsize31Increase for noise or multimodality; decrease cautiously for very expensive objectives.
mu0Zero selects half the population. Usually leave automatic.
max_evaluations100,000Give enough budget for multiple covariance updates.
accuracy1.0Leave at default unless stopping behavior has been validated.
stop_fitnessnegative infinityUse a known target.
stop_tol_hist_fun-1Negative selects the automatic history tolerance. Avoid tight tolerances on noise.
update_gap-1Negative selects the automatic covariance update interval.
seed / runid0 / 0Use 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:

FieldDefault
popsize32, rounded up to even
max_evaluations100,000
stop_fitnessnegative infinity
lr_decay_steps1,000
use_rankingtrue
center_learning_rate0.15
stdev_learning_rate0.1
stdev_max_change0.2
b1 / b2 / eps0.9 / 0.999 / 1e-8
decay_coef1.0
seed / runid0 / 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.

Unknown bounded black box

Run two fair baselines:

  1. BiteOpt with automatic population and depth 1.
  2. 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 moretry rows are [objective_0, ..., objective_m, constraint_0, ..., constraint_k].
  • A constraint is feasible when constraint <= 0.
  • Convert an equality h(x) = 0 to abs(h(x)) - tolerance <= 0 when 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.md for 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_indices for the objective prefix.
  • Use parallel_batch to 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.

  1. Define decision bounds and descriptor bounds.
  2. Construct Archive::try_new.
  3. Seed parent candidates with seed_uniform.
  4. Evaluate that initial population and update the archive.
  5. Run map_elites or map_elites_batch.
  6. Optionally run diversify or diversify_batch to improve/fill niches.
  7. 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 = 0 selects the fast regular grid with O(1) lookup. Non-factorable capacities have ragged rows; use Archive::grid_layout() for rendering and Archive::capacity() for the coverage denominator rather than multiplying grid_shape() dimensions.
  • Positive samples_per_niche selects 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_batch and diversify_batch evaluate concurrently, then mutate the archive serially in candidate order. QdBatchFitness must return exactly one (fitness, descriptor) pair per input in the same order.
  • qd_score is 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:

TutorialMain problem propertyRecommended lesson
NeXosim production lineStochastic discrete events and mixed controlsCompare outer candidate parallelism with simulator-internal parallelism; use common random numbers and holdout seeds.
Rapier trebuchetContact and release discontinuitiesUse BiteOpt retry for one target, MODE for engineering trade-offs, and QD only for meaningful trajectory behaviors.
ReBop oscillatorIntrinsic stochastic simulation noiseFix candidate-comparison seeds, report true simulation counts, and validate Pareto/QD results on disjoint paths.
Oscillator topology searchDiscrete signed graphs around variable-dimensional stochastic inner problemsKeep the agent or evolutionary proposer outside deterministic grammar, optimization, persistence, and held-out motif scoring; match random and evolutionary controls.
Brahe constellationAccess-window discontinuities and worst-gap aggregationKeep feasibility explicit and assign either fcmaes or the simulator ownership of parallelism.
RustPower voltage controlMixed-integer controls, contingencies, and solver failuresReturn calibrated constraint violations for failed power flows; reject a QD formulation when descriptors do not produce an informative archive.
Atmospheric source localizationCensored inverse inference, model mismatch, and non-identifiabilityUse 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 ventilationCustom numerical backend, variable geometry, and grid sensitivityA 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 tuningMixed variables, nested stochastic fitting, and validation overfittingUse 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 search118-dimensional fixed-topology policy and randomized rolloutsUse 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 differencesUse a staged fidelity ladder, repropagate the final continuous-thrust solution independently, and keep ephemeris/model qualifications attached to every score claim.
GTOC1 route searchAgent-proposed variable planet orders with unequal fidelity costsLet 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 designSmooth circuit features plus discrete catalogue realizationInterpolate peaks and crossings instead of optimizing sampled arg-max staircases; validate the final catalogue under component tolerances.
thevenin gate driverTransient thresholds, ringing, and simulator disagreementInterpolate time-domain metrics and require timestep refinement plus an independent ngspice comparison before accepting a front.
Optical lens designMultimodal ray tracing with hard ray-loss regionsVerify 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 gaitContact-driven behavior repertoires and terrain overfittingTreat QD as the primary output only when descriptors remain meaningful and elites survive disjoint terrain replay.
Phased-array codebookQuantized phase/attenuation registers and element failuresCross-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 hubDiscontinuous sizing around a convex dispatch problemSolve the inner LP to optimality, optimize only the outer nonconvex choices, and report candidate calls, LP solves, and pivots separately.
Field-service routingAssignment/permutation plateaus and disrupted task setsProve decoder invariants, validate hard feasibility on disrupted scenarios, and skip QD when coverage or retention fails.
Water-network schedulingQuantized controls with hydraulic state memorySeparate DDA optimization from PDA validation, record hydraulic failures, and reject repertoires that migrate under unseen demand.
Truss topology and sizingExact-cardinality topology plus mechanisms and conditioningDecode exact-k structures, classify FEM failures, and test removal robustness instead of replacing mechanisms with arbitrary large stresses.
Network coverageLarge binary submodular structure with available certificatesRun 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_seed when a run must reproduce independently of worker scheduling; context.seed retains 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:

  1. Outer parallelism: retry, advanced_retry, or moretry runs independent optimizers on worker threads.
  2. Inner parallelism: Cmaes::optimize, Fitness::eval_population*, parallel_batch, map_elites_batch, or diversify_batch evaluates 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:

  1. Validate objective values, signs, constraints, decoding, and bounds.
  2. Transform variables with extreme scale differences. Use log coordinates for positive variables spanning orders of magnitude.
  3. Choose the algorithm family from the desired result and problem structure.
  4. Set the evaluation budget and worker topology.
  5. Set population/chunk size so several generations fit in the budget and all workers receive work.
  6. Set initialization and sigma from real uncertainty.
  7. 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, &parameters).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 --workspace and use --release for 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

SymptomLikely causeFirst action
No improvement from the initial populationWrong sign, invalid decoding, huge flat penalty, or bounds too broadPrint and test several decoded points and individual objective terms.
All results are infeasibleConstraint sign error or penalty scale too weak/strongVerify feasible means <= 0 and test a known feasible point.
CMA-ES immediately hits boundsGuess or sigma is in the wrong coordinate scaleCheck whether Fitness normalization is enabled and rescale sigma.
DE consumes budget without refinementPopulation too large for the budget or smooth local convergence is neededReduce population or hand the best point to CMA-ES.
Parallel run is slowerObjective is too cheap, batches are too small, or worker pools are nestedUse one worker level and increase work per batch.
Multi-objective front covers only one extremeObjective scaling/weight ranges are poor or population/retries are too smallNormalize objectives and increase MODE population or scalarization diversity.
MAP-Elites coverage stays lowDescriptor bounds are wrong, capacity is too high, or invalid descriptors are returnedInspect descriptor distributions and reduce capacity for the pilot.
Noisy runs stop inconsistentlyTarget/tolerance is tighter than noiseIncrease 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.