use serde::{Deserialize, Serialize};
use std::fmt;
use std::hash::{Hash, Hasher};
use super::uncertainty::FitnessEstimate;
use crate::genome::traits::EvolutionaryGenome;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub struct CandidateId(pub usize);
impl fmt::Display for CandidateId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Candidate({})", self.0)
}
}
impl From<usize> for CandidateId {
fn from(id: usize) -> Self {
Self(id)
}
}
impl From<CandidateId> for usize {
fn from(id: CandidateId) -> Self {
id.0
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(bound = "G: Serialize + for<'a> Deserialize<'a>")]
pub struct Candidate<G>
where
G: EvolutionaryGenome,
{
pub id: CandidateId,
pub genome: G,
pub fitness_estimate: Option<f64>,
#[serde(default)]
pub fitness_with_uncertainty: Option<FitnessEstimate>,
pub birth_generation: usize,
pub evaluation_count: usize,
}
impl<G> Candidate<G>
where
G: EvolutionaryGenome,
{
pub fn new(id: CandidateId, genome: G) -> Self {
Self {
id,
genome,
fitness_estimate: None,
fitness_with_uncertainty: None,
birth_generation: 0,
evaluation_count: 0,
}
}
pub fn with_generation(id: CandidateId, genome: G, generation: usize) -> Self {
Self {
id,
genome,
fitness_estimate: None,
fitness_with_uncertainty: None,
birth_generation: generation,
evaluation_count: 0,
}
}
pub fn set_fitness(&mut self, fitness: f64) {
self.fitness_estimate = Some(fitness);
}
pub fn set_fitness_with_uncertainty(&mut self, estimate: FitnessEstimate) {
self.fitness_estimate = Some(estimate.mean);
self.fitness_with_uncertainty = Some(estimate);
}
pub fn fitness(&self) -> Option<f64> {
self.fitness_estimate
}
pub fn fitness_uncertainty(&self) -> Option<&FitnessEstimate> {
self.fitness_with_uncertainty.as_ref()
}
pub fn fitness_variance(&self) -> Option<f64> {
self.fitness_with_uncertainty.as_ref().map(|e| e.variance)
}
pub fn is_evaluated(&self) -> bool {
self.evaluation_count > 0
}
pub fn record_evaluation(&mut self) {
self.evaluation_count += 1;
}
pub fn age(&self, current_generation: usize) -> usize {
current_generation.saturating_sub(self.birth_generation)
}
pub fn is_uncertain(&self, min_observations: usize) -> bool {
self.fitness_with_uncertainty
.as_ref()
.map(|e| e.is_uncertain(min_observations))
.unwrap_or(true) }
}
impl<G> PartialEq for Candidate<G>
where
G: EvolutionaryGenome,
{
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<G> Eq for Candidate<G> where G: EvolutionaryGenome {}
impl<G> Hash for Candidate<G>
where
G: EvolutionaryGenome,
{
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RatingScale {
pub min: f64,
pub max: f64,
pub allow_ties: bool,
pub step: Option<f64>,
}
impl RatingScale {
pub fn new(min: f64, max: f64) -> Self {
Self {
min,
max,
allow_ties: true,
step: None,
}
}
pub fn one_to_ten() -> Self {
Self {
min: 1.0,
max: 10.0,
allow_ties: true,
step: Some(1.0),
}
}
pub fn one_to_five() -> Self {
Self {
min: 1.0,
max: 5.0,
allow_ties: true,
step: Some(1.0),
}
}
pub fn binary() -> Self {
Self {
min: 0.0,
max: 1.0,
allow_ties: false,
step: Some(1.0),
}
}
pub fn with_ties(mut self, allow: bool) -> Self {
self.allow_ties = allow;
self
}
pub fn with_step(mut self, step: f64) -> Self {
self.step = Some(step);
self
}
pub fn validate(&self, rating: f64) -> bool {
if rating < self.min || rating > self.max {
return false;
}
if let Some(step) = self.step {
let steps_from_min = (rating - self.min) / step;
(steps_from_min - steps_from_min.round()).abs() < 1e-9
} else {
true
}
}
pub fn clamp(&self, rating: f64) -> f64 {
rating.clamp(self.min, self.max)
}
pub fn normalize(&self, rating: f64) -> f64 {
(rating - self.min) / (self.max - self.min)
}
pub fn denormalize(&self, normalized: f64) -> f64 {
normalized * (self.max - self.min) + self.min
}
}
impl Default for RatingScale {
fn default() -> Self {
Self::one_to_ten()
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(bound = "G: Serialize + for<'a> Deserialize<'a>")]
pub enum EvaluationRequest<G>
where
G: EvolutionaryGenome,
{
RateCandidates {
candidates: Vec<Candidate<G>>,
scale: RatingScale,
},
PairwiseComparison {
candidate_a: Candidate<G>,
candidate_b: Candidate<G>,
allow_tie: bool,
},
BatchSelection {
candidates: Vec<Candidate<G>>,
select_count: usize,
min_select: usize,
},
}
impl<G> EvaluationRequest<G>
where
G: EvolutionaryGenome,
{
pub fn rate(candidates: Vec<Candidate<G>>) -> Self {
Self::RateCandidates {
candidates,
scale: RatingScale::default(),
}
}
pub fn rate_with_scale(candidates: Vec<Candidate<G>>, scale: RatingScale) -> Self {
Self::RateCandidates { candidates, scale }
}
pub fn compare(a: Candidate<G>, b: Candidate<G>) -> Self {
Self::PairwiseComparison {
candidate_a: a,
candidate_b: b,
allow_tie: true,
}
}
pub fn select_from_batch(candidates: Vec<Candidate<G>>, select_count: usize) -> Self {
Self::BatchSelection {
candidates,
select_count,
min_select: 1,
}
}
pub fn candidate_count(&self) -> usize {
match self {
Self::RateCandidates { candidates, .. } => candidates.len(),
Self::PairwiseComparison { .. } => 2,
Self::BatchSelection { candidates, .. } => candidates.len(),
}
}
pub fn candidate_ids(&self) -> Vec<CandidateId> {
match self {
Self::RateCandidates { candidates, .. } => candidates.iter().map(|c| c.id).collect(),
Self::PairwiseComparison {
candidate_a,
candidate_b,
..
} => vec![candidate_a.id, candidate_b.id],
Self::BatchSelection { candidates, .. } => candidates.iter().map(|c| c.id).collect(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EvaluationResponse {
Ratings(Vec<(CandidateId, f64)>),
PairwiseWinner(Option<CandidateId>),
BatchSelected(Vec<CandidateId>),
Skip,
}
impl EvaluationResponse {
pub fn ratings(ratings: Vec<(CandidateId, f64)>) -> Self {
Self::Ratings(ratings)
}
pub fn winner(id: CandidateId) -> Self {
Self::PairwiseWinner(Some(id))
}
pub fn tie() -> Self {
Self::PairwiseWinner(None)
}
pub fn selected(ids: Vec<CandidateId>) -> Self {
Self::BatchSelected(ids)
}
pub fn skip() -> Self {
Self::Skip
}
pub fn is_skip(&self) -> bool {
matches!(self, Self::Skip)
}
pub fn mentioned_ids(&self) -> Vec<CandidateId> {
match self {
Self::Ratings(ratings) => ratings.iter().map(|(id, _)| *id).collect(),
Self::PairwiseWinner(Some(id)) => vec![*id],
Self::PairwiseWinner(None) => vec![],
Self::BatchSelected(ids) => ids.clone(),
Self::Skip => vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::genome::real_vector::RealVector;
#[test]
fn test_candidate_id() {
let id1 = CandidateId(0);
let id2 = CandidateId(1);
let id3 = CandidateId(0);
assert_eq!(id1, id3);
assert_ne!(id1, id2);
assert_eq!(format!("{}", id1), "Candidate(0)");
}
#[test]
fn test_candidate_creation() {
let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
let candidate: Candidate<RealVector> = Candidate::new(CandidateId(0), genome);
assert_eq!(candidate.id, CandidateId(0));
assert!(candidate.fitness_estimate.is_none());
assert!(!candidate.is_evaluated());
assert_eq!(candidate.evaluation_count, 0);
}
#[test]
fn test_candidate_evaluation() {
let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
let mut candidate: Candidate<RealVector> = Candidate::new(CandidateId(0), genome);
candidate.set_fitness(7.5);
candidate.record_evaluation();
assert_eq!(candidate.fitness(), Some(7.5));
assert!(candidate.is_evaluated());
assert_eq!(candidate.evaluation_count, 1);
}
#[test]
fn test_rating_scale_validation() {
let scale = RatingScale::one_to_ten();
assert!(scale.validate(1.0));
assert!(scale.validate(5.0));
assert!(scale.validate(10.0));
assert!(!scale.validate(0.0));
assert!(!scale.validate(11.0));
assert!(!scale.validate(5.5)); }
#[test]
fn test_rating_scale_normalization() {
let scale = RatingScale::one_to_ten();
assert!((scale.normalize(1.0) - 0.0).abs() < 1e-9);
assert!((scale.normalize(5.5) - 0.5).abs() < 1e-9);
assert!((scale.normalize(10.0) - 1.0).abs() < 1e-9);
assert!((scale.denormalize(0.0) - 1.0).abs() < 1e-9);
assert!((scale.denormalize(0.5) - 5.5).abs() < 1e-9);
assert!((scale.denormalize(1.0) - 10.0).abs() < 1e-9);
}
#[test]
fn test_evaluation_request_rate() {
let c1: Candidate<RealVector> = Candidate::new(CandidateId(0), RealVector::new(vec![1.0]));
let c2: Candidate<RealVector> = Candidate::new(CandidateId(1), RealVector::new(vec![2.0]));
let request = EvaluationRequest::rate(vec![c1, c2]);
assert_eq!(request.candidate_count(), 2);
assert_eq!(
request.candidate_ids(),
vec![CandidateId(0), CandidateId(1)]
);
}
#[test]
fn test_evaluation_request_compare() {
let c1: Candidate<RealVector> = Candidate::new(CandidateId(0), RealVector::new(vec![1.0]));
let c2: Candidate<RealVector> = Candidate::new(CandidateId(1), RealVector::new(vec![2.0]));
let request = EvaluationRequest::compare(c1, c2);
assert_eq!(request.candidate_count(), 2);
}
#[test]
fn test_evaluation_response() {
let response = EvaluationResponse::winner(CandidateId(0));
assert_eq!(response.mentioned_ids(), vec![CandidateId(0)]);
let response = EvaluationResponse::tie();
assert!(response.mentioned_ids().is_empty());
let response = EvaluationResponse::skip();
assert!(response.is_skip());
}
}