Skip to content

Refactor#7

Merged
raimannma merged 48 commits intomasterfrom
refactor
Feb 13, 2026
Merged

Refactor#7
raimannma merged 48 commits intomasterfrom
refactor

Conversation

@raimannma
Copy link
Owner

No description provided.

@raimannma raimannma self-assigned this Feb 12, 2026
Copilot AI review requested due to automatic review settings February 12, 2026 11:48
- Sampler, pruner, storage, parameter, and multi_objective types are no
  longer re-exported at the crate root; access via module paths instead
  (e.g. optimizer::sampler::TpeSampler, optimizer::parameter::FloatParam)
- Prelude continues to re-export everything for convenience
- Add module-level pub use re-exports in sampler/mod.rs and parameter.rs
- Update derive macro to reference optimizer::parameter::Categorical
- Rename sampler::differential_evolution to sampler::de
- Fix all downstream imports in tests, benches, and doctests
- Fix all rustdoc intra-doc links to use explicit module paths
…topic files

- Split pruning_and_callbacks into pruning and early_stopping
- Split advanced_features into async_parallel, journal_storage, ask_and_tell, multi_objective
- Each example now requires only its own feature flag
- Trim sampler_comparison winner logic and verbose header
- Update CI workflow and README to match new example names
- Add `Objective<V>` trait with lifecycle hooks (`before_trial`,
  `after_trial`, `max_retries`) in new `src/objective.rs`
- Replace 14+ optimize variants with 6 methods: `optimize`,
  `optimize_with`, and async/parallel counterparts
- `optimize*` methods accept closures directly (FnMut for sync, Fn for
  async); `optimize_with*` methods accept `impl Objective<V>` for
  struct-based objectives with hooks and retries
- Remove `optimize_until`, `optimize_with_callback`,
  `optimize_with_retries`, `optimize_with_checkpoint`, and all
  deprecated `_with_sampler` methods
- Delete 20 duplicate tests already covered by parameter_tests.rs
- Move 11 pure Trial unit tests into src/trial.rs
- Split remaining 84 integration tests into tests/study/ (9 modules)
- Group sampler tests into tests/sampler/ (7 modules)
- Group pruner tests into tests/pruner/ (2 modules)
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a comprehensive refactoring and documentation enhancement effort. The changes include:

Changes:

  • Reorganizes test suite into modular structure with dedicated directories for study/, sampler/, and pruner/ tests
  • Enhances API documentation across all samplers, pruners, and storage modules with detailed descriptions, usage guidelines, and examples
  • Introduces breaking API changes: moves ParamValue to optimizer::parameter, renames differential_evolution module to de, moves Decomposition to optimizer::sampler, and simplifies async API
  • Replaces old examples with new focused examples covering basic optimization, parameter types, sampler comparison, pruning, early stopping, async/parallel execution, journal storage, ask-and-tell, and multi-objective optimization
  • Adds project infrastructure: cliff.toml for changelog generation, CLAUDE.md for AI guidelines, .claude/commands/ for git workflows, and Makefile with changelog target
  • Updates README.md with streamlined quick start and feature summary

Reviewed changes

Copilot reviewed 87 out of 91 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/study/* New modular test organization for study functionality
tests/sampler/* New test files and module structure for samplers
tests/pruner/* New comprehensive pruner test suite
tests/*_tests.rs Updated import paths and async API usage
src/sampler/*.rs Enhanced documentation with algorithm descriptions and configuration tables
src/pruner/*.rs Added comprehensive module and type documentation
src/storage/*.rs Expanded documentation for storage backends
src/objective.rs New file documenting the Objective trait
src/*.rs Documentation improvements across error, param, pareto, fanova, importance, multi_objective
examples/*.rs Replaced verbose examples with focused, runnable examples
Cargo.toml Updated description and example configuration
README.md Streamlined with quick start and feature table
.github/workflows/ci.yml Updated to run new examples and doc tests
cliff.toml, CHANGELOG.md, Makefile, CLAUDE.md, .claude/commands/* New project infrastructure files
Comments suppressed due to low confidence (1)

tests/sampler/differential_evolution.rs:2

  • The import path has changed from optimizer::sampler::differential_evolution to optimizer::sampler::de. All references to DifferentialEvolutionSampler and DifferentialEvolutionStrategy need to be updated throughout the codebase to use the new module name.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov
Copy link

codecov bot commented Feb 12, 2026

Codecov Report

❌ Patch coverage is 90.61433% with 220 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.26%. Comparing base (3581187) to head (93d7a63).

Files with missing lines Patch % Lines
src/sampler/motpe.rs 63.01% 54 Missing ⚠️
src/sampler/tpe/multivariate/engine.rs 90.66% 38 Missing ⚠️
src/study/mod.rs 87.27% 28 Missing ⚠️
src/sampler/common.rs 80.72% 16 Missing ⚠️
src/storage/journal.rs 82.27% 14 Missing ⚠️
src/study/analysis.rs 90.62% 12 Missing ⚠️
src/study/async_impl.rs 92.08% 11 Missing ⚠️
src/sampler/tpe/sampler.rs 93.86% 10 Missing ⚠️
src/sampler/mod.rs 82.00% 9 Missing ⚠️
src/study/export.rs 91.96% 9 Missing ⚠️
... and 9 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master       #7      +/-   ##
==========================================
+ Coverage   91.53%   93.26%   +1.72%     
==========================================
  Files          41       53      +12     
  Lines       14833    14563     -270     
==========================================
+ Hits        13578    13582       +4     
+ Misses       1255      981     -274     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link

github-actions bot commented Feb 12, 2026

Benchmark Comparison

Base: 3581187cd3936b499d9f2af87f5e63b70b4f56f9 | Head: 93d7a63057533d456488475f6bc53ba4c8d518e5

Click to expand full results
group                         base                                   head
-----                         ----                                   ----
grid_sample/points/10         1.02   346.3±20.40ns        ? ?/sec    1.00    340.2±5.46ns        ? ?/sec
grid_sample/points/5          1.01    331.4±5.62ns        ? ?/sec    1.00    329.7±3.09ns        ? ?/sec
grid_sample/points/50         1.01    421.1±6.98ns        ? ?/sec    1.00   417.6±11.36ns        ? ?/sec
random_sample/history/10      1.03     15.3±0.14ns        ? ?/sec    1.00     14.8±0.25ns        ? ?/sec
random_sample/history/100     1.03     15.3±0.13ns        ? ?/sec    1.00     14.8±0.23ns        ? ?/sec
random_sample/history/1000    1.03     15.3±0.13ns        ? ?/sec    1.00     14.8±0.25ns        ? ?/sec
random_vs_tpe/random_5d       1.15    166.4±0.15µs        ? ?/sec    1.00    144.8±1.64µs        ? ?/sec
random_vs_tpe/tpe_5d          3.64     18.7±0.11ms        ? ?/sec    1.00      5.2±0.01ms        ? ?/sec
tpe_rosenbrock/dims/10        6.99     71.5±0.09ms        ? ?/sec    1.00     10.2±0.04ms        ? ?/sec
tpe_rosenbrock/dims/2         1.61      3.4±0.00ms        ? ?/sec    1.00      2.1±0.00ms        ? ?/sec
tpe_sample/history/10         1.40      4.2±0.07µs        ? ?/sec    1.00      3.0±0.05µs        ? ?/sec
tpe_sample/history/100        1.67     31.3±0.38µs        ? ?/sec    1.00     18.8±0.29µs        ? ?/sec
tpe_sample/history/1000       1.60    307.8±3.03µs        ? ?/sec    1.00    192.9±2.34µs        ? ?/sec
tpe_sphere/dims/10            7.01     71.6±0.29ms        ? ?/sec    1.00     10.2±0.06ms        ? ?/sec
tpe_sphere/dims/2             1.61      3.4±0.01ms        ? ?/sec    1.00      2.1±0.03ms        ? ?/sec
tpe_sphere/dims/50            33.86 1738.9±13.86ms        ? ?/sec    1.00     51.4±0.14ms        ? ?/sec

Benchmarks run on ubuntu-latest via Criterion + critcmp.
Results may vary due to shared CI runners. Look for consistent >5% changes.

- Add blanket `impl Objective<V> for Fn(&mut Trial) -> Result<V, E>`
  so closures work directly with `optimize`
- Rewrite optimize, optimize_async, optimize_parallel to accept
  `impl Objective<V>` with before_trial/after_trial hooks
- Remove optimize_with, optimize_with_async, optimize_with_parallel
- Remove max_retries and retry logic from Objective trait
- Add explicit closure type annotations for HRTB inference
- Convert FnMut test closures to Fn via RefCell/Cell
Copilot AI review requested due to automatic review settings February 12, 2026 12:09
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 95 out of 97 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- GridSearchSampler → GridSampler
- DifferentialEvolutionSampler → DESampler
- DifferentialEvolutionStrategy → DEStrategy
- DifferentialEvolutionSamplerBuilder → DESamplerBuilder
Copilot AI review requested due to automatic review settings February 12, 2026 12:21
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 95 out of 97 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 12, 2026 12:29
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 95 out of 97 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings February 12, 2026 14:39
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 97 out of 113 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

path.file_name().unwrap_or_default().to_string_lossy()
));
let file = std::fs::File::create(&tmp_path)?;
serde_json::to_writer_pretty(file, &snapshot).map_err(std::io::Error::other)?;
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::fs::rename is not a reliable atomic replace on all platforms (notably Windows, where renaming over an existing destination fails). To keep the 'atomic write' guarantee, replace this with a platform-safe atomic replace strategy (e.g., remove existing destination before rename, or use a temp file + persist/replace pattern that overwrites the destination).

Suggested change
serde_json::to_writer_pretty(file, &snapshot).map_err(std::io::Error::other)?;
serde_json::to_writer_pretty(file, &snapshot).map_err(std::io::Error::other)?;
// On Windows, std::fs::rename fails if the destination already exists.
// Remove the destination file first (if present) to make this replace
// behavior work consistently across platforms.
if path.exists() {
if let Err(e) = std::fs::remove_file(path) {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(e);
}
}
}

Copilot uses AI. Check for mistakes.
objective: impl crate::objective::Objective<V>,
) -> crate::Result<()>
where
V: Clone + Default,
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optimize adds a V: Default bound but the implementation shown doesn’t use Default. This unnecessarily restricts which objective value types can be optimized and is an API footgun for users with non-Default value types. Consider removing the Default bound (keeping only what’s required by into_completed/storage/etc.).

Suggested change
V: Clone + Default,
V: Clone,

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +76
// Collect all unique attribute keys (sorted).
let mut attr_keys: Vec<String> = Vec::new();
for trial in trials.iter() {
for key in trial.user_attrs.keys() {
if !attr_keys.contains(key) {
attr_keys.push(key.clone());
}
}
}
attr_keys.sort();
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collecting attr_keys with Vec::contains makes this O(trials × attrs²) in the worst case. Using a BTreeSet<String> (or HashSet + final sort) would make key collection scale much better for large studies while keeping deterministic column order.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +63
seed: u64,
/// Monotonic counter to disambiguate calls with identical (`trial_id`, distribution).
call_seq: AtomicU64,
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a single global call_seq means the RNG stream depends on cross-thread call interleaving. In parallel optimization, this can make seeded runs non-reproducible across executions with the same seed (because seq assignment is scheduling-dependent). If reproducibility under concurrency is a goal, consider deriving the per-sample seed from stable identifiers only (e.g., include ParamId / param label / per-trial suggestion index maintained inside Trial) rather than a global atomic sequence.

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +128
let seq = self.call_seq.fetch_add(1, Ordering::Relaxed);
let mut rng = fastrand::Rng::with_seed(rng_util::mix_seed(
self.seed,
trial_id,
rng_util::distribution_fingerprint(distribution).wrapping_add(seq),
));
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a single global call_seq means the RNG stream depends on cross-thread call interleaving. In parallel optimization, this can make seeded runs non-reproducible across executions with the same seed (because seq assignment is scheduling-dependent). If reproducibility under concurrency is a goal, consider deriving the per-sample seed from stable identifiers only (e.g., include ParamId / param label / per-trial suggestion index maintained inside Trial) rather than a global atomic sequence.

Copilot uses AI. Check for mistakes.
Comment on lines +185 to +199
trials
.iter()
.map(|trial| {
param_order
.iter()
.filter_map(|param_id| {
trial.params.get(param_id).and_then(|value| match value {
crate::param::ParamValue::Float(f) => Some(*f),
crate::param::ParamValue::Int(i) => Some(*i as f64),
crate::param::ParamValue::Categorical(_) => None, // Skip categorical
})
})
.collect()
})
.collect()
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because categorical parameters are skipped via filter_map, each row’s length depends on how many categorical ParamIds are present in param_order. Many multivariate modeling flows expect fixed-width observation vectors aligned to a consistent parameter order. Consider either: (1) ensuring param_order contains only numeric params before calling this, or (2) emitting a fixed-width vector (e.g., placeholder value) so downstream code can rely on consistent dimensionality.

Copilot uses AI. Check for mistakes.
Comment on lines 52 to 56
pub fn export_json(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
let file = std::fs::File::create(path)?;
let trials = self.trials();
serde_json::to_writer_pretty(file, &trials).map_err(std::io::Error::other)
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new JSON persistence API (export_json, save, load) introduces important behavior (schema versioning, trial round-tripping, ID continuity) but there are no tests shown here validating round-trip correctness. Adding a test that runs a small study, save()s it, then load()s and asserts trials/direction/best value match would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines 63 to 73
pub fn save(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
let path = path.as_ref();
let trials = self.trials();
let next_trial_id = trials.iter().map(|t| t.id).max().map_or(0, |id| id + 1);
let snapshot = StudySnapshot {
version: 1,
direction: self.direction,
trials,
next_trial_id,
metadata: HashMap::new(),
};
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new JSON persistence API (export_json, save, load) introduces important behavior (schema versioning, trial round-tripping, ID continuity) but there are no tests shown here validating round-trip correctness. Adding a test that runs a small study, save()s it, then load()s and asserts trials/direction/best value match would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +99 to +111
pub fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
use crate::sampler::random::RandomSampler;

let file = std::fs::File::open(path)?;
let snapshot: StudySnapshot<V> = serde_json::from_reader(file)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let storage = crate::storage::MemoryStorage::with_trials(snapshot.trials);
Ok(Self::with_sampler_and_storage(
snapshot.direction,
RandomSampler::new(),
storage,
))
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new JSON persistence API (export_json, save, load) introduces important behavior (schema versioning, trial round-tripping, ID continuity) but there are no tests shown here validating round-trip correctness. Adding a test that runs a small study, save()s it, then load()s and asserts trials/direction/best value match would help prevent regressions.

Copilot uses AI. Check for mistakes.
Fix direct path references for in-scope items (GammaStrategy,
CompletedTrial) and convert feature-gated items to plain backtick
formatting so docs build cleanly without --all-features.
…pers

- Remove stale #[allow(dead_code)] and #[allow(too_many_lines)] attributes
- Replace #[allow(dead_code)] with #[cfg(test)] for test-only methods
- Delete unused fill_remaining_independent() and ParamMeta.dist field
- Extract sample_float/int/categorical helpers from TpeSampler, MotpeSampler,
  and SamplingEngine to shorten match-heavy sample() methods
- Extract try_fit_kdes() helper to consolidate repeated fallback blocks
- Refactor sample_tpe_float/int to take &FloatDistribution/&IntDistribution
  instead of destructured args, removing #[allow(too_many_arguments)]
Copilot AI review requested due to automatic review settings February 12, 2026 15:03
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 97 out of 115 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +67 to +94
pub(crate) fn compute_imputation_value(&self, completed_trials: &[CompletedTrial]) -> f64 {
match self.constant_liar {
ConstantLiarStrategy::None => 0.0, // This case is handled before calling this method
ConstantLiarStrategy::Mean => {
if completed_trials.is_empty() {
0.0
} else {
let sum: f64 = completed_trials.iter().map(|t| t.value).sum();
sum / completed_trials.len() as f64
}
}
ConstantLiarStrategy::Best => {
// Best means minimum for minimization problems
completed_trials
.iter()
.map(|t| t.value)
.fold(f64::INFINITY, f64::min)
}
ConstantLiarStrategy::Worst => {
// Worst means maximum for minimization problems
completed_trials
.iter()
.map(|t| t.value)
.fold(f64::NEG_INFINITY, f64::max)
}
ConstantLiarStrategy::Custom(v) => v,
}
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConstantLiarStrategy::Best/Worst return +∞/-∞ when completed_trials is empty. In parallel optimization it’s common to have pending trials before the first completion, and injecting infinite objective values can poison downstream sorting/model fitting. Handle the empty-history case explicitly for Best/Worst (similar to Mean) and return a finite fallback (e.g., 0.0 or a configured default).

Copilot uses AI. Check for mistakes.
Comment on lines 117 to 131
fn sample(
&self,
distribution: &Distribution,
_trial_id: u64,
trial_id: u64,
_history: &[CompletedTrial],
) -> ParamValue {
let mut rng = self.rng.lock();
let seq = self.call_seq.fetch_add(1, Ordering::Relaxed);
let mut rng = fastrand::Rng::with_seed(rng_util::mix_seed(
self.seed,
trial_id,
rng_util::distribution_fingerprint(distribution).wrapping_add(seq),
));

match distribution {
Distribution::Float(d) => {
let value = if d.log_scale {
// Sample uniformly in log space
let log_low = d.low.ln();
let log_high = d.high.ln();
let log_value = rng_util::f64_range(&mut rng, log_low, log_high);
log_value.exp()
} else if let Some(step) = d.step {
// Sample from step grid
let n_steps = ((d.high - d.low) / step).floor() as i64;
let k = rng.i64(0..=n_steps);
d.low + (k as f64) * step
} else {
// Uniform sampling
rng_util::f64_range(&mut rng, d.low, d.high)
};
ParamValue::Float(value)
}
Distribution::Int(d) => {
let value = if d.log_scale {
// Sample uniformly in log space, then round
let log_low = (d.low as f64).ln();
let log_high = (d.high as f64).ln();
let log_value = rng_util::f64_range(&mut rng, log_low, log_high);
let raw = log_value.exp().round() as i64;
// Clamp to bounds since rounding might push outside
raw.clamp(d.low, d.high)
} else if let Some(step) = d.step {
// Sample from step grid
let n_steps = (d.high - d.low) / step;
let k = rng.i64(0..=n_steps);
d.low + k * step
} else {
// Uniform sampling
rng.i64(d.low..=d.high)
};
ParamValue::Int(value)
}
Distribution::Categorical(d) => {
let index = rng.usize(0..d.n_choices);
ParamValue::Categorical(index)
}
}
super::common::sample_random(&mut rng, distribution)
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global call_seq makes sampling order-dependent across threads. In parallel runs, scheduling differences will change seq assignment and therefore produced samples, undermining “with_seed” reproducibility even when trial_id and distributions are identical. Consider removing the global counter from the seed mix and instead deriving any disambiguator deterministically from per-trial information (e.g., include ParamId/a per-trial call index provided by the caller), or accept collisions for identical distributions within a trial rather than introducing cross-thread nondeterminism.

Copilot uses AI. Check for mistakes.
// Fill in labels from other trials that might have better labels.
for trial in trials.iter() {
for (&id, label) in &trial.param_labels {
param_columns.entry(id).or_insert_with(|| label.clone());
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says “fill in labels … that might have better labels”, but or_insert_with never overwrites existing entries (including placeholders like id.to_string()). This can cause CSV headers to remain as numeric IDs even when a better human label exists in later trials. A concrete fix is to overwrite when the existing value is the placeholder (e.g., equals id.to_string()), or to build param_columns primarily from param_labels first and only fall back to id.to_string() at the end.

Suggested change
param_columns.entry(id).or_insert_with(|| label.clone());
param_columns
.entry(id)
.and_modify(|existing| {
// Upgrade placeholder labels (e.g., "param_<id>" or `id.to_string()`)
// to a better human-readable label when available.
if *existing == id.to_string() {
*existing = label.clone();
}
})
.or_insert_with(|| label.clone());

Copilot uses AI. Check for mistakes.
Comment on lines +174 to +179
/// Validates that all floating-point fields are finite (not NaN or
/// Infinity).
///
/// Checks distribution bounds, parameter values, constraints, and
/// intermediate values. Returns a description of the first invalid
/// field found, or `Ok(())` if everything is valid.
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says “all floating-point fields”, but the implementation doesn’t validate the trial’s objective value (for CompletedTrial<f64>) and doesn’t validate user_attrs float values (which can also carry NaN/∞). Either tighten the documentation to match what’s checked, or extend validation to include the objective value (when applicable) and float-valued attributes to make the guarantee true.

Suggested change
/// Validates that all floating-point fields are finite (not NaN or
/// Infinity).
///
/// Checks distribution bounds, parameter values, constraints, and
/// intermediate values. Returns a description of the first invalid
/// field found, or `Ok(())` if everything is valid.
/// Validates that floating-point fields in distributions, parameters,
/// constraints, and intermediate values are finite (not NaN or Infinity).
///
/// Checks distribution bounds, parameter values, constraints, and
/// intermediate values. This does not validate the trial's objective
/// value or any float values stored in `user_attrs`. Returns a
/// description of the first invalid field found, or `Ok(())` if
/// everything is valid.

Copilot uses AI. Check for mistakes.
Comment on lines +184 to +208
pub fn validate(&self) -> core::result::Result<(), String> {
for (id, dist) in &self.distributions {
if let Distribution::Float(fd) = dist {
if !fd.low.is_finite() {
return Err(format!(
"trial {}: float distribution for param {id} has non-finite low bound ({})",
self.id, fd.low
));
}
if !fd.high.is_finite() {
return Err(format!(
"trial {}: float distribution for param {id} has non-finite high bound ({})",
self.id, fd.high
));
}
if let Some(step) = fd.step
&& !step.is_finite()
{
return Err(format!(
"trial {}: float distribution for param {id} has non-finite step ({step})",
self.id
));
}
}
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says “all floating-point fields”, but the implementation doesn’t validate the trial’s objective value (for CompletedTrial<f64>) and doesn’t validate user_attrs float values (which can also carry NaN/∞). Either tighten the documentation to match what’s checked, or extend validation to include the objective value (when applicable) and float-valued attributes to make the guarantee true.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +38
//! The output is a single HTML file that can be opened in any browser.
//! An internet connection is needed on first load to fetch `Plotly.js`
//! from a CDN.
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module docs describe the report as “self-contained” with “embedded Plotly.js”, but later state a network fetch from a CDN is required. Pick one behavior and make the docs consistent (either embed Plotly.js in the HTML, or describe it as a single-file report that loads Plotly.js externally).

Copilot uses AI. Check for mistakes.
/// # Errors
///
/// Returns an I/O error if the file cannot be created or written.
pub fn save(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Study::save/Study::load introduce new persistence behavior (schema/version, atomic write, restore semantics), but there’s no corresponding integration test shown here validating round-trip correctness and error cases (e.g., malformed JSON, missing fields, version mismatch handling). Add a test that saves a study with a few trials, loads it back, and asserts direction/trials/IDs are consistent.

Copilot uses AI. Check for mistakes.
/// # Errors
///
/// Returns an I/O error if the file cannot be read or parsed.
pub fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Study::save/Study::load introduce new persistence behavior (schema/version, atomic write, restore semantics), but there’s no corresponding integration test shown here validating round-trip correctness and error cases (e.g., malformed JSON, missing fields, version mismatch handling). Add a test that saves a study with a few trials, loads it back, and asserts direction/trials/IDs are consistent.

Copilot uses AI. Check for mistakes.
- 10k sequential trials with RandomSampler
- 128 parameters with TPE sampler
- 128 concurrent workers with optimize_parallel
- 5k trials with TPE and 32 parallel workers
- Timing-based proof that trials run in parallel, not sequentially
- Atomic counter proof that multiple workers overlap simultaneously
- Panic propagation returns TaskError with panic message
- Partial failures with parallel path store only successful trials
…nd pruners

- Expand sampler module docs with available samplers tables, custom
  sampler walkthrough, stateless/stateful patterns, cold start handling,
  history reading, thread safety, and testing guidance
- Expand pruner module docs with stateful/stateless classification,
  warmup parameters, decorator composition, thread safety, and testing
- Add code examples to both trait docs (NoisySampler, StalePruner)
Resolve symlinks and `../` traversals best-effort in `new()` and
`open()`, falling back to the original path if canonicalization fails
(e.g. file doesn't exist yet).
Copilot AI review requested due to automatic review settings February 12, 2026 16:05
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 88 out of 116 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…and CSV export

- Rename stale "GridSearchSampler" panic message to "GridSampler"
- Assert concurrency > 0 in optimize_parallel to prevent deadlock
- Fix inaccurate comment in CSV export (empty for all non-complete trials, not just pruned)
* fix: address 18 bugs found during codebase audit

High severity:
- TPE/MOTPE: match parameters by exact distribution equality instead of
  flat-mapping over all param values, preventing cross-parameter mixing
- MultivariateTpeSampler: find_matching_param now uses search space
  distributions for exact matching instead of type+range heuristic
- JournalStorage: write_to_file no longer advances file_offset (left to
  refresh), both operations serialized under single io_lock mutex,
  refresh uses fetch_max and deduplicates by trial ID

Medium severity:
- NSGA-III: use actual Pareto front ranks for tournament selection
  instead of artificial cyclic indices
- sample_random: apply step quantization after log-scale sampling
- internal_bounds: return None for non-positive log-scale bounds
- SobolSampler: use per-trial dimension HashMap for concurrent safety
- JournalStorage refresh: protect with io_lock mutex, use fetch_max
- n_trials(): filter by TrialState::Complete as documented
- FloatParam: reject NaN/Infinity in validate()
- Pruners: assert n_min_trials >= 1, guard compute_percentile on empty
- Visualization: escape_js for importance chart parameter names

Low severity:
- save(): use peek_next_trial_id() from Storage trait
- csv_escape: handle carriage return per RFC 4180
- from_internal: use saturating arithmetic for stepped Int distributions
- BoolParam: bounds-check categorical index < 2
- min_max: skip NaN values with safe fallback

* ci: trigger CI on pull requests targeting any branch
Copilot AI review requested due to automatic review settings February 13, 2026 09:03
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 86 out of 116 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (1)

src/storage/mod.rs:1

  • Adding peek_next_trial_id() as a required method on the public Storage trait is a breaking change for downstream custom storage implementations. If backward compatibility is desired, consider providing a default implementation (even if it returns a conservative/less accurate value) or introducing a separate extension trait used only for persistence. Otherwise, this change should be accompanied by an appropriate semver bump and release notes.
//! Trial storage backends.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +85 to +97
pub fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
use crate::sampler::random::RandomSampler;

let file = std::fs::File::open(path)?;
let snapshot: StudySnapshot<V> = serde_json::from_reader(file)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let storage = crate::storage::MemoryStorage::with_trials(snapshot.trials);
Ok(Self::with_sampler_and_storage(
snapshot.direction,
RandomSampler::new(),
storage,
))
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StudySnapshot persists next_trial_id, but load() ignores it and reconstructs the counter from max(trial.id)+1 via MemoryStorage::with_trials. This will reuse IDs if the saved study had already allocated IDs for failed trials (or any other non-stored allocation), which can cause ID collisions / inaccurate bookkeeping after reload. Fix by restoring the counter to snapshot.next_trial_id (e.g., add a MemoryStorage::with_trials_and_next_id(...) constructor, or a public/feature-appropriate way to bump/set the counter during load). Also consider validating/handling snapshot.version during load for forward compatibility.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +21
/// Run async optimization with an objective.
///
/// Like [`optimize`](Self::optimize), but each evaluation is wrapped in
/// [`spawn_blocking`](tokio::task::spawn_blocking), keeping the async
/// runtime responsive for CPU-bound objectives. Trials run sequentially.
///
/// Accepts any [`Objective`](crate::Objective) implementation, including
/// plain closures. Struct-based objectives can override lifecycle hooks.
///
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optimize_async no longer appears to accept an async objective (i.e., a closure returning a Future) and instead always runs a sync Objective::evaluate inside spawn_blocking. If the crate previously supported truly-async objectives (network I/O, etc.), this is a significant behavioral/API shift that may surprise users. Consider either (a) adding a separate API that accepts async objectives (e.g., Fn(Trial) -> Future<Output=...>), or (b) renaming/documenting this more explicitly as an async driver for blocking/CPU-bound work (e.g., optimize_blocking_async) to avoid confusion.

Suggested change
/// Run async optimization with an objective.
///
/// Like [`optimize`](Self::optimize), but each evaluation is wrapped in
/// [`spawn_blocking`](tokio::task::spawn_blocking), keeping the async
/// runtime responsive for CPU-bound objectives. Trials run sequentially.
///
/// Accepts any [`Objective`](crate::Objective) implementation, including
/// plain closures. Struct-based objectives can override lifecycle hooks.
///
/// Run optimization on an async runtime with a synchronous objective.
///
/// Like [`optimize`](Self::optimize), but each **synchronous** evaluation is
/// wrapped in [`spawn_blocking`](tokio::task::spawn_blocking), keeping the
/// async runtime responsive for CPU-bound or other blocking objectives.
/// Trials run sequentially.
///
/// This method does **not** take an async objective (e.g., a closure that
/// returns a `Future`). Instead, it accepts any synchronous
/// [`Objective`](crate::Objective) implementation, including plain closures.
/// Struct-based objectives can override lifecycle hooks.
///
/// Use this when your objective performs CPU-bound or blocking work that
/// would otherwise block the async runtime. If you need truly async,
/// I/O-bound behavior, keep the objective itself synchronous and have it
/// call into async code via your own coordination, rather than by making
/// the objective return a `Future`.
///

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +53
pub async fn optimize_async<O>(&self, n_trials: usize, objective: O) -> crate::Result<()>
where
O: crate::objective::Objective<V> + Send + Sync + 'static,
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optimize_async no longer appears to accept an async objective (i.e., a closure returning a Future) and instead always runs a sync Objective::evaluate inside spawn_blocking. If the crate previously supported truly-async objectives (network I/O, etc.), this is a significant behavioral/API shift that may surprise users. Consider either (a) adding a separate API that accepts async objectives (e.g., Fn(Trial) -> Future<Output=...>), or (b) renaming/documenting this more explicitly as an async driver for blocking/CPU-bound work (e.g., optimize_blocking_async) to avoid confusion.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to 60
/// Internal state for tracking per-trial dimension counters.
struct SobolState {
/// The `trial_id` of the current trial (used to reset dimension counter).
current_trial: u64,
/// Next Sobol dimension to use for the current trial.
next_dimension: u32,
/// Next Sobol dimension for each in-flight trial.
dimensions: HashMap<u64, u32>,
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dimensions grows monotonically with trial_id and entries are never removed, so long-running studies will retain one HashMap entry per trial forever. That’s a concrete memory-growth issue. Consider moving the per-trial dimension counter into Trial state (so it drops with the trial), or providing a sampler hook to clear state when a trial completes; if neither is possible, a bounded cache/cleanup strategy (e.g., remove entries once dimension exceeds expected param count, or periodically retain only recent IDs) would mitigate unbounded growth.

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +134
let dimension = state.dimensions.entry(trial_id).or_insert(0);
let dim = *dimension;
*dimension = dim + 1;
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dimensions grows monotonically with trial_id and entries are never removed, so long-running studies will retain one HashMap entry per trial forever. That’s a concrete memory-growth issue. Consider moving the per-trial dimension counter into Trial state (so it drops with the trial), or providing a sampler hook to clear state when a trial completes; if neither is possible, a bounded cache/cleanup strategy (e.g., remove entries once dimension exceeds expected param count, or periodically retain only recent IDs) would mitigate unbounded growth.

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +91
ConstantLiarStrategy::Best => {
// Best means minimum for minimization problems
completed_trials
.iter()
.map(|t| t.value)
.fold(f64::INFINITY, f64::min)
}
ConstantLiarStrategy::Worst => {
// Worst means maximum for minimization problems
completed_trials
.iter()
.map(|t| t.value)
.fold(f64::NEG_INFINITY, f64::max)
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant-liar Best/Worst strategies are hard-coded for minimization (min == best, max == worst). If the sampler supports Direction::Maximize, the imputation step will invert the intended behavior and can skew model fitting during parallel optimization. Fix by selecting min/max based on the study/sampler direction (e.g., store Direction in MultivariateTpeSampler and branch here).

Copilot uses AI. Check for mistakes.
Comment on lines 200 to 220
let sampler = RandomSampler::with_seed(42);
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);

let x_param = FloatParam::new(0.0, 10.0);

let start = tokio::time::Instant::now();
study
.optimize_parallel(4, 4, move |trial: &mut optimizer::Trial| {
let x = x_param.suggest(trial)?;
std::thread::sleep(std::time::Duration::from_millis(100));
Ok::<_, Error>(x)
})
.await
.expect("parallel optimization should succeed");

let elapsed = start.elapsed();
assert_eq!(study.n_trials(), 4);
// Sequential would take ~400ms; parallel with concurrency=4 should be ~100ms
assert!(
elapsed < std::time::Duration::from_millis(350),
"expected parallel execution under 350ms, took {elapsed:?}"
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is timing-based and likely to be flaky under CI load or on slower machines (threadpool warmup, contention, noisy neighbors). A more deterministic approach is to assert concurrency via synchronization primitives (e.g., a barrier/latch + atomic max-active assertion), similar to test_parallel_max_concurrency_reached, and avoid hard wall-clock thresholds.

Suggested change
let sampler = RandomSampler::with_seed(42);
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
let x_param = FloatParam::new(0.0, 10.0);
let start = tokio::time::Instant::now();
study
.optimize_parallel(4, 4, move |trial: &mut optimizer::Trial| {
let x = x_param.suggest(trial)?;
std::thread::sleep(std::time::Duration::from_millis(100));
Ok::<_, Error>(x)
})
.await
.expect("parallel optimization should succeed");
let elapsed = start.elapsed();
assert_eq!(study.n_trials(), 4);
// Sequential would take ~400ms; parallel with concurrency=4 should be ~100ms
assert!(
elapsed < std::time::Duration::from_millis(350),
"expected parallel execution under 350ms, took {elapsed:?}"
use std::sync::atomic::{AtomicUsize, Ordering};
let sampler = RandomSampler::with_seed(42);
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
let x_param = FloatParam::new(0.0, 10.0);
let active = Arc::new(AtomicUsize::new(0));
let max_active = Arc::new(AtomicUsize::new(0));
let active_c = Arc::clone(&active);
let max_active_c = Arc::clone(&max_active);
study
.optimize_parallel(4, 4, move |trial: &mut optimizer::Trial| {
let x = x_param.suggest(trial)?;
let current = active_c.fetch_add(1, Ordering::SeqCst) + 1;
max_active_c.fetch_max(current, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(100));
active_c.fetch_sub(1, Ordering::SeqCst);
Ok::<_, Error>(x)
})
.await
.expect("parallel optimization should succeed");
assert_eq!(study.n_trials(), 4);
let max = max_active.load(Ordering::SeqCst);
assert!(
max >= 2,
"expected at least 2 concurrent workers, but max was {max}"

Copilot uses AI. Check for mistakes.
- Clamp KDE candidates to parameter bounds before evaluating l(x)/g(x),
  matching the univariate TPE behavior; without this, candidates scored
  well at out-of-bounds locations but became suboptimal when clamped
- Sort HashMap iterations by ParamId before consuming the seeded RNG to
  eliminate non-deterministic sampling caused by global ParamId counter
- Replace wall-clock timing assertion in async concurrency test with
  atomic max-active counter to avoid CI flakiness
- Gate unused HashSet import behind cfg(feature = "async")
@raimannma raimannma enabled auto-merge (rebase) February 13, 2026 09:22
@raimannma raimannma merged commit 69028da into master Feb 13, 2026
29 of 30 checks passed
@raimannma raimannma deleted the refactor branch February 13, 2026 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant