mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
FSRS - Ignore revlogs before date while optimizing (#2922)
* Added: Date input button * Added: ignoreDate to config * Added: Backend * Optimize function passes value * Fix: Spelling * Moved: filter logic from revlog_for_srs to update_memory_state * fmt * Copyright header * ./check * Fix: Test * Renamed: Ignore_date -> Ignore_before_date * Neaten parameters * evaluate weights * ./check * Optimize all presets * Added: Label localizations * Removed globe label * Added: Tooltip * Changed error type * fmt * Moved filter to own function * missing function call replacement * Fix: Typo * Apply suggestions from code review Co-authored-by: Damien Elmes <dae@users.noreply.github.com> * timestamp * 1000 -> timestamp_millis * ignoreBefore -> ignore_before * clarified ignore_before variables * i64 -> TimestampMillis * Un-traitified remove_revlogs_before * Added: ms == 0 guard * Added: Ignore_before affects scheduling * Moved filter to fsrs_items_for_training * removed filter from revlog_for_srs * Tuple -> UpdateMemoryStateEntry * Removed unused function * Removed superfluous _ms from variables * cid -> id * Different ignore method * Added: Unit test * cid -> id * Test: Exact ms edge case * ./check * Fix: re-learns could be before ignore date in cards without learning steps * getignoreRevlogsBeforeMs -> getIgnoreRevlogsBeforeMs * Removed pub(crate) * Clarified unit test * last_learn_entry -> first_of_last_learn_entries * @user1823's method * IOS fix * ./check * Fix: width defined twice
This commit is contained in:
parent
9642a69b88
commit
8b18a08b3b
13 changed files with 276 additions and 43 deletions
|
@ -346,6 +346,7 @@ deck-config-compute-optimal-weights = Optimize FSRS parameters
|
||||||
deck-config-compute-optimal-retention = Compute optimal retention
|
deck-config-compute-optimal-retention = Compute optimal retention
|
||||||
deck-config-optimize-button = Optimize
|
deck-config-optimize-button = Optimize
|
||||||
deck-config-compute-button = Compute
|
deck-config-compute-button = Compute
|
||||||
|
deck-config-ignore-before = Ignore reviews before
|
||||||
deck-config-optimize-all-tip = You can optimize all presets at once by using the dropdown button next to "Save".
|
deck-config-optimize-all-tip = You can optimize all presets at once by using the dropdown button next to "Save".
|
||||||
deck-config-evaluate-button = Evaluate
|
deck-config-evaluate-button = Evaluate
|
||||||
deck-config-desired-retention = Desired retention
|
deck-config-desired-retention = Desired retention
|
||||||
|
@ -398,6 +399,9 @@ deck-config-reschedule-cards-warning =
|
||||||
|
|
||||||
Use this option sparingly, as it will add a review entry to each of your cards, and
|
Use this option sparingly, as it will add a review entry to each of your cards, and
|
||||||
increase the size of your collection.
|
increase the size of your collection.
|
||||||
|
deck-config-ignore-before-tooltip =
|
||||||
|
If set, reviews before the provided date will be ignored when optimizing & evaluating FSRS parameters.
|
||||||
|
This can be useful if you imported someone else's scheduling data, or have changed the way you use the answer buttons.
|
||||||
deck-config-compute-optimal-weights-tooltip =
|
deck-config-compute-optimal-weights-tooltip =
|
||||||
Once you've done 1000+ reviews in Anki, you can use the Optimize button to analyze your review history,
|
Once you've done 1000+ reviews in Anki, you can use the Optimize button to analyze your review history,
|
||||||
and automatically generate parameters that are optimal for your memory and the content you're studying.
|
and automatically generate parameters that are optimal for your memory and the content you're studying.
|
||||||
|
|
|
@ -154,6 +154,7 @@ message DeckConfig {
|
||||||
|
|
||||||
// for fsrs
|
// for fsrs
|
||||||
float desired_retention = 37;
|
float desired_retention = 37;
|
||||||
|
string ignore_revlogs_before_date = 46;
|
||||||
// used for fsrs_reschedule in the past
|
// used for fsrs_reschedule in the past
|
||||||
reserved 39;
|
reserved 39;
|
||||||
float sm2_retention = 40;
|
float sm2_retention = 40;
|
||||||
|
|
|
@ -339,6 +339,7 @@ message ComputeFsrsWeightsRequest {
|
||||||
/// The search used to gather cards for training
|
/// The search used to gather cards for training
|
||||||
string search = 1;
|
string search = 1;
|
||||||
repeated float current_weights = 2;
|
repeated float current_weights = 2;
|
||||||
|
int64 ignore_revlogs_before_ms = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ComputeFsrsWeightsResponse {
|
message ComputeFsrsWeightsResponse {
|
||||||
|
@ -400,6 +401,7 @@ message GetOptimalRetentionParametersResponse {
|
||||||
message EvaluateWeightsRequest {
|
message EvaluateWeightsRequest {
|
||||||
repeated float weights = 1;
|
repeated float weights = 1;
|
||||||
string search = 2;
|
string search = 2;
|
||||||
|
int64 ignore_revlogs_before_ms = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message EvaluateWeightsResponse {
|
message EvaluateWeightsResponse {
|
||||||
|
|
|
@ -77,6 +77,7 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
|
||||||
other: Vec::new(),
|
other: Vec::new(),
|
||||||
sm2_retention: 0.9,
|
sm2_retention: 0.9,
|
||||||
weight_search: String::new(),
|
weight_search: String::new(),
|
||||||
|
ignore_revlogs_before_date: String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Default for DeckConfig {
|
impl Default for DeckConfig {
|
||||||
|
|
|
@ -74,6 +74,8 @@ pub struct DeckConfSchema11 {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
desired_retention: f32,
|
desired_retention: f32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
ignore_revlogs_before_date: String,
|
||||||
|
#[serde(default)]
|
||||||
stop_timer_on_answer: bool,
|
stop_timer_on_answer: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
seconds_to_show_question: f32,
|
seconds_to_show_question: f32,
|
||||||
|
@ -294,6 +296,7 @@ impl Default for DeckConfSchema11 {
|
||||||
desired_retention: 0.9,
|
desired_retention: 0.9,
|
||||||
sm2_retention: 0.9,
|
sm2_retention: 0.9,
|
||||||
weight_search: "".to_string(),
|
weight_search: "".to_string(),
|
||||||
|
ignore_revlogs_before_date: "".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -368,6 +371,7 @@ impl From<DeckConfSchema11> for DeckConfig {
|
||||||
bury_reviews: c.rev.bury,
|
bury_reviews: c.rev.bury,
|
||||||
bury_interday_learning: c.bury_interday_learning,
|
bury_interday_learning: c.bury_interday_learning,
|
||||||
fsrs_weights: c.fsrs_weights,
|
fsrs_weights: c.fsrs_weights,
|
||||||
|
ignore_revlogs_before_date: c.ignore_revlogs_before_date,
|
||||||
desired_retention: c.desired_retention,
|
desired_retention: c.desired_retention,
|
||||||
sm2_retention: c.sm2_retention,
|
sm2_retention: c.sm2_retention,
|
||||||
weight_search: c.weight_search,
|
weight_search: c.weight_search,
|
||||||
|
@ -477,6 +481,7 @@ impl From<DeckConfig> for DeckConfSchema11 {
|
||||||
desired_retention: i.desired_retention,
|
desired_retention: i.desired_retention,
|
||||||
sm2_retention: i.sm2_retention,
|
sm2_retention: i.sm2_retention,
|
||||||
weight_search: i.weight_search,
|
weight_search: i.weight_search,
|
||||||
|
ignore_revlogs_before_date: i.ignore_revlogs_before_date,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -507,6 +512,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! {
|
||||||
"waitForAudio",
|
"waitForAudio",
|
||||||
"sm2Retention",
|
"sm2Retention",
|
||||||
"weightSearch",
|
"weightSearch",
|
||||||
|
"ignoreRevlogsBeforeDate",
|
||||||
};
|
};
|
||||||
|
|
||||||
static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! {
|
static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! {
|
||||||
|
|
|
@ -17,7 +17,9 @@ use fsrs::DEFAULT_WEIGHTS;
|
||||||
use crate::config::StringKey;
|
use crate::config::StringKey;
|
||||||
use crate::decks::NormalDeck;
|
use crate::decks::NormalDeck;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateEntry;
|
||||||
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest;
|
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest;
|
||||||
|
use crate::scheduler::fsrs::weights::ignore_revlogs_before_ms_from_config;
|
||||||
use crate::search::JoinSearches;
|
use crate::search::JoinSearches;
|
||||||
use crate::search::SearchNode;
|
use crate::search::SearchNode;
|
||||||
use crate::storage::comma_separated_ids;
|
use crate::storage::comma_separated_ids;
|
||||||
|
@ -238,29 +240,32 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decks_needing_memory_recompute.is_empty() {
|
if !decks_needing_memory_recompute.is_empty() {
|
||||||
let input: Vec<(Option<UpdateMemoryStateRequest>, SearchNode)> =
|
let input: Vec<UpdateMemoryStateEntry> = decks_needing_memory_recompute
|
||||||
decks_needing_memory_recompute
|
.into_iter()
|
||||||
.into_iter()
|
.map(|(conf_id, search)| {
|
||||||
.map(|(conf_id, search)| {
|
let config = configs_after_update.get(&conf_id);
|
||||||
let weights = configs_after_update.get(&conf_id).and_then(|c| {
|
let weights = config.and_then(|c| {
|
||||||
if req.fsrs {
|
if req.fsrs {
|
||||||
Some(UpdateMemoryStateRequest {
|
Some(UpdateMemoryStateRequest {
|
||||||
weights: c.inner.fsrs_weights.clone(),
|
weights: c.inner.fsrs_weights.clone(),
|
||||||
desired_retention: c.inner.desired_retention,
|
desired_retention: c.inner.desired_retention,
|
||||||
max_interval: c.inner.maximum_review_interval,
|
max_interval: c.inner.maximum_review_interval,
|
||||||
reschedule: req.fsrs_reschedule,
|
reschedule: req.fsrs_reschedule,
|
||||||
sm2_retention: c.inner.sm2_retention,
|
sm2_retention: c.inner.sm2_retention,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Ok((
|
Ok(UpdateMemoryStateEntry {
|
||||||
weights,
|
req: weights,
|
||||||
SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search)),
|
search: SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search)),
|
||||||
))
|
ignore_before: config
|
||||||
|
.map(ignore_revlogs_before_ms_from_config)
|
||||||
|
.unwrap_or(Ok(0.into()))?,
|
||||||
})
|
})
|
||||||
.collect::<Result<_>>()?;
|
})
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
self.update_memory_state(input)?;
|
self.update_memory_state(input)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,8 +337,10 @@ impl Collection {
|
||||||
} else {
|
} else {
|
||||||
config.inner.weight_search.clone()
|
config.inner.weight_search.clone()
|
||||||
};
|
};
|
||||||
|
let ignore_revlogs_before_ms = ignore_revlogs_before_ms_from_config(config)?;
|
||||||
match self.compute_weights(
|
match self.compute_weights(
|
||||||
&search,
|
&search,
|
||||||
|
ignore_revlogs_before_ms,
|
||||||
idx as u32 + 1,
|
idx as u32 + 1,
|
||||||
config_len,
|
config_len,
|
||||||
&config.inner.fsrs_weights,
|
&config.inner.fsrs_weights,
|
||||||
|
|
|
@ -14,6 +14,7 @@ use rand::prelude::*;
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use revlog::RevlogEntryPartial;
|
use revlog::RevlogEntryPartial;
|
||||||
|
|
||||||
|
use super::fsrs::weights::ignore_revlogs_before_ms_from_config;
|
||||||
use super::queue::BuryMode;
|
use super::queue::BuryMode;
|
||||||
use super::states::steps::LearningSteps;
|
use super::states::steps::LearningSteps;
|
||||||
use super::states::CardState;
|
use super::states::CardState;
|
||||||
|
@ -382,6 +383,7 @@ impl Collection {
|
||||||
revlog,
|
revlog,
|
||||||
timing.next_day_at,
|
timing.next_day_at,
|
||||||
config.inner.sm2_retention,
|
config.inner.sm2_retention,
|
||||||
|
ignore_revlogs_before_ms_from_config(&config)?,
|
||||||
)?;
|
)?;
|
||||||
card.set_memory_state(&fsrs, item, config.inner.sm2_retention)?;
|
card.set_memory_state(&fsrs, item, config.inner.sm2_retention)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ use fsrs::MemoryState;
|
||||||
use fsrs::FSRS;
|
use fsrs::FSRS;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
use super::weights::ignore_revlogs_before_ms_from_config;
|
||||||
use crate::card::CardType;
|
use crate::card::CardType;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::revlog::RevlogEntry;
|
use crate::revlog::RevlogEntry;
|
||||||
|
@ -35,6 +36,12 @@ pub(crate) struct UpdateMemoryStateRequest {
|
||||||
pub reschedule: bool,
|
pub reschedule: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct UpdateMemoryStateEntry {
|
||||||
|
pub req: Option<UpdateMemoryStateRequest>,
|
||||||
|
pub search: SearchNode,
|
||||||
|
pub ignore_before: TimestampMillis,
|
||||||
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
/// For each provided set of weights, locate cards with the provided search,
|
/// For each provided set of weights, locate cards with the provided search,
|
||||||
/// and update their memory state.
|
/// and update their memory state.
|
||||||
|
@ -43,11 +50,16 @@ impl Collection {
|
||||||
/// memory state should be removed.
|
/// memory state should be removed.
|
||||||
pub(crate) fn update_memory_state(
|
pub(crate) fn update_memory_state(
|
||||||
&mut self,
|
&mut self,
|
||||||
entries: Vec<(Option<UpdateMemoryStateRequest>, SearchNode)>,
|
entries: Vec<UpdateMemoryStateEntry>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let timing = self.timing_today()?;
|
let timing = self.timing_today()?;
|
||||||
let usn = self.usn()?;
|
let usn = self.usn()?;
|
||||||
for (req, search) in entries {
|
for UpdateMemoryStateEntry {
|
||||||
|
req,
|
||||||
|
search,
|
||||||
|
ignore_before,
|
||||||
|
} in entries
|
||||||
|
{
|
||||||
let search =
|
let search =
|
||||||
SearchBuilder::all([search.into(), SearchNode::State(StateKind::New).negated()]);
|
SearchBuilder::all([search.into(), SearchNode::State(StateKind::New).negated()]);
|
||||||
let revlog = self.revlog_for_srs(search)?;
|
let revlog = self.revlog_for_srs(search)?;
|
||||||
|
@ -64,6 +76,7 @@ impl Collection {
|
||||||
revlog,
|
revlog,
|
||||||
timing.next_day_at,
|
timing.next_day_at,
|
||||||
sm2_retention.unwrap_or(0.9),
|
sm2_retention.unwrap_or(0.9),
|
||||||
|
ignore_before,
|
||||||
)?;
|
)?;
|
||||||
let desired_retention = req.as_ref().map(|w| w.desired_retention);
|
let desired_retention = req.as_ref().map(|w| w.desired_retention);
|
||||||
let mut progress = self.new_progress_handler::<ComputeMemoryProgress>();
|
let mut progress = self.new_progress_handler::<ComputeMemoryProgress>();
|
||||||
|
@ -148,6 +161,7 @@ impl Collection {
|
||||||
revlog,
|
revlog,
|
||||||
self.timing_today()?.next_day_at,
|
self.timing_today()?.next_day_at,
|
||||||
sm2_retention,
|
sm2_retention,
|
||||||
|
ignore_revlogs_before_ms_from_config(&config)?,
|
||||||
)?;
|
)?;
|
||||||
card.set_memory_state(&fsrs, item, sm2_retention)?;
|
card.set_memory_state(&fsrs, item, sm2_retention)?;
|
||||||
Ok(ComputeMemoryStateResponse {
|
Ok(ComputeMemoryStateResponse {
|
||||||
|
@ -196,6 +210,7 @@ pub(crate) fn fsrs_items_for_memory_state(
|
||||||
revlogs: Vec<RevlogEntry>,
|
revlogs: Vec<RevlogEntry>,
|
||||||
next_day_at: TimestampSecs,
|
next_day_at: TimestampSecs,
|
||||||
sm2_retention: f32,
|
sm2_retention: f32,
|
||||||
|
ignore_revlogs_before: TimestampMillis,
|
||||||
) -> Result<Vec<(CardId, Option<FsrsItemWithStartingState>)>> {
|
) -> Result<Vec<(CardId, Option<FsrsItemWithStartingState>)>> {
|
||||||
revlogs
|
revlogs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -204,7 +219,13 @@ pub(crate) fn fsrs_items_for_memory_state(
|
||||||
.map(|(card_id, group)| {
|
.map(|(card_id, group)| {
|
||||||
Ok((
|
Ok((
|
||||||
card_id,
|
card_id,
|
||||||
single_card_revlog_to_item(fsrs, group.collect(), next_day_at, sm2_retention)?,
|
single_card_revlog_to_item(
|
||||||
|
fsrs,
|
||||||
|
group.collect(),
|
||||||
|
next_day_at,
|
||||||
|
sm2_retention,
|
||||||
|
ignore_revlogs_before,
|
||||||
|
)?,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -257,6 +278,7 @@ pub(crate) fn single_card_revlog_to_item(
|
||||||
entries: Vec<RevlogEntry>,
|
entries: Vec<RevlogEntry>,
|
||||||
next_day_at: TimestampSecs,
|
next_day_at: TimestampSecs,
|
||||||
sm2_retention: f32,
|
sm2_retention: f32,
|
||||||
|
ignore_revlogs_before: TimestampMillis,
|
||||||
) -> Result<Option<FsrsItemWithStartingState>> {
|
) -> Result<Option<FsrsItemWithStartingState>> {
|
||||||
struct FirstReview {
|
struct FirstReview {
|
||||||
interval: f32,
|
interval: f32,
|
||||||
|
@ -275,7 +297,7 @@ pub(crate) fn single_card_revlog_to_item(
|
||||||
/ 1000.0,
|
/ 1000.0,
|
||||||
});
|
});
|
||||||
if let Some((mut items, revlogs_complete)) =
|
if let Some((mut items, revlogs_complete)) =
|
||||||
single_card_revlog_to_items(entries, next_day_at, false)
|
single_card_revlog_to_items(entries, next_day_at, false, ignore_revlogs_before)
|
||||||
{
|
{
|
||||||
let mut item = items.pop().unwrap();
|
let mut item = items.pop().unwrap();
|
||||||
if revlogs_complete {
|
if revlogs_complete {
|
||||||
|
@ -345,6 +367,7 @@ mod tests {
|
||||||
],
|
],
|
||||||
TimestampSecs::now(),
|
TimestampSecs::now(),
|
||||||
0.9,
|
0.9,
|
||||||
|
0.into(),
|
||||||
)?
|
)?
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_int_eq(
|
assert_int_eq(
|
||||||
|
@ -377,6 +400,7 @@ mod tests {
|
||||||
}],
|
}],
|
||||||
TimestampSecs::now(),
|
TimestampSecs::now(),
|
||||||
0.9,
|
0.9,
|
||||||
|
0.into(),
|
||||||
)?;
|
)?;
|
||||||
assert!(item.is_none());
|
assert!(item.is_none());
|
||||||
card.interval = 123;
|
card.interval = 123;
|
||||||
|
|
|
@ -9,6 +9,8 @@ use anki_io::write_file;
|
||||||
use anki_proto::scheduler::ComputeFsrsWeightsResponse;
|
use anki_proto::scheduler::ComputeFsrsWeightsResponse;
|
||||||
use anki_proto::stats::revlog_entry;
|
use anki_proto::stats::revlog_entry;
|
||||||
use anki_proto::stats::RevlogEntries;
|
use anki_proto::stats::RevlogEntries;
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use chrono::NaiveTime;
|
||||||
use fsrs::CombinedProgressState;
|
use fsrs::CombinedProgressState;
|
||||||
use fsrs::FSRSItem;
|
use fsrs::FSRSItem;
|
||||||
use fsrs::FSRSReview;
|
use fsrs::FSRSReview;
|
||||||
|
@ -26,6 +28,23 @@ use crate::search::SortMode;
|
||||||
|
|
||||||
pub(crate) type Weights = Vec<f32>;
|
pub(crate) type Weights = Vec<f32>;
|
||||||
|
|
||||||
|
fn ignore_revlogs_before_date_to_ms(
|
||||||
|
ignore_revlogs_before_date: &String,
|
||||||
|
) -> Result<TimestampMillis> {
|
||||||
|
Ok(match ignore_revlogs_before_date {
|
||||||
|
s if s.is_empty() => 0,
|
||||||
|
s => NaiveDate::parse_from_str(s.as_str(), "%Y-%m-%d")
|
||||||
|
.or_else(|err| invalid_input!(err, "Error parsing date: {s}"))?
|
||||||
|
.and_time(NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap())
|
||||||
|
.timestamp_millis(),
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn ignore_revlogs_before_ms_from_config(config: &DeckConfig) -> Result<TimestampMillis> {
|
||||||
|
ignore_revlogs_before_date_to_ms(&config.inner.ignore_revlogs_before_date)
|
||||||
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
/// Note this does not return an error if there are less than 400 items -
|
/// Note this does not return an error if there are less than 400 items -
|
||||||
/// the caller should instead check the fsrs_items count in the return
|
/// the caller should instead check the fsrs_items count in the return
|
||||||
|
@ -33,6 +52,7 @@ impl Collection {
|
||||||
pub fn compute_weights(
|
pub fn compute_weights(
|
||||||
&mut self,
|
&mut self,
|
||||||
search: &str,
|
search: &str,
|
||||||
|
ignore_revlogs_before: TimestampMillis,
|
||||||
current_preset: u32,
|
current_preset: u32,
|
||||||
total_presets: u32,
|
total_presets: u32,
|
||||||
current_weights: &Weights,
|
current_weights: &Weights,
|
||||||
|
@ -45,7 +65,8 @@ impl Collection {
|
||||||
count: revlogs.len(),
|
count: revlogs.len(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let items = fsrs_items_for_training(revlogs.clone(), timing.next_day_at);
|
let items =
|
||||||
|
fsrs_items_for_training(revlogs.clone(), timing.next_day_at, ignore_revlogs_before);
|
||||||
let fsrs_items = items.len() as u32;
|
let fsrs_items = items.len() as u32;
|
||||||
anki_progress.update(false, |p| {
|
anki_progress.update(false, |p| {
|
||||||
p.fsrs_items = fsrs_items;
|
p.fsrs_items = fsrs_items;
|
||||||
|
@ -122,11 +143,16 @@ impl Collection {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn evaluate_weights(&mut self, weights: &Weights, search: &str) -> Result<ModelEvaluation> {
|
pub fn evaluate_weights(
|
||||||
|
&mut self,
|
||||||
|
weights: &Weights,
|
||||||
|
search: &str,
|
||||||
|
ignore_revlogs_before: TimestampMillis,
|
||||||
|
) -> Result<ModelEvaluation> {
|
||||||
let timing = self.timing_today()?;
|
let timing = self.timing_today()?;
|
||||||
let mut anki_progress = self.new_progress_handler::<ComputeWeightsProgress>();
|
let mut anki_progress = self.new_progress_handler::<ComputeWeightsProgress>();
|
||||||
let guard = self.search_cards_into_table(search, SortMode::NoOrder)?;
|
let guard = self.search_cards_into_table(search, SortMode::NoOrder)?;
|
||||||
let revlogs = guard
|
let revlogs: Vec<RevlogEntry> = guard
|
||||||
.col
|
.col
|
||||||
.storage
|
.storage
|
||||||
.get_revlog_entries_for_searched_cards_in_card_order()?;
|
.get_revlog_entries_for_searched_cards_in_card_order()?;
|
||||||
|
@ -135,8 +161,8 @@ impl Collection {
|
||||||
count: revlogs.len(),
|
count: revlogs.len(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let items = fsrs_items_for_training(revlogs, timing.next_day_at);
|
anki_progress.state.fsrs_items = revlogs.len() as u32;
|
||||||
anki_progress.state.fsrs_items = items.len() as u32;
|
let items = fsrs_items_for_training(revlogs, timing.next_day_at, ignore_revlogs_before);
|
||||||
let fsrs = FSRS::new(Some(weights))?;
|
let fsrs = FSRS::new(Some(weights))?;
|
||||||
Ok(fsrs.evaluate(items, |ip| {
|
Ok(fsrs.evaluate(items, |ip| {
|
||||||
anki_progress
|
anki_progress
|
||||||
|
@ -161,13 +187,17 @@ pub struct ComputeWeightsProgress {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a series of revlog entries sorted by card id into FSRS items.
|
/// Convert a series of revlog entries sorted by card id into FSRS items.
|
||||||
fn fsrs_items_for_training(revlogs: Vec<RevlogEntry>, next_day_at: TimestampSecs) -> Vec<FSRSItem> {
|
fn fsrs_items_for_training(
|
||||||
|
revlogs: Vec<RevlogEntry>,
|
||||||
|
next_day_at: TimestampSecs,
|
||||||
|
review_revlogs_before: TimestampMillis,
|
||||||
|
) -> Vec<FSRSItem> {
|
||||||
let mut revlogs = revlogs
|
let mut revlogs = revlogs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.group_by(|r| r.cid)
|
.group_by(|r| r.cid)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(_cid, entries)| {
|
.filter_map(|(_cid, entries)| {
|
||||||
single_card_revlog_to_items(entries.collect(), next_day_at, true)
|
single_card_revlog_to_items(entries.collect(), next_day_at, true, review_revlogs_before)
|
||||||
})
|
})
|
||||||
.flat_map(|i| i.0)
|
.flat_map(|i| i.0)
|
||||||
.collect_vec();
|
.collect_vec();
|
||||||
|
@ -189,24 +219,25 @@ pub(crate) fn single_card_revlog_to_items(
|
||||||
mut entries: Vec<RevlogEntry>,
|
mut entries: Vec<RevlogEntry>,
|
||||||
next_day_at: TimestampSecs,
|
next_day_at: TimestampSecs,
|
||||||
training: bool,
|
training: bool,
|
||||||
|
ignore_revlogs_before: TimestampMillis,
|
||||||
) -> Option<(Vec<FSRSItem>, bool)> {
|
) -> Option<(Vec<FSRSItem>, bool)> {
|
||||||
let mut last_learn_entry = None;
|
let mut first_of_last_learn_entries = None;
|
||||||
let mut revlogs_complete = false;
|
let mut revlogs_complete = false;
|
||||||
for (index, entry) in entries.iter().enumerate().rev() {
|
for (index, entry) in entries.iter().enumerate().rev() {
|
||||||
if matches!(
|
if matches!(
|
||||||
(entry.review_kind, entry.button_chosen),
|
(entry.review_kind, entry.button_chosen),
|
||||||
(RevlogReviewKind::Learning, 1..=4)
|
(RevlogReviewKind::Learning, 1..=4)
|
||||||
) {
|
) {
|
||||||
last_learn_entry = Some(index);
|
first_of_last_learn_entries = Some(index);
|
||||||
revlogs_complete = true;
|
revlogs_complete = true;
|
||||||
} else if last_learn_entry.is_some() {
|
} else if first_of_last_learn_entries.is_some() {
|
||||||
break;
|
break;
|
||||||
// if we find the `Forget` entry before the `Learn` entry, we should
|
// if we find the `Forget` entry before the `Learn` entry, we should
|
||||||
// ignore all the entries
|
// ignore all the entries
|
||||||
} else if matches!(
|
} else if matches!(
|
||||||
(entry.review_kind, entry.ease_factor),
|
(entry.review_kind, entry.ease_factor),
|
||||||
(RevlogReviewKind::Manual, 0)
|
(RevlogReviewKind::Manual, 0)
|
||||||
) && last_learn_entry.is_none()
|
) && first_of_last_learn_entries.is_none()
|
||||||
{
|
{
|
||||||
revlogs_complete = false;
|
revlogs_complete = false;
|
||||||
break;
|
break;
|
||||||
|
@ -221,12 +252,40 @@ pub(crate) fn single_card_revlog_to_items(
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if training {
|
||||||
|
// While training ignore the entire card if the first learning step of the last
|
||||||
|
// group of learning steps is before the ignore_revlogs_before date
|
||||||
|
if let Some(idx) = first_of_last_learn_entries {
|
||||||
|
if entries[idx].id.0 < ignore_revlogs_before.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// While reviewing if the first learning step is before the ignore date,
|
||||||
|
// ignore every review before and including the last learning step
|
||||||
|
if let Some(idx) = first_of_last_learn_entries {
|
||||||
|
if entries[idx].id.0 < ignore_revlogs_before.0 && idx < entries.len() - 1 {
|
||||||
|
let last_learn_entry = entries
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.rev()
|
||||||
|
.find(|(_idx, e)| e.review_kind == RevlogReviewKind::Learning)
|
||||||
|
.map(|(idx, _)| idx);
|
||||||
|
|
||||||
|
entries.drain(..(last_learn_entry? + 1));
|
||||||
|
revlogs_complete = false;
|
||||||
|
first_of_last_learn_entries = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let first_relearn = entries
|
let first_relearn = entries
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|(_idx, e)| e.review_kind == RevlogReviewKind::Relearning)
|
.find(|(_idx, e)| {
|
||||||
|
e.id.0 > ignore_revlogs_before.0 && e.review_kind == RevlogReviewKind::Relearning
|
||||||
|
})
|
||||||
.map(|(idx, _)| idx);
|
.map(|(idx, _)| idx);
|
||||||
if let Some(idx) = last_learn_entry.or(first_relearn) {
|
if let Some(idx) = first_of_last_learn_entries.or(first_relearn) {
|
||||||
// start from the (re)learning step
|
// start from the (re)learning step
|
||||||
if idx > 0 {
|
if idx > 0 {
|
||||||
entries.drain(..idx);
|
entries.drain(..idx);
|
||||||
|
@ -315,10 +374,14 @@ pub(crate) mod tests {
|
||||||
|
|
||||||
const NEXT_DAY_AT: TimestampSecs = TimestampSecs(86400 * 100);
|
const NEXT_DAY_AT: TimestampSecs = TimestampSecs(86400 * 100);
|
||||||
|
|
||||||
|
fn days_ago_ms(days_ago: i64) -> TimestampMillis {
|
||||||
|
((NEXT_DAY_AT.0 - days_ago * 86400) * 1000).into()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn revlog(review_kind: RevlogReviewKind, days_ago: i64) -> RevlogEntry {
|
pub(crate) fn revlog(review_kind: RevlogReviewKind, days_ago: i64) -> RevlogEntry {
|
||||||
RevlogEntry {
|
RevlogEntry {
|
||||||
review_kind,
|
review_kind,
|
||||||
id: ((NEXT_DAY_AT.0 - days_ago * 86400) * 1000).into(),
|
id: days_ago_ms(days_ago).into(),
|
||||||
button_chosen: 3,
|
button_chosen: 3,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
@ -328,8 +391,17 @@ pub(crate) mod tests {
|
||||||
FSRSReview { rating: 3, delta_t }
|
FSRSReview { rating: 3, delta_t }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn convert_ignore_before(
|
||||||
|
revlog: &[RevlogEntry],
|
||||||
|
training: bool,
|
||||||
|
ignore_before: TimestampMillis,
|
||||||
|
) -> Option<Vec<FSRSItem>> {
|
||||||
|
single_card_revlog_to_items(revlog.to_vec(), NEXT_DAY_AT, training, ignore_before)
|
||||||
|
.map(|i| i.0)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn convert(revlog: &[RevlogEntry], training: bool) -> Option<Vec<FSRSItem>> {
|
pub(crate) fn convert(revlog: &[RevlogEntry], training: bool) -> Option<Vec<FSRSItem>> {
|
||||||
single_card_revlog_to_items(revlog.to_vec(), NEXT_DAY_AT, training).map(|i| i.0)
|
convert_ignore_before(revlog, training, 0.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
@ -454,4 +526,53 @@ pub(crate) mod tests {
|
||||||
fsrs_items!([review(0)])
|
fsrs_items!([review(0)])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_cards_before_ignore_before_date_when_training() {
|
||||||
|
let revlogs = &[
|
||||||
|
revlog(RevlogReviewKind::Learning, 10),
|
||||||
|
revlog(RevlogReviewKind::Learning, 8),
|
||||||
|
];
|
||||||
|
// | = Ignore before
|
||||||
|
// L = learning step
|
||||||
|
// L L |
|
||||||
|
assert_eq!(convert_ignore_before(revlogs, true, days_ago_ms(7)), None);
|
||||||
|
// L | L
|
||||||
|
assert_eq!(convert_ignore_before(revlogs, true, days_ago_ms(9)), None);
|
||||||
|
// L (|L) (exact same millisecond)
|
||||||
|
assert_eq!(
|
||||||
|
convert_ignore_before(revlogs, true, days_ago_ms(10)),
|
||||||
|
convert(revlogs, true)
|
||||||
|
);
|
||||||
|
// | L L
|
||||||
|
assert_eq!(
|
||||||
|
convert_ignore_before(revlogs, true, days_ago_ms(11)),
|
||||||
|
convert(revlogs, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignore_before_date_between_learning_steps_when_reviewing() {
|
||||||
|
let revlogs = &[
|
||||||
|
revlog(RevlogReviewKind::Learning, 10),
|
||||||
|
revlog(RevlogReviewKind::Learning, 8),
|
||||||
|
revlog(RevlogReviewKind::Review, 2),
|
||||||
|
];
|
||||||
|
// L | L R
|
||||||
|
assert_ne!(
|
||||||
|
convert_ignore_before(revlogs, false, days_ago_ms(9)),
|
||||||
|
convert(revlogs, false)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_ignore_before(revlogs, false, days_ago_ms(9))
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
// | L L R
|
||||||
|
assert_eq!(
|
||||||
|
convert_ignore_before(revlogs, false, days_ago_ms(11)),
|
||||||
|
convert(revlogs, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -254,7 +254,13 @@ impl crate::services::SchedulerService for Collection {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: scheduler::ComputeFsrsWeightsRequest,
|
input: scheduler::ComputeFsrsWeightsRequest,
|
||||||
) -> Result<scheduler::ComputeFsrsWeightsResponse> {
|
) -> Result<scheduler::ComputeFsrsWeightsResponse> {
|
||||||
self.compute_weights(&input.search, 1, 1, &input.current_weights)
|
self.compute_weights(
|
||||||
|
&input.search,
|
||||||
|
input.ignore_revlogs_before_ms.into(),
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
&input.current_weights,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_optimal_retention(
|
fn compute_optimal_retention(
|
||||||
|
@ -270,7 +276,11 @@ impl crate::services::SchedulerService for Collection {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: scheduler::EvaluateWeightsRequest,
|
input: scheduler::EvaluateWeightsRequest,
|
||||||
) -> Result<scheduler::EvaluateWeightsResponse> {
|
) -> Result<scheduler::EvaluateWeightsResponse> {
|
||||||
let ret = self.evaluate_weights(&input.weights, &input.search)?;
|
let ret = self.evaluate_weights(
|
||||||
|
&input.weights,
|
||||||
|
&input.search,
|
||||||
|
input.ignore_revlogs_before_ms.into(),
|
||||||
|
)?;
|
||||||
Ok(scheduler::EvaluateWeightsResponse {
|
Ok(scheduler::EvaluateWeightsResponse {
|
||||||
log_loss: ret.log_loss,
|
log_loss: ret.log_loss,
|
||||||
rmse_bins: ret.rmse_bins,
|
rmse_bins: ret.rmse_bins,
|
||||||
|
|
|
@ -62,6 +62,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
help: tr.deckConfigRescheduleCardsOnChangeTooltip(),
|
help: tr.deckConfigRescheduleCardsOnChangeTooltip(),
|
||||||
sched: HelpItemScheduler.FSRS,
|
sched: HelpItemScheduler.FSRS,
|
||||||
},
|
},
|
||||||
|
ignoreRevlogsBeforeMs: {
|
||||||
|
title: tr.deckConfigIgnoreBefore(),
|
||||||
|
help: tr.deckConfigIgnoreBeforeTooltip(),
|
||||||
|
sched: HelpItemScheduler.FSRS,
|
||||||
|
},
|
||||||
computeOptimalWeights: {
|
computeOptimalWeights: {
|
||||||
title: tr.deckConfigComputeOptimalWeights(),
|
title: tr.deckConfigComputeOptimalWeights(),
|
||||||
help: tr.deckConfigComputeOptimalWeightsTooltip(),
|
help: tr.deckConfigComputeOptimalWeightsTooltip(),
|
||||||
|
|
34
ts/deck-options/DateInput.svelte
Normal file
34
ts/deck-options/DateInput.svelte
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script>
|
||||||
|
import Col from "../components/Col.svelte";
|
||||||
|
import ConfigInput from "../components/ConfigInput.svelte";
|
||||||
|
import Row from "../components/Row.svelte";
|
||||||
|
|
||||||
|
export let date;
|
||||||
|
$: date = date ? date : "1970-01-01";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ConfigInput>
|
||||||
|
<Row --cols={2}>
|
||||||
|
<Col>
|
||||||
|
<slot />
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<input bind:value={date} type="date" />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</ConfigInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -19,6 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import SwitchRow from "components/SwitchRow.svelte";
|
import SwitchRow from "components/SwitchRow.svelte";
|
||||||
|
|
||||||
import SettingTitle from "../components/SettingTitle.svelte";
|
import SettingTitle from "../components/SettingTitle.svelte";
|
||||||
|
import DateInput from "./DateInput.svelte";
|
||||||
import GlobalLabel from "./GlobalLabel.svelte";
|
import GlobalLabel from "./GlobalLabel.svelte";
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
|
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
|
||||||
|
@ -86,6 +87,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getIgnoreRevlogsBeforeMs() {
|
||||||
|
return BigInt(
|
||||||
|
$config.ignoreRevlogsBeforeDate
|
||||||
|
? new Date($config.ignoreRevlogsBeforeDate).getTime()
|
||||||
|
: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function computeWeights(): Promise<void> {
|
async function computeWeights(): Promise<void> {
|
||||||
if (computingWeights) {
|
if (computingWeights) {
|
||||||
await setWantsAbort({});
|
await setWantsAbort({});
|
||||||
|
@ -104,6 +113,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
search: $config.weightSearch
|
search: $config.weightSearch
|
||||||
? $config.weightSearch
|
? $config.weightSearch
|
||||||
: defaultWeightSearch,
|
: defaultWeightSearch,
|
||||||
|
ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(),
|
||||||
currentWeights: $config.fsrsWeights,
|
currentWeights: $config.fsrsWeights,
|
||||||
});
|
});
|
||||||
if (computeWeightsProgress) {
|
if (computeWeightsProgress) {
|
||||||
|
@ -148,6 +158,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
const resp = await evaluateWeights({
|
const resp = await evaluateWeights({
|
||||||
weights: $config.fsrsWeights,
|
weights: $config.fsrsWeights,
|
||||||
search,
|
search,
|
||||||
|
ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(),
|
||||||
});
|
});
|
||||||
if (computeWeightsProgress) {
|
if (computeWeightsProgress) {
|
||||||
computeWeightsProgress.current = computeWeightsProgress.total;
|
computeWeightsProgress.current = computeWeightsProgress.total;
|
||||||
|
@ -317,6 +328,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
{tr.deckConfigEvaluateButton()}
|
{tr.deckConfigEvaluateButton()}
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
<DateInput bind:date={$config.ignoreRevlogsBeforeDate}>
|
||||||
|
<SettingTitle on:click={() => openHelpModal("ignoreBefore")}>
|
||||||
|
{tr.deckConfigIgnoreBefore()}
|
||||||
|
</SettingTitle>
|
||||||
|
</DateInput>
|
||||||
{#if computingWeights || checkingWeights}<div>
|
{#if computingWeights || checkingWeights}<div>
|
||||||
{computeWeightsProgressString}
|
{computeWeightsProgressString}
|
||||||
</div>{/if}
|
</div>{/if}
|
||||||
|
|
Loading…
Reference in a new issue