Feat/fsrs simulator backend part (#3075)
* [WIP] FSRS simulator * add desired_retention as input * cargo fmt * fix format * add standard copyright header * support existing cards * fix format * pass days_elapsed into Card::convert & return None
This commit is contained in:
parent
bbfe83a8d3
commit
8d197a1555
|
@ -1792,9 +1792,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fsrs"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eca50c5f619d6fe0e00962be6f68bf45d67de2fa211a12645882619f5900ff3"
|
||||
checksum = "84a04c31041078628c5ce7310be96c987bf7f33a3f8815fa0fcdb084eb31feba"
|
||||
dependencies = [
|
||||
"burn",
|
||||
"itertools 0.12.1",
|
||||
|
|
|
@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git"
|
|||
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
|
||||
|
||||
[workspace.dependencies.fsrs]
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
|
||||
# rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941"
|
||||
# path = "../open-spaced-repetition/fsrs-rs"
|
||||
|
|
|
@ -1198,7 +1198,7 @@
|
|||
},
|
||||
{
|
||||
"name": "fsrs",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"authors": "Open Spaced Repetition",
|
||||
"repository": "https://github.com/open-spaced-repetition/fsrs-rs",
|
||||
"license": "BSD-3-Clause",
|
||||
|
|
|
@ -51,6 +51,8 @@ service SchedulerService {
|
|||
returns (GetOptimalRetentionParametersResponse);
|
||||
rpc ComputeOptimalRetention(ComputeOptimalRetentionRequest)
|
||||
returns (ComputeOptimalRetentionResponse);
|
||||
rpc SimulateFsrsReview(SimulateFsrsReviewRequest)
|
||||
returns (SimulateFsrsReviewResponse);
|
||||
rpc EvaluateWeights(EvaluateWeightsRequest) returns (EvaluateWeightsResponse);
|
||||
rpc ComputeMemoryState(cards.CardId) returns (ComputeMemoryStateResponse);
|
||||
// The number of days the calculated interval was fuzzed by on the previous
|
||||
|
@ -371,6 +373,24 @@ message FsrsReview {
|
|||
uint32 delta_t = 2;
|
||||
}
|
||||
|
||||
message SimulateFsrsReviewRequest {
|
||||
repeated float weights = 1;
|
||||
float desired_retention = 2;
|
||||
uint32 deck_size = 3;
|
||||
uint32 days_to_simulate = 4;
|
||||
uint32 new_limit = 5;
|
||||
uint32 review_limit = 6;
|
||||
uint32 max_interval = 7;
|
||||
string search = 8;
|
||||
}
|
||||
|
||||
message SimulateFsrsReviewResponse {
|
||||
repeated float accumulated_knowledge_acquisition = 1;
|
||||
repeated uint32 daily_review_count = 2;
|
||||
repeated uint32 daily_new_count = 3;
|
||||
repeated float daily_time_cost = 4;
|
||||
}
|
||||
|
||||
message ComputeOptimalRetentionRequest {
|
||||
repeated float weights = 1;
|
||||
uint32 days_to_simulate = 2;
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
mod error;
|
||||
pub mod memory_state;
|
||||
pub mod retention;
|
||||
pub mod simulator;
|
||||
pub mod try_collect;
|
||||
pub mod weights;
|
||||
|
|
|
@ -8,6 +8,7 @@ use fsrs::FSRS;
|
|||
use itertools::Itertools;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::revlog::RevlogEntry;
|
||||
use crate::revlog::RevlogReviewKind;
|
||||
use crate::search::SortMode;
|
||||
|
||||
|
@ -27,7 +28,12 @@ impl Collection {
|
|||
if req.days_to_simulate == 0 {
|
||||
invalid_input!("no days to simulate")
|
||||
}
|
||||
let p = self.get_optimal_retention_parameters(&req.search)?;
|
||||
let revlogs = self
|
||||
.search_cards_into_table(&req.search, SortMode::NoOrder)?
|
||||
.col
|
||||
.storage
|
||||
.get_revlog_entries_for_searched_cards_in_card_order()?;
|
||||
let p = self.get_optimal_retention_parameters(revlogs)?;
|
||||
let learn_span = req.days_to_simulate as usize;
|
||||
let learn_limit = 10;
|
||||
let deck_size = learn_span * learn_limit;
|
||||
|
@ -71,13 +77,8 @@ impl Collection {
|
|||
|
||||
pub fn get_optimal_retention_parameters(
|
||||
&mut self,
|
||||
search: &str,
|
||||
revlogs: Vec<RevlogEntry>,
|
||||
) -> Result<OptimalRetentionParameters> {
|
||||
let revlogs = self
|
||||
.search_cards_into_table(search, SortMode::NoOrder)?
|
||||
.col
|
||||
.storage
|
||||
.get_revlog_entries_for_searched_cards_in_card_order()?;
|
||||
let first_rating_count = revlogs
|
||||
.iter()
|
||||
.group_by(|r| r.cid)
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use anki_proto::scheduler::SimulateFsrsReviewRequest;
|
||||
use anki_proto::scheduler::SimulateFsrsReviewResponse;
|
||||
use fsrs::simulate;
|
||||
use fsrs::SimulatorConfig;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::search::SortMode;
|
||||
|
||||
impl Collection {
|
||||
pub fn simulate_review(
|
||||
&mut self,
|
||||
req: SimulateFsrsReviewRequest,
|
||||
) -> Result<SimulateFsrsReviewResponse> {
|
||||
let guard = self.search_cards_into_table(&req.search, SortMode::NoOrder)?;
|
||||
let revlogs = guard
|
||||
.col
|
||||
.storage
|
||||
.get_revlog_entries_for_searched_cards_in_card_order()?;
|
||||
let cards = guard.col.storage.all_searched_cards()?;
|
||||
drop(guard);
|
||||
let p = self.get_optimal_retention_parameters(revlogs)?;
|
||||
let config = SimulatorConfig {
|
||||
deck_size: req.deck_size as usize,
|
||||
learn_span: req.days_to_simulate as usize,
|
||||
max_cost_perday: f64::MAX,
|
||||
max_ivl: req.max_interval as f64,
|
||||
recall_costs: [p.recall_secs_hard, p.recall_secs_good, p.recall_secs_easy],
|
||||
forget_cost: p.forget_secs,
|
||||
learn_cost: p.learn_secs,
|
||||
first_rating_prob: [
|
||||
p.first_rating_probability_again,
|
||||
p.first_rating_probability_hard,
|
||||
p.first_rating_probability_good,
|
||||
p.first_rating_probability_easy,
|
||||
],
|
||||
review_rating_prob: [
|
||||
p.review_rating_probability_hard,
|
||||
p.review_rating_probability_good,
|
||||
p.review_rating_probability_easy,
|
||||
],
|
||||
loss_aversion: 1.0,
|
||||
learn_limit: req.new_limit as usize,
|
||||
review_limit: req.review_limit as usize,
|
||||
};
|
||||
let days_elapsed = self.timing_today().unwrap().days_elapsed as i32;
|
||||
let (
|
||||
accumulated_knowledge_acquisition,
|
||||
daily_review_count,
|
||||
daily_new_count,
|
||||
daily_time_cost,
|
||||
) = simulate(
|
||||
&config,
|
||||
&req.weights.iter().map(|w| *w as f64).collect_vec(),
|
||||
req.desired_retention as f64,
|
||||
None,
|
||||
Some(
|
||||
cards
|
||||
.into_iter()
|
||||
.filter_map(|c| Card::convert(c, days_elapsed))
|
||||
.collect_vec(),
|
||||
),
|
||||
);
|
||||
Ok(SimulateFsrsReviewResponse {
|
||||
accumulated_knowledge_acquisition: accumulated_knowledge_acquisition
|
||||
.iter()
|
||||
.map(|x| *x as f32)
|
||||
.collect_vec(),
|
||||
daily_review_count: daily_review_count.iter().map(|x| *x as u32).collect_vec(),
|
||||
daily_new_count: daily_new_count.iter().map(|x| *x as u32).collect_vec(),
|
||||
daily_time_cost: daily_time_cost.iter().map(|x| *x as f32).collect_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Card {
|
||||
fn convert(card: Card, days_elapsed: i32) -> Option<fsrs::Card> {
|
||||
match card.memory_state {
|
||||
Some(state) => {
|
||||
let due = card.original_or_current_due();
|
||||
let relative_due = due - days_elapsed;
|
||||
Some(fsrs::Card {
|
||||
difficulty: state.difficulty as f64,
|
||||
stability: state.stability as f64,
|
||||
last_date: (relative_due - card.interval as i32) as f64,
|
||||
due: relative_due as f64,
|
||||
})
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,8 @@ use anki_proto::scheduler::FsrsBenchmarkResponse;
|
|||
use anki_proto::scheduler::FuzzDeltaRequest;
|
||||
use anki_proto::scheduler::FuzzDeltaResponse;
|
||||
use anki_proto::scheduler::GetOptimalRetentionParametersResponse;
|
||||
use anki_proto::scheduler::SimulateFsrsReviewRequest;
|
||||
use anki_proto::scheduler::SimulateFsrsReviewResponse;
|
||||
use fsrs::FSRSItem;
|
||||
use fsrs::FSRSReview;
|
||||
use fsrs::FSRS;
|
||||
|
@ -24,6 +26,7 @@ use crate::prelude::*;
|
|||
use crate::scheduler::new::NewCardDueOrder;
|
||||
use crate::scheduler::states::CardState;
|
||||
use crate::scheduler::states::SchedulingStates;
|
||||
use crate::search::SortMode;
|
||||
use crate::stats::studied_today;
|
||||
|
||||
impl crate::services::SchedulerService for Collection {
|
||||
|
@ -264,6 +267,13 @@ impl crate::services::SchedulerService for Collection {
|
|||
)
|
||||
}
|
||||
|
||||
fn simulate_fsrs_review(
|
||||
&mut self,
|
||||
input: SimulateFsrsReviewRequest,
|
||||
) -> Result<SimulateFsrsReviewResponse> {
|
||||
self.simulate_review(input)
|
||||
}
|
||||
|
||||
fn compute_optimal_retention(
|
||||
&mut self,
|
||||
input: ComputeOptimalRetentionRequest,
|
||||
|
@ -292,7 +302,12 @@ impl crate::services::SchedulerService for Collection {
|
|||
&mut self,
|
||||
input: scheduler::GetOptimalRetentionParametersRequest,
|
||||
) -> Result<scheduler::GetOptimalRetentionParametersResponse> {
|
||||
self.get_optimal_retention_parameters(&input.search)
|
||||
let revlogs = self
|
||||
.search_cards_into_table(&input.search, SortMode::NoOrder)?
|
||||
.col
|
||||
.storage
|
||||
.get_revlog_entries_for_searched_cards_in_card_order()?;
|
||||
self.get_optimal_retention_parameters(revlogs)
|
||||
.map(|params| GetOptimalRetentionParametersResponse {
|
||||
params: Some(params),
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue