diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 5eefe0030..364bf50ad 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -51,7 +51,7 @@ service SchedulerService { returns (ComputeFsrsParamsResponse); rpc GetOptimalRetentionParameters(GetOptimalRetentionParametersRequest) returns (GetOptimalRetentionParametersResponse); - rpc ComputeOptimalRetention(ComputeOptimalRetentionRequest) + rpc ComputeOptimalRetention(SimulateFsrsReviewRequest) returns (ComputeOptimalRetentionResponse); rpc SimulateFsrsReview(SimulateFsrsReviewRequest) returns (SimulateFsrsReviewResponse); @@ -409,16 +409,6 @@ message SimulateFsrsReviewResponse { repeated float daily_time_cost = 4; } -message ComputeOptimalRetentionRequest { - repeated float params = 1; - uint32 days_to_simulate = 2; - uint32 max_interval = 3; - string search = 4; - double loss_aversion = 5; - repeated float easy_days_percentages = 6; - optional uint32 suspend_after_lapse_count = 7; -} - message ComputeOptimalRetentionResponse { float optimal_retention = 1; } diff --git a/rslib/src/scheduler/fsrs/retention.rs b/rslib/src/scheduler/fsrs/retention.rs index a0fc1cda9..c724709fb 100644 --- a/rslib/src/scheduler/fsrs/retention.rs +++ b/rslib/src/scheduler/fsrs/retention.rs @@ -1,18 +1,12 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::sync::Arc; - -use anki_proto::scheduler::ComputeOptimalRetentionRequest; +use anki_proto::scheduler::SimulateFsrsReviewRequest; use fsrs::extract_simulator_config; -use fsrs::PostSchedulingFn; use fsrs::SimulatorConfig; use fsrs::FSRS; -use super::simulator::apply_load_balance_and_easy_days; use crate::prelude::*; use crate::revlog::RevlogEntry; -use crate::scheduler::states::load_balancer::parse_easy_days_percentages; -use crate::search::SortMode; #[derive(Default, Clone, Copy, Debug)] pub struct ComputeRetentionProgress { @@ -21,65 +15,16 @@ pub struct ComputeRetentionProgress { } impl Collection { - pub fn compute_optimal_retention( - &mut self, - req: ComputeOptimalRetentionRequest, - ) -> Result { + pub fn compute_optimal_retention(&mut self, req: SimulateFsrsReviewRequest) -> Result { let mut anki_progress = self.new_progress_handler::(); let fsrs = FSRS::new(None)?; if req.days_to_simulate == 0 { invalid_input!("no days to simulate") } - 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; - let easy_days_percentages = parse_easy_days_percentages(req.easy_days_percentages)?; - let next_day_at = self.timing_today()?.next_day_at; - let post_scheduling_fn: Option = - if self.get_config_bool(BoolKey::LoadBalancerEnabled) { - Some(PostSchedulingFn(Arc::new( - move |card, max_interval, today, due_cnt_per_day, rng| { - apply_load_balance_and_easy_days( - card.interval, - max_interval, - today, - due_cnt_per_day, - rng, - next_day_at, - &easy_days_percentages, - ) - }, - ))) - } else { - None - }; + let (config, cards) = self.simulate_request_to_config(&req)?; Ok(fsrs .optimal_retention( - &SimulatorConfig { - deck_size, - learn_span: req.days_to_simulate as usize, - max_cost_perday: f32::MAX, - max_ivl: req.max_interval as f32, - first_rating_prob: p.first_rating_prob, - review_rating_prob: p.review_rating_prob, - learn_limit, - review_limit: usize::MAX, - new_cards_ignore_review_limit: true, - suspend_after_lapses: None, - post_scheduling_fn, - review_priority_fn: None, - learning_step_transitions: p.learning_step_transitions, - relearning_step_transitions: p.relearning_step_transitions, - state_rating_costs: p.state_rating_costs, - learning_step_count: p.learning_step_count, - relearning_step_count: p.relearning_step_count, - }, + &config, &req.params, |ip| { anki_progress @@ -88,7 +33,7 @@ impl Collection { }) .is_ok() }, - None, + Some(cards), )? .clamp(0.7, 0.95)) } diff --git a/rslib/src/scheduler/fsrs/simulator.rs b/rslib/src/scheduler/fsrs/simulator.rs index 144741f0a..133b3ff2c 100644 --- a/rslib/src/scheduler/fsrs/simulator.rs +++ b/rslib/src/scheduler/fsrs/simulator.rs @@ -115,10 +115,10 @@ fn create_review_priority_fn( } impl Collection { - pub fn simulate_review( + pub fn simulate_request_to_config( &mut self, - req: SimulateFsrsReviewRequest, - ) -> Result { + req: &SimulateFsrsReviewRequest, + ) -> Result<(SimulatorConfig, Vec)> { let guard = self.search_cards_into_table(&req.search, SortMode::NoOrder)?; let revlogs = guard .col @@ -170,7 +170,7 @@ impl Collection { let deck_size = converted_cards.len(); let p = self.get_optimal_retention_parameters(revlogs)?; - let easy_days_percentages = parse_easy_days_percentages(req.easy_days_percentages)?; + let easy_days_percentages = parse_easy_days_percentages(&req.easy_days_percentages)?; let next_day_at = self.timing_today()?.next_day_at; let post_scheduling_fn: Option = @@ -217,12 +217,21 @@ impl Collection { learning_step_count: p.learning_step_count, relearning_step_count: p.relearning_step_count, }; + + Ok((config, converted_cards)) + } + + pub fn simulate_review( + &mut self, + req: SimulateFsrsReviewRequest, + ) -> Result { + let (config, cards) = self.simulate_request_to_config(&req)?; let result = simulate( &config, &req.params, req.desired_retention, None, - Some(converted_cards), + Some(cards), )?; Ok(SimulateFsrsReviewResponse { accumulated_knowledge_acquisition: result.memorized_cnt_per_day, diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 1d5d1ccb4..e7d9a04eb 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -9,7 +9,6 @@ use anki_proto::generic; use anki_proto::scheduler; use anki_proto::scheduler::ComputeFsrsParamsResponse; use anki_proto::scheduler::ComputeMemoryStateResponse; -use anki_proto::scheduler::ComputeOptimalRetentionRequest; use anki_proto::scheduler::ComputeOptimalRetentionResponse; use anki_proto::scheduler::FsrsBenchmarkResponse; use anki_proto::scheduler::FuzzDeltaRequest; @@ -284,7 +283,7 @@ impl crate::services::SchedulerService for Collection { fn compute_optimal_retention( &mut self, - input: ComputeOptimalRetentionRequest, + input: SimulateFsrsReviewRequest, ) -> Result { Ok(ComputeOptimalRetentionResponse { optimal_retention: self.compute_optimal_retention(input)?, diff --git a/rslib/src/scheduler/states/load_balancer.rs b/rslib/src/scheduler/states/load_balancer.rs index 7029bb1ba..915b0b8b3 100644 --- a/rslib/src/scheduler/states/load_balancer.rs +++ b/rslib/src/scheduler/states/load_balancer.rs @@ -277,7 +277,7 @@ impl LoadBalancer { } } -pub(crate) fn parse_easy_days_percentages(percentages: Vec) -> Result<[EasyDay; 7]> { +pub(crate) fn parse_easy_days_percentages(percentages: &[f32]) -> Result<[EasyDay; 7]> { if percentages.is_empty() { return Ok([EasyDay::Normal; 7]); } @@ -300,7 +300,7 @@ pub(crate) fn build_easy_days_percentages( .into_iter() .map(|(dcid, conf)| { let easy_days_percentages = - parse_easy_days_percentages(conf.inner.easy_days_percentages)?; + parse_easy_days_percentages(&conf.inner.easy_days_percentages)?; Ok((dcid, easy_days_percentages)) }) .collect() diff --git a/ts/routes/deck-options/FsrsOptions.svelte b/ts/routes/deck-options/FsrsOptions.svelte index ef2487719..356437cb9 100644 --- a/ts/routes/deck-options/FsrsOptions.svelte +++ b/ts/routes/deck-options/FsrsOptions.svelte @@ -7,13 +7,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ComputeRetentionProgress, type ComputeParamsProgress, } from "@generated/anki/collection_pb"; - import { - ComputeOptimalRetentionRequest, - SimulateFsrsReviewRequest, - } from "@generated/anki/scheduler_pb"; + import { SimulateFsrsReviewRequest } from "@generated/anki/scheduler_pb"; import { computeFsrsParams, - computeOptimalRetention, evaluateParams, setWantsAbort, } from "@generated/backend"; @@ -26,7 +22,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import GlobalLabel from "./GlobalLabel.svelte"; import { commitEditing, fsrsParams, type DeckOptionsState } from "./lib"; import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte"; - import SpinBoxRow from "./SpinBoxRow.svelte"; import Warning from "./Warning.svelte"; import ParamsInputRow from "./ParamsInputRow.svelte"; import ParamsSearchRow from "./ParamsSearchRow.svelte"; @@ -37,8 +32,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let openHelpModal: (String) => void; export let onPresetChange: () => void; - const presetName = state.currentPresetName; - const config = state.currentConfig; const defaults = state.defaults; const fsrsReschedule = state.fsrsReschedule; @@ -50,13 +43,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let computeParamsProgress: ComputeParamsProgress | undefined; let computingParams = false; let checkingParams = false; - let computingRetention = false; - let optimalRetention = 0; - $: if ($presetName) { - optimalRetention = 0; - } - $: computing = computingParams || checkingParams || computingRetention; + $: computing = computingParams || checkingParams; $: defaultparamSearch = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`; $: roundedRetention = Number($config.desiredRetention.toFixed(2)); $: desiredRetentionWarning = getRetentionWarning( @@ -65,19 +53,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ); $: retentionWarningClass = getRetentionWarningClass(roundedRetention); - let computeRetentionProgress: - | ComputeParamsProgress - | ComputeRetentionProgress - | undefined; - - const optimalRetentionRequest = new ComputeOptimalRetentionRequest({ - daysToSimulate: 365, - lossAversion: 2.5, - }); - $: if (optimalRetentionRequest.daysToSimulate > 3650) { - optimalRetentionRequest.daysToSimulate = 3650; - } - $: newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit; $: simulateFsrsRequest = new SimulateFsrsReviewRequest({ @@ -233,44 +208,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } - async function computeRetention(): Promise { - if (computingRetention) { - await setWantsAbort({}); - return; - } - if (state.presetAssignmentsChanged()) { - alert(tr.deckConfigPleaseSaveYourChangesFirst()); - return; - } - computingRetention = true; - computeRetentionProgress = undefined; - try { - await runWithBackendProgress( - async () => { - optimalRetentionRequest.maxInterval = $config.maximumReviewInterval; - optimalRetentionRequest.params = fsrsParams($config); - optimalRetentionRequest.search = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`; - optimalRetentionRequest.easyDaysPercentages = - $config.easyDaysPercentages; - const resp = await computeOptimalRetention(optimalRetentionRequest); - optimalRetention = resp.optimalRetention; - computeRetentionProgress = undefined; - }, - (progress) => { - if (progress.value.case === "computeRetention") { - computeRetentionProgress = progress.value.value; - } - }, - ); - } finally { - computingRetention = false; - } - } - $: computeParamsProgressString = renderWeightProgress(computeParamsProgress); - $: computeRetentionProgressString = renderRetentionProgress( - computeRetentionProgress, - ); $: totalReviews = computeParamsProgress?.reviews ?? undefined; function renderWeightProgress(val: ComputeParamsProgress | undefined): String { @@ -285,22 +223,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } - function renderRetentionProgress( - val: ComputeRetentionProgress | undefined, - ): String { - if (!val) { - return ""; - } - return tr.deckConfigIterations({ count: val.current }); - } - - function estimatedRetention(retention: number): String { - if (!retention) { - return ""; - } - return tr.deckConfigPredictedOptimalRetention({ num: retention.toFixed(2) }); - } - async function computeAllParams(): Promise { await commitEditing(); state.save(UpdateDeckConfigsMode.COMPUTE_ALL_PARAMS); @@ -390,49 +312,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/if} -
-
- {tr.deckConfigComputeOptimalRetention()} - - - openHelpModal("computeOptimalRetention")}> - {tr.deckConfigDaysToSimulate()} - - - - - - {#if optimalRetention} - {estimatedRetention(optimalRetention)} - {#if optimalRetention - $config.desiredRetention >= 0.01} - - {/if} - {/if} - - {#if computingRetention} -
{computeRetentionProgressString}
- {/if} -
-
-
+ + {#if optimalRetention} + {estimatedRetention(optimalRetention)} + {#if optimalRetention - $config.desiredRetention >= 0.01} + + {/if} + {/if} + + {#if computingRetention} +
{computeRetentionProgressString}
+ {/if} + +
+