mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Easy days: revisited (#3661)
* new easy days algorithm * take easy day percent totals in account when determining reduced scheduling * Use variant method to avoid repeated mapping to a constant (dae) It was probably not worth the time I took to change this ^_^;
This commit is contained in:
parent
58bcab2484
commit
aaf8b4dddb
1 changed files with 94 additions and 52 deletions
|
@ -26,6 +26,35 @@ const MAX_LOAD_BALANCE_INTERVAL: usize = 90;
|
||||||
const LOAD_BALANCE_DAYS: usize = (MAX_LOAD_BALANCE_INTERVAL as f32 * 1.1) as usize;
|
const LOAD_BALANCE_DAYS: usize = (MAX_LOAD_BALANCE_INTERVAL as f32 * 1.1) as usize;
|
||||||
const SIBLING_PENALTY: f32 = 0.001;
|
const SIBLING_PENALTY: f32 = 0.001;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum EasyDay {
|
||||||
|
Minimum,
|
||||||
|
Reduced,
|
||||||
|
Normal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f32> for EasyDay {
|
||||||
|
fn from(other: f32) -> EasyDay {
|
||||||
|
match other {
|
||||||
|
1.0 => EasyDay::Normal,
|
||||||
|
0.0 => EasyDay::Minimum,
|
||||||
|
_ => EasyDay::Reduced,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EasyDay {
|
||||||
|
fn load_modifier(&self) -> f32 {
|
||||||
|
match self {
|
||||||
|
// this is a non-zero value so if all days are minimum, the load balancer will
|
||||||
|
// proceed as normal
|
||||||
|
EasyDay::Minimum => 0.0001,
|
||||||
|
EasyDay::Reduced => 0.5,
|
||||||
|
EasyDay::Normal => 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct LoadBalancerDay {
|
struct LoadBalancerDay {
|
||||||
cards: Vec<(CardId, NoteId)>,
|
cards: Vec<(CardId, NoteId)>,
|
||||||
|
@ -84,7 +113,7 @@ pub struct LoadBalancer {
|
||||||
/// Load balancer operates at the preset level, it only counts
|
/// Load balancer operates at the preset level, it only counts
|
||||||
/// cards in the same preset as the card being balanced.
|
/// cards in the same preset as the card being balanced.
|
||||||
days_by_preset: HashMap<DeckConfigId, [LoadBalancerDay; LOAD_BALANCE_DAYS]>,
|
days_by_preset: HashMap<DeckConfigId, [LoadBalancerDay; LOAD_BALANCE_DAYS]>,
|
||||||
easy_days_percentages_by_preset: HashMap<DeckConfigId, [f32; 7]>,
|
easy_days_percentages_by_preset: HashMap<DeckConfigId, [EasyDay; 7]>,
|
||||||
next_day_at: TimestampSecs,
|
next_day_at: TimestampSecs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,21 +162,26 @@ impl LoadBalancer {
|
||||||
);
|
);
|
||||||
let configs = storage.get_deck_config_map()?;
|
let configs = storage.get_deck_config_map()?;
|
||||||
|
|
||||||
let mut easy_days_percentages_by_preset = HashMap::with_capacity(configs.len());
|
let easy_days_percentages_by_preset = configs
|
||||||
for (dcid, conf) in configs {
|
.into_iter()
|
||||||
let easy_days_percentages = if conf.inner.easy_days_percentages.is_empty() {
|
.map(|(dcid, conf)| {
|
||||||
[1.0; 7]
|
let easy_days_percentages: [EasyDay; 7] =
|
||||||
} else {
|
if conf.inner.easy_days_percentages.is_empty() {
|
||||||
conf.inner.easy_days_percentages.try_into().map_err(|_| {
|
[EasyDay::Normal; 7]
|
||||||
AnkiError::from(InvalidInputError {
|
} else {
|
||||||
message: "expected 7 days".into(),
|
TryInto::<[_; 7]>::try_into(conf.inner.easy_days_percentages)
|
||||||
source: None,
|
.map_err(|_| {
|
||||||
backtrace: None,
|
AnkiError::from(InvalidInputError {
|
||||||
})
|
message: "expected 7 days".into(),
|
||||||
})?
|
source: None,
|
||||||
};
|
backtrace: None,
|
||||||
easy_days_percentages_by_preset.insert(dcid, easy_days_percentages);
|
})
|
||||||
}
|
})?
|
||||||
|
.map(EasyDay::from)
|
||||||
|
};
|
||||||
|
Ok((dcid, easy_days_percentages))
|
||||||
|
})
|
||||||
|
.collect::<Result<HashMap<_, [EasyDay; 7]>, AnkiError>>()?;
|
||||||
|
|
||||||
Ok(LoadBalancer {
|
Ok(LoadBalancer {
|
||||||
days_by_preset,
|
days_by_preset,
|
||||||
|
@ -220,22 +254,46 @@ impl LoadBalancer {
|
||||||
})
|
})
|
||||||
.unzip();
|
.unzip();
|
||||||
|
|
||||||
let easy_days_percentages = self.easy_days_percentages_by_preset.get(&deckconfig_id)?;
|
// Determine which days to schedule to with respect to Easy Day settings
|
||||||
// check if easy days are in effect by seeing if all days have the same
|
// If a day is Normal, it will always be an option to schedule to
|
||||||
// configuration. If all days are the same, we can skip out on calculating
|
// If a day is Minimum, it will almost never be an option to schedule to
|
||||||
// the distribution
|
// If a day is Reduced, it will look at the amount of cards due in the fuzz
|
||||||
let easy_days_are_all_the_same = easy_days_percentages
|
// range to determine if scheduling a card on that day would put it
|
||||||
|
// above the reduced threshold or not.
|
||||||
|
// the resulting easy_days_modifier will be a vec of 0.0s and 1.0s, to be
|
||||||
|
// used when calculating the day's weight. This turns the day on or off.
|
||||||
|
// Note that it does not actually set it to 0.0, but a small
|
||||||
|
// 0.0-ish number (see EASY_DAYS_MINIMUM_LOAD) to remove the need to
|
||||||
|
// handle a handful of zero-related corner cases.
|
||||||
|
let easy_days_load = self.easy_days_percentages_by_preset.get(&deckconfig_id)?;
|
||||||
|
let total_review_count: usize = review_counts.iter().sum();
|
||||||
|
let total_percents: f32 = weekdays
|
||||||
.iter()
|
.iter()
|
||||||
.all(|day| easy_days_percentages[0] == *day);
|
.map(|&weekday| easy_days_load[weekday].load_modifier())
|
||||||
let expected_distribution = if easy_days_are_all_the_same {
|
.sum();
|
||||||
vec![1.0; weekdays.len()]
|
let easy_days_modifier = weekdays
|
||||||
} else {
|
.iter()
|
||||||
let percentages = weekdays
|
.zip(review_counts.iter())
|
||||||
.iter()
|
.map(|(&weekday, &review_count)| {
|
||||||
.map(|&wd| easy_days_percentages[wd])
|
let day = match easy_days_load[weekday] {
|
||||||
.collect::<Vec<_>>();
|
EasyDay::Reduced => {
|
||||||
check_review_distribution(&review_counts, &percentages)
|
const HALF: f32 = 0.5;
|
||||||
};
|
let other_days_review_total = (total_review_count - review_count) as f32;
|
||||||
|
let other_days_percent_total = total_percents - HALF;
|
||||||
|
let normalized_count = review_count as f32 / HALF;
|
||||||
|
let reduced_day_threshold =
|
||||||
|
other_days_review_total / other_days_percent_total;
|
||||||
|
if normalized_count > reduced_day_threshold {
|
||||||
|
EasyDay::Minimum
|
||||||
|
} else {
|
||||||
|
EasyDay::Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
day.load_modifier()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// calculate params for each day
|
// calculate params for each day
|
||||||
let intervals_and_params = interval_days
|
let intervals_and_params = interval_days
|
||||||
|
@ -259,14 +317,14 @@ impl LoadBalancer {
|
||||||
let card_count_weight = (1.0 / card_count as f32).powi(2);
|
let card_count_weight = (1.0 / card_count as f32).powi(2);
|
||||||
let card_interval_weight = 1.0 / target_interval as f32;
|
let card_interval_weight = 1.0 / target_interval as f32;
|
||||||
|
|
||||||
card_count_weight * card_interval_weight * sibling_multiplier
|
card_count_weight
|
||||||
|
* card_interval_weight
|
||||||
|
* sibling_multiplier
|
||||||
|
* easy_days_modifier[interval_index]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
(
|
(target_interval, weight)
|
||||||
target_interval,
|
|
||||||
weight * expected_distribution[interval_index],
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
@ -303,19 +361,3 @@ fn interval_to_weekday(interval: u32, next_day_at: TimestampSecs) -> usize {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
target_datetime.weekday().num_days_from_monday() as usize
|
target_datetime.weekday().num_days_from_monday() as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_review_distribution(actual_reviews: &[usize], percentages: &[f32]) -> Vec<f32> {
|
|
||||||
if percentages.iter().sum::<f32>() == 0.0 {
|
|
||||||
return vec![1.0; actual_reviews.len()];
|
|
||||||
}
|
|
||||||
let total_actual = actual_reviews.iter().sum::<usize>() as f32;
|
|
||||||
let expected_distribution: Vec<f32> = percentages
|
|
||||||
.iter()
|
|
||||||
.map(|&p| p * (total_actual / percentages.iter().sum::<f32>()))
|
|
||||||
.collect();
|
|
||||||
expected_distribution
|
|
||||||
.iter()
|
|
||||||
.zip(actual_reviews.iter())
|
|
||||||
.map(|(&e, &a)| (e - a as f32).max(0.0))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue