use scirs2_core::numeric::Float;
use std::collections::{HashMap, VecDeque};
use std::fmt::Debug;
use crate::error::Result;
#[derive(Debug, Clone, Copy)]
pub enum CurriculumStrategy {
None,
DifficultyProgression,
DiversityBased,
SelfPaced,
TeacherStudent,
Adversarial,
MultiTask,
Adaptive,
}
#[derive(Debug, Clone)]
pub struct CurriculumLearner<T: Float + Debug + Send + Sync + 'static> {
strategy: CurriculumStrategy,
curriculum_params: CurriculumParams<T>,
progress_tracker: LearningProgressTracker<T>,
curriculum_state: CurriculumState<T>,
task_scheduler: TaskScheduler<T>,
performance_history: VecDeque<PerformanceRecord<T>>,
}
#[derive(Debug, Clone)]
pub struct CurriculumParams<T: Float + Debug + Send + Sync + 'static> {
initial_difficulty: T,
max_difficulty: T,
difficulty_increment: T,
progression_threshold: T,
patience: usize,
self_pacing_factor: T,
diversity_weight: T,
teacher_confidence: T,
}
#[derive(Debug, Clone)]
pub struct LearningProgressTracker<T: Float + Debug + Send + Sync + 'static> {
performance_timeline: VecDeque<T>,
learning_rates: VecDeque<T>,
competency_levels: HashMap<String, T>,
}
#[derive(Debug, Clone)]
pub struct CurriculumState<T: Float + Debug + Send + Sync + 'static> {
current_difficulty: T,
active_tasks: Vec<String>,
recent_performance: T,
epochs_since_increase: usize,
learning_phase: LearningPhase,
}
#[derive(Debug, Clone)]
pub struct TaskScheduler<T: Float + Debug + Send + Sync + 'static> {
task_queue: VecDeque<ScheduledTask<T>>,
scheduling_policy: SchedulingPolicy,
task_weights: HashMap<String, T>,
load_balancing: HashMap<String, T>,
}
#[derive(Debug, Clone)]
pub struct PerformanceRecord<T: Float + Debug + Send + Sync + 'static> {
task_id: String,
performance: T,
difficulty_level: T,
training_steps: usize,
timestamp: usize,
}
impl<T: Float + Debug + Send + Sync + 'static> PerformanceRecord<T> {
pub fn task_id(&self) -> &str {
&self.task_id
}
pub fn performance(&self) -> T {
self.performance
}
pub fn difficulty_level(&self) -> T {
self.difficulty_level
}
pub fn training_steps(&self) -> usize {
self.training_steps
}
pub fn timestamp(&self) -> usize {
self.timestamp
}
}
#[derive(Debug, Clone)]
pub struct ScheduledTask<T: Float + Debug + Send + Sync + 'static> {
task_id: String,
priority: T,
difficulty: T,
required_competency: T,
}
#[derive(Debug, Clone, Copy)]
pub enum LearningPhase {
Exploration,
SkillBuilding,
Mastery,
Transfer,
Generalization,
}
#[derive(Debug, Clone, Copy)]
pub enum SchedulingPolicy {
FIFO,
Priority,
WeightedRandom,
Balanced,
Adaptive,
}
impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> CurriculumLearner<T> {
pub fn new(strategy: CurriculumStrategy) -> Result<Self> {
Ok(Self {
strategy,
curriculum_params: CurriculumParams::default(),
progress_tracker: LearningProgressTracker::new(),
curriculum_state: CurriculumState::new()?,
task_scheduler: TaskScheduler::new()?,
performance_history: VecDeque::new(),
})
}
pub fn performance_history(&self) -> impl ExactSizeIterator<Item = &PerformanceRecord<T>> {
self.performance_history.iter()
}
pub fn update_curriculum(
&mut self,
task_id: &str,
performance: T,
training_steps: usize,
) -> Result<()> {
let record = PerformanceRecord {
task_id: task_id.to_string(),
performance,
difficulty_level: self.curriculum_state.current_difficulty,
training_steps,
timestamp: self.performance_history.len(),
};
self.performance_history.push_back(record);
if self.performance_history.len() > 1000 {
self.performance_history.pop_front();
}
self.progress_tracker.update_performance(performance);
match self.strategy {
CurriculumStrategy::None => Ok(()),
CurriculumStrategy::DifficultyProgression => {
self.update_difficulty_progression(performance)
}
CurriculumStrategy::DiversityBased => {
self.update_diversity_curriculum(task_id, performance)
}
CurriculumStrategy::SelfPaced => self.update_self_paced_curriculum(performance),
CurriculumStrategy::TeacherStudent => {
self.update_teacher_student_curriculum(performance)
}
CurriculumStrategy::Adversarial => self.update_adversarial_curriculum(performance),
CurriculumStrategy::MultiTask => {
self.update_multi_task_curriculum(task_id, performance)
}
CurriculumStrategy::Adaptive => self.update_adaptive_curriculum(task_id, performance),
}
}
fn clamp_difficulty(&self, value: T) -> T {
value
.max(self.curriculum_params.initial_difficulty)
.min(self.curriculum_params.max_difficulty)
}
fn update_diversity_curriculum(&mut self, task_id: &str, performance: T) -> Result<()> {
self.curriculum_state.recent_performance = performance;
if !self
.curriculum_state
.active_tasks
.iter()
.any(|t| t == task_id)
{
self.curriculum_state.active_tasks.push(task_id.to_string());
}
let attempts = self.performance_history.len().max(1);
let distinct = self.curriculum_state.active_tasks.len();
let diversity: T = scirs2_core::numeric::NumCast::from(distinct as f64 / attempts as f64)
.unwrap_or_else(|| T::zero());
let increment = self.curriculum_params.difficulty_increment
* self.curriculum_params.diversity_weight
* diversity;
self.curriculum_state.current_difficulty =
self.clamp_difficulty(self.curriculum_state.current_difficulty + increment);
self.update_learning_phase();
Ok(())
}
fn update_teacher_student_curriculum(&mut self, performance: T) -> Result<()> {
self.curriculum_state.recent_performance = performance;
let increment = self.curriculum_params.difficulty_increment;
let proposal = if performance >= self.curriculum_params.teacher_confidence {
self.curriculum_state.epochs_since_increase = 0;
self.curriculum_state.current_difficulty + increment
} else {
self.curriculum_state.epochs_since_increase += 1;
let half: T = scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::one());
self.curriculum_state.current_difficulty - increment * half
};
self.curriculum_state.current_difficulty = self.clamp_difficulty(proposal);
self.update_learning_phase();
Ok(())
}
fn update_adversarial_curriculum(&mut self, performance: T) -> Result<()> {
self.curriculum_state.recent_performance = performance;
let two: T = scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::one());
let half: T = scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::one());
let threshold = self.curriculum_params.progression_threshold;
let proposal = if performance > threshold {
self.curriculum_state.current_difficulty
+ self.curriculum_params.difficulty_increment * two
} else if performance < threshold * half {
self.curriculum_state.current_difficulty - self.curriculum_params.difficulty_increment
} else {
self.curriculum_state.current_difficulty
};
self.curriculum_state.current_difficulty = self.clamp_difficulty(proposal);
self.update_learning_phase();
Ok(())
}
fn update_multi_task_curriculum(&mut self, task_id: &str, performance: T) -> Result<()> {
self.curriculum_state.recent_performance = performance;
let alpha: T = scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(|| T::zero());
let competency = self
.progress_tracker
.competency_levels
.get(task_id)
.copied()
.unwrap_or(T::zero());
let updated = competency * (T::one() - alpha) + performance * alpha;
self.progress_tracker
.competency_levels
.insert(task_id.to_string(), updated);
let weakest = self
.progress_tracker
.competency_levels
.values()
.cloned()
.fold(T::infinity(), |a, b| a.min(b));
let proposal = if weakest.is_finite()
&& weakest > self.curriculum_params.progression_threshold
{
self.curriculum_state.current_difficulty + self.curriculum_params.difficulty_increment
} else {
self.curriculum_state.current_difficulty
};
self.curriculum_state.current_difficulty = self.clamp_difficulty(proposal);
self.update_learning_phase();
Ok(())
}
pub fn get_next_task(&mut self) -> Result<Option<String>> {
match self.strategy {
CurriculumStrategy::None => Ok(None),
_ => Ok(self.task_scheduler.schedule_next_task()),
}
}
fn update_difficulty_progression(&mut self, performance: T) -> Result<()> {
self.curriculum_state.recent_performance = performance;
if performance > self.curriculum_params.progression_threshold {
self.curriculum_state.epochs_since_increase += 1;
if self.curriculum_state.epochs_since_increase >= self.curriculum_params.patience {
let new_difficulty = (self.curriculum_state.current_difficulty
+ self.curriculum_params.difficulty_increment)
.min(self.curriculum_params.max_difficulty);
self.curriculum_state.current_difficulty = new_difficulty;
self.curriculum_state.epochs_since_increase = 0;
self.update_learning_phase();
}
} else {
self.curriculum_state.epochs_since_increase = 0;
}
Ok(())
}
fn update_self_paced_curriculum(&mut self, performance: T) -> Result<()> {
let pacing_factor = self.curriculum_params.self_pacing_factor;
let performance_ratio = performance / self.get_expected_performance();
let difficulty_adjustment = (performance_ratio - T::one()) * pacing_factor;
let new_difficulty = (self.curriculum_state.current_difficulty + difficulty_adjustment)
.max(self.curriculum_params.initial_difficulty)
.min(self.curriculum_params.max_difficulty);
self.curriculum_state.current_difficulty = new_difficulty;
Ok(())
}
fn update_adaptive_curriculum(&mut self, task_id: &str, performance: T) -> Result<()> {
let competency = self
.progress_tracker
.competency_levels
.get(task_id)
.copied()
.unwrap_or(T::zero());
let alpha = scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(|| T::zero());
let new_competency = competency * (T::one() - alpha) + performance * alpha;
self.progress_tracker
.competency_levels
.insert(task_id.to_string(), new_competency);
self.adapt_curriculum_parameters(task_id)?;
Ok(())
}
fn update_learning_phase(&mut self) {
let difficulty_ratio =
self.curriculum_state.current_difficulty / self.curriculum_params.max_difficulty;
self.curriculum_state.learning_phase = match difficulty_ratio {
x if x < scirs2_core::numeric::NumCast::from(0.2).unwrap_or_else(|| T::zero()) => {
LearningPhase::Exploration
}
x if x < scirs2_core::numeric::NumCast::from(0.4).unwrap_or_else(|| T::zero()) => {
LearningPhase::SkillBuilding
}
x if x < scirs2_core::numeric::NumCast::from(0.7).unwrap_or_else(|| T::zero()) => {
LearningPhase::Mastery
}
x if x < scirs2_core::numeric::NumCast::from(0.9).unwrap_or_else(|| T::zero()) => {
LearningPhase::Transfer
}
_ => LearningPhase::Generalization,
};
}
fn adapt_curriculum_parameters(&mut self, task_id: &str) -> Result<()> {
let performance_variance = self.calculate_performance_variance(task_id);
if performance_variance
> scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(|| T::zero())
{
self.curriculum_params.patience = self.curriculum_params.patience.max(5);
} else {
self.curriculum_params.patience =
(self.curriculum_params.patience.saturating_sub(1)).max(1);
}
let trend = self.calculate_performance_trend();
if trend > T::zero() {
self.curriculum_params.progression_threshold =
(self.curriculum_params.progression_threshold
* scirs2_core::numeric::NumCast::from(0.95).unwrap_or_else(|| T::zero()))
.max(scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::zero()));
} else {
self.curriculum_params.progression_threshold =
(self.curriculum_params.progression_threshold
* scirs2_core::numeric::NumCast::from(1.05).unwrap_or_else(|| T::zero()))
.min(scirs2_core::numeric::NumCast::from(0.95).unwrap_or_else(|| T::zero()));
}
Ok(())
}
fn calculate_performance_variance(&self, task_id: &str) -> T {
let task_performances: Vec<T> = self
.performance_history
.iter()
.filter(|record| record.task_id == task_id)
.map(|record| record.performance)
.collect();
if task_performances.len() < 2 {
return T::zero();
}
let mean = task_performances
.iter()
.cloned()
.fold(T::zero(), |a, b| a + b)
/ scirs2_core::numeric::NumCast::from(task_performances.len() as f64)
.unwrap_or_else(|| T::one());
let variance = task_performances
.iter()
.map(|&x| (x - mean) * (x - mean))
.fold(T::zero(), |a, b| a + b)
/ scirs2_core::numeric::NumCast::from((task_performances.len() - 1) as f64)
.unwrap_or_else(|| T::one());
variance
}
fn calculate_performance_trend(&self) -> T {
if self.performance_history.len() < 10 {
return T::zero();
}
let recent: Vec<T> = self
.performance_history
.iter()
.rev()
.take(10)
.map(|record| record.performance)
.collect();
let first_half_avg = recent[5..].iter().cloned().fold(T::zero(), |a, b| a + b)
/ scirs2_core::numeric::NumCast::from(5.0).unwrap_or_else(|| T::zero());
let second_half_avg = recent[..5].iter().cloned().fold(T::zero(), |a, b| a + b)
/ scirs2_core::numeric::NumCast::from(5.0).unwrap_or_else(|| T::zero());
second_half_avg - first_half_avg
}
fn get_expected_performance(&self) -> T {
let difficulty_factor =
self.curriculum_state.current_difficulty / self.curriculum_params.max_difficulty;
T::one()
- difficulty_factor
* scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::zero())
}
pub fn add_task(
&mut self,
task_id: String,
estimated_difficulty: T,
required_competency: T,
) -> Result<()> {
let scheduled_task = ScheduledTask {
task_id: task_id.clone(),
priority: T::one() / estimated_difficulty, difficulty: estimated_difficulty,
required_competency,
};
self.task_scheduler.add_task(scheduled_task);
self.progress_tracker
.competency_levels
.insert(task_id, T::zero());
Ok(())
}
pub fn get_curriculum_statistics(&self) -> HashMap<String, T> {
let mut stats = HashMap::new();
stats.insert(
"current_difficulty".to_string(),
self.curriculum_state.current_difficulty,
);
stats.insert(
"recent_performance".to_string(),
self.curriculum_state.recent_performance,
);
stats.insert(
"epochs_since_increase".to_string(),
scirs2_core::numeric::NumCast::from(self.curriculum_state.epochs_since_increase as f64)
.unwrap_or_else(|| T::zero()),
);
stats.insert(
"active_tasks_count".to_string(),
scirs2_core::numeric::NumCast::from(self.curriculum_state.active_tasks.len() as f64)
.unwrap_or_else(|| T::zero()),
);
if !self.progress_tracker.competency_levels.is_empty() {
let avg_competency = self
.progress_tracker
.competency_levels
.values()
.cloned()
.fold(T::zero(), |a, b| a + b)
/ scirs2_core::numeric::NumCast::from(
self.progress_tracker.competency_levels.len() as f64,
)
.unwrap_or_else(|| T::one());
stats.insert("average_competency".to_string(), avg_competency);
}
stats
}
pub fn strategy(&self) -> CurriculumStrategy {
self.strategy
}
pub fn current_difficulty(&self) -> T {
self.curriculum_state.current_difficulty
}
pub fn learning_phase(&self) -> LearningPhase {
self.curriculum_state.learning_phase
}
pub fn set_scheduling_policy(&mut self, policy: SchedulingPolicy) {
self.task_scheduler.set_policy(policy);
}
pub fn reset(&mut self) {
self.curriculum_state = CurriculumState::new_state();
self.progress_tracker.reset();
self.performance_history.clear();
self.task_scheduler.reset();
}
}
impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> LearningProgressTracker<T> {
fn new() -> Self {
Self {
performance_timeline: VecDeque::new(),
learning_rates: VecDeque::new(),
competency_levels: HashMap::new(),
}
}
fn update_performance(&mut self, performance: T) {
self.performance_timeline.push_back(performance);
if self.performance_timeline.len() > 1000 {
self.performance_timeline.pop_front();
}
}
fn reset(&mut self) {
self.performance_timeline.clear();
self.learning_rates.clear();
self.competency_levels.clear();
}
}
impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> CurriculumState<T> {
fn new() -> Result<Self> {
Ok(Self::new_state())
}
fn new_state() -> Self {
Self {
current_difficulty: scirs2_core::numeric::NumCast::from(0.1)
.unwrap_or_else(|| T::zero()),
active_tasks: Vec::new(),
recent_performance: T::zero(),
epochs_since_increase: 0,
learning_phase: LearningPhase::Exploration,
}
}
}
impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> TaskScheduler<T> {
fn new() -> Result<Self> {
Ok(Self {
task_queue: VecDeque::new(),
scheduling_policy: SchedulingPolicy::Priority,
task_weights: HashMap::new(),
load_balancing: HashMap::new(),
})
}
fn add_task(&mut self, task: ScheduledTask<T>) {
self.task_queue.push_back(task);
}
fn schedule_next_task(&mut self) -> Option<String> {
if self.task_queue.is_empty() {
return None;
}
let index = match self.scheduling_policy {
SchedulingPolicy::FIFO => 0,
SchedulingPolicy::Priority => Self::arg_extreme(&self.task_queue, true),
SchedulingPolicy::Balanced => Self::arg_extreme(&self.task_queue, false),
SchedulingPolicy::WeightedRandom => {
let total: f64 = self
.task_queue
.iter()
.map(|t| t.priority.to_f64().unwrap_or(0.0).max(0.0))
.sum();
if total <= 0.0 {
0
} else {
let mut target =
(self.task_weights.len() as f64 * 0.618_033_988_75).fract() * total;
let mut chosen = self.task_queue.len() - 1;
for (i, task) in self.task_queue.iter().enumerate() {
target -= task.priority.to_f64().unwrap_or(0.0).max(0.0);
if target <= 0.0 {
chosen = i;
break;
}
}
chosen
}
}
SchedulingPolicy::Adaptive => {
let mut best = 0;
let mut best_gap = T::infinity();
for (i, task) in self.task_queue.iter().enumerate() {
let gap = (task.difficulty - task.required_competency).abs();
if gap < best_gap {
best_gap = gap;
best = i;
}
}
best
}
};
let task = self.task_queue.remove(index)?;
let counter = self
.load_balancing
.entry(task.task_id.clone())
.or_insert(T::zero());
*counter = *counter + T::one();
self.task_weights
.insert(task.task_id.clone(), task.priority);
Some(task.task_id)
}
fn arg_extreme(queue: &VecDeque<ScheduledTask<T>>, highest: bool) -> usize {
let mut best = 0;
let mut best_priority = match queue.front() {
Some(task) => task.priority,
None => return 0,
};
for (i, task) in queue.iter().enumerate() {
let better = if highest {
task.priority > best_priority
} else {
task.priority < best_priority
};
if better {
best_priority = task.priority;
best = i;
}
}
best
}
fn set_policy(&mut self, policy: SchedulingPolicy) {
self.scheduling_policy = policy;
}
fn reset(&mut self) {
self.task_queue.clear();
self.task_weights.clear();
self.load_balancing.clear();
}
}
impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> Default for CurriculumParams<T> {
fn default() -> Self {
Self {
initial_difficulty: scirs2_core::numeric::NumCast::from(0.1)
.unwrap_or_else(|| T::zero()),
max_difficulty: scirs2_core::numeric::NumCast::from(1.0).unwrap_or_else(|| T::zero()),
difficulty_increment: scirs2_core::numeric::NumCast::from(0.05)
.unwrap_or_else(|| T::zero()),
progression_threshold: scirs2_core::numeric::NumCast::from(0.8)
.unwrap_or_else(|| T::zero()),
patience: 5,
self_pacing_factor: scirs2_core::numeric::NumCast::from(0.1)
.unwrap_or_else(|| T::zero()),
diversity_weight: scirs2_core::numeric::NumCast::from(0.2).unwrap_or_else(|| T::zero()),
teacher_confidence: scirs2_core::numeric::NumCast::from(0.9)
.unwrap_or_else(|| T::zero()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn all_strategies() -> [CurriculumStrategy; 8] {
[
CurriculumStrategy::None,
CurriculumStrategy::DifficultyProgression,
CurriculumStrategy::DiversityBased,
CurriculumStrategy::SelfPaced,
CurriculumStrategy::TeacherStudent,
CurriculumStrategy::Adversarial,
CurriculumStrategy::MultiTask,
CurriculumStrategy::Adaptive,
]
}
#[test]
fn every_strategy_runs_and_stays_in_range() {
for strategy in all_strategies() {
let mut learner = CurriculumLearner::<f64>::new(strategy).expect("curriculum creation");
for step in 0..50 {
let task = if step % 3 == 0 { "a" } else { "b" };
learner
.update_curriculum(task, 0.9, step)
.unwrap_or_else(|e| panic!("{strategy:?} failed: {e}"));
}
let difficulty = learner.current_difficulty();
assert!(
(0.1..=1.0).contains(&difficulty),
"{strategy:?} produced difficulty {difficulty}"
);
}
}
#[test]
fn strategies_do_not_all_behave_identically() {
let run = |strategy| {
let mut learner = CurriculumLearner::<f64>::new(strategy).expect("curriculum creation");
for step in 0..30 {
learner
.update_curriculum("task", 0.95, step)
.expect("update");
}
learner.current_difficulty()
};
let teacher = run(CurriculumStrategy::TeacherStudent);
let adversarial = run(CurriculumStrategy::Adversarial);
let diversity = run(CurriculumStrategy::DiversityBased);
let multi_task = run(CurriculumStrategy::MultiTask);
let mut distinct = vec![teacher, adversarial, diversity, multi_task];
distinct.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
distinct.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
assert!(
distinct.len() >= 3,
"strategies collapsed to the same behaviour: {distinct:?}"
);
}
#[test]
fn zero_performance_keeps_difficulty_finite() {
for strategy in all_strategies() {
let mut learner = CurriculumLearner::<f64>::new(strategy).expect("curriculum creation");
for step in 0..20 {
learner.update_curriculum("t", 0.0, step).expect("update");
}
assert!(
learner.current_difficulty().is_finite(),
"{strategy:?} produced a non-finite difficulty"
);
}
}
#[test]
fn scheduling_policies_pick_different_tasks() {
let mut learner =
CurriculumLearner::<f64>::new(CurriculumStrategy::Adaptive).expect("creation");
learner.add_task("hard".to_string(), 4.0, 0.9).expect("add");
learner.add_task("easy".to_string(), 1.0, 0.9).expect("add");
learner.set_scheduling_policy(SchedulingPolicy::FIFO);
assert_eq!(
learner.get_next_task().expect("schedule"),
Some("hard".to_string())
);
let mut learner =
CurriculumLearner::<f64>::new(CurriculumStrategy::Adaptive).expect("creation");
learner.add_task("hard".to_string(), 4.0, 0.9).expect("add");
learner.add_task("easy".to_string(), 1.0, 0.9).expect("add");
learner.set_scheduling_policy(SchedulingPolicy::Priority);
assert_eq!(
learner.get_next_task().expect("schedule"),
Some("easy".to_string())
);
}
#[test]
fn statistics_are_reported() {
let mut learner =
CurriculumLearner::<f64>::new(CurriculumStrategy::Adaptive).expect("creation");
learner.update_curriculum("t", 0.7, 1).expect("update");
let stats = learner.get_curriculum_statistics();
assert!(stats.contains_key("current_difficulty"));
assert!(stats.contains_key("recent_performance"));
}
#[test]
fn reset_restores_the_initial_state() {
let mut learner =
CurriculumLearner::<f64>::new(CurriculumStrategy::Adversarial).expect("creation");
for step in 0..20 {
learner.update_curriculum("t", 0.99, step).expect("update");
}
assert!(learner.current_difficulty() > 0.1);
learner.reset();
assert!((learner.current_difficulty() - 0.1).abs() < 1e-12);
}
}