Skip to content

Commit ad073ab

Browse files
authored
Feat/CMRR uses simulate config (#3947)
* Added: simulate_request_to_config * Use SimulateConfig for CMRR * ./check * Fix: ComputingRetention * Use actual cards for optimal_retention
1 parent e748cec commit ad073ab

File tree

7 files changed

+117
-207
lines changed

7 files changed

+117
-207
lines changed

proto/anki/scheduler.proto

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ service SchedulerService {
5151
returns (ComputeFsrsParamsResponse);
5252
rpc GetOptimalRetentionParameters(GetOptimalRetentionParametersRequest)
5353
returns (GetOptimalRetentionParametersResponse);
54-
rpc ComputeOptimalRetention(ComputeOptimalRetentionRequest)
54+
rpc ComputeOptimalRetention(SimulateFsrsReviewRequest)
5555
returns (ComputeOptimalRetentionResponse);
5656
rpc SimulateFsrsReview(SimulateFsrsReviewRequest)
5757
returns (SimulateFsrsReviewResponse);
@@ -409,16 +409,6 @@ message SimulateFsrsReviewResponse {
409409
repeated float daily_time_cost = 4;
410410
}
411411

412-
message ComputeOptimalRetentionRequest {
413-
repeated float params = 1;
414-
uint32 days_to_simulate = 2;
415-
uint32 max_interval = 3;
416-
string search = 4;
417-
double loss_aversion = 5;
418-
repeated float easy_days_percentages = 6;
419-
optional uint32 suspend_after_lapse_count = 7;
420-
}
421-
422412
message ComputeOptimalRetentionResponse {
423413
float optimal_retention = 1;
424414
}

rslib/src/scheduler/fsrs/retention.rs

Lines changed: 5 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
// Copyright: Ankitects Pty Ltd and contributors
22
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
3-
use std::sync::Arc;
4-
5-
use anki_proto::scheduler::ComputeOptimalRetentionRequest;
3+
use anki_proto::scheduler::SimulateFsrsReviewRequest;
64
use fsrs::extract_simulator_config;
7-
use fsrs::PostSchedulingFn;
85
use fsrs::SimulatorConfig;
96
use fsrs::FSRS;
107

11-
use super::simulator::apply_load_balance_and_easy_days;
128
use crate::prelude::*;
139
use crate::revlog::RevlogEntry;
14-
use crate::scheduler::states::load_balancer::parse_easy_days_percentages;
15-
use crate::search::SortMode;
1610

1711
#[derive(Default, Clone, Copy, Debug)]
1812
pub struct ComputeRetentionProgress {
@@ -21,65 +15,16 @@ pub struct ComputeRetentionProgress {
2115
}
2216

2317
impl Collection {
24-
pub fn compute_optimal_retention(
25-
&mut self,
26-
req: ComputeOptimalRetentionRequest,
27-
) -> Result<f32> {
18+
pub fn compute_optimal_retention(&mut self, req: SimulateFsrsReviewRequest) -> Result<f32> {
2819
let mut anki_progress = self.new_progress_handler::<ComputeRetentionProgress>();
2920
let fsrs = FSRS::new(None)?;
3021
if req.days_to_simulate == 0 {
3122
invalid_input!("no days to simulate")
3223
}
33-
let revlogs = self
34-
.search_cards_into_table(&req.search, SortMode::NoOrder)?
35-
.col
36-
.storage
37-
.get_revlog_entries_for_searched_cards_in_card_order()?;
38-
let p = self.get_optimal_retention_parameters(revlogs)?;
39-
let learn_span = req.days_to_simulate as usize;
40-
let learn_limit = 10;
41-
let deck_size = learn_span * learn_limit;
42-
let easy_days_percentages = parse_easy_days_percentages(req.easy_days_percentages)?;
43-
let next_day_at = self.timing_today()?.next_day_at;
44-
let post_scheduling_fn: Option<PostSchedulingFn> =
45-
if self.get_config_bool(BoolKey::LoadBalancerEnabled) {
46-
Some(PostSchedulingFn(Arc::new(
47-
move |card, max_interval, today, due_cnt_per_day, rng| {
48-
apply_load_balance_and_easy_days(
49-
card.interval,
50-
max_interval,
51-
today,
52-
due_cnt_per_day,
53-
rng,
54-
next_day_at,
55-
&easy_days_percentages,
56-
)
57-
},
58-
)))
59-
} else {
60-
None
61-
};
24+
let (config, cards) = self.simulate_request_to_config(&req)?;
6225
Ok(fsrs
6326
.optimal_retention(
64-
&SimulatorConfig {
65-
deck_size,
66-
learn_span: req.days_to_simulate as usize,
67-
max_cost_perday: f32::MAX,
68-
max_ivl: req.max_interval as f32,
69-
first_rating_prob: p.first_rating_prob,
70-
review_rating_prob: p.review_rating_prob,
71-
learn_limit,
72-
review_limit: usize::MAX,
73-
new_cards_ignore_review_limit: true,
74-
suspend_after_lapses: None,
75-
post_scheduling_fn,
76-
review_priority_fn: None,
77-
learning_step_transitions: p.learning_step_transitions,
78-
relearning_step_transitions: p.relearning_step_transitions,
79-
state_rating_costs: p.state_rating_costs,
80-
learning_step_count: p.learning_step_count,
81-
relearning_step_count: p.relearning_step_count,
82-
},
27+
&config,
8328
&req.params,
8429
|ip| {
8530
anki_progress
@@ -88,7 +33,7 @@ impl Collection {
8833
})
8934
.is_ok()
9035
},
91-
None,
36+
Some(cards),
9237
)?
9338
.clamp(0.7, 0.95))
9439
}

rslib/src/scheduler/fsrs/simulator.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,10 @@ fn create_review_priority_fn(
115115
}
116116

117117
impl Collection {
118-
pub fn simulate_review(
118+
pub fn simulate_request_to_config(
119119
&mut self,
120-
req: SimulateFsrsReviewRequest,
121-
) -> Result<SimulateFsrsReviewResponse> {
120+
req: &SimulateFsrsReviewRequest,
121+
) -> Result<(SimulatorConfig, Vec<fsrs::Card>)> {
122122
let guard = self.search_cards_into_table(&req.search, SortMode::NoOrder)?;
123123
let revlogs = guard
124124
.col
@@ -170,7 +170,7 @@ impl Collection {
170170
let deck_size = converted_cards.len();
171171
let p = self.get_optimal_retention_parameters(revlogs)?;
172172

173-
let easy_days_percentages = parse_easy_days_percentages(req.easy_days_percentages)?;
173+
let easy_days_percentages = parse_easy_days_percentages(&req.easy_days_percentages)?;
174174
let next_day_at = self.timing_today()?.next_day_at;
175175

176176
let post_scheduling_fn: Option<PostSchedulingFn> =
@@ -217,12 +217,21 @@ impl Collection {
217217
learning_step_count: p.learning_step_count,
218218
relearning_step_count: p.relearning_step_count,
219219
};
220+
221+
Ok((config, converted_cards))
222+
}
223+
224+
pub fn simulate_review(
225+
&mut self,
226+
req: SimulateFsrsReviewRequest,
227+
) -> Result<SimulateFsrsReviewResponse> {
228+
let (config, cards) = self.simulate_request_to_config(&req)?;
220229
let result = simulate(
221230
&config,
222231
&req.params,
223232
req.desired_retention,
224233
None,
225-
Some(converted_cards),
234+
Some(cards),
226235
)?;
227236
Ok(SimulateFsrsReviewResponse {
228237
accumulated_knowledge_acquisition: result.memorized_cnt_per_day,

rslib/src/scheduler/service/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use anki_proto::generic;
99
use anki_proto::scheduler;
1010
use anki_proto::scheduler::ComputeFsrsParamsResponse;
1111
use anki_proto::scheduler::ComputeMemoryStateResponse;
12-
use anki_proto::scheduler::ComputeOptimalRetentionRequest;
1312
use anki_proto::scheduler::ComputeOptimalRetentionResponse;
1413
use anki_proto::scheduler::FsrsBenchmarkResponse;
1514
use anki_proto::scheduler::FuzzDeltaRequest;
@@ -284,7 +283,7 @@ impl crate::services::SchedulerService for Collection {
284283

285284
fn compute_optimal_retention(
286285
&mut self,
287-
input: ComputeOptimalRetentionRequest,
286+
input: SimulateFsrsReviewRequest,
288287
) -> Result<ComputeOptimalRetentionResponse> {
289288
Ok(ComputeOptimalRetentionResponse {
290289
optimal_retention: self.compute_optimal_retention(input)?,

rslib/src/scheduler/states/load_balancer.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ impl LoadBalancer {
277277
}
278278
}
279279

280-
pub(crate) fn parse_easy_days_percentages(percentages: Vec<f32>) -> Result<[EasyDay; 7]> {
280+
pub(crate) fn parse_easy_days_percentages(percentages: &[f32]) -> Result<[EasyDay; 7]> {
281281
if percentages.is_empty() {
282282
return Ok([EasyDay::Normal; 7]);
283283
}
@@ -300,7 +300,7 @@ pub(crate) fn build_easy_days_percentages(
300300
.into_iter()
301301
.map(|(dcid, conf)| {
302302
let easy_days_percentages =
303-
parse_easy_days_percentages(conf.inner.easy_days_percentages)?;
303+
parse_easy_days_percentages(&conf.inner.easy_days_percentages)?;
304304
Ok((dcid, easy_days_percentages))
305305
})
306306
.collect()

ts/routes/deck-options/FsrsOptions.svelte

Lines changed: 2 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
77
ComputeRetentionProgress,
88
type ComputeParamsProgress,
99
} from "@generated/anki/collection_pb";
10-
import {
11-
ComputeOptimalRetentionRequest,
12-
SimulateFsrsReviewRequest,
13-
} from "@generated/anki/scheduler_pb";
10+
import { SimulateFsrsReviewRequest } from "@generated/anki/scheduler_pb";
1411
import {
1512
computeFsrsParams,
16-
computeOptimalRetention,
1713
evaluateParams,
1814
setWantsAbort,
1915
} from "@generated/backend";
@@ -26,7 +22,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
2622
import GlobalLabel from "./GlobalLabel.svelte";
2723
import { commitEditing, fsrsParams, type DeckOptionsState } from "./lib";
2824
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
29-
import SpinBoxRow from "./SpinBoxRow.svelte";
3025
import Warning from "./Warning.svelte";
3126
import ParamsInputRow from "./ParamsInputRow.svelte";
3227
import ParamsSearchRow from "./ParamsSearchRow.svelte";
@@ -37,8 +32,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
3732
export let openHelpModal: (String) => void;
3833
export let onPresetChange: () => void;
3934
40-
const presetName = state.currentPresetName;
41-
4235
const config = state.currentConfig;
4336
const defaults = state.defaults;
4437
const fsrsReschedule = state.fsrsReschedule;
@@ -50,13 +43,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
5043
let computeParamsProgress: ComputeParamsProgress | undefined;
5144
let computingParams = false;
5245
let checkingParams = false;
53-
let computingRetention = false;
5446
55-
let optimalRetention = 0;
56-
$: if ($presetName) {
57-
optimalRetention = 0;
58-
}
59-
$: computing = computingParams || checkingParams || computingRetention;
47+
$: computing = computingParams || checkingParams;
6048
$: defaultparamSearch = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
6149
$: roundedRetention = Number($config.desiredRetention.toFixed(2));
6250
$: desiredRetentionWarning = getRetentionWarning(
@@ -65,19 +53,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
6553
);
6654
$: retentionWarningClass = getRetentionWarningClass(roundedRetention);
6755
68-
let computeRetentionProgress:
69-
| ComputeParamsProgress
70-
| ComputeRetentionProgress
71-
| undefined;
72-
73-
const optimalRetentionRequest = new ComputeOptimalRetentionRequest({
74-
daysToSimulate: 365,
75-
lossAversion: 2.5,
76-
});
77-
$: if (optimalRetentionRequest.daysToSimulate > 3650) {
78-
optimalRetentionRequest.daysToSimulate = 3650;
79-
}
80-
8156
$: newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;
8257
8358
$: simulateFsrsRequest = new SimulateFsrsReviewRequest({
@@ -233,44 +208,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
233208
}
234209
}
235210
236-
async function computeRetention(): Promise<void> {
237-
if (computingRetention) {
238-
await setWantsAbort({});
239-
return;
240-
}
241-
if (state.presetAssignmentsChanged()) {
242-
alert(tr.deckConfigPleaseSaveYourChangesFirst());
243-
return;
244-
}
245-
computingRetention = true;
246-
computeRetentionProgress = undefined;
247-
try {
248-
await runWithBackendProgress(
249-
async () => {
250-
optimalRetentionRequest.maxInterval = $config.maximumReviewInterval;
251-
optimalRetentionRequest.params = fsrsParams($config);
252-
optimalRetentionRequest.search = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
253-
optimalRetentionRequest.easyDaysPercentages =
254-
$config.easyDaysPercentages;
255-
const resp = await computeOptimalRetention(optimalRetentionRequest);
256-
optimalRetention = resp.optimalRetention;
257-
computeRetentionProgress = undefined;
258-
},
259-
(progress) => {
260-
if (progress.value.case === "computeRetention") {
261-
computeRetentionProgress = progress.value.value;
262-
}
263-
},
264-
);
265-
} finally {
266-
computingRetention = false;
267-
}
268-
}
269-
270211
$: computeParamsProgressString = renderWeightProgress(computeParamsProgress);
271-
$: computeRetentionProgressString = renderRetentionProgress(
272-
computeRetentionProgress,
273-
);
274212
$: totalReviews = computeParamsProgress?.reviews ?? undefined;
275213
276214
function renderWeightProgress(val: ComputeParamsProgress | undefined): String {
@@ -285,22 +223,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
285223
}
286224
}
287225
288-
function renderRetentionProgress(
289-
val: ComputeRetentionProgress | undefined,
290-
): String {
291-
if (!val) {
292-
return "";
293-
}
294-
return tr.deckConfigIterations({ count: val.current });
295-
}
296-
297-
function estimatedRetention(retention: number): String {
298-
if (!retention) {
299-
return "";
300-
}
301-
return tr.deckConfigPredictedOptimalRetention({ num: retention.toFixed(2) });
302-
}
303-
304226
async function computeAllParams(): Promise<void> {
305227
await commitEditing();
306228
state.save(UpdateDeckConfigsMode.COMPUTE_ALL_PARAMS);
@@ -390,49 +312,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
390312
{/if}
391313
</div>
392314

393-
<div class="m-2">
394-
<details>
395-
<summary>{tr.deckConfigComputeOptimalRetention()}</summary>
396-
397-
<SpinBoxRow
398-
bind:value={optimalRetentionRequest.daysToSimulate}
399-
defaultValue={365}
400-
min={1}
401-
max={3650}
402-
>
403-
<SettingTitle on:click={() => openHelpModal("computeOptimalRetention")}>
404-
{tr.deckConfigDaysToSimulate()}
405-
</SettingTitle>
406-
</SpinBoxRow>
407-
408-
<button
409-
class="btn {computingRetention ? 'btn-warning' : 'btn-primary'}"
410-
disabled={!computingRetention && computing}
411-
on:click={() => computeRetention()}
412-
>
413-
{#if computingRetention}
414-
{tr.actionsCancel()}
415-
{:else}
416-
{tr.deckConfigComputeButton()}
417-
{/if}
418-
</button>
419-
420-
{#if optimalRetention}
421-
{estimatedRetention(optimalRetention)}
422-
{#if optimalRetention - $config.desiredRetention >= 0.01}
423-
<Warning
424-
warning={tr.deckConfigDesiredRetentionBelowOptimal()}
425-
className="alert-warning"
426-
/>
427-
{/if}
428-
{/if}
429-
430-
{#if computingRetention}
431-
<div>{computeRetentionProgressString}</div>
432-
{/if}
433-
</details>
434-
</div>
435-
436315
<div class="m-2">
437316
<button class="btn btn-primary" on:click={() => (showSimulator = true)}>
438317
{tr.deckConfigFsrsSimulatorExperimental()}

0 commit comments

Comments
 (0)