Skip to content

Commit 39e28c5

Browse files
committed
Merge branch 'main' into fsrs-browser
2 parents f6b3713 + 611980a commit 39e28c5

File tree

7 files changed

+426
-103
lines changed

7 files changed

+426
-103
lines changed

.github/workflows/check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626

2727
- uses: dprint/check@v2.2
2828
build-macos:
29-
runs-on: macos-latest
29+
runs-on: macos-14
3030
steps:
3131
- uses: actions/checkout@v2
3232

Cargo.lock

Lines changed: 40 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fsrs"
3-
version = "5.1.0"
3+
version = "5.2.0"
44
authors = ["Open Spaced Repetition"]
55
categories = ["algorithms", "science"]
66
edition = "2024"
@@ -33,17 +33,17 @@ features = ["std", "train", "ndarray", "sqlite-bundled", "metrics"]
3333
itertools = "0.14.0"
3434
log = "0.4"
3535
ndarray = "0.16.1"
36-
priority-queue = "=2.5.0"
36+
priority-queue = "=2.7.0"
3737
rand = "0.9.2"
38-
rayon = "1.8.0"
39-
serde = "1.0.219"
40-
snafu = "0.8.6"
38+
rayon = "1.11.0"
39+
serde = "1.0.228"
40+
snafu = "0.8.9"
4141
strum = { version = "0.27.2", features = ["derive"] }
42-
chrono = { version = "0.4.41", default-features = false, features = ["std", "clock"] }
4342
chrono-tz = "0.10.4"
4443
wasm-bindgen = "0.2.91"
4544

4645
[dev-dependencies]
46+
chrono = { version = "0.4.42", default-features = false, features = ["std", "clock"] }
4747
criterion = { version = "0.7.0" }
4848
csv = "1.3.0"
4949
fern = "0.7.1"

README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
This crate contains a Rust API for training FSRS parameters, and for using them to schedule cards.
66

7-
The Free Spaced Repetition Scheduler ([FSRS](https://github.com/open-spaced-repetition/fsrs4anki)) is a modern spaced repetition algorithm. It is based on the [DSR model](https://supermemo.guru/wiki/Three_component_model_of_memory) proposed by [Piotr Wozniak](https://supermemo.guru/wiki/Piotr_Wozniak), the creator of SuperMemo.
7+
The Free Spaced Repetition Scheduler ([FSRS](https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm)) is a modern spaced repetition algorithm. It springs from [MaiMemo's DHP model](https://www.maimemo.com/paper/), which is a variant of the [DSR model](https://supermemo.guru/wiki/Three_component_model_of_memory) proposed by [Piotr Wozniak](https://supermemo.guru/wiki/Piotr_Wozniak), the creator of SuperMemo.
88

99
FSRS-rs is a Rust implementation of FSRS. It is designed to be used in [Anki](https://apps.ankiweb.net/), a popular spaced repetition software. [Anki 23.10](https://github.com/ankitects/anki/releases/tag/23.10) has already integrated FSRS as an alternative scheduler.
1010

@@ -14,18 +14,18 @@ For more information about the algorithm, please refer to [the wiki page of FSRS
1414

1515
## Quickstart
1616

17-
Read up [this](https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Optimal-Retention) to determine the optimal retention for your use case.
17+
Read [this](https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Optimal-Retention) for an explanation of how to determine the optimal retention for your use case.
1818

1919
```rust
20-
// Pick to your liking (see above)
20+
// Pick whichever percentage is to your liking (see above)
2121
let optimal_retention = 0.75;
22-
// Use default parameters/Weights for scheduler
22+
// Use default parameters/weights for the scheduler
2323
let fsrs = FSRS::new(Some(&[]))?;
2424

2525
// Create a completely new card
2626
let day1_states = fsrs.next_states(None, optimal_retention, 0)?;
2727

28-
// Rate as `hard` on first day
28+
// Rate as `hard` on the first day
2929
let day1 = day1_states.hard;
3030
dbg!(&day1); // scheduled as `in 4 days`
3131

@@ -39,7 +39,7 @@ dbg!(day3);
3939

4040
## Online development
4141

42-
go to <https://idx.google.com/import>
42+
You can use <https://idx.google.com/import>.
4343

4444
## Local development
4545

@@ -64,20 +64,20 @@ to `.git/hooks/pre-commit`, then `chmod +x .git/hooks/pre-commit`
6464

6565
## Q&A
6666

67-
- What is the difference with [rs-fsrs](https://github.com/open-spaced-repetition/rs-fsrs)
67+
- What is the difference between `fsrs-rs` and [`rs-fsrs`](https://github.com/open-spaced-repetition/rs-fsrs)
6868

69-
If you want to schedule the card, use \[lang\]-fsrs or the [bindings](https://github.com/open-spaced-repetition/rs-fsrs?tab=readme-ov-file#bindings),
69+
If you only want to schedule cards, use \[lang\]-fsrs or the [bindings](https://github.com/open-spaced-repetition/rs-fsrs?tab=readme-ov-file#bindings).
7070

71-
If you do the optimization, use this crate or its bindings.
71+
If you need to optimize, use this crate or its bindings.
7272

73-
- Why not in one crate but two?
73+
- Why use two crates instead of one?
7474

75-
Calculating the weight involves tensor operations. So the initial data type is different(Tensor vs Vec/Slice). In one crate means use `cfg` to change type, which is tedious, so here we keep two versions.
75+
Calculating the weights involves tensor operations so the data types are different (Tensor vs Vec/Slice). If we were to use one crate, this would mean using `cfg` to change the variable type, which would be tedious. Because of this, instead we publish two separate crates.
7676

77-
Another reason is, other languages will be hard to port their version when `Tensor` is used.
77+
Another reason is, it would be hard to port to other languages while using `Tensor`s.
7878

7979
- What about the name?
8080

81-
At first, there are `go-fsrs` and other libraries, so `rs-fsrs` is used.
81+
Before this crate was made, `go-fsrs` and other libraries already existed, so the name `rs-fsrs` was chosen.
8282

83-
Then we want to port the torch version to rust so everyone can calculate on their own devices (tch-rs use libtorch which is too heavy), since the algorithm is called `fsrs`, add `-rs`.
83+
Then we wanted to port the torch version to Rust so that everyone could optimize on their own devices (tch-rs uses libtorch which is too heavy). Since the algorithm is called `fsrs`, we add an `-rs` on the end.

benches/benchmark.rs

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,47 @@
44
use std::hint::black_box;
55
use std::iter::repeat;
66

7-
use criterion::Criterion;
87
use criterion::criterion_group;
98
use criterion::criterion_main;
9+
use criterion::{Criterion, Throughput};
1010
use fsrs::FSRS;
1111
use fsrs::FSRSReview;
1212
use fsrs::NextStates;
1313
use fsrs::{FSRSItem, MemoryState};
1414
use itertools::Itertools;
1515

16-
pub(crate) fn calc_mem(inf: &FSRS, past_reviews: usize) -> MemoryState {
16+
pub(crate) fn calc_mem(inf: &FSRS, past_reviews: usize, card_cnt: usize) -> Vec<MemoryState> {
1717
let review = FSRSReview {
1818
rating: 3,
1919
delta_t: 21,
2020
};
2121
let reviews = repeat(review).take(past_reviews + 1).collect_vec();
22-
inf.memory_state(FSRSItem { reviews }, None).unwrap()
22+
(0..card_cnt)
23+
.map(|_| {
24+
inf.memory_state(
25+
FSRSItem {
26+
reviews: reviews.clone(),
27+
},
28+
None,
29+
)
30+
.unwrap()
31+
})
32+
.collect_vec()
33+
}
34+
35+
pub(crate) fn calc_mem_batch(inf: &FSRS, past_reviews: usize, card_cnt: usize) -> Vec<MemoryState> {
36+
let reviews = repeat(FSRSReview {
37+
rating: 3,
38+
delta_t: 21,
39+
})
40+
.take(past_reviews)
41+
.collect_vec();
42+
let items = repeat(FSRSItem {
43+
reviews: reviews.clone(),
44+
})
45+
.take(card_cnt)
46+
.collect_vec();
47+
inf.memory_state_batch(items, vec![None; card_cnt]).unwrap()
2348
}
2449

2550
pub(crate) fn next_states(inf: &FSRS) -> NextStates {
@@ -55,8 +80,42 @@ pub fn criterion_benchmark(c: &mut Criterion) {
5580
2.6646678,
5681
]))
5782
.unwrap();
58-
c.bench_function("calc_mem", |b| b.iter(|| black_box(calc_mem(&fsrs, 100))));
83+
5984
c.bench_function("next_states", |b| b.iter(|| black_box(next_states(&fsrs))));
85+
86+
{
87+
let mut single_group = c.benchmark_group("calc_mem");
88+
let n_cards = 1000;
89+
let n_reviews = 10;
90+
single_group.throughput(Throughput::Elements(n_cards));
91+
single_group.bench_function(
92+
format!("calc_mem n_cards={n_cards}, n_reviews={n_reviews}"),
93+
|b| b.iter(|| black_box(calc_mem(&fsrs, n_reviews, n_cards.try_into().unwrap()))),
94+
);
95+
single_group.finish();
96+
}
97+
98+
{
99+
let mut batch_group = c.benchmark_group("calc_mem_batch");
100+
for n_cards in [1000, 10_000] {
101+
for n_reviews in [10, 100, 200] {
102+
batch_group.throughput(Throughput::Elements(n_cards));
103+
batch_group.bench_function(
104+
format!("calc_mem_batch n_cards={n_cards}, n_reviews={n_reviews}"),
105+
|b| {
106+
b.iter(|| {
107+
black_box(calc_mem_batch(
108+
&fsrs,
109+
n_reviews,
110+
n_cards.try_into().unwrap(),
111+
))
112+
})
113+
},
114+
);
115+
}
116+
}
117+
batch_group.finish();
118+
}
60119
}
61120

62121
criterion_group!(benches, criterion_benchmark);

0 commit comments

Comments
 (0)