From 0c6e3eaa93a469577f66139774f0521be4c173e9 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 5 Sep 2023 18:45:05 +1000 Subject: [PATCH] Integrate the FSRS optimizer (#2633) * Support searching for deck configs by name * Integrate FSRS optimizer into Anki * Hack in a rough implementation of evaluate_weights() * Interrupt calculation if user closes dialog * Fix interrupted error check * log_loss/rmse * Update to latest fsrs commit; add progress info to weight evaluation * Fix progress not appearing when pretrain takes a while * Update to latest commit --- .deny.toml | 2 + Cargo.lock | 515 ++++++++++++++++++++++++- Cargo.toml | 3 + cargo/licenses.json | 445 ++++++++++++++++++++- proto/anki/collection.proto | 13 + proto/anki/deck_config.proto | 7 +- proto/anki/scheduler.proto | 39 ++ qt/aqt/deckoptions.py | 1 + qt/aqt/mediasrv.py | 5 + rslib/Cargo.toml | 1 + rslib/src/deckconfig/mod.rs | 3 + rslib/src/deckconfig/schema11.rs | 21 +- rslib/src/deckconfig/service.rs | 21 +- rslib/src/progress.rs | 31 ++ rslib/src/scheduler/fsrs/error.rs | 20 + rslib/src/scheduler/fsrs/mod.rs | 5 + rslib/src/scheduler/fsrs/retention.rs | 50 +++ rslib/src/scheduler/fsrs/weights.rs | 285 ++++++++++++++ rslib/src/scheduler/mod.rs | 1 + rslib/src/scheduler/service/mod.rs | 31 ++ rslib/src/search/parser.rs | 1 + rslib/src/search/sqlwriter.rs | 21 + rslib/src/search/writer.rs | 1 + rslib/src/storage/deckconfig/mod.rs | 9 + rslib/src/storage/revlog/mod.rs | 12 + ts/deck-options/AdvancedOptions.svelte | 9 + ts/deck-options/DeckOptionsPage.svelte | 22 +- ts/deck-options/FsrsOptions.svelte | 289 ++++++++++++++ ts/deck-options/WeightsInput.svelte | 16 + ts/deck-options/WeightsInputRow.svelte | 18 + ts/lib/post.ts | 2 +- ts/lib/progress.ts | 8 +- 32 files changed, 1871 insertions(+), 36 deletions(-) create mode 100644 rslib/src/scheduler/fsrs/error.rs create mode 100644 rslib/src/scheduler/fsrs/mod.rs create mode 100644 rslib/src/scheduler/fsrs/retention.rs create mode 100644 rslib/src/scheduler/fsrs/weights.rs create mode 100644 ts/deck-options/FsrsOptions.svelte create mode 100644 ts/deck-options/WeightsInput.svelte create mode 100644 ts/deck-options/WeightsInputRow.svelte diff --git a/.deny.toml b/.deny.toml index 6239fe0fb..e0ece6eb4 100644 --- a/.deny.toml +++ b/.deny.toml @@ -30,6 +30,8 @@ allow = [ "BSD-3-Clause", "OpenSSL", "CC0-1.0", + "Unlicense", + "Zlib", ] confidence-threshold = 0.8 # eg { allow = ["Zlib"], name = "adler32", version = "*" }, diff --git a/Cargo.lock b/Cargo.lock index 5f31b5f39..b5a25ead2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,7 +98,7 @@ dependencies = [ "coarsetime", "convert_case", "criterion", - "csv", + "csv 1.1.6", "data-encoding", "difflib", "dirs", @@ -107,6 +107,7 @@ dependencies = [ "fluent", "fluent-bundle", "fnv", + "fsrs-optimizer", "futures", "hex", "htmlescape", @@ -138,7 +139,7 @@ dependencies = [ "serde_tuple", "sha1", "snafu", - "strum", + "strum 0.25.0", "syn 2.0.29", "tempfile", "tokio", @@ -215,7 +216,7 @@ dependencies = [ "prost-types", "serde", "snafu", - "strum", + "strum 0.25.0", ] [[package]] @@ -489,6 +490,15 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +[[package]] +name = "bincode" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -562,6 +572,152 @@ version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +[[package]] +name = "burn" +version = "0.9.0" +source = "git+https://github.com/burn-rs/burn.git?rev=36446e8d35694a9158f97e85e44b84544b8c4afb#36446e8d35694a9158f97e85e44b84544b8c4afb" +dependencies = [ + "burn-core", + "burn-train", +] + +[[package]] +name = "burn-autodiff" +version = "0.9.0" +source = "git+https://github.com/burn-rs/burn.git?rev=36446e8d35694a9158f97e85e44b84544b8c4afb#36446e8d35694a9158f97e85e44b84544b8c4afb" +dependencies = [ + "burn-common", + "burn-tensor", + "burn-tensor-testgen", + "derive-new", + "spin 0.9.8", +] + +[[package]] +name = "burn-common" +version = "0.9.0" +source = "git+https://github.com/burn-rs/burn.git?rev=36446e8d35694a9158f97e85e44b84544b8c4afb#36446e8d35694a9158f97e85e44b84544b8c4afb" +dependencies = [ + "const-random", + "rand 0.8.5", + "spin 0.9.8", + "uuid", +] + +[[package]] +name = "burn-core" +version = "0.9.0" +source = "git+https://github.com/burn-rs/burn.git?rev=36446e8d35694a9158f97e85e44b84544b8c4afb#36446e8d35694a9158f97e85e44b84544b8c4afb" +dependencies = [ + "bincode", + "burn-autodiff", + "burn-common", + "burn-dataset", + "burn-derive", + "burn-ndarray", + "burn-tensor", + "derive-new", + "flate2", + "half 2.3.1", + "hashbrown 0.14.0", + "libm", + "log", + "rand 0.8.5", + "rmp-serde", + "serde", + "serde_json", + "spin 0.9.8", +] + +[[package]] +name = "burn-dataset" +version = "0.9.0" +source = "git+https://github.com/burn-rs/burn.git?rev=36446e8d35694a9158f97e85e44b84544b8c4afb#36446e8d35694a9158f97e85e44b84544b8c4afb" +dependencies = [ + "csv 1.2.2", + "derive-new", + "dirs", + "gix-tempfile", + "rand 0.8.5", + "rmp-serde", + "sanitize-filename", + "serde", + "serde_json", + "strum 0.24.1", + "strum_macros 0.24.3", + "tempfile", + "thiserror", +] + +[[package]] +name = "burn-derive" +version = "0.9.0" +source = "git+https://github.com/burn-rs/burn.git?rev=36446e8d35694a9158f97e85e44b84544b8c4afb#36446e8d35694a9158f97e85e44b84544b8c4afb" +dependencies = [ + "derive-new", + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "burn-ndarray" +version = "0.9.0" +source = "git+https://github.com/burn-rs/burn.git?rev=36446e8d35694a9158f97e85e44b84544b8c4afb#36446e8d35694a9158f97e85e44b84544b8c4afb" +dependencies = [ + "burn-autodiff", + "burn-common", + "burn-tensor", + "derive-new", + "libm", + "matrixmultiply", + "ndarray", + "num-traits", + "rand 0.8.5", + "rayon", + "spin 0.9.8", +] + +[[package]] +name = "burn-tensor" +version = "0.9.0" +source = "git+https://github.com/burn-rs/burn.git?rev=36446e8d35694a9158f97e85e44b84544b8c4afb#36446e8d35694a9158f97e85e44b84544b8c4afb" +dependencies = [ + "burn-tensor-testgen", + "derive-new", + "half 2.3.1", + "hashbrown 0.14.0", + "libm", + "num-traits", + "rand 0.8.5", + "rand_distr", + "serde", +] + +[[package]] +name = "burn-tensor-testgen" +version = "0.9.0" +source = "git+https://github.com/burn-rs/burn.git?rev=36446e8d35694a9158f97e85e44b84544b8c4afb#36446e8d35694a9158f97e85e44b84544b8c4afb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "burn-train" +version = "0.9.0" +source = "git+https://github.com/burn-rs/burn.git?rev=36446e8d35694a9158f97e85e44b84544b8c4afb#36446e8d35694a9158f97e85e44b84544b8c4afb" +dependencies = [ + "burn-core", + "derive-new", + "log", + "serde", + "tracing-appender", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "byteorder" version = "1.4.3" @@ -647,7 +803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" dependencies = [ "ciborium-io", - "half", + "half 1.8.2", ] [[package]] @@ -769,6 +925,28 @@ dependencies = [ "ninja_gen", ] +[[package]] +name = "const-random" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" +dependencies = [ + "const-random-macro", + "proc-macro-hack", +] + +[[package]] +name = "const-random-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" +dependencies = [ + "getrandom 0.2.10", + "once_cell", + "proc-macro-hack", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.3.0" @@ -897,6 +1075,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -913,12 +1097,33 @@ version = "1.1.6" source = "git+https://github.com/ankitects/rust-csv.git?rev=1c9d3aab6f79a7d815c69f925a46a4590c115f90#1c9d3aab6f79a7d815c69f925a46a4590c115f90" dependencies = [ "bstr 0.2.17", - "csv-core", + "csv-core 0.1.10 (git+https://github.com/ankitects/rust-csv.git?rev=1c9d3aab6f79a7d815c69f925a46a4590c115f90)", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086" +dependencies = [ + "csv-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "itoa", "ryu", "serde", ] +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + [[package]] name = "csv-core" version = "0.1.10" @@ -927,6 +1132,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.0", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.4.0" @@ -958,6 +1176,17 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +[[package]] +name = "derive-new" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "des" version = "0.8.1" @@ -1134,6 +1363,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "faster-hex" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9042d281a5eec0f2387f8c3ea6c4514e2cf2732c90a85aaf383b761ee3b290d" +dependencies = [ + "serde", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1291,6 +1529,22 @@ dependencies = [ "libc", ] +[[package]] +name = "fsrs-optimizer" +version = "0.1.0" +source = "git+https://github.com/open-spaced-repetition/fsrs-optimizer-burn?rev=e0b15cce555a94de6fdaa4bf1e096d19704a397d#e0b15cce555a94de6fdaa4bf1e096d19704a397d" +dependencies = [ + "burn", + "itertools 0.11.0", + "log", + "ndarray", + "ndarray-rand", + "rand 0.8.5", + "serde", + "snafu", + "strum 0.25.0", +] + [[package]] name = "ftl" version = "0.0.0" @@ -1476,6 +1730,58 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "gix-features" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f77decb545f63a52852578ef5f66ecd71017ffc1983d551d5fa2328d6d9817f" +dependencies = [ + "gix-hash", + "gix-trace", + "libc", +] + +[[package]] +name = "gix-fs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d5089f3338647776733a75a800a664ab046f56f21c515fa4722e395f877ef8" +dependencies = [ + "gix-features", +] + +[[package]] +name = "gix-hash" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4796bac3aaf0c2f8bea152ca924ae3bdc5f135caefe6431116bcd67e98eab9" +dependencies = [ + "faster-hex", + "thiserror", +] + +[[package]] +name = "gix-tempfile" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea558d3daf3b1d0001052b12218c66c8f84788852791333b633d7eeb6999db1" +dependencies = [ + "dashmap", + "gix-fs", + "libc", + "once_cell", + "parking_lot", + "signal-hook", + "signal-hook-registry", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b6d623a1152c3facb79067d6e2ecdae48130030cf27d6eb21109f13bd7b836" + [[package]] name = "glob" version = "0.3.1" @@ -1520,6 +1826,18 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "half" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "serde", +] + [[package]] name = "handlebars" version = "4.3.7" @@ -1548,6 +1866,7 @@ checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ "ahash", "allocator-api2", + "serde", ] [[package]] @@ -2016,6 +2335,12 @@ version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + [[package]] name = "libsqlite3-sys" version = "0.26.0" @@ -2071,7 +2396,7 @@ dependencies = [ "linkcheck", "regex", "reqwest", - "strum", + "strum 0.25.0", "tokio", ] @@ -2209,6 +2534,19 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" +[[package]] +name = "matrixmultiply" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77" +dependencies = [ + "autocfg", + "num_cpus", + "once_cell", + "rawpointer", + "thread-tree", +] + [[package]] name = "mdbook" version = "0.4.34" @@ -2356,6 +2694,31 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", + "rayon", +] + +[[package]] +name = "ndarray-rand" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65608f937acc725f5b164dcf40f4f0bc5d67dc268ab8a649d3002606718c4588" +dependencies = [ + "ndarray", + "rand 0.8.5", + "rand_distr", +] + [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -2442,6 +2805,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-format" version = "0.4.4" @@ -2452,6 +2824,16 @@ dependencies = [ "itoa", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -2459,6 +2841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2636,6 +3019,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pem" version = "1.1.1" @@ -3156,6 +3545,16 @@ dependencies = [ "getrandom 0.2.10", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -3165,6 +3564,12 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.7.0" @@ -3349,6 +3754,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "rmp" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rsbridge" version = "0.0.0" @@ -3502,6 +3929,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sanitize-filename" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "schannel" version = "0.1.22" @@ -3570,9 +4007,9 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.185" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] @@ -3590,9 +4027,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.185" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", @@ -3722,6 +4159,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -3818,6 +4265,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "string_cache" @@ -3851,13 +4301,32 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + [[package]] name = "strum" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ - "strum_macros", + "strum_macros 0.25.2", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", ] [[package]] @@ -3987,6 +4456,15 @@ dependencies = [ "syn 2.0.29", ] +[[package]] +name = "thread-tree" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbd370cb847953a25954d9f63e14824a36113f8c72eecf6eccef5dc4b45d630" +dependencies = [ + "crossbeam-channel", +] + [[package]] name = "thread_local" version = "1.1.7" @@ -4025,6 +4503,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.7.1" @@ -4581,6 +5068,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9284bb17a..aeb44a125 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,9 @@ anki_process = { path = "rslib/process" } anki_proto_gen = { path = "rslib/proto_gen" } ninja_gen = { "path" = "build/ninja_gen" } +fsrs-optimizer = { git = "https://github.com/open-spaced-repetition/fsrs-optimizer-burn", rev = "e0b15cce555a94de6fdaa4bf1e096d19704a397d" } +# fsrs-optimizer.path = "../../../fsrs-optimizer-burn" + # forked csv = { git = "https://github.com/ankitects/rust-csv.git", rev = "1c9d3aab6f79a7d815c69f925a46a4590c115f90" } percent-encoding-iri = { git = "https://github.com/ankitects/rust-url.git", rev = "bb930b8d089f4d30d7d19c12e54e66191de47b88" } diff --git a/cargo/licenses.json b/cargo/licenses.json index a5308194a..c9ef69734 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -269,6 +269,15 @@ "license_file": null, "description": "encodes and decodes base64 as bytes or utf8" }, + { + "name": "bincode", + "version": "2.0.0-rc.3", + "authors": "Ty Overby |Zoey Riordan |Victor Koenders ", + "repository": "https://github.com/bincode-org/bincode", + "license": "MIT", + "license_file": null, + "description": "A binary serialization / deserialization strategy for transforming structs into bytes and vice versa!" + }, { "name": "bitflags", "version": "1.3.2", @@ -323,6 +332,96 @@ "license_file": null, "description": "A fast bump allocation arena for Rust." }, + { + "name": "burn", + "version": "0.9.0", + "authors": "nathanielsimard ", + "repository": "https://github.com/burn-rs/burn", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Flexible and Comprehensive Deep Learning Framework in Rust" + }, + { + "name": "burn-autodiff", + "version": "0.9.0", + "authors": "nathanielsimard ", + "repository": "https://github.com/burn-rs/burn/tree/main/burn-autodiff", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Automatic differentiation backend for the Burn framework" + }, + { + "name": "burn-common", + "version": "0.9.0", + "authors": "Dilshod Tadjibaev (@antimora)", + "repository": "https://github.com/burn-rs/burn/tree/main/burn-common", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Common crate for the Burn framework" + }, + { + "name": "burn-core", + "version": "0.9.0", + "authors": "nathanielsimard ", + "repository": "https://github.com/burn-rs/burn/tree/main/burn-core", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Flexible and Comprehensive Deep Learning Framework in Rust" + }, + { + "name": "burn-dataset", + "version": "0.9.0", + "authors": "nathanielsimard ", + "repository": "https://github.com/burn-rs/burn/tree/main/burn-dataset", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Library with simple dataset APIs for creating ML data pipelines" + }, + { + "name": "burn-derive", + "version": "0.9.0", + "authors": "nathanielsimard ", + "repository": "https://github.com/burn-rs/burn/tree/main/burn-derive", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Derive crate for the Burn framework" + }, + { + "name": "burn-ndarray", + "version": "0.9.0", + "authors": "nathanielsimard ", + "repository": "https://github.com/burn-rs/burn/tree/main/burn-ndarray", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Ndarray backend for the Burn framework" + }, + { + "name": "burn-tensor", + "version": "0.9.0", + "authors": "nathanielsimard ", + "repository": "https://github.com/burn-rs/burn/tree/main/burn-tensor", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Tensor library with user-friendly APIs and automatic differentiation support" + }, + { + "name": "burn-tensor-testgen", + "version": "0.9.0", + "authors": "nathanielsimard ", + "repository": "https://github.com/burn-rs/burn/tree/main/burn-tensor-testgen", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Test generation crate for burn-tensor" + }, + { + "name": "burn-train", + "version": "0.9.0", + "authors": "nathanielsimard ", + "repository": "https://github.com/burn-rs/burn/tree/main/burn-train", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Training crate for the Burn framework" + }, { "name": "byteorder", "version": "1.4.3", @@ -395,6 +494,24 @@ "license_file": null, "description": "Concurrent multi-producer multi-consumer queue" }, + { + "name": "const-random", + "version": "0.1.15", + "authors": "Tom Kaitchuck ", + "repository": "https://github.com/tkaitchuck/constrandom", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Provides compile time random number generation." + }, + { + "name": "const-random-macro", + "version": "0.1.15", + "authors": "Tom Kaitchuck ", + "repository": "https://github.com/tkaitchuck/constrandom", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Provides the procedural macro used by const-random" + }, { "name": "constant_time_eq", "version": "0.3.0", @@ -458,6 +575,24 @@ "license_file": null, "description": "Multi-producer multi-consumer channels for message passing" }, + { + "name": "crossbeam-deque", + "version": "0.8.3", + "authors": null, + "repository": "https://github.com/crossbeam-rs/crossbeam", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Concurrent work-stealing deque" + }, + { + "name": "crossbeam-epoch", + "version": "0.9.15", + "authors": null, + "repository": "https://github.com/crossbeam-rs/crossbeam", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Epoch-based garbage collection" + }, { "name": "crossbeam-utils", "version": "0.8.16", @@ -467,6 +602,15 @@ "license_file": null, "description": "Utilities for concurrent programming" }, + { + "name": "crunchy", + "version": "0.2.2", + "authors": "Vurich ", + "repository": null, + "license": "MIT", + "license_file": null, + "description": "Crunchy unroller: deterministically unroll constant loops" + }, { "name": "crypto-common", "version": "0.1.6", @@ -485,6 +629,15 @@ "license_file": null, "description": "Fast CSV parsing with support for serde." }, + { + "name": "csv", + "version": "1.2.2", + "authors": "Andrew Gallant ", + "repository": "https://github.com/BurntSushi/rust-csv", + "license": "MIT OR Unlicense", + "license_file": null, + "description": "Fast CSV parsing with support for serde." + }, { "name": "csv-core", "version": "0.1.10", @@ -494,6 +647,24 @@ "license_file": null, "description": "Bare bones CSV parsing with no_std support." }, + { + "name": "csv-core", + "version": "0.1.10", + "authors": "Andrew Gallant ", + "repository": "https://github.com/BurntSushi/rust-csv", + "license": "MIT OR Unlicense", + "license_file": null, + "description": "Bare bones CSV parsing with no_std support." + }, + { + "name": "dashmap", + "version": "5.5.3", + "authors": "Acrimon ", + "repository": "https://github.com/xacrimon/dashmap", + "license": "MIT", + "license_file": null, + "description": "Blazing fast concurrent HashMap for Rust." + }, { "name": "data-encoding", "version": "2.4.0", @@ -530,6 +701,15 @@ "license_file": null, "description": "Ranged integers" }, + { + "name": "derive-new", + "version": "0.5.9", + "authors": "Nick Cameron ", + "repository": "https://github.com/nrc/derive-new", + "license": "MIT", + "license_file": null, + "description": "`#[derive(new)]` implements simple constructor functions for structs and enums." + }, { "name": "difflib", "version": "0.4.0", @@ -665,6 +845,15 @@ "license_file": null, "description": "Fallible streaming iteration" }, + { + "name": "faster-hex", + "version": "0.8.0", + "authors": "zhangsoledad <787953403@qq.com>", + "repository": "https://github.com/NervosFoundation/faster-hex", + "license": "MIT", + "license_file": null, + "description": "Fast hex encoding." + }, { "name": "fastrand", "version": "1.9.0", @@ -782,6 +971,15 @@ "license_file": null, "description": "Parser for values from the Forwarded header (RFC 7239)" }, + { + "name": "fsrs-optimizer", + "version": "0.1.0", + "authors": null, + "repository": null, + "license": "BSD-3-Clause", + "license_file": null, + "description": null + }, { "name": "futf", "version": "0.1.5", @@ -935,6 +1133,51 @@ "license_file": null, "description": "A library for reading and writing the DWARF debugging format." }, + { + "name": "gix-features", + "version": "0.33.0", + "authors": "Sebastian Thiel ", + "repository": "https://github.com/Byron/gitoxide", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A crate to integrate various capabilities using compile-time feature flags" + }, + { + "name": "gix-fs", + "version": "0.5.0", + "authors": "Sebastian Thiel ", + "repository": "https://github.com/Byron/gitoxide", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A crate providing file system specific utilities to `gitoxide`" + }, + { + "name": "gix-hash", + "version": "0.12.0", + "authors": "Sebastian Thiel ", + "repository": "https://github.com/Byron/gitoxide", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Borrowed and owned git hash digests used to identify git objects" + }, + { + "name": "gix-tempfile", + "version": "8.0.0", + "authors": "Sebastian Thiel ", + "repository": "https://github.com/Byron/gitoxide", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A tempfile implementation with a global registry to assure cleanup" + }, + { + "name": "gix-trace", + "version": "0.1.3", + "authors": "Sebastian Thiel ", + "repository": "https://github.com/Byron/gitoxide", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A crate to provide minimal `tracing` support that can be turned off to zero cost" + }, { "name": "h2", "version": "0.3.21", @@ -944,6 +1187,15 @@ "license_file": null, "description": "An HTTP/2 client and server" }, + { + "name": "half", + "version": "2.3.1", + "authors": "Kathryn Long ", + "repository": "https://github.com/starkat99/half-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Half-precision floating point f16 and bf16 types for Rust implementing the IEEE 754-2008 standard binary16 and bfloat16 types." + }, { "name": "hashbrown", "version": "0.12.3", @@ -1286,6 +1538,15 @@ "license_file": null, "description": "Raw FFI bindings to platform libraries like libc." }, + { + "name": "libm", + "version": "0.2.7", + "authors": "Jorge Aparicio ", + "repository": "https://github.com/rust-lang/libm", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "libm in pure Rust" + }, { "name": "libsqlite3-sys", "version": "0.26.0", @@ -1376,6 +1637,15 @@ "license_file": null, "description": "A blazing fast URL router." }, + { + "name": "matrixmultiply", + "version": "0.3.7", + "authors": "bluss|R. Janis Goldschmidt", + "repository": "https://github.com/bluss/matrixmultiply/", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "General matrix multiplication for f32 and f64 matrices. Operates on matrices with general layout (they can use arbitrary row and column stride). Detects and uses AVX or SSE2 on x86 platforms transparently for higher performance. Uses a microkernel strategy, so that the implementation is easy to parallelize and optimize. Supports multithreading." + }, { "name": "memchr", "version": "2.5.0", @@ -1385,6 +1655,15 @@ "license_file": null, "description": "Safe interface to memchr." }, + { + "name": "memoffset", + "version": "0.9.0", + "authors": "Gilad Naaman ", + "repository": "https://github.com/Gilnaa/memoffset", + "license": "MIT", + "license_file": null, + "description": "offset_of functionality for Rust structs." + }, { "name": "mime", "version": "0.3.17", @@ -1457,6 +1736,24 @@ "license_file": null, "description": "A wrapper over a platform's native TLS implementation" }, + { + "name": "ndarray", + "version": "0.15.6", + "authors": "Ulrik Sverdrup \"bluss\"|Jim Turner", + "repository": "https://github.com/rust-ndarray/ndarray", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "An n-dimensional array for general elements and for numerics. Lightweight array views and slicing; views support chunking and splitting." + }, + { + "name": "ndarray-rand", + "version": "0.14.0", + "authors": "bluss", + "repository": "https://github.com/rust-ndarray/ndarray", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Constructors for randomized arrays. `rand` integration for `ndarray`." + }, { "name": "new_debug_unreachable", "version": "1.0.4", @@ -1493,6 +1790,15 @@ "license_file": null, "description": "Library for ANSI terminal colors and styles (bold, underline)" }, + { + "name": "num-complex", + "version": "0.4.4", + "authors": "The Rust Project Developers", + "repository": "https://github.com/rust-num/num-complex", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Complex numbers implementation for Rust" + }, { "name": "num-format", "version": "0.4.4", @@ -1502,6 +1808,15 @@ "license_file": null, "description": "A Rust crate for producing string-representations of numbers, formatted according to international standards" }, + { + "name": "num-integer", + "version": "0.1.45", + "authors": "The Rust Project Developers", + "repository": "https://github.com/rust-num/num-integer", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Integer traits and functions" + }, { "name": "num-traits", "version": "0.2.16", @@ -1637,6 +1952,15 @@ "license_file": null, "description": "An advanced API for creating custom synchronization primitives." }, + { + "name": "paste", + "version": "1.0.14", + "authors": "David Tolnay ", + "repository": "https://github.com/dtolnay/paste", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Macros for all your token pasting needs" + }, { "name": "percent-encoding", "version": "2.3.0", @@ -1961,6 +2285,15 @@ "license_file": null, "description": "Core random number generator traits and tools for implementation." }, + { + "name": "rand_distr", + "version": "0.4.3", + "authors": "The Rand Project Developers", + "repository": "https://github.com/rust-random/rand", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Sampling from random number distributions" + }, { "name": "rand_hc", "version": "0.2.0", @@ -1970,6 +2303,33 @@ "license_file": null, "description": "HC128 random number generator" }, + { + "name": "rawpointer", + "version": "0.2.1", + "authors": "bluss", + "repository": "https://github.com/bluss/rawpointer/", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Extra methods for raw pointers and `NonNull`. For example `.post_inc()` and `.pre_dec()` (c.f. `ptr++` and `--ptr`), `offset` and `add` for `NonNull`, and the function `ptrdistance`." + }, + { + "name": "rayon", + "version": "1.7.0", + "authors": "Niko Matsakis |Josh Stone ", + "repository": "https://github.com/rayon-rs/rayon", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Simple work-stealing parallelism for Rust" + }, + { + "name": "rayon-core", + "version": "1.11.0", + "authors": "Niko Matsakis |Josh Stone ", + "repository": "https://github.com/rayon-rs/rayon", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Core APIs for Rayon" + }, { "name": "redox_syscall", "version": "0.2.16", @@ -2069,6 +2429,24 @@ "license_file": "LICENSE", "description": "Safe, fast, small crypto using Rust." }, + { + "name": "rmp", + "version": "0.8.12", + "authors": "Evgeny Safronov ", + "repository": "https://github.com/3Hren/msgpack-rust", + "license": "MIT", + "license_file": null, + "description": "Pure Rust MessagePack serialization implementation" + }, + { + "name": "rmp-serde", + "version": "1.1.2", + "authors": "Evgeny Safronov ", + "repository": "https://github.com/3Hren/msgpack-rust", + "license": "MIT", + "license_file": null, + "description": "Serde bindings for RMP" + }, { "name": "rusqlite", "version": "0.29.0", @@ -2168,6 +2546,15 @@ "license_file": null, "description": "A simple crate for determining whether two file paths point to the same file." }, + { + "name": "sanitize-filename", + "version": "0.5.0", + "authors": "Jacob Brown ", + "repository": "https://github.com/kardeiz/sanitize-filename", + "license": "MIT", + "license_file": null, + "description": "A simple filename sanitizer, based on Node's sanitize-filename" + }, { "name": "schannel", "version": "0.1.22", @@ -2224,7 +2611,7 @@ }, { "name": "serde", - "version": "1.0.185", + "version": "1.0.188", "authors": "Erick Tryzelaar |David Tolnay ", "repository": "https://github.com/serde-rs/serde", "license": "Apache-2.0 OR MIT", @@ -2242,7 +2629,7 @@ }, { "name": "serde_derive", - "version": "1.0.185", + "version": "1.0.188", "authors": "Erick Tryzelaar |David Tolnay ", "repository": "https://github.com/serde-rs/serde", "license": "Apache-2.0 OR MIT", @@ -2330,6 +2717,15 @@ "license_file": null, "description": "A lock-free concurrent slab." }, + { + "name": "signal-hook", + "version": "0.3.17", + "authors": "Michal 'vorner' Vaner |Thomas Himmelstoss ", + "repository": "https://github.com/vorner/signal-hook", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Unix signal handling" + }, { "name": "signal-hook-registry", "version": "1.4.1", @@ -2447,6 +2843,15 @@ "license_file": null, "description": "A codegen library for string-cache, developed as part of the Servo project." }, + { + "name": "strum", + "version": "0.24.1", + "authors": "Peter Glotfelty ", + "repository": "https://github.com/Peternator7/strum", + "license": "MIT", + "license_file": null, + "description": "Helpful macros for working with enums and strings" + }, { "name": "strum", "version": "0.25.0", @@ -2456,6 +2861,15 @@ "license_file": null, "description": "Helpful macros for working with enums and strings" }, + { + "name": "strum_macros", + "version": "0.24.3", + "authors": "Peter Glotfelty ", + "repository": "https://github.com/Peternator7/strum", + "license": "MIT", + "license_file": null, + "description": "Helpful macros for working with enums and strings" + }, { "name": "strum_macros", "version": "0.25.2", @@ -2537,6 +2951,15 @@ "license_file": null, "description": "Implementation detail of the `thiserror` crate" }, + { + "name": "thread-tree", + "version": "0.3.3", + "authors": "bluss <>", + "repository": "https://github.com/bluss/thread-tree", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A tree-structured thread pool for splitting jobs hierarchically on worker threads. The tree structure means that there is no contention between workers when delivering jobs." + }, { "name": "thread_local", "version": "1.1.7", @@ -2573,6 +2996,15 @@ "license_file": null, "description": "Procedural macros for the time crate. This crate is an implementation detail and should not be relied upon directly." }, + { + "name": "tiny-keccak", + "version": "2.0.2", + "authors": "debris ", + "repository": null, + "license": "CC0-1.0", + "license_file": null, + "description": "An implementation of Keccak derived functions." + }, { "name": "tinystr", "version": "0.7.1", @@ -2960,6 +3392,15 @@ "license_file": null, "description": "A missing utime function for Rust." }, + { + "name": "uuid", + "version": "1.4.1", + "authors": "Ashley Mannix|Christopher Armstrong|Dylan DPC|Hunar Roop Kahlon", + "repository": "https://github.com/uuid-rs/uuid", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A library to generate and parse UUIDs." + }, { "name": "valuable", "version": "0.1.0", diff --git a/proto/anki/collection.proto b/proto/anki/collection.proto index ec97f83c2..b0928acc1 100644 --- a/proto/anki/collection.proto +++ b/proto/anki/collection.proto @@ -126,6 +126,17 @@ message Progress { uint32 stage_current = 3; } + message ComputeWeights { + uint32 current = 1; + uint32 total = 2; + uint32 revlog_entries = 3; + } + + message ComputeRetention { + uint32 current = 1; + uint32 total = 2; + } + oneof value { generic.Empty none = 1; MediaSync media_sync = 2; @@ -135,6 +146,8 @@ message Progress { DatabaseCheck database_check = 6; string importing = 7; string exporting = 8; + ComputeWeights compute_weights = 9; + ComputeRetention compute_retention = 10; } } diff --git a/proto/anki/deck_config.proto b/proto/anki/deck_config.proto index d1ca4a0be..10b081aed 100644 --- a/proto/anki/deck_config.proto +++ b/proto/anki/deck_config.proto @@ -92,7 +92,9 @@ message DeckConfig { repeated float learn_steps = 1; repeated float relearn_steps = 2; - reserved 3 to 8; + repeated float fsrs_weights = 3; + + reserved 4 to 8; uint32 new_per_day = 9; uint32 reviews_per_day = 10; @@ -133,6 +135,9 @@ message DeckConfig { bool bury_reviews = 28; bool bury_interday_learning = 29; + bool fsrs_enabled = 36; + float desired_retention = 37; // for fsrs + bytes other = 255; } diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 10580aacd..e279d2dd1 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -45,6 +45,11 @@ service SchedulerService { rpc CustomStudyDefaults(CustomStudyDefaultsRequest) returns (CustomStudyDefaultsResponse); rpc RepositionDefaults(generic.Empty) returns (RepositionDefaultsResponse); + rpc ComputeFsrsWeights(ComputeFsrsWeightsRequest) + returns (ComputeFsrsWeightsResponse); + rpc ComputeOptimalRetention(ComputeOptimalRetentionRequest) + returns (ComputeOptimalRetentionResponse); + rpc EvaluateWeights(EvaluateWeightsRequest) returns (EvaluateWeightsResponse); } // Implicitly includes any of the above methods that are not listed in the @@ -317,3 +322,37 @@ message RepositionDefaultsResponse { bool random = 1; bool shift = 2; } + +message ComputeFsrsWeightsRequest { + /// The search used to gather cards for training + string search = 1; +} + +message ComputeFsrsWeightsResponse { + repeated float weights = 1; +} + +message ComputeOptimalRetentionRequest { + repeated float weights = 1; + uint32 deck_size = 2; + uint32 days_to_simulate = 3; + uint32 max_seconds_of_study_per_day = 4; + uint32 max_interval = 5; + uint32 recall_secs = 6; + uint32 forget_secs = 7; + uint32 learn_secs = 8; +} + +message ComputeOptimalRetentionResponse { + float optimal_retention = 1; +} + +message EvaluateWeightsRequest { + repeated float weights = 1; + string search = 2; +} + +message EvaluateWeightsResponse { + float log_loss = 1; + float rmse = 2; +} diff --git a/qt/aqt/deckoptions.py b/qt/aqt/deckoptions.py index 2e8d2d4a4..fb4428f9e 100644 --- a/qt/aqt/deckoptions.py +++ b/qt/aqt/deckoptions.py @@ -60,6 +60,7 @@ class DeckOptionsDialog(QDialog): gui_hooks.deck_options_did_load(self) def reject(self) -> None: + self.mw.col.set_wants_abort() self.web.cleanup() self.web = None saveGeom(self, self.TITLE) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index c207773da..b169023ec 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -535,6 +535,11 @@ exposed_backend_list = [ "add_image_occlusion_note", "get_image_occlusion_note", "update_image_occlusion_note", + # SchedulerService + "compute_fsrs_weights", + "compute_optimal_retention", + "set_wants_abort", + "evaluate_weights", ] diff --git a/rslib/Cargo.toml b/rslib/Cargo.toml index 03e8fbe71..8e90fd980 100644 --- a/rslib/Cargo.toml +++ b/rslib/Cargo.toml @@ -60,6 +60,7 @@ flate2.workspace = true fluent.workspace = true fluent-bundle.workspace = true fnv.workspace = true +fsrs-optimizer.workspace = true futures.workspace = true hex.workspace = true htmlescape.workspace = true diff --git a/rslib/src/deckconfig/mod.rs b/rslib/src/deckconfig/mod.rs index d1515d61b..de099652d 100644 --- a/rslib/src/deckconfig/mod.rs +++ b/rslib/src/deckconfig/mod.rs @@ -66,6 +66,9 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner { bury_new: false, bury_reviews: false, bury_interday_learning: false, + fsrs_enabled: false, + fsrs_weights: vec![], + desired_retention: 0.9, other: Vec::new(), }; diff --git a/rslib/src/deckconfig/schema11.rs b/rslib/src/deckconfig/schema11.rs index d9b5f876f..d8185ce10 100644 --- a/rslib/src/deckconfig/schema11.rs +++ b/rslib/src/deckconfig/schema11.rs @@ -65,6 +65,13 @@ pub struct DeckConfSchema11 { #[serde(default)] bury_interday_learning: bool, + #[serde(default)] + fsrs_weights: Vec, + #[serde(default)] + fsrs_enabled: bool, + #[serde(default)] + desired_retention: f32, + #[serde(flatten)] other: HashMap, } @@ -250,6 +257,9 @@ impl Default for DeckConfSchema11 { new_sort_order: 0, new_gather_priority: 0, bury_interday_learning: false, + fsrs_weights: vec![], + fsrs_enabled: false, + desired_retention: 0.9, } } } @@ -318,6 +328,9 @@ impl From for DeckConfig { bury_new: c.new.bury, bury_reviews: c.rev.bury, bury_interday_learning: c.bury_interday_learning, + fsrs_weights: c.fsrs_weights, + fsrs_enabled: c.fsrs_enabled, + desired_retention: c.desired_retention, other: other_bytes, }, } @@ -409,6 +422,9 @@ impl From for DeckConfSchema11 { new_sort_order: i.new_card_sort_order, new_gather_priority: i.new_card_gather_priority, bury_interday_learning: i.bury_interday_learning, + fsrs_weights: i.fsrs_weights, + fsrs_enabled: i.fsrs_enabled, + desired_retention: i.desired_retention, } } } @@ -429,7 +445,10 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! { "timer", "name", "interdayLearningMix", - "newGatherPriority" + "newGatherPriority", + "fsrsWeights", + "desiredRetention", + "fsrsEnabled", }; static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! { diff --git a/rslib/src/deckconfig/service.rs b/rslib/src/deckconfig/service.rs index 451187afc..8d49f3506 100644 --- a/rslib/src/deckconfig/service.rs +++ b/rslib/src/deckconfig/service.rs @@ -7,13 +7,13 @@ use crate::deckconfig::DeckConfSchema11; use crate::deckconfig::DeckConfig; use crate::deckconfig::DeckConfigId; use crate::deckconfig::UpdateDeckConfigsRequest; -use crate::error; +use crate::error::Result; impl crate::services::DeckConfigService for Collection { fn add_or_update_deck_config_legacy( &mut self, input: generic::Json, - ) -> error::Result { + ) -> Result { let conf: DeckConfSchema11 = serde_json::from_slice(&input.json)?; let mut conf: DeckConfig = conf.into(); @@ -24,7 +24,7 @@ impl crate::services::DeckConfigService for Collection { .map(Into::into) } - fn all_deck_config_legacy(&mut self) -> error::Result { + fn all_deck_config_legacy(&mut self) -> Result { let conf: Vec = self .storage .all_deck_config()? @@ -39,7 +39,7 @@ impl crate::services::DeckConfigService for Collection { fn get_deck_config( &mut self, input: anki_proto::deck_config::DeckConfigId, - ) -> error::Result { + ) -> Result { Ok(Collection::get_deck_config(self, input.into(), true)? .unwrap() .into()) @@ -48,22 +48,19 @@ impl crate::services::DeckConfigService for Collection { fn get_deck_config_legacy( &mut self, input: anki_proto::deck_config::DeckConfigId, - ) -> error::Result { + ) -> Result { let conf = Collection::get_deck_config(self, input.into(), true)?.unwrap(); let conf: DeckConfSchema11 = conf.into(); Ok(serde_json::to_vec(&conf)?).map(Into::into) } - fn new_deck_config_legacy(&mut self) -> error::Result { + fn new_deck_config_legacy(&mut self) -> Result { serde_json::to_vec(&DeckConfSchema11::default()) .map_err(Into::into) .map(Into::into) } - fn remove_deck_config( - &mut self, - input: anki_proto::deck_config::DeckConfigId, - ) -> error::Result<()> { + fn remove_deck_config(&mut self, input: anki_proto::deck_config::DeckConfigId) -> Result<()> { self.transact_no_undo(|col| col.remove_deck_config_inner(input.into())) .map(Into::into) } @@ -71,14 +68,14 @@ impl crate::services::DeckConfigService for Collection { fn get_deck_configs_for_update( &mut self, input: anki_proto::decks::DeckId, - ) -> error::Result { + ) -> Result { self.get_deck_configs_for_update(input.did.into()) } fn update_deck_configs( &mut self, input: anki_proto::deck_config::UpdateDeckConfigsRequest, - ) -> error::Result { + ) -> Result { self.update_deck_configs(input.into()).map(Into::into) } } diff --git a/rslib/src/progress.rs b/rslib/src/progress.rs index 3224bcd8b..25e115d29 100644 --- a/rslib/src/progress.rs +++ b/rslib/src/progress.rs @@ -6,6 +6,8 @@ use std::sync::Arc; use std::sync::Mutex; use anki_i18n::I18n; +use anki_proto::collection::progress::ComputeRetention; +use anki_proto::collection::progress::ComputeWeights; use futures::future::AbortHandle; use crate::dbcheck::DatabaseCheckProgress; @@ -14,6 +16,8 @@ use crate::error::Result; use crate::import_export::ExportProgress; use crate::import_export::ImportProgress; use crate::prelude::Collection; +use crate::scheduler::fsrs::retention::ComputeRetentionProgress; +use crate::scheduler::fsrs::weights::ComputeWeightsProgress; use crate::sync::collection::normal::NormalSyncProgress; use crate::sync::collection::progress::FullSyncProgress; use crate::sync::collection::progress::SyncStage; @@ -131,6 +135,8 @@ pub enum Progress { DatabaseCheck(DatabaseCheckProgress), Import(ImportProgress), Export(ExportProgress), + ComputeWeights(ComputeWeightsProgress), + ComputeRetention(ComputeRetentionProgress), } pub(crate) fn progress_to_proto( @@ -216,6 +222,19 @@ pub(crate) fn progress_to_proto( } .into(), ), + Progress::ComputeWeights(progress) => { + anki_proto::collection::progress::Value::ComputeWeights(ComputeWeights { + current: progress.current, + total: progress.total, + revlog_entries: progress.revlog_entries, + }) + } + Progress::ComputeRetention(progress) => { + anki_proto::collection::progress::Value::ComputeRetention(ComputeRetention { + current: progress.current, + total: progress.total, + }) + } } } else { anki_proto::collection::progress::Value::None(anki_proto::generic::Empty {}) @@ -282,6 +301,18 @@ impl From for Progress { } } +impl From for Progress { + fn from(p: ComputeWeightsProgress) -> Self { + Progress::ComputeWeights(p) + } +} + +impl From for Progress { + fn from(p: ComputeRetentionProgress) -> Self { + Progress::ComputeRetention(p) + } +} + impl Collection { pub fn new_progress_handler + Default + Clone>( &self, diff --git a/rslib/src/scheduler/fsrs/error.rs b/rslib/src/scheduler/fsrs/error.rs new file mode 100644 index 000000000..e21ab902a --- /dev/null +++ b/rslib/src/scheduler/fsrs/error.rs @@ -0,0 +1,20 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use fsrs_optimizer::FSRSError; + +use crate::error::AnkiError; +use crate::error::InvalidInputError; + +impl From for AnkiError { + fn from(err: FSRSError) -> Self { + match err { + FSRSError::NotEnoughData => InvalidInputError { + message: "Not enough data available".to_string(), + source: None, + backtrace: None, + } + .into(), + FSRSError::Interrupted => AnkiError::Interrupted, + } + } +} diff --git a/rslib/src/scheduler/fsrs/mod.rs b/rslib/src/scheduler/fsrs/mod.rs new file mode 100644 index 000000000..436c7c5a4 --- /dev/null +++ b/rslib/src/scheduler/fsrs/mod.rs @@ -0,0 +1,5 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +mod error; +pub mod retention; +pub mod weights; diff --git a/rslib/src/scheduler/fsrs/retention.rs b/rslib/src/scheduler/fsrs/retention.rs new file mode 100644 index 000000000..d46337ded --- /dev/null +++ b/rslib/src/scheduler/fsrs/retention.rs @@ -0,0 +1,50 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use anki_proto::scheduler::ComputeOptimalRetentionRequest; +use fsrs_optimizer::find_optimal_retention; +use fsrs_optimizer::SimulatorConfig; +use itertools::Itertools; + +use crate::prelude::*; + +#[derive(Default, Clone, Copy, Debug)] +pub struct ComputeRetentionProgress { + pub current: u32, + pub total: u32, +} + +impl Collection { + pub fn compute_optimal_retention( + &mut self, + req: ComputeOptimalRetentionRequest, + ) -> Result { + let mut anki_progress = self.new_progress_handler::(); + if req.weights.len() != 17 { + invalid_input!("must have 17 weights"); + } + let mut weights = [0f64; 17]; + weights + .iter_mut() + .set_from(req.weights.into_iter().map(|v| v as f64)); + Ok(find_optimal_retention( + &SimulatorConfig { + w: weights, + deck_size: req.deck_size as usize, + learn_span: req.days_to_simulate as usize, + max_cost_perday: req.max_seconds_of_study_per_day as f64, + max_ivl: req.max_interval as f64, + recall_cost: req.recall_secs as f64, + forget_cost: req.forget_secs as f64, + learn_cost: req.learn_secs as f64, + }, + |ip| { + anki_progress + .update(false, |p| { + p.total = ip.total as u32; + p.current = ip.current as u32; + }) + .is_ok() + }, + )? as f32) + } +} diff --git a/rslib/src/scheduler/fsrs/weights.rs b/rslib/src/scheduler/fsrs/weights.rs new file mode 100644 index 000000000..b2b87b96d --- /dev/null +++ b/rslib/src/scheduler/fsrs/weights.rs @@ -0,0 +1,285 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use std::iter; +use std::thread; +use std::time::Duration; + +use fsrs_optimizer::compute_weights; +use fsrs_optimizer::evaluate; +use fsrs_optimizer::FSRSItem; +use fsrs_optimizer::FSRSReview; +use fsrs_optimizer::ProgressState; +use itertools::Itertools; + +use crate::prelude::*; +use crate::revlog::RevlogEntry; +use crate::revlog::RevlogReviewKind; +use crate::search::SortMode; + +impl Collection { + pub fn compute_weights(&mut self, search: &str) -> Result> { + let timing = self.timing_today()?; + let mut anki_progress = self.new_progress_handler::(); + let guard = self.search_cards_into_table(search, SortMode::NoOrder)?; + let revlogs = guard + .col + .storage + .get_revlog_entries_for_searched_cards_in_order()?; + anki_progress.state.revlog_entries = revlogs.len() as u32; + let items = anki_to_fsrs(revlogs, timing.next_day_at); + // adapt the progress handler to our built-in progress handling + let progress = ProgressState::new_shared(); + let progress2 = progress.clone(); + thread::spawn(move || { + let mut finished = false; + while !finished { + thread::sleep(Duration::from_millis(100)); + let mut guard = progress.lock().unwrap(); + if let Err(_err) = anki_progress.update(false, |s| { + s.total = guard.total() as u32; + s.current = guard.current() as u32; + finished = s.total > 0 && s.total == s.current; + }) { + guard.want_abort = true; + return; + } + } + }); + compute_weights(items, Some(progress2)).map_err(Into::into) + } + + pub fn evaluate_weights(&mut self, weights: &[f32], search: &str) -> Result<(f32, f32)> { + let timing = self.timing_today()?; + if weights.len() != 17 { + invalid_input!("must have 17 weights"); + } + let mut weights_arr = [0f32; 17]; + weights_arr.iter_mut().set_from(weights.iter().cloned()); + let mut anki_progress = self.new_progress_handler::(); + let guard = self.search_cards_into_table(search, SortMode::NoOrder)?; + let revlogs = guard + .col + .storage + .get_revlog_entries_for_searched_cards_in_order()?; + anki_progress.state.revlog_entries = revlogs.len() as u32; + let items = anki_to_fsrs(revlogs, timing.next_day_at); + + Ok(evaluate(weights_arr, items, |ip| { + anki_progress + .update(false, |p| { + p.total = ip.total as u32; + p.current = ip.current as u32; + }) + .is_ok() + })?) + } +} + +#[derive(Default, Clone, Copy, Debug)] +pub struct ComputeWeightsProgress { + pub current: u32, + pub total: u32, + pub revlog_entries: u32, +} + +/// Convert a series of revlog entries sorted by card id into FSRS items. +fn anki_to_fsrs(revlogs: Vec, next_day_at: TimestampSecs) -> Vec { + let mut revlogs = revlogs + .into_iter() + .group_by(|r| r.cid) + .into_iter() + .filter_map(|(_cid, entries)| single_card_revlog_to_items(entries.collect(), next_day_at)) + .flatten() + .collect_vec(); + revlogs.sort_by_cached_key(|r| r.reviews.len()); + revlogs +} + +fn single_card_revlog_to_items( + mut entries: Vec, + next_day_at: TimestampSecs, +) -> Option> { + // Find the index of the first learn entry in the last continuous group + let mut index_to_keep = 0; + let mut i = entries.len(); + + while i > 0 { + i -= 1; + if entries[i].review_kind == RevlogReviewKind::Learning { + index_to_keep = i; + } else if index_to_keep != 0 { + // Found a continuous group + break; + } + } + + // Remove all entries before this one + entries.drain(..index_to_keep); + + // we ignore cards that don't start in the learning state + if let Some(entry) = entries.first() { + if entry.review_kind != RevlogReviewKind::Learning { + return None; + } + } else { + // no revlog entries + return None; + } + + // Keep only the first review when multiple reviews done on one day + let mut unique_dates = std::collections::HashSet::new(); + entries.retain(|entry| unique_dates.insert(entry.days_elapsed(next_day_at))); + + // Old versions of Anki did not record Manual entries in the review log when + // cards were manually rescheduled. So we look for times when the card has + // gone from Review to Learning, indicating it has been reset, and remove + // entries after. + for (i, (a, b)) in entries.iter().tuple_windows().enumerate() { + if let ( + RevlogReviewKind::Review | RevlogReviewKind::Relearning, + RevlogReviewKind::Learning, + ) = (a.review_kind, b.review_kind) + { + // Remove entry and all following + entries.truncate(i + 1); + break; + } + } + + // Compute delta_t for each entry + let delta_ts = iter::once(0) + .chain(entries.iter().tuple_windows().map(|(previous, current)| { + previous.days_elapsed(next_day_at) - current.days_elapsed(next_day_at) + })) + .collect_vec(); + + // Skip the first learning step, then convert the remaining entries into + // separate FSRSItems, where each item contains all reviews done until then. + Some( + entries + .iter() + .enumerate() + .skip(1) + .map(|(outer_idx, _)| { + let reviews = entries + .iter() + .take(outer_idx + 1) + .enumerate() + .map(|(inner_idx, r)| FSRSReview { + rating: r.button_chosen as i32, + delta_t: delta_ts[inner_idx] as i32, + }) + .collect(); + FSRSItem { reviews } + }) + .collect(), + ) +} + +impl RevlogEntry { + fn days_elapsed(&self, next_day_at: TimestampSecs) -> u32 { + (next_day_at.elapsed_secs_since(self.id.as_secs()) / 86_400).max(0) as u32 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const NEXT_DAY_AT: TimestampSecs = TimestampSecs(86400 * 100); + + fn revlog(review_kind: RevlogReviewKind, days_ago: i64) -> RevlogEntry { + RevlogEntry { + review_kind, + id: ((NEXT_DAY_AT.0 - days_ago * 86400) * 1000).into(), + ..Default::default() + } + } + + #[test] + fn delta_t_is_correct() -> Result<()> { + assert_eq!( + single_card_revlog_to_items( + vec![ + revlog(RevlogReviewKind::Learning, 1), + revlog(RevlogReviewKind::Review, 0) + ], + NEXT_DAY_AT + ), + Some(vec![FSRSItem { + reviews: vec![ + FSRSReview { + rating: 0, + delta_t: 0 + }, + FSRSReview { + rating: 0, + delta_t: 1 + } + ] + }]) + ); + assert_eq!( + single_card_revlog_to_items( + vec![ + revlog(RevlogReviewKind::Learning, 15), + revlog(RevlogReviewKind::Learning, 13), + revlog(RevlogReviewKind::Review, 10), + revlog(RevlogReviewKind::Review, 5) + ], + NEXT_DAY_AT, + ), + Some(vec![ + FSRSItem { + reviews: vec![ + FSRSReview { + rating: 0, + delta_t: 0 + }, + FSRSReview { + rating: 0, + delta_t: 2 + } + ] + }, + FSRSItem { + reviews: vec![ + FSRSReview { + rating: 0, + delta_t: 0 + }, + FSRSReview { + rating: 0, + delta_t: 2 + }, + FSRSReview { + rating: 0, + delta_t: 3 + } + ] + }, + FSRSItem { + reviews: vec![ + FSRSReview { + rating: 0, + delta_t: 0 + }, + FSRSReview { + rating: 0, + delta_t: 2 + }, + FSRSReview { + rating: 0, + delta_t: 3 + }, + FSRSReview { + rating: 0, + delta_t: 5 + } + ] + } + ]) + ); + Ok(()) + } +} diff --git a/rslib/src/scheduler/mod.rs b/rslib/src/scheduler/mod.rs index e24053c8d..0e70a89a2 100644 --- a/rslib/src/scheduler/mod.rs +++ b/rslib/src/scheduler/mod.rs @@ -10,6 +10,7 @@ pub mod answering; pub mod bury_and_suspend; pub(crate) mod congrats; pub(crate) mod filtered; +pub mod fsrs; mod learning; pub mod new; pub(crate) mod queue; diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 9e862c34d..c0fb04246 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -6,6 +6,8 @@ mod states; use anki_proto::generic; use anki_proto::scheduler; +use anki_proto::scheduler::ComputeOptimalRetentionRequest; +use anki_proto::scheduler::ComputeOptimalRetentionResponse; use crate::prelude::*; use crate::scheduler::new::NewCardDueOrder; @@ -237,4 +239,33 @@ impl crate::services::SchedulerService for Collection { ) -> Result { self.custom_study_defaults(input.deck_id.into()) } + + fn compute_fsrs_weights( + &mut self, + input: scheduler::ComputeFsrsWeightsRequest, + ) -> Result { + Ok(scheduler::ComputeFsrsWeightsResponse { + weights: self.compute_weights(&input.search)?, + }) + } + + fn compute_optimal_retention( + &mut self, + input: ComputeOptimalRetentionRequest, + ) -> Result { + Ok(ComputeOptimalRetentionResponse { + optimal_retention: self.compute_optimal_retention(input)?, + }) + } + + fn evaluate_weights( + &mut self, + input: scheduler::EvaluateWeightsRequest, + ) -> Result { + let ret = self.evaluate_weights(&input.weights, &input.search)?; + Ok(scheduler::EvaluateWeightsResponse { + log_loss: ret.0, + rmse: ret.1, + }) + } } diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 2ea62e62a..4a411c05e 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -93,6 +93,7 @@ pub enum SearchNode { NoCombining(String), WordBoundary(String), CustomData(String), + Preset(String), } #[derive(Debug, PartialEq, Clone)] diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index f20fc89b1..d13374fa7 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -181,6 +181,7 @@ impl SqlWriter<'_> { SearchNode::Property { operator, kind } => self.write_prop(operator, kind)?, SearchNode::CustomData(key) => self.write_custom_data(key)?, SearchNode::WholeCollection => write!(self.sql, "true").unwrap(), + SearchNode::Preset(name) => self.write_deck_preset(name)?, }; Ok(()) } @@ -824,6 +825,25 @@ impl SqlWriter<'_> { self.col.get_config_bool(BoolKey::IgnoreAccentsInSearch), ) } + fn write_deck_preset(&mut self, name: &str) -> Result<()> { + let dcid = self.col.storage.get_deck_config_id_by_name(name)?; + let mut str_ids = String::new(); + let deck_ids = self + .col + .storage + .get_all_decks()? + .into_iter() + .filter_map(|d| { + if d.config_id() == dcid { + Some(d.id) + } else { + None + } + }); + ids_to_string(&mut str_ids, deck_ids); + write!(self.sql, "c.did in {str_ids}").unwrap(); + Ok(()) + } } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -958,6 +978,7 @@ impl SearchNode { SearchNode::WholeCollection => RequiredTable::CardsOrNotes, SearchNode::CardTemplate(_) => RequiredTable::CardsAndNotes, + SearchNode::Preset(_) => RequiredTable::Cards, } } } diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index 3937c6a38..37813e571 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -87,6 +87,7 @@ fn write_search_node(node: &SearchNode) -> String { NoCombining(s) => maybe_quote(&format!("nc:{}", s)), WordBoundary(s) => maybe_quote(&format!("w:{}", s)), CustomData(k) => maybe_quote(&format!("has-cd:{}", k)), + Preset(s) => maybe_quote(&format!("preset:{}", s)), } } diff --git a/rslib/src/storage/deckconfig/mod.rs b/rslib/src/storage/deckconfig/mod.rs index 1c95234d6..896b0fa13 100644 --- a/rslib/src/storage/deckconfig/mod.rs +++ b/rslib/src/storage/deckconfig/mod.rs @@ -62,6 +62,15 @@ impl SqliteStorage { .transpose() } + pub(crate) fn get_deck_config_id_by_name(&self, name: &str) -> Result> { + self.db + .prepare_cached("select id from deck_config where WHERE name = ?")? + .query_and_then([name], |row| Ok::<_, AnkiError>(DeckConfigId(row.get(0)?)))? + .next() + .transpose() + .map_err(Into::into) + } + pub(crate) fn add_deck_conf(&self, conf: &mut DeckConfig) -> Result<()> { let mut conf_bytes = vec![]; conf.inner.encode(&mut conf_bytes)?; diff --git a/rslib/src/storage/revlog/mod.rs b/rslib/src/storage/revlog/mod.rs index f9d38eb40..5c6d62d6d 100644 --- a/rslib/src/storage/revlog/mod.rs +++ b/rslib/src/storage/revlog/mod.rs @@ -132,6 +132,18 @@ impl SqliteStorage { .collect() } + pub(crate) fn get_revlog_entries_for_searched_cards_in_order( + &self, + ) -> Result> { + self.db + .prepare_cached(concat!( + include_str!("get.sql"), + " where cid in (select cid from search_cids) order by cid" + ))? + .query_and_then([], row_to_revlog_entry)? + .collect() + } + pub(crate) fn get_all_revlog_entries(&self, after: TimestampSecs) -> Result> { self.db .prepare_cached(concat!(include_str!("get.sql"), " where id >= ?"))? diff --git a/ts/deck-options/AdvancedOptions.svelte b/ts/deck-options/AdvancedOptions.svelte index 8cfc99539..7135b4e99 100644 --- a/ts/deck-options/AdvancedOptions.svelte +++ b/ts/deck-options/AdvancedOptions.svelte @@ -17,6 +17,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import SettingTitle from "./SettingTitle.svelte"; import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte"; import SpinBoxRow from "./SpinBoxRow.svelte"; + import SwitchRow from "./SwitchRow.svelte"; import type { DeckOption } from "./types"; export let state: DeckOptionsState; @@ -183,6 +184,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + {#if state.v3Scheduler} + + + FSRS optimizer + + + {/if} + {#if state.v3Scheduler} > { return state.currentAuxData; @@ -112,11 +114,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - - - - - + {#if $addons.length} + + + + + + {/if} + + {#if state.v3Scheduler && $config.fsrsEnabled} + + + + + + {/if} diff --git a/ts/deck-options/FsrsOptions.svelte b/ts/deck-options/FsrsOptions.svelte new file mode 100644 index 000000000..12cdfb011 --- /dev/null +++ b/ts/deck-options/FsrsOptions.svelte @@ -0,0 +1,289 @@ + + + + + + Weights + +
Optimal retention
+ + + + + + +
+ +
+ Optimize weights +
+ + + +
{computeWeightsProgressString}
+
+ +
+ Calculate optimal retention +
+ + Deck size: +
+ +
+ + Days to simulate +
+ +
+ + Max seconds of study per day: +
+ +
+ + Maximum interval: +
+ +
+ + Seconds to recall a card: +
+ +
+ + Seconds to forget a card: +
+ +
+ + Seconds to learn a card: +
+ +
+ + +
{computeRetentionProgressString}
+
+ + + diff --git a/ts/deck-options/WeightsInput.svelte b/ts/deck-options/WeightsInput.svelte new file mode 100644 index 000000000..229f6cece --- /dev/null +++ b/ts/deck-options/WeightsInput.svelte @@ -0,0 +1,16 @@ + + + +