Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions math_explorer/src/applied/favoritism/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use std::fmt;

/// Errors that can occur during favoritism score calculation.
#[derive(Debug, Clone)]
pub enum FavoritismError {
/// Invalid input parameter (e.g. NaN, Infinity, or negative where not allowed).
InvalidInput(String),
}

impl fmt::Display for FavoritismError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
}
}
}

impl std::error::Error for FavoritismError {}
6 changes: 3 additions & 3 deletions math_explorer/src/applied/favoritism/favorite_child.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::{FavoritismInputs, calculate_favoritism_score};
use super::{FavoritismInputs, try_calculate_favoritism_score};

/// Represents a child with a name and a set of attributes for favoritism calculation.
#[derive(Debug, Clone)]
Expand All @@ -22,8 +22,8 @@ pub struct Child {
/// Returns `None` if the slice is empty.
pub fn find_favorite_child(children: &[Child]) -> Option<&Child> {
children.iter().max_by(|a, b| {
let score_a = calculate_favoritism_score(&a.inputs);
let score_b = calculate_favoritism_score(&b.inputs);
let score_a = try_calculate_favoritism_score(&a.inputs).unwrap_or(f64::NEG_INFINITY);
let score_b = try_calculate_favoritism_score(&b.inputs).unwrap_or(f64::NEG_INFINITY);
score_a
.partial_cmp(&score_b)
.unwrap_or(std::cmp::Ordering::Equal)
Expand Down
5 changes: 4 additions & 1 deletion math_explorer/src/applied/favoritism/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,17 @@
//! println!("Your Favoritism Score: {:.2}", score);
//! ```

pub mod error;
pub mod favorite_child;
pub mod scoring;
pub mod strategies;
pub mod types;

pub use error::FavoritismError;
#[allow(deprecated)]
pub use scoring::{
calculate_favoritism_score, calculate_favoritism_score_full,
calculate_favoritism_score_with_rng,
calculate_favoritism_score_with_rng, try_calculate_favoritism_score,
};
pub use types::{
ComplimentParams, ContactParams, FamilyParams, FavoritismInputs, GiftParams, PersonalityParams,
Expand Down
16 changes: 14 additions & 2 deletions math_explorer/src/applied/favoritism/scoring.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
use super::strategies::UnifiedFavoritismModel;
use super::types::FavoritismInputs;
use super::FavoritismError;
use crate::pure_math::analysis::integration::{ClenshawCurtis, Integrator};
use rand::Rng;

/// Calculates the favoritism score safely, validating inputs first.
///
/// # Returns
/// * `Ok(f64)` - The calculated score.
/// * `Err(FavoritismError)` - If inputs are invalid (NaN, Inf, negative time, etc.).
pub fn try_calculate_favoritism_score(inputs: &FavoritismInputs) -> Result<f64, FavoritismError> {
inputs.validate()?;
let mut rng = rand::thread_rng();
Ok(calculate_favoritism_score_with_rng(inputs, &mut rng))
}

/// Calculates the favoritism score based on the provided inputs.
///
/// This function implements the core logic of the Unified Favoritism Theory (UFT).
Expand Down Expand Up @@ -34,9 +46,9 @@ use rand::Rng;
/// let score = calculate_favoritism_score(&inputs);
/// assert!(score > 0.0);
/// ```
#[deprecated(since = "0.2.0", note = "Use try_calculate_favoritism_score instead")]
pub fn calculate_favoritism_score(inputs: &FavoritismInputs) -> f64 {
let mut rng = rand::thread_rng();
calculate_favoritism_score_with_rng(inputs, &mut rng)
try_calculate_favoritism_score(inputs).unwrap_or(f64::NAN)
}

/// Calculates the favoritism score using an injected RNG.
Expand Down
98 changes: 98 additions & 0 deletions math_explorer/src/applied/favoritism/types.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::FavoritismError;
use nalgebra::DVector;

/// Time and proximity related parameters.
Expand Down Expand Up @@ -203,3 +204,100 @@ pub struct FavoritismInputs {
/// Family context (siblings).
pub family: FamilyParams,
}

impl FavoritismInputs {
/// Validates the inputs for security and numerical stability.
pub fn validate(&self) -> Result<(), FavoritismError> {
// TimeParams
if !self.time.t.is_finite() || self.time.t < 0.0 {
return Err(FavoritismError::InvalidInput(
"Time 't' must be non-negative and finite".into(),
));
}
if !self.time.x_0.is_finite() {
return Err(FavoritismError::InvalidInput(
"Initial distance 'x_0' must be finite".into(),
));
}

// GiftParams
if !self.gifts.g_emotional.is_finite() || !self.gifts.g_practical.is_finite() {
return Err(FavoritismError::InvalidInput(
"Gift values must be finite".into(),
));
}

// ContactParams
if !self.contact.f_initial.is_finite() {
return Err(FavoritismError::InvalidInput(
"Initial contact frequency must be finite".into(),
));
}
if !self.contact.decay_constant.is_finite() || self.contact.decay_constant < 0.0 {
return Err(FavoritismError::InvalidInput(
"Decay constant must be non-negative and finite".into(),
));
}
if !self.contact.time_since_last_contact.is_finite()
|| self.contact.time_since_last_contact < 0.0
{
return Err(FavoritismError::InvalidInput(
"Time since last contact must be non-negative and finite".into(),
));
}

// PersonalityParams
if !self.personality.intelligence.is_finite()
|| !self.personality.emotional_sensitivity.is_finite()
|| !self.personality.wealth.is_finite()
|| !self.personality.talent.is_finite()
|| !self.personality.w_i.is_finite()
|| !self.personality.w_es.is_finite()
|| !self.personality.w_w.is_finite()
|| !self.personality.w_t.is_finite()
{
return Err(FavoritismError::InvalidInput(
"Personality traits and weights must be finite".into(),
));
}

// SocialParams
if !self.social.birth_order_weight.is_finite() || !self.social.major_life_events.is_finite()
{
return Err(FavoritismError::InvalidInput(
"Social parameters must be finite".into(),
));
}

// ComplimentParams
if self.compliments.compliments.iter().any(|x| !x.is_finite()) {
return Err(FavoritismError::InvalidInput(
"Compliment values must be finite".into(),
));
}
if self
.compliments
.compliment_weights
.iter()
.any(|x| !x.is_finite())
{
return Err(FavoritismError::InvalidInput(
"Compliment weights must be finite".into(),
));
}

// FamilyParams
if self
.family
.sibling_distances
.iter()
.any(|x| !x.is_finite() || *x < 0.0)
{
return Err(FavoritismError::InvalidInput(
"Sibling distances must be non-negative and finite".into(),
));
}

Ok(())
}
}
73 changes: 44 additions & 29 deletions math_explorer/tests/test_favoritism_security.rs
Original file line number Diff line number Diff line change
@@ -1,49 +1,64 @@
use math_explorer::applied::favoritism::{FavoritismInputs, calculate_favoritism_score};
use math_explorer::applied::favoritism::{
calculate_favoritism_score, try_calculate_favoritism_score, FavoritismInputs,
};

#[test]
fn test_security_division_by_zero_prevention() {
fn test_favoritism_nan_input_legacy() {
let mut inputs = FavoritismInputs::default();
// Set x_0 to zero to trigger division by zero in proximity integral
inputs.time.x_0 = 0.0;
inputs.time.t = f64::NAN;

// Legacy function should return NaN now (due to try_calculate wrapper)
#[allow(deprecated)]
let score = calculate_favoritism_score(&inputs);
assert!(score.is_nan(), "Legacy function should return NaN for invalid input");
}

// Should return a finite value (clamped), not Infinity or NaN
assert!(
score.is_finite(),
"Score should be finite even when x_0 is 0.0"
);
// Since x_0 is small (high proximity), score should be high/positive
assert!(score > 0.0);
#[test]
fn test_favoritism_nan_input_safe() {
let mut inputs = FavoritismInputs::default();
inputs.time.t = f64::NAN;

let result = try_calculate_favoritism_score(&inputs);
assert!(result.is_err(), "Safe function should return Err for NaN input");
}

#[test]
fn test_security_empty_siblings_prevention() {
fn test_favoritism_inf_input_safe() {
let mut inputs = FavoritismInputs::default();
// Empty siblings list means denominator integral becomes 0
inputs.family.sibling_distances = vec![];
inputs.time.t = f64::INFINITY;

let score = calculate_favoritism_score(&inputs);
let result = try_calculate_favoritism_score(&inputs);
assert!(result.is_err(), "Safe function should return Err for Inf input");
}

// Should handle empty siblings gracefully (e.g. treat as no competition)
// and not return Infinity
assert!(
score.is_finite(),
"Score should be finite even with no siblings"
);
assert!(score > 0.0);
#[test]
fn test_favoritism_negative_time_safe() {
let mut inputs = FavoritismInputs::default();
inputs.time.t = -10.0;

let result = try_calculate_favoritism_score(&inputs);
assert!(result.is_err(), "Safe function should return Err for negative time");
}

#[test]
fn test_security_log_domain_prevention() {
fn test_favoritism_personality_nan() {
let mut inputs = FavoritismInputs::default();
// Set f_initial to -1.0 or less to trigger log(0) or log(negative)
inputs.contact.f_initial = -5.0;
inputs.personality.intelligence = f64::NAN;

#[allow(deprecated)]
let score = calculate_favoritism_score(&inputs);
assert!(score.is_nan(), "Legacy function should return NaN for personality NaN");

assert!(
score.is_finite(),
"Score should be finite even with invalid contact frequency"
);
let result = try_calculate_favoritism_score(&inputs);
assert!(result.is_err(), "Safe function should return Err for personality NaN");
}

#[test]
fn test_favoritism_valid_input() {
let inputs = FavoritismInputs::default();
let result = try_calculate_favoritism_score(&inputs);
assert!(result.is_ok());
let score = result.unwrap();
assert!(score.is_finite());
assert!(score > 0.0);
}
Loading