diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs
new file mode 100644
index 0000000..373701c
--- /dev/null
+++ b/src/algorithms/mod.rs
@@ -0,0 +1,5 @@
+//! Built-in reference optimizers.
+
+pub mod random_search;
+
+pub use random_search::*;
diff --git a/src/algorithms/random_search.rs b/src/algorithms/random_search.rs
new file mode 100644
index 0000000..0b07627
--- /dev/null
+++ b/src/algorithms/random_search.rs
@@ -0,0 +1,122 @@
+//! Baseline `RandomSearch` optimizer.
+//!
+//! This is the reference example for spec §2.4 / §12.1: read this file before
+//! writing your own optimizer.
+
+use crate::core::candidate::Candidate;
+use crate::core::population::Population;
+use crate::core::problem::Problem;
+use crate::core::result::OptimizationResult;
+use crate::core::rng::rng_from_seed;
+use crate::pareto::{best_candidate, pareto_front};
+use crate::traits::{Initializer, Optimizer};
+
+/// Configuration for [`RandomSearch`].
+#[derive(Debug, Clone)]
+pub struct RandomSearchConfig {
+ /// Number of iterations (equals `generations` in the result).
+ pub iterations: usize,
+ /// Decisions sampled per iteration.
+ pub batch_size: usize,
+ /// Seed for the deterministic RNG.
+ pub seed: u64,
+}
+
+impl Default for RandomSearchConfig {
+ fn default() -> Self {
+ Self { iterations: 100, batch_size: 1, seed: 42 }
+ }
+}
+
+/// Sample-evaluate-keep baseline optimizer.
+///
+/// Each iteration the configured `Initializer` produces `batch_size` decisions
+/// which are evaluated and pushed into the population. Cheap, parallelism-free,
+/// and useful as a sanity-check baseline.
+#[derive(Debug, Clone)]
+pub struct RandomSearch {
+ /// Algorithm configuration.
+ pub config: RandomSearchConfig,
+ /// Decision-sampling strategy.
+ pub initializer: I,
+}
+
+impl RandomSearch {
+ /// Construct a `RandomSearch` from its config and initializer.
+ pub fn new(config: RandomSearchConfig, initializer: I) -> Self {
+ Self { config, initializer }
+ }
+}
+
+impl
Optimizer
for RandomSearch
+where
+ P: Problem,
+ I: Initializer,
+{
+ fn run(&mut self, problem: &P) -> OptimizationResult {
+ let objectives = problem.objectives();
+ let mut rng = rng_from_seed(self.config.seed);
+ let mut all: Vec> = Vec::new();
+ let mut evaluations = 0usize;
+
+ for _ in 0..self.config.iterations {
+ let decisions = self.initializer.initialize(self.config.batch_size, &mut rng);
+ for decision in decisions {
+ let eval = problem.evaluate(&decision);
+ evaluations += 1;
+ all.push(Candidate::new(decision, eval));
+ }
+ }
+
+ let front = pareto_front(&all, &objectives);
+ let best = best_candidate(&all, &objectives);
+ OptimizationResult::new(
+ Population::new(all),
+ front,
+ best,
+ evaluations,
+ self.config.iterations,
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::operators::RealBounds;
+ use crate::tests_support::{SchafferN1, Sphere1D};
+
+ #[test]
+ fn evaluation_count_matches_iterations_times_batch() {
+ let mut opt = RandomSearch::new(
+ RandomSearchConfig { iterations: 30, batch_size: 4, seed: 1 },
+ RealBounds::new(vec![(-2.0, 2.0)]),
+ );
+ let r = opt.run(&Sphere1D);
+ assert_eq!(r.evaluations, 30 * 4);
+ assert_eq!(r.population.len(), 30 * 4);
+ assert_eq!(r.generations, 30);
+ }
+
+ #[test]
+ fn pareto_front_non_empty_for_multi_objective() {
+ let mut opt = RandomSearch::new(
+ RandomSearchConfig { iterations: 50, batch_size: 1, seed: 42 },
+ RealBounds::new(vec![(-5.0, 5.0)]),
+ );
+ let r = opt.run(&SchafferN1);
+ assert!(!r.pareto_front.is_empty());
+ // Multi-objective ⇒ best is None.
+ assert!(r.best.is_none());
+ }
+
+ #[test]
+ fn single_objective_returns_best() {
+ let mut opt = RandomSearch::new(
+ RandomSearchConfig { iterations: 100, batch_size: 1, seed: 7 },
+ RealBounds::new(vec![(-1.0, 1.0)]),
+ );
+ let r = opt.run(&Sphere1D);
+ assert!(r.best.is_some());
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index bc00fd2..ce754aa 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,9 +2,13 @@
//! many-objective optimization. See `docs/heuropt_tech_design_spec.md` for the
//! full design.
+pub mod algorithms;
pub mod core;
pub mod operators;
pub mod pareto;
pub mod prelude;
pub mod selection;
pub mod traits;
+
+#[cfg(test)]
+pub(crate) mod tests_support;
diff --git a/src/prelude.rs b/src/prelude.rs
index 1be8d1f..51ad9e1 100644
--- a/src/prelude.rs
+++ b/src/prelude.rs
@@ -17,3 +17,5 @@ pub use crate::pareto::{
};
pub use crate::operators::{BitFlipMutation, GaussianMutation, RealBounds, SwapMutation};
+
+pub use crate::algorithms::{RandomSearch, RandomSearchConfig};
diff --git a/src/tests_support/mod.rs b/src/tests_support/mod.rs
new file mode 100644
index 0000000..f44d8fb
--- /dev/null
+++ b/src/tests_support/mod.rs
@@ -0,0 +1,40 @@
+//! Shared, deliberately tiny test problems used by algorithm unit tests.
+//!
+//! Not part of the public API.
+
+use crate::core::evaluation::Evaluation;
+use crate::core::objective::{Objective, ObjectiveSpace};
+use crate::core::problem::Problem;
+
+/// 1-D minimization sphere `f(x) = x^2`. Single objective, always feasible.
+pub struct Sphere1D;
+
+impl Problem for Sphere1D {
+ type Decision = Vec;
+
+ fn objectives(&self) -> ObjectiveSpace {
+ ObjectiveSpace::new(vec![Objective::minimize("f")])
+ }
+
+ fn evaluate(&self, decision: &Vec) -> Evaluation {
+ Evaluation::new(vec![decision[0] * decision[0]])
+ }
+}
+
+/// Schaffer N.1 — the textbook two-objective minimization warm-up:
+///
+/// `f1(x) = x^2`, `f2(x) = (x - 2)^2`. Always feasible.
+pub struct SchafferN1;
+
+impl Problem for SchafferN1 {
+ type Decision = Vec;
+
+ fn objectives(&self) -> ObjectiveSpace {
+ ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
+ }
+
+ fn evaluate(&self, x: &Vec) -> Evaluation {
+ let v = x[0];
+ Evaluation::new(vec![v * v, (v - 2.0).powi(2)])
+ }
+}