feat(core): add data types and Rng alias
Plain-data structs and the seeded Rng alias from spec §7. Each lives in its own file under src/core/ with unit tests: - Direction, Objective, ObjectiveSpace (with as_minimization negating only Maximize axes) - Evaluation (is_feasible == constraint_violation <= 0.0) - Candidate<D>, Population<D> (concrete, public fields, From<Vec<...>>) - OptimizationResult<D> - type Rng = rand::rngs::StdRng + rng_from_seed, so no public trait is generic over the RNG (spec §2.5) All public types behind #[cfg_attr(feature = "serde", derive(...))] so the optional feature wires up without changing the default surface.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
//! A decision paired with its evaluation.
|
||||
|
||||
use crate::core::evaluation::Evaluation;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A decision together with its evaluated objective values.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Candidate<D> {
|
||||
/// The decision (input to the problem).
|
||||
pub decision: D,
|
||||
/// The evaluated objective values and constraint violation.
|
||||
pub evaluation: Evaluation,
|
||||
}
|
||||
|
||||
impl<D> Candidate<D> {
|
||||
/// Pair a decision with its evaluation.
|
||||
pub fn new(decision: D, evaluation: Evaluation) -> Self {
|
||||
Self { decision, evaluation }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_pairs_decision_and_evaluation() {
|
||||
let c = Candidate::new(vec![1.0, 2.0], Evaluation::new(vec![5.0]));
|
||||
assert_eq!(c.decision, vec![1.0, 2.0]);
|
||||
assert_eq!(c.evaluation.objectives, vec![5.0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//! Objective values and total constraint violation for a single decision.
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The result of evaluating a decision: objective values plus total constraint violation.
|
||||
///
|
||||
/// A non-positive `constraint_violation` means the candidate is feasible.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Evaluation {
|
||||
/// Objective values in the order declared by the problem.
|
||||
pub objectives: Vec<f64>,
|
||||
/// Total constraint violation. `<= 0.0` is feasible; positive is infeasible.
|
||||
pub constraint_violation: f64,
|
||||
}
|
||||
|
||||
impl Evaluation {
|
||||
/// Build a feasible evaluation from objective values.
|
||||
pub fn new(objectives: Vec<f64>) -> Self {
|
||||
Self { objectives, constraint_violation: 0.0 }
|
||||
}
|
||||
|
||||
/// Build an evaluation with a known total constraint violation.
|
||||
pub fn constrained(objectives: Vec<f64>, constraint_violation: f64) -> Self {
|
||||
Self { objectives, constraint_violation }
|
||||
}
|
||||
|
||||
/// Returns `true` when `constraint_violation <= 0.0`.
|
||||
pub fn is_feasible(&self) -> bool {
|
||||
self.constraint_violation <= 0.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_is_feasible() {
|
||||
let e = Evaluation::new(vec![1.0, 2.0]);
|
||||
assert_eq!(e.constraint_violation, 0.0);
|
||||
assert!(e.is_feasible());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constrained_sets_violation() {
|
||||
let e = Evaluation::constrained(vec![0.0], 0.5);
|
||||
assert!(!e.is_feasible());
|
||||
assert_eq!(e.constraint_violation, 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_or_negative_violation_is_feasible() {
|
||||
assert!(Evaluation::constrained(vec![0.0], 0.0).is_feasible());
|
||||
assert!(Evaluation::constrained(vec![0.0], -1.0).is_feasible());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Concrete data types and the `Problem` trait that the rest of the crate is built on.
|
||||
|
||||
pub mod candidate;
|
||||
pub mod evaluation;
|
||||
pub mod objective;
|
||||
pub mod population;
|
||||
pub mod result;
|
||||
pub mod rng;
|
||||
|
||||
pub use candidate::*;
|
||||
pub use evaluation::*;
|
||||
pub use objective::*;
|
||||
pub use population::*;
|
||||
pub use result::*;
|
||||
pub use rng::*;
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Objective directions, named objectives, and the objective space.
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Whether an objective should be minimized or maximized.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
/// Smaller objective values are better.
|
||||
Minimize,
|
||||
/// Larger objective values are better.
|
||||
Maximize,
|
||||
}
|
||||
|
||||
/// A named objective and its optimization direction.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Objective {
|
||||
/// Human-readable name of the objective.
|
||||
pub name: String,
|
||||
/// Whether to minimize or maximize.
|
||||
pub direction: Direction,
|
||||
}
|
||||
|
||||
impl Objective {
|
||||
/// Create a minimize objective with the given name.
|
||||
pub fn minimize(name: impl Into<String>) -> Self {
|
||||
Self { name: name.into(), direction: Direction::Minimize }
|
||||
}
|
||||
|
||||
/// Create a maximize objective with the given name.
|
||||
pub fn maximize(name: impl Into<String>) -> Self {
|
||||
Self { name: name.into(), direction: Direction::Maximize }
|
||||
}
|
||||
}
|
||||
|
||||
/// The collection of objectives that define a problem's objective space.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ObjectiveSpace {
|
||||
/// Objectives in declaration order.
|
||||
pub objectives: Vec<Objective>,
|
||||
}
|
||||
|
||||
impl ObjectiveSpace {
|
||||
/// Build an objective space from the given objectives.
|
||||
pub fn new(objectives: Vec<Objective>) -> Self {
|
||||
Self { objectives }
|
||||
}
|
||||
|
||||
/// Number of objectives.
|
||||
pub fn len(&self) -> usize {
|
||||
self.objectives.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if there are zero objectives.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.objectives.is_empty()
|
||||
}
|
||||
|
||||
/// Returns `true` if there is exactly one objective.
|
||||
pub fn is_single_objective(&self) -> bool {
|
||||
self.objectives.len() == 1
|
||||
}
|
||||
|
||||
/// Returns `true` if there are two or more objectives.
|
||||
pub fn is_multi_objective(&self) -> bool {
|
||||
self.objectives.len() >= 2
|
||||
}
|
||||
|
||||
/// Convert objective values into minimization orientation.
|
||||
///
|
||||
/// Minimize objectives are returned unchanged; Maximize objectives are
|
||||
/// negated. In v1, this zips to the shorter of the two lengths.
|
||||
pub fn as_minimization(&self, values: &[f64]) -> Vec<f64> {
|
||||
debug_assert_eq!(
|
||||
values.len(),
|
||||
self.objectives.len(),
|
||||
"objective value count must match ObjectiveSpace length",
|
||||
);
|
||||
self.objectives
|
||||
.iter()
|
||||
.zip(values.iter())
|
||||
.map(|(obj, &v)| match obj.direction {
|
||||
Direction::Minimize => v,
|
||||
Direction::Maximize => -v,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn minimize_constructor_sets_direction() {
|
||||
let o = Objective::minimize("cost");
|
||||
assert_eq!(o.name, "cost");
|
||||
assert_eq!(o.direction, Direction::Minimize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximize_constructor_sets_direction() {
|
||||
let o = Objective::maximize("accuracy");
|
||||
assert_eq!(o.name, "accuracy");
|
||||
assert_eq!(o.direction, Direction::Maximize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_minimization_negates_maximize_only() {
|
||||
let space = ObjectiveSpace::new(vec![
|
||||
Objective::minimize("cost"),
|
||||
Objective::maximize("accuracy"),
|
||||
]);
|
||||
assert_eq!(space.as_minimization(&[10.0, 0.8]), vec![10.0, -0.8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lengths_and_predicates() {
|
||||
let single = ObjectiveSpace::new(vec![Objective::minimize("f")]);
|
||||
assert!(single.is_single_objective());
|
||||
assert!(!single.is_multi_objective());
|
||||
assert!(!single.is_empty());
|
||||
assert_eq!(single.len(), 1);
|
||||
|
||||
let multi = ObjectiveSpace::new(vec![
|
||||
Objective::minimize("f1"),
|
||||
Objective::minimize("f2"),
|
||||
]);
|
||||
assert!(multi.is_multi_objective());
|
||||
assert!(!multi.is_single_objective());
|
||||
|
||||
let empty = ObjectiveSpace::new(Vec::new());
|
||||
assert!(empty.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//! A friendly wrapper around `Vec<Candidate<D>>`.
|
||||
|
||||
use crate::core::candidate::Candidate;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A collection of evaluated candidates.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Population<D> {
|
||||
/// The candidates.
|
||||
pub candidates: Vec<Candidate<D>>,
|
||||
}
|
||||
|
||||
impl<D> Population<D> {
|
||||
/// Wrap a vector of candidates as a `Population`.
|
||||
pub fn new(candidates: Vec<Candidate<D>>) -> Self {
|
||||
Self { candidates }
|
||||
}
|
||||
|
||||
/// Number of candidates.
|
||||
pub fn len(&self) -> usize {
|
||||
self.candidates.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if there are no candidates.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.candidates.is_empty()
|
||||
}
|
||||
|
||||
/// Iterate over the candidates by reference.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Candidate<D>> {
|
||||
self.candidates.iter()
|
||||
}
|
||||
|
||||
/// Unwrap into the inner `Vec<Candidate<D>>`.
|
||||
pub fn into_vec(self) -> Vec<Candidate<D>> {
|
||||
self.candidates
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> From<Vec<Candidate<D>>> for Population<D> {
|
||||
fn from(candidates: Vec<Candidate<D>>) -> Self {
|
||||
Self::new(candidates)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::core::evaluation::Evaluation;
|
||||
|
||||
fn cand(x: f64) -> Candidate<f64> {
|
||||
Candidate::new(x, Evaluation::new(vec![x]))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_len_iter_into_vec() {
|
||||
let pop = Population::new(vec![cand(1.0), cand(2.0)]);
|
||||
assert_eq!(pop.len(), 2);
|
||||
assert!(!pop.is_empty());
|
||||
assert_eq!(pop.iter().count(), 2);
|
||||
assert_eq!(pop.into_vec().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_vec_works() {
|
||||
let pop: Population<f64> = vec![cand(1.0)].into();
|
||||
assert_eq!(pop.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_population() {
|
||||
let pop: Population<f64> = Population::new(Vec::new());
|
||||
assert!(pop.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//! Standard return type for optimizers.
|
||||
|
||||
use crate::core::candidate::Candidate;
|
||||
use crate::core::population::Population;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The output of an optimization run.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OptimizationResult<D> {
|
||||
/// The final population (or all sampled candidates, depending on algorithm).
|
||||
pub population: Population<D>,
|
||||
/// The non-dominated subset of the final population or archive.
|
||||
pub pareto_front: Vec<Candidate<D>>,
|
||||
/// The single-objective best, or `None` for multi-objective problems.
|
||||
pub best: Option<Candidate<D>>,
|
||||
/// Total number of `Problem::evaluate` calls.
|
||||
pub evaluations: usize,
|
||||
/// Total number of major optimizer iterations.
|
||||
pub generations: usize,
|
||||
}
|
||||
|
||||
impl<D> OptimizationResult<D> {
|
||||
/// Construct an `OptimizationResult` from its parts.
|
||||
pub fn new(
|
||||
population: Population<D>,
|
||||
pareto_front: Vec<Candidate<D>>,
|
||||
best: Option<Candidate<D>>,
|
||||
evaluations: usize,
|
||||
generations: usize,
|
||||
) -> Self {
|
||||
Self { population, pareto_front, best, evaluations, generations }
|
||||
}
|
||||
|
||||
/// The final population.
|
||||
pub fn population(&self) -> &Population<D> {
|
||||
&self.population
|
||||
}
|
||||
|
||||
/// The non-dominated subset.
|
||||
pub fn pareto_front(&self) -> &[Candidate<D>] {
|
||||
&self.pareto_front
|
||||
}
|
||||
|
||||
/// The single-objective best, when meaningful.
|
||||
pub fn best(&self) -> Option<&Candidate<D>> {
|
||||
self.best.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::core::evaluation::Evaluation;
|
||||
|
||||
#[test]
|
||||
fn accessors_return_expected_data() {
|
||||
let cand = Candidate::new(1.0_f64, Evaluation::new(vec![1.0]));
|
||||
let pop = Population::new(vec![cand.clone()]);
|
||||
let r = OptimizationResult::new(pop, vec![cand.clone()], Some(cand.clone()), 5, 2);
|
||||
assert_eq!(r.population().len(), 1);
|
||||
assert_eq!(r.pareto_front().len(), 1);
|
||||
assert!(r.best().is_some());
|
||||
assert_eq!(r.evaluations, 5);
|
||||
assert_eq!(r.generations, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//! Single seeded RNG type used throughout the crate.
|
||||
|
||||
use rand::SeedableRng;
|
||||
|
||||
/// The standard RNG used by `Initializer`, `Variation`, and built-in optimizers.
|
||||
///
|
||||
/// Fixed to a single concrete type so the public traits never need to be
|
||||
/// generic over the RNG.
|
||||
pub type Rng = rand::rngs::StdRng;
|
||||
|
||||
/// Build a deterministic [`Rng`] from a 64-bit seed.
|
||||
pub fn rng_from_seed(seed: u64) -> Rng {
|
||||
Rng::seed_from_u64(seed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::Rng as _;
|
||||
|
||||
#[test]
|
||||
fn same_seed_same_sequence() {
|
||||
let mut a = rng_from_seed(42);
|
||||
let mut b = rng_from_seed(42);
|
||||
let av: u64 = a.random();
|
||||
let bv: u64 = b.random();
|
||||
assert_eq!(av, bv);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_seed_different_sequence() {
|
||||
let mut a = rng_from_seed(1);
|
||||
let mut b = rng_from_seed(2);
|
||||
let av: u64 = a.random();
|
||||
let bv: u64 = b.random();
|
||||
assert_ne!(av, bv);
|
||||
}
|
||||
}
|
||||
+4
-13
@@ -1,14 +1,5 @@
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
//! `heuropt` — a practical Rust toolkit for heuristic single-, multi-, and
|
||||
//! many-objective optimization. See `docs/heuropt_tech_design_spec.md` for the
|
||||
//! full design.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
pub mod core;
|
||||
|
||||
Reference in New Issue
Block a user