From c2fdf474ddbf41daf4d4e50147c6272e87cfd243 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Sat, 2 Aug 2025 12:09:26 +0100 Subject: [PATCH 1/8] cheesecake method --- rslib/src/scheduler/fsrs/simulator.rs | 3 ++- ts/routes/graphs/simulator.ts | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/rslib/src/scheduler/fsrs/simulator.rs b/rslib/src/scheduler/fsrs/simulator.rs index 005f7ca86..ef329ee59 100644 --- a/rslib/src/scheduler/fsrs/simulator.rs +++ b/rslib/src/scheduler/fsrs/simulator.rs @@ -292,7 +292,8 @@ impl Collection { Ok(( dr, ( - *result.memorized_cnt_per_day.last().unwrap_or(&0.), + *result.memorized_cnt_per_day.last().unwrap_or(&0.) + - *result.memorized_cnt_per_day.first().unwrap_or(&0.), result.cost_per_day.iter().sum::(), result.review_cnt_per_day.iter().sum::() as u32 + result.learn_cnt_per_day.iter().sum::() as u32, diff --git a/ts/routes/graphs/simulator.ts b/ts/routes/graphs/simulator.ts index feba9ce57..dbd4a4e7b 100644 --- a/ts/routes/graphs/simulator.ts +++ b/ts/routes/graphs/simulator.ts @@ -8,6 +8,7 @@ import { bisector, line, max, + min, pointer, rollup, scaleLinear, @@ -62,7 +63,7 @@ export function renderWorkloadChart( .range([bounds.marginLeft, bounds.width - bounds.marginRight]); const subgraph_data = ({ - [SimulateWorkloadSubgraph.ratio]: data.map(d => ({ ...d, y: d.timeCost / d.memorized })), + [SimulateWorkloadSubgraph.ratio]: data.map(d => ({ ...d, y: d.memorized / d.timeCost })), [SimulateWorkloadSubgraph.time]: data.map(d => ({ ...d, y: d.timeCost / d.learnSpan })), [SimulateWorkloadSubgraph.count]: data.map(d => ({ ...d, y: d.count / d.learnSpan })), [SimulateWorkloadSubgraph.memorized]: data.map(d => ({ ...d, y: d.memorized })), @@ -215,9 +216,10 @@ function _renderSimulationChart( // y scale const yMax = max(subgraph_data, d => d.y)!; + const yMin = min(subgraph_data, d => d.y)!; const y = scaleLinear() .range([bounds.height - bounds.marginBottom, bounds.marginTop]) - .domain([0, yMax]) + .domain([yMin, yMax]) .nice(); svg.select(".y-ticks") .call((selection) => From c0a67c3eaa2229a7383ba9ef57beb7dce3958be4 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 1 Sep 2025 16:47:02 +0100 Subject: [PATCH 2/8] Added: start_memorized --- ftl/core/deck-config.ftl | 3 +-- proto/anki/scheduler.proto | 5 +++-- rslib/src/scheduler/fsrs/simulator.rs | 7 +++++-- ts/routes/deck-options/SimulatorModal.svelte | 1 + ts/routes/graphs/simulator.ts | 10 +++++++--- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index da3e4ea34..608e9b078 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -522,8 +522,7 @@ deck-config-save-options-to-preset-confirm = Overwrite the options in your curre # specific date. deck-config-fsrs-simulator-radio-memorized = Memorized deck-config-fsrs-simulator-radio-ratio = Time / Memorized Ratio -# $time here is pre-formatted e.g. "10 Seconds" -deck-config-fsrs-simulator-ratio-tooltip = { $time } per memorized card +deck-config-fsrs-simulator-ratio-tooltip = { $time } memorized cards per hour ## Messages related to the FSRS scheduler’s health check. The health check determines whether the correlation between FSRS predictions and your memory is good or bad. It can be optionally triggered as part of the "Optimize" function. diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 34b350642..15aaa1d99 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -420,8 +420,9 @@ message SimulateFsrsReviewResponse { message SimulateFsrsWorkloadResponse { map cost = 1; - map memorized = 2; - map review_count = 3; + float start_memorized = 2; + map memorized = 3; + map review_count = 4; } message ComputeOptimalRetentionResponse { diff --git a/rslib/src/scheduler/fsrs/simulator.rs b/rslib/src/scheduler/fsrs/simulator.rs index ef329ee59..7bc6836dc 100644 --- a/rslib/src/scheduler/fsrs/simulator.rs +++ b/rslib/src/scheduler/fsrs/simulator.rs @@ -292,8 +292,7 @@ impl Collection { Ok(( dr, ( - *result.memorized_cnt_per_day.last().unwrap_or(&0.) - - *result.memorized_cnt_per_day.first().unwrap_or(&0.), + *result.memorized_cnt_per_day.last().unwrap_or(&0.), result.cost_per_day.iter().sum::(), result.review_cnt_per_day.iter().sum::() as u32 + result.learn_cnt_per_day.iter().sum::() as u32, @@ -301,7 +300,11 @@ impl Collection { )) }) .collect::>>()?; + let start_memorized = cards + .iter() + .fold(0., |p, c| p + c.retention_on(&req.params, 0.)); Ok(SimulateFsrsWorkloadResponse { + start_memorized, memorized: dr_workload.iter().map(|(k, v)| (*k, v.0)).collect(), cost: dr_workload.iter().map(|(k, v)| (*k, v.1)).collect(), review_count: dr_workload.iter().map(|(k, v)| (*k, v.2)).collect(), diff --git a/ts/routes/deck-options/SimulatorModal.svelte b/ts/routes/deck-options/SimulatorModal.svelte index c60f90455..d88f5790d 100644 --- a/ts/routes/deck-options/SimulatorModal.svelte +++ b/ts/routes/deck-options/SimulatorModal.svelte @@ -212,6 +212,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html x: parseInt(dr), timeCost: resp!.cost[dr], memorized: v, + start_memorized: resp!.startMemorized, count: resp!.reviewCount[dr], label: simulationNumber, learnSpan: simulateFsrsRequest.daysToSimulate, diff --git a/ts/routes/graphs/simulator.ts b/ts/routes/graphs/simulator.ts index dbd4a4e7b..e3fd94de0 100644 --- a/ts/routes/graphs/simulator.ts +++ b/ts/routes/graphs/simulator.ts @@ -34,6 +34,7 @@ export interface Point { export type WorkloadPoint = Point & { learnSpan: number; + start_memorized: number; }; export enum SimulateSubgraph { @@ -63,14 +64,17 @@ export function renderWorkloadChart( .range([bounds.marginLeft, bounds.width - bounds.marginRight]); const subgraph_data = ({ - [SimulateWorkloadSubgraph.ratio]: data.map(d => ({ ...d, y: d.memorized / d.timeCost })), + [SimulateWorkloadSubgraph.ratio]: data.map(d => ({ + ...d, + y: (60 * 60 * (d.memorized - d.start_memorized)) / d.timeCost, + })), [SimulateWorkloadSubgraph.time]: data.map(d => ({ ...d, y: d.timeCost / d.learnSpan })), [SimulateWorkloadSubgraph.count]: data.map(d => ({ ...d, y: d.count / d.learnSpan })), [SimulateWorkloadSubgraph.memorized]: data.map(d => ({ ...d, y: d.memorized })), })[subgraph]; const yTickFormat = (n: number): string => { - return subgraph == SimulateWorkloadSubgraph.time || subgraph == SimulateWorkloadSubgraph.ratio + return subgraph == SimulateWorkloadSubgraph.time ? timeSpan(n, true) : n.toString(); }; @@ -84,7 +88,7 @@ export function renderWorkloadChart( const formatY: (value: number) => string = ({ [SimulateWorkloadSubgraph.ratio]: (value: number) => - tr.deckConfigFsrsSimulatorRatioTooltip({ time: timeSpan(value) }), + tr.deckConfigFsrsSimulatorRatioTooltip({ time: value.toFixed(2) }), [SimulateWorkloadSubgraph.time]: (value: number) => tr.statisticsMinutesPerDay({ count: parseFloat((value / 60).toPrecision(2)) }), [SimulateWorkloadSubgraph.count]: (value: number) => tr.statisticsReviewsPerDay({ count: Math.round(value) }), From b30e2df201929bbe92b974149ded2151c9675522 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 00:03:52 +0100 Subject: [PATCH 3/8] Render Extra --- ts/routes/graphs/simulator.ts | 52 +++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/ts/routes/graphs/simulator.ts b/ts/routes/graphs/simulator.ts index e3fd94de0..e1c707029 100644 --- a/ts/routes/graphs/simulator.ts +++ b/ts/routes/graphs/simulator.ts @@ -11,10 +11,13 @@ import { min, pointer, rollup, + type ScaleLinear, scaleLinear, + type ScaleTime, scaleTime, schemeCategory10, select, + type Selection, } from "d3"; import * as tr from "@generated/ftl"; @@ -66,7 +69,7 @@ export function renderWorkloadChart( const subgraph_data = ({ [SimulateWorkloadSubgraph.ratio]: data.map(d => ({ ...d, - y: (60 * 60 * (d.memorized - d.start_memorized)) / d.timeCost, + y: (60 * 60 * (d.memorized - d.start_memorized)) / d.count, })), [SimulateWorkloadSubgraph.time]: data.map(d => ({ ...d, y: d.timeCost / d.learnSpan })), [SimulateWorkloadSubgraph.count]: data.map(d => ({ ...d, y: d.count / d.learnSpan })), @@ -100,6 +103,19 @@ export function renderWorkloadChart( return `${tr.deckConfigDesiredRetention()}: ${xTickFormat(dr)}
`; } + select(svgElem) + .enter() + .datum(subgraph_data[subgraph_data.length - 1]) + .append("line") + .attr("x1", bounds.marginLeft) + .attr("x2", bounds.width - bounds.marginRight) + .attr("y1", bounds.marginTop) + .attr("y2", bounds.marginTop) + .attr("stroke", "black") + .attr("stroke-width", 1); + + const startMemorized = subgraph_data[0].start_memorized; + return _renderSimulationChart( svgElem, bounds, @@ -110,6 +126,20 @@ export function renderWorkloadChart( (_e: MouseEvent, _d: number) => undefined, yTickFormat, xTickFormat, + (svg, x, y) => { + svg + .selectAll("line") + .data(subgraph == SimulateWorkloadSubgraph.memorized ? [startMemorized] : []) + .enter() + .attr("x1", x(xMin)) + .attr("x2", x(xMax)) + .attr("y1",d => y(d)) + .attr("y2",d => y(d)) + .attr("stroke", "black") + .attr("stroke-dasharray", "5,5") + .attr("stroke-width", 1); + }, + subgraph == SimulateWorkloadSubgraph.memorized ? startMemorized : 0, ); } @@ -190,16 +220,25 @@ export function renderSimulationChart( ); } -function _renderSimulationChart( +function _renderSimulationChart< + X extends ScaleLinear | ScaleTime, + T extends { x: any; y: any; label: number }, +>( svgElem: SVGElement, bounds: GraphBounds, subgraph_data: T[], - x: any, + x: X, formatY: (n: T["y"]) => string, formatX: (n: T["x"]) => string, legendMouseMove: (e: MouseEvent, d: number) => void, yTickFormat?: (n: number) => string, xTickFormat?: (n: number) => string, + renderExtra?: ( + svg: Selection, + x: X, + y: ScaleLinear, + ) => void, + y_min: number = Infinity, ): TableDatum[] { const svg = select(svgElem); svg.selectAll(".lines").remove(); @@ -220,7 +259,8 @@ function _renderSimulationChart( // y scale const yMax = max(subgraph_data, d => d.y)!; - const yMin = min(subgraph_data, d => d.y)!; + let yMin = min(subgraph_data, d => d.y)!; + yMin = min([yMin, y_min])!; const y = scaleLinear() .range([bounds.height - bounds.marginBottom, bounds.marginTop]) .domain([yMin, yMax]) @@ -248,7 +288,7 @@ function _renderSimulationChart( .attr("fill", "currentColor"); // x lines - const points = subgraph_data.map((d) => [x(d.x), y(d.y), d.label]); + const points = subgraph_data.map((d) => [x(d.x)!, y(d.y)!, d.label]); const groups = rollup(points, v => Object.assign(v, { z: v[0][2] }), d => d[2]); const color = schemeCategory10; @@ -370,6 +410,8 @@ function _renderSimulationChart( setDataAvailable(svg, true); + renderExtra?.(svg, x, y); + const tableData: TableDatum[] = []; return tableData; From b554556cc38550249d99891d40b22999f246776d Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 17:00:45 +0100 Subject: [PATCH 4/8] Fix: Wrong denominator --- ts/routes/graphs/simulator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/graphs/simulator.ts b/ts/routes/graphs/simulator.ts index e1c707029..ff3f81880 100644 --- a/ts/routes/graphs/simulator.ts +++ b/ts/routes/graphs/simulator.ts @@ -69,7 +69,7 @@ export function renderWorkloadChart( const subgraph_data = ({ [SimulateWorkloadSubgraph.ratio]: data.map(d => ({ ...d, - y: (60 * 60 * (d.memorized - d.start_memorized)) / d.count, + y: (60 * 60 * (d.memorized - d.start_memorized)) / d.timeCost, })), [SimulateWorkloadSubgraph.time]: data.map(d => ({ ...d, y: d.timeCost / d.learnSpan })), [SimulateWorkloadSubgraph.count]: data.map(d => ({ ...d, y: d.count / d.learnSpan })), From d11b803b8847e508c43ba3f95808c5f3a2601216 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 26 Nov 2025 17:36:43 +0000 Subject: [PATCH 5/8] Fix: Tooltips --- ftl/core/deck-config.ftl | 7 +++++-- ts/routes/deck-options/SimulatorModal.svelte | 2 +- ts/routes/graphs/simulator.ts | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index 608e9b078..acaa9802d 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -521,8 +521,8 @@ deck-config-save-options-to-preset-confirm = Overwrite the options in your curre # to show the total number of cards that can be recalled or retrieved on a # specific date. deck-config-fsrs-simulator-radio-memorized = Memorized -deck-config-fsrs-simulator-radio-ratio = Time / Memorized Ratio -deck-config-fsrs-simulator-ratio-tooltip = { $time } memorized cards per hour +deck-config-fsrs-simulator-radio-ratio2 = Memorized / Time Ratio +deck-config-fsrs-simulator-ratio-tooltip2 = { $time } memorized cards per hour ## Messages related to the FSRS scheduler’s health check. The health check determines whether the correlation between FSRS predictions and your memory is good or bad. It can be optionally triggered as part of the "Optimize" function. @@ -543,6 +543,9 @@ deck-config-fsrs-good-fit = Health Check: ## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future. +deck-config-fsrs-simulator-radio-ratio = Time / Memorized Ratio +# $time here is pre-formatted e.g. "10 Seconds" +deck-config-fsrs-simulator-ratio-tooltip = { $time } per memorized card deck-config-plotted-on-x-axis = (Plotted on the X-axis) deck-config-a-100-day-interval = { $days -> diff --git a/ts/routes/deck-options/SimulatorModal.svelte b/ts/routes/deck-options/SimulatorModal.svelte index d88f5790d..50e5502ca 100644 --- a/ts/routes/deck-options/SimulatorModal.svelte +++ b/ts/routes/deck-options/SimulatorModal.svelte @@ -571,7 +571,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html value={SimulateWorkloadSubgraph.ratio} bind:group={simulateWorkloadSubgraph} /> - {tr.deckConfigFsrsSimulatorRadioRatio()} + {tr.deckConfigFsrsSimulatorRadioRatio2()}