Revert "Feat/Cmrr target selector (#4116)"

This reverts commit ad0dbb563a.

https://forums.ankiweb.net/t/anki-25-06-beta/62271/156
This commit is contained in:
Damien Elmes 2025-07-01 11:16:39 +07:00
parent 7720c7de1a
commit b22b3310d6
5 changed files with 8 additions and 255 deletions

View file

@ -402,31 +402,6 @@ message SimulateFsrsReviewRequest {
repeated float easy_days_percentages = 10; repeated float easy_days_percentages = 10;
deck_config.DeckConfig.Config.ReviewCardOrder review_order = 11; deck_config.DeckConfig.Config.ReviewCardOrder review_order = 11;
optional uint32 suspend_after_lapse_count = 12; optional uint32 suspend_after_lapse_count = 12;
// For CMRR
message CMRRTarget {
message Memorized {
float loss_aversion = 1;
};
message Stability {};
message FutureMemorized {
int32 days = 1;
};
message AverageFutureMemorized {
int32 days = 1;
};
oneof kind {
Memorized memorized = 1;
Stability stability = 2;
FutureMemorized future_memorized = 3;
AverageFutureMemorized average_future_memorized = 4;
};
};
optional CMRRTarget target = 13;
} }
message SimulateFsrsReviewResponse { message SimulateFsrsReviewResponse {

View file

@ -1,9 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use anki_proto::scheduler::simulate_fsrs_review_request::cmrr_target::Kind;
use anki_proto::scheduler::SimulateFsrsReviewRequest; use anki_proto::scheduler::SimulateFsrsReviewRequest;
use fsrs::extract_simulator_config; use fsrs::extract_simulator_config;
use fsrs::SimulationResult;
use fsrs::SimulatorConfig; use fsrs::SimulatorConfig;
use fsrs::FSRS; use fsrs::FSRS;
@ -16,115 +14,14 @@ pub struct ComputeRetentionProgress {
pub total: u32, pub total: u32,
} }
pub fn average_r_power_forgetting_curve(
learn_span: usize,
cards: &[fsrs::Card],
offset: f32,
decay: f32,
) -> f32 {
let factor = 0.9_f32.powf(1.0 / decay) - 1.0;
let exp = decay + 1.0;
let den_factor = factor * exp;
// Closure equivalent to the inner integral function
let integral_calc = |card: &fsrs::Card| -> f32 {
// Performs element-wise: (s / den_factor) * (1.0 + factor * t / s).powf(exp)
let t1 = learn_span as f32 - card.last_date;
let t2 = t1 + offset;
(card.stability / den_factor) * (1.0 + factor * t2 / card.stability).powf(exp)
- (card.stability / den_factor) * (1.0 + factor * t1 / card.stability).powf(exp)
};
// Calculate integral difference and divide by time difference element-wise
cards.iter().map(integral_calc).sum::<f32>() / offset
}
impl Collection { impl Collection {
pub fn compute_optimal_retention(&mut self, req: SimulateFsrsReviewRequest) -> Result<f32> { pub fn compute_optimal_retention(&mut self, req: SimulateFsrsReviewRequest) -> Result<f32> {
// Helper macro to wrap the closure for "CMRRTargetFn"s
macro_rules! wrap {
($f:expr) => {
Some(fsrs::CMRRTargetFn(std::sync::Arc::new($f)))
};
}
let target_type = req.target.unwrap().kind;
let days_to_simulate = req.days_to_simulate as f32;
let target = match target_type {
Some(Kind::Memorized(_)) => None,
Some(Kind::FutureMemorized(settings)) => {
wrap!(move |SimulationResult {
cards,
cost_per_day,
..
},
w| {
let total_cost = cost_per_day.iter().sum::<f32>();
total_cost
/ cards.iter().fold(0., |p, c| {
c.retention_on(w, days_to_simulate + settings.days as f32) + p
})
})
}
Some(Kind::AverageFutureMemorized(settings)) => {
wrap!(move |SimulationResult {
cards,
cost_per_day,
..
},
w| {
let total_cost = cost_per_day.iter().sum::<f32>();
total_cost
/ average_r_power_forgetting_curve(
days_to_simulate as usize,
cards,
settings.days as f32,
-w[20],
)
})
}
Some(Kind::Stability(_)) => {
wrap!(move |SimulationResult {
cards,
cost_per_day,
..
},
w| {
let total_cost = cost_per_day.iter().sum::<f32>();
total_cost
/ cards.iter().fold(0., |p, c| {
p + (c.retention_on(w, days_to_simulate) * c.stability)
})
})
}
None => None,
};
let mut anki_progress = self.new_progress_handler::<ComputeRetentionProgress>(); let mut anki_progress = self.new_progress_handler::<ComputeRetentionProgress>();
let fsrs = FSRS::new(None)?; let fsrs = FSRS::new(None)?;
if req.days_to_simulate == 0 { if req.days_to_simulate == 0 {
invalid_input!("no days to simulate") invalid_input!("no days to simulate")
} }
let (mut config, cards) = self.simulate_request_to_config(&req)?; let (config, cards) = self.simulate_request_to_config(&req)?;
if let Some(Kind::Memorized(settings)) = target_type {
let loss_aversion = settings.loss_aversion;
config.relearning_step_transitions[0][0] *= loss_aversion;
config.relearning_step_transitions[1][0] *= loss_aversion;
config.relearning_step_transitions[2][0] *= loss_aversion;
config.learning_step_transitions[0][0] *= loss_aversion;
config.learning_step_transitions[1][0] *= loss_aversion;
config.learning_step_transitions[2][0] *= loss_aversion;
config.state_rating_costs[0][0] *= loss_aversion;
config.state_rating_costs[1][0] *= loss_aversion;
config.state_rating_costs[2][0] *= loss_aversion;
}
Ok(fsrs Ok(fsrs
.optimal_retention( .optimal_retention(
&config, &config,
@ -137,7 +34,7 @@ impl Collection {
.is_ok() .is_ok()
}, },
Some(cards), Some(cards),
target, None,
)? )?
.clamp(0.7, 0.95)) .clamp(0.7, 0.95))
} }

View file

@ -7,11 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
ComputeRetentionProgress, ComputeRetentionProgress,
type ComputeParamsProgress, type ComputeParamsProgress,
} from "@generated/anki/collection_pb"; } from "@generated/anki/collection_pb";
import { import { SimulateFsrsReviewRequest } from "@generated/anki/scheduler_pb";
SimulateFsrsReviewRequest,
SimulateFsrsReviewRequest_CMRRTarget,
SimulateFsrsReviewRequest_CMRRTarget_Memorized,
} from "@generated/anki/scheduler_pb";
import { import {
computeFsrsParams, computeFsrsParams,
evaluateParams, evaluateParams,
@ -99,14 +95,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
newCardsIgnoreReviewLimit: $newCardsIgnoreReviewLimit, newCardsIgnoreReviewLimit: $newCardsIgnoreReviewLimit,
easyDaysPercentages: $config.easyDaysPercentages, easyDaysPercentages: $config.easyDaysPercentages,
reviewOrder: $config.reviewOrder, reviewOrder: $config.reviewOrder,
target: new SimulateFsrsReviewRequest_CMRRTarget({
kind: {
case: "memorized",
value: new SimulateFsrsReviewRequest_CMRRTarget_Memorized({
lossAversion: 1.6,
}),
},
}),
}); });
const DESIRED_RETENTION_LOW_THRESHOLD = 0.8; const DESIRED_RETENTION_LOW_THRESHOLD = 0.8;

View file

@ -18,30 +18,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { renderSimulationChart } from "../graphs/simulator"; import { renderSimulationChart } from "../graphs/simulator";
import { computeOptimalRetention, simulateFsrsReview } from "@generated/backend"; import { computeOptimalRetention, simulateFsrsReview } from "@generated/backend";
import { runWithBackendProgress } from "@tslib/progress"; import { runWithBackendProgress } from "@tslib/progress";
import { import type {
SimulateFsrsReviewRequest_CMRRTarget_AverageFutureMemorized, ComputeOptimalRetentionResponse,
SimulateFsrsReviewRequest_CMRRTarget_FutureMemorized, SimulateFsrsReviewRequest,
SimulateFsrsReviewRequest_CMRRTarget_Memorized, SimulateFsrsReviewResponse,
SimulateFsrsReviewRequest_CMRRTarget_Stability,
type ComputeOptimalRetentionResponse,
type SimulateFsrsReviewRequest,
type SimulateFsrsReviewResponse,
} from "@generated/anki/scheduler_pb"; } from "@generated/anki/scheduler_pb";
import type { DeckOptionsState } from "./lib"; import type { DeckOptionsState } from "./lib";
import SwitchRow from "$lib/components/SwitchRow.svelte"; import SwitchRow from "$lib/components/SwitchRow.svelte";
import GlobalLabel from "./GlobalLabel.svelte"; import GlobalLabel from "./GlobalLabel.svelte";
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte"; import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
import { import { reviewOrderChoices } from "./choices";
DEFAULT_CMRR_TARGET,
CMRRTargetChoices,
reviewOrderChoices,
} from "./choices";
import EnumSelectorRow from "$lib/components/EnumSelectorRow.svelte"; import EnumSelectorRow from "$lib/components/EnumSelectorRow.svelte";
import { DeckConfig_Config_LeechAction } from "@generated/anki/deck_config_pb"; import { DeckConfig_Config_LeechAction } from "@generated/anki/deck_config_pb";
import EasyDaysInput from "./EasyDaysInput.svelte"; import EasyDaysInput from "./EasyDaysInput.svelte";
import Warning from "./Warning.svelte"; import Warning from "./Warning.svelte";
import type { ComputeRetentionProgress } from "@generated/anki/collection_pb"; import type { ComputeRetentionProgress } from "@generated/anki/collection_pb";
import Item from "$lib/components/Item.svelte";
import Modal from "bootstrap/js/dist/modal"; import Modal from "bootstrap/js/dist/modal";
export let state: DeckOptionsState; export let state: DeckOptionsState;
@ -50,45 +41,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let openHelpModal: (key: string) => void; export let openHelpModal: (key: string) => void;
export let onPresetChange: () => void; export let onPresetChange: () => void;
let cmrrTargetType = DEFAULT_CMRR_TARGET;
// All added types must be updated in the proceeding switch statement.
let lastCmrrTargetType = cmrrTargetType;
$: if (simulateFsrsRequest?.target && cmrrTargetType !== lastCmrrTargetType) {
switch (cmrrTargetType) {
case "memorized":
simulateFsrsRequest.target.kind = {
case: "memorized",
value: new SimulateFsrsReviewRequest_CMRRTarget_Memorized({
lossAversion: 1.6,
}),
};
break;
case "stability":
simulateFsrsRequest.target.kind = {
case: "stability",
value: new SimulateFsrsReviewRequest_CMRRTarget_Stability({}),
};
break;
case "futureMemorized":
simulateFsrsRequest.target.kind = {
case: "futureMemorized",
value: new SimulateFsrsReviewRequest_CMRRTarget_FutureMemorized({
days: 365,
}),
};
break;
case "averageFutureMemorized":
simulateFsrsRequest.target.kind = {
case: "averageFutureMemorized",
value: new SimulateFsrsReviewRequest_CMRRTarget_AverageFutureMemorized(
{ days: 365 },
),
};
break;
}
lastCmrrTargetType = cmrrTargetType;
}
const config = state.currentConfig; const config = state.currentConfig;
let simulateSubgraph: SimulateSubgraph = SimulateSubgraph.count; let simulateSubgraph: SimulateSubgraph = SimulateSubgraph.count;
let tableData: TableDatum[] = []; let tableData: TableDatum[] = [];
@ -470,42 +422,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{#if computingRetention} {#if computingRetention}
<div>{computeRetentionProgressString}</div> <div>{computeRetentionProgressString}</div>
{/if} {/if}
<Item>
<EnumSelectorRow
choices={CMRRTargetChoices()}
bind:value={cmrrTargetType}
defaultValue={DEFAULT_CMRR_TARGET}
>
<SettingTitle>
{"Target: "}
</SettingTitle>
</EnumSelectorRow>
</Item>
{#if simulateFsrsRequest.target?.kind.case === "memorized"}
<SpinBoxFloatRow
bind:value={simulateFsrsRequest.target.kind.value
.lossAversion}
defaultValue={1.6}
>
<SettingTitle>
{"Fail Cost Multiplier: "}
</SettingTitle>
</SpinBoxFloatRow>
{/if}
{#if simulateFsrsRequest.target?.kind.case === "futureMemorized" || simulateFsrsRequest.target?.kind.case === "averageFutureMemorized"}
<SpinBoxFloatRow
bind:value={simulateFsrsRequest.target.kind.value.days}
defaultValue={365}
step={1}
>
<SettingTitle>
{"Days after simulation end: "}
</SettingTitle>
</SpinBoxFloatRow>
{/if}
</details> </details>
<button <button

View file

@ -199,29 +199,6 @@ export function questionActionChoices(): Choice<DeckConfig_Config_QuestionAction
]; ];
} }
export const DEFAULT_CMRR_TARGET = "memorized";
export function CMRRTargetChoices(): Choice<string>[] {
return [
{
label: "Memorized (Default)",
value: "memorized",
},
{
label: "Stability (Experimental)",
value: "stability",
},
{
label: "Post Abandon Memorized (Experimental)",
value: "futureMemorized",
},
{
label: "Average Post Abandon Memorized (Experimental)",
value: "averageFutureMemorized",
},
];
}
function difficultyOrders(fsrs: boolean): Choice<DeckConfig_Config_ReviewCardOrder>[] { function difficultyOrders(fsrs: boolean): Choice<DeckConfig_Config_ReviewCardOrder>[] {
const order = [ const order = [
{ {