diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index 088ac79bf..143839f1a 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -327,6 +327,7 @@ deck-config-analyze-button = Analyze deck-config-desired-retention = Desired retention deck-config-smaller-is-better = Smaller numbers indicate better memory estimates. deck-config-steps-too-large-for-fsrs = When FSRS is enabled, interday (re)learning steps are not recommended. +deck-config-get-params = Get Params ## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future. diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 493d3422b..6aefd4fc8 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -47,6 +47,8 @@ service SchedulerService { rpc RepositionDefaults(generic.Empty) returns (RepositionDefaultsResponse); rpc ComputeFsrsWeights(ComputeFsrsWeightsRequest) returns (ComputeFsrsWeightsResponse); + rpc GetOptimalRetentionParameters(GetOptimalRetentionParametersRequest) + returns (GetOptimalRetentionParametersResponse); rpc ComputeOptimalRetention(ComputeOptimalRetentionRequest) returns (ComputeOptimalRetentionResponse); rpc EvaluateWeights(EvaluateWeightsRequest) returns (EvaluateWeightsResponse); @@ -338,6 +340,14 @@ message ComputeFsrsWeightsResponse { message ComputeOptimalRetentionRequest { repeated float weights = 1; + OptimalRetentionParameters params = 2; +} + +message ComputeOptimalRetentionResponse { + float optimal_retention = 1; +} + +message OptimalRetentionParameters { uint32 deck_size = 2; uint32 days_to_simulate = 3; uint32 max_seconds_of_study_per_day = 4; @@ -356,8 +366,12 @@ message ComputeOptimalRetentionRequest { double review_rating_probability_easy = 17; } -message ComputeOptimalRetentionResponse { - float optimal_retention = 1; +message GetOptimalRetentionParametersRequest { + string search = 1; +} + +message GetOptimalRetentionParametersResponse { + OptimalRetentionParameters params = 1; } message EvaluateWeightsRequest { diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index a17a7fd8b..5988dcb25 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -549,6 +549,7 @@ exposed_backend_list = [ "compute_optimal_retention", "set_wants_abort", "evaluate_weights", + "get_optimal_retention_parameters", ] diff --git a/rslib/src/scheduler/fsrs/retention.rs b/rslib/src/scheduler/fsrs/retention.rs index d30b05ffa..11aae11f7 100644 --- a/rslib/src/scheduler/fsrs/retention.rs +++ b/rslib/src/scheduler/fsrs/retention.rs @@ -2,10 +2,12 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use anki_proto::scheduler::ComputeOptimalRetentionRequest; +use anki_proto::scheduler::OptimalRetentionParameters; use fsrs::SimulatorConfig; use fsrs::FSRS; use crate::prelude::*; +use crate::search::SortMode; #[derive(Default, Clone, Copy, Debug)] pub struct ComputeRetentionProgress { @@ -20,29 +22,26 @@ impl Collection { ) -> Result { let mut anki_progress = self.new_progress_handler::(); let fsrs = FSRS::new(None)?; + let p = req.params.as_ref().or_invalid("missing params")?; Ok(fsrs.optimal_retention( &SimulatorConfig { - deck_size: req.deck_size as usize, - learn_span: req.days_to_simulate as usize, - max_cost_perday: req.max_seconds_of_study_per_day as f64, - max_ivl: req.max_interval as f64, - recall_costs: [ - req.recall_secs_hard, - req.recall_secs_good, - req.recall_secs_easy, - ], - forget_cost: req.forget_secs as f64, - learn_cost: req.learn_secs as f64, + deck_size: p.deck_size as usize, + learn_span: p.days_to_simulate as usize, + max_cost_perday: p.max_seconds_of_study_per_day as f64, + max_ivl: p.max_interval as f64, + recall_costs: [p.recall_secs_hard, p.recall_secs_good, p.recall_secs_easy], + forget_cost: p.forget_secs as f64, + learn_cost: p.learn_secs as f64, first_rating_prob: [ - req.first_rating_probability_again, - req.first_rating_probability_hard, - req.first_rating_probability_good, - req.first_rating_probability_easy, + p.first_rating_probability_again, + p.first_rating_probability_hard, + p.first_rating_probability_good, + p.first_rating_probability_easy, ], review_rating_prob: [ - req.review_rating_probability_hard, - req.review_rating_probability_good, - req.review_rating_probability_easy, + p.review_rating_probability_hard, + p.review_rating_probability_good, + p.review_rating_probability_easy, ], }, &req.weights, @@ -56,4 +55,42 @@ impl Collection { }, )? as f32) } + + pub fn get_optimal_retention_parameters( + &mut self, + search: &str, + ) -> Result { + let guard = self.search_cards_into_table(search, SortMode::NoOrder)?; + let deck_size = guard.cards as u32; + + // if you need access to cards too: + // let cards = self.storage.all_searched_cards()?; + + let _revlogs = guard + .col + .storage + .get_revlog_entries_for_searched_cards_in_order()?; + + // todo: compute values from revlogs + let params = OptimalRetentionParameters { + deck_size, + days_to_simulate: 365, + max_seconds_of_study_per_day: 1800, + // this should be filled in by the frontend based on their configured value + max_interval: 0, + recall_secs_hard: 14.0, + recall_secs_good: 10.0, + recall_secs_easy: 6.0, + forget_secs: 50, + learn_secs: 20, + first_rating_probability_again: 0.15, + first_rating_probability_hard: 0.2, + first_rating_probability_good: 0.6, + first_rating_probability_easy: 0.05, + review_rating_probability_hard: 0.3, + review_rating_probability_good: 0.6, + review_rating_probability_easy: 0.1, + }; + Ok(params) + } } diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 2a3a94cfd..8e535633d 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -8,6 +8,7 @@ use anki_proto::generic; use anki_proto::scheduler; use anki_proto::scheduler::ComputeOptimalRetentionRequest; use anki_proto::scheduler::ComputeOptimalRetentionResponse; +use anki_proto::scheduler::GetOptimalRetentionParametersResponse; use crate::prelude::*; use crate::scheduler::new::NewCardDueOrder; @@ -266,4 +267,14 @@ impl crate::services::SchedulerService for Collection { rmse_bins: ret.rmse_bins, }) } + + fn get_optimal_retention_parameters( + &mut self, + input: scheduler::GetOptimalRetentionParametersRequest, + ) -> Result { + self.get_optimal_retention_parameters(&input.search) + .map(|params| GetOptimalRetentionParametersResponse { + params: Some(params), + }) + } } diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 597ad07d3..da989b07b 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -894,7 +894,10 @@ mod test { vec![Search(Deck("default one".into()))] ); - assert_eq!(parse("preset:default")?, vec![Search(Preset("default".into()))]); + assert_eq!( + parse("preset:default")?, + vec![Search(Preset("default".into()))] + ); assert_eq!(parse("note:basic")?, vec![Search(Notetype("basic".into()))]); assert_eq!( diff --git a/ts/deck-options/FsrsOptions.svelte b/ts/deck-options/FsrsOptions.svelte index 460eb90d9..02d17dda6 100644 --- a/ts/deck-options/FsrsOptions.svelte +++ b/ts/deck-options/FsrsOptions.svelte @@ -7,11 +7,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ComputeRetentionProgress, type ComputeWeightsProgress, } from "@tslib/anki/collection_pb"; - import { ComputeOptimalRetentionRequest } from "@tslib/anki/scheduler_pb"; + import { OptimalRetentionParameters } from "@tslib/anki/scheduler_pb"; import { computeFsrsWeights, computeOptimalRetention, evaluateWeights, + getOptimalRetentionParameters, setWantsAbort, } from "@tslib/backend"; import * as tr from "@tslib/ftl"; @@ -38,25 +39,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html | ComputeRetentionProgress | undefined; - const computeOptimalRequest = new ComputeOptimalRetentionRequest({ - deckSize: 10000, - daysToSimulate: 365, - maxSecondsOfStudyPerDay: 1800, - maxInterval: 36500, - recallSecsHard: 14.0, - recallSecsGood: 10.0, - recallSecsEasy: 6.0, - forgetSecs: 50, - learnSecs: 20, - firstRatingProbabilityAgain: 0.15, - firstRatingProbabilityHard: 0.2, - firstRatingProbabilityGood: 0.6, - firstRatingProbabilityEasy: 0.05, - reviewRatingProbabilityHard: 0.3, - reviewRatingProbabilityGood: 0.6, - reviewRatingProbabilityEasy: 0.1, - }); - + let optimalParams = new OptimalRetentionParameters({}); async function computeWeights(): Promise { if (computing) { await setWantsAbort({}); @@ -142,8 +125,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html try { await runWithBackendProgress( async () => { - computeOptimalRequest.weights = $config.fsrsWeights; - const resp = await computeOptimalRetention(computeOptimalRequest); + const resp = await computeOptimalRetention({ + params: optimalParams, + weights: $config.fsrsWeights, + }); $config.desiredRetention = resp.optimalRetention; if (computeRetentionProgress) { computeRetentionProgress.current = @@ -161,6 +146,23 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } + async function getRetentionParams(): Promise { + if (computing) { + return; + } + computing = true; + try { + // await + const resp = await getOptimalRetentionParameters({ + search: `preset:"${state.getCurrentName()}"`, + }); + optimalParams = resp.params!; + optimalParams.maxInterval = $config.maximumReviewInterval; + } finally { + computing = false; + } + } + $: computeWeightsProgressString = renderWeightProgress(computeWeightsProgress); $: computeRetentionProgressString = renderRetentionProgress( computeRetentionProgress, @@ -246,108 +248,90 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html Deck size:
- +
Days to simulate
- +
Max seconds of study per day:
- -
- - Maximum interval: -
- +
Seconds to forget a card (again):
- +
Seconds to recall a card (hard):
- +
Seconds to recall a card (good):
- +
Seconds to recall a card (easy):
- +
Seconds to learn a card:
- +
First rating probability (again):
- +
First rating probability (hard):
- +
First rating probability (good):
- +
First rating probability (easy):
- +
Review rating probability (hard):
- +
Review rating probability (good):
- +
Review rating probability (easy):
- +
+ +