fix: address 18 bugs found during codebase audit (#8)

* 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
This commit is contained in:
Manuel
2026-02-13 10:03:37 +01:00
committed by GitHub
parent 1cd18c16f9
commit 11a8534b38
17 changed files with 350 additions and 156 deletions
+1 -1
View File
@@ -255,7 +255,7 @@ impl Study<f64> {
/// Escape a string for CSV output. If the value contains a comma, quote, or
/// newline, wrap it in double-quotes and double any embedded quotes.
fn csv_escape(s: &str) -> String {
if s.contains(',') || s.contains('"') || s.contains('\n') {
if s.contains(',') || s.contains('"') || s.contains('\n') || s.contains('\r') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_string()
+8 -2
View File
@@ -639,7 +639,8 @@ where
/// Return the number of completed trials.
///
/// Failed trials are not counted.
/// Pruned and failed trials are not counted. Use
/// [`n_pruned_trials()`](Self::n_pruned_trials) for the pruned count.
///
/// # Examples
///
@@ -658,7 +659,12 @@ where
/// ```
#[must_use]
pub fn n_trials(&self) -> usize {
self.storage.trials_arc().read().len()
self.storage
.trials_arc()
.read()
.iter()
.filter(|t| t.state == TrialState::Complete)
.count()
}
/// Return the number of pruned trials.
+1 -1
View File
@@ -49,7 +49,7 @@ impl<V: PartialOrd + Clone + serde::Serialize> Study<V> {
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 next_trial_id = self.storage.peek_next_trial_id();
let snapshot = StudySnapshot {
version: 1,
direction: self.direction,