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
This commit is contained in:
Damien Elmes 2023-09-05 18:45:05 +10:00 committed by GitHub
parent fe8cf5ae7d
commit 0c6e3eaa93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1871 additions and 36 deletions

View File

@ -30,6 +30,8 @@ allow = [
"BSD-3-Clause",
"OpenSSL",
"CC0-1.0",
"Unlicense",
"Zlib",
]
confidence-threshold = 0.8
# eg { allow = ["Zlib"], name = "adler32", version = "*" },

515
Cargo.lock generated
View File

@ -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"

View File

@ -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" }

View File

@ -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 <ty@pre-alpha.com>|Zoey Riordan <zoey@dos.cafe>|Victor Koenders <bincode@trangar.com>",
"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 <nathaniel.simard.42@gmail.com>",
"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 <nathaniel.simard.42@gmail.com>",
"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 <nathaniel.simard.42@gmail.com>",
"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 <nathaniel.simard.42@gmail.com>",
"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 <nathaniel.simard.42@gmail.com>",
"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 <nathaniel.simard.42@gmail.com>",
"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 <nathaniel.simard.42@gmail.com>",
"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 <nathaniel.simard.42@gmail.com>",
"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 <nathaniel.simard.42@gmail.com>",
"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 <Tom.Kaitchuck@gmail.com>",
"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 <Tom.Kaitchuck@gmail.com>",
"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 <jackefransham@hotmail.co.uk>",
"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 <jamslam@gmail.com>",
"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 <jamslam@gmail.com>",
"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 <joel.wejdenstal@gmail.com>",
"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 <ncameron@mozilla.com>",
"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 <sebastian.thiel@icloud.com>",
"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 <sebastian.thiel@icloud.com>",
"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 <sebastian.thiel@icloud.com>",
"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 <sebastian.thiel@icloud.com>",
"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 <sebastian.thiel@icloud.com>",
"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 <squeeself@gmail.com>",
"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 <jorge@japaric.io>",
"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 <gilad.naaman@gmail.com>",
"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 <dtolnay@gmail.com>",
"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<T>`. For example `.post_inc()` and `.pre_dec()` (c.f. `ptr++` and `--ptr`), `offset` and `add` for `NonNull<T>`, and the function `ptrdistance`."
},
{
"name": "rayon",
"version": "1.7.0",
"authors": "Niko Matsakis <niko@alum.mit.edu>|Josh Stone <cuviper@gmail.com>",
"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 <niko@alum.mit.edu>|Josh Stone <cuviper@gmail.com>",
"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 <division494@gmail.com>",
"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 <division494@gmail.com>",
"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 <kardeiz@gmail.com>",
"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 <erick.tryzelaar@gmail.com>|David Tolnay <dtolnay@gmail.com>",
"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 <erick.tryzelaar@gmail.com>|David Tolnay <dtolnay@gmail.com>",
"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 <vorner@vorner.cz>|Thomas Himmelstoss <thimm@posteo.de>",
"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 <peter.glotfelty@microsoft.com>",
"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 <peter.glotfelty@microsoft.com>",
"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 <marek.kotewicz@gmail.com>",
"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<ashleymannix@live.com.au>|Christopher Armstrong|Dylan DPC<dylan.dpc@gmail.com>|Hunar Roop Kahlon<hunar.roop@gmail.com>",
"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",

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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)

View File

@ -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",
]

View File

@ -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

View File

@ -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(),
};

View File

@ -65,6 +65,13 @@ pub struct DeckConfSchema11 {
#[serde(default)]
bury_interday_learning: bool,
#[serde(default)]
fsrs_weights: Vec<f32>,
#[serde(default)]
fsrs_enabled: bool,
#[serde(default)]
desired_retention: f32,
#[serde(flatten)]
other: HashMap<String, Value>,
}
@ -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<DeckConfSchema11> 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<DeckConfig> 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! {

View File

@ -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<anki_proto::deck_config::DeckConfigId> {
) -> Result<anki_proto::deck_config::DeckConfigId> {
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<generic::Json> {
fn all_deck_config_legacy(&mut self) -> Result<generic::Json> {
let conf: Vec<DeckConfSchema11> = 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<anki_proto::deck_config::DeckConfig> {
) -> Result<anki_proto::deck_config::DeckConfig> {
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<generic::Json> {
) -> Result<generic::Json> {
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<generic::Json> {
fn new_deck_config_legacy(&mut self) -> Result<generic::Json> {
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<anki_proto::deck_config::DeckConfigsForUpdate> {
) -> Result<anki_proto::deck_config::DeckConfigsForUpdate> {
self.get_deck_configs_for_update(input.did.into())
}
fn update_deck_configs(
&mut self,
input: anki_proto::deck_config::UpdateDeckConfigsRequest,
) -> error::Result<anki_proto::collection::OpChanges> {
) -> Result<anki_proto::collection::OpChanges> {
self.update_deck_configs(input.into()).map(Into::into)
}
}

View File

@ -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<ExportProgress> for Progress {
}
}
impl From<ComputeWeightsProgress> for Progress {
fn from(p: ComputeWeightsProgress) -> Self {
Progress::ComputeWeights(p)
}
}
impl From<ComputeRetentionProgress> for Progress {
fn from(p: ComputeRetentionProgress) -> Self {
Progress::ComputeRetention(p)
}
}
impl Collection {
pub fn new_progress_handler<P: Into<Progress> + Default + Clone>(
&self,

View File

@ -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<FSRSError> 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,
}
}
}

View File

@ -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;

View File

@ -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<f32> {
let mut anki_progress = self.new_progress_handler::<ComputeRetentionProgress>();
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)
}
}

View File

@ -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<Vec<f32>> {
let timing = self.timing_today()?;
let mut anki_progress = self.new_progress_handler::<ComputeWeightsProgress>();
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::<ComputeWeightsProgress>();
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<RevlogEntry>, next_day_at: TimestampSecs) -> Vec<FSRSItem> {
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<RevlogEntry>,
next_day_at: TimestampSecs,
) -> Option<Vec<FSRSItem>> {
// 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(())
}
}

View File

@ -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;

View File

@ -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<scheduler::CustomStudyDefaultsResponse> {
self.custom_study_defaults(input.deck_id.into())
}
fn compute_fsrs_weights(
&mut self,
input: scheduler::ComputeFsrsWeightsRequest,
) -> Result<scheduler::ComputeFsrsWeightsResponse> {
Ok(scheduler::ComputeFsrsWeightsResponse {
weights: self.compute_weights(&input.search)?,
})
}
fn compute_optimal_retention(
&mut self,
input: ComputeOptimalRetentionRequest,
) -> Result<ComputeOptimalRetentionResponse> {
Ok(ComputeOptimalRetentionResponse {
optimal_retention: self.compute_optimal_retention(input)?,
})
}
fn evaluate_weights(
&mut self,
input: scheduler::EvaluateWeightsRequest,
) -> Result<scheduler::EvaluateWeightsResponse> {
let ret = self.evaluate_weights(&input.weights, &input.search)?;
Ok(scheduler::EvaluateWeightsResponse {
log_loss: ret.0,
rmse: ret.1,
})
}
}

View File

@ -93,6 +93,7 @@ pub enum SearchNode {
NoCombining(String),
WordBoundary(String),
CustomData(String),
Preset(String),
}
#[derive(Debug, PartialEq, Clone)]

View File

@ -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,
}
}
}

View File

@ -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)),
}
}

View File

@ -62,6 +62,15 @@ impl SqliteStorage {
.transpose()
}
pub(crate) fn get_deck_config_id_by_name(&self, name: &str) -> Result<Option<DeckConfigId>> {
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)?;

View File

@ -132,6 +132,18 @@ impl SqliteStorage {
.collect()
}
pub(crate) fn get_revlog_entries_for_searched_cards_in_order(
&self,
) -> Result<Vec<RevlogEntry>> {
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<Vec<RevlogEntry>> {
self.db
.prepare_cached(concat!(include_str!("get.sql"), " where id >= ?"))?

View File

@ -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
</SpinBoxFloatRow>
</Item>
{#if state.v3Scheduler}
<Item>
<SwitchRow bind:value={$config.fsrsEnabled} defaultValue={false}>
<SettingTitle>FSRS optimizer</SettingTitle>
</SwitchRow>
</Item>
{/if}
{#if state.v3Scheduler}
<Item>
<CardStateCustomizer

View File

@ -17,6 +17,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ConfigSelector from "./ConfigSelector.svelte";
import DailyLimits from "./DailyLimits.svelte";
import DisplayOrder from "./DisplayOrder.svelte";
import FsrsOptions from "./FsrsOptions.svelte";
import HtmlAddon from "./HtmlAddon.svelte";
import LapseOptions from "./LapseOptions.svelte";
import type { DeckOptionsState } from "./lib";
@ -25,6 +26,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let state: DeckOptionsState;
const addons = state.addonComponents;
const config = state.currentConfig;
export function auxData(): Writable<Record<string, unknown>> {
return state.currentAuxData;
@ -112,11 +114,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</Row>
</Item>
<Item>
<Row class="row-columns">
<Addons {state} />
</Row>
</Item>
{#if $addons.length}
<Item>
<Row class="row-columns">
<Addons {state} />
</Row>
</Item>
{/if}
{#if state.v3Scheduler && $config.fsrsEnabled}
<Item>
<Row class="row-columns">
<FsrsOptions {state} />
</Row>
</Item>
{/if}
<Item>
<Row class="row-columns">

View File

@ -0,0 +1,289 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import {
Progress_ComputeRetention,
type Progress_ComputeWeights,
} from "@tslib/anki/collection_pb";
import { ComputeOptimalRetentionRequest } from "@tslib/anki/scheduler_pb";
import {
computeFsrsWeights,
computeOptimalRetention,
evaluateWeights,
setWantsAbort,
} from "@tslib/backend";
import { runWithBackendProgress } from "@tslib/progress";
import TitledContainer from "components/TitledContainer.svelte";
import ConfigInput from "./ConfigInput.svelte";
import type { DeckOptionsState } from "./lib";
import RevertButton from "./RevertButton.svelte";
import SettingTitle from "./SettingTitle.svelte";
import WeightsInputRow from "./WeightsInputRow.svelte";
export let state: DeckOptionsState;
const config = state.currentConfig;
let computeWeightsProgress: Progress_ComputeWeights | undefined;
let customSearch = "";
let computing = false;
let computeRetentionProgress:
| Progress_ComputeWeights
| Progress_ComputeRetention
| undefined;
const computeOptimalRequest = new ComputeOptimalRetentionRequest({
deckSize: 10000,
daysToSimulate: 365,
maxSecondsOfStudyPerDay: 1800,
maxInterval: 36500,
recallSecs: 10,
forgetSecs: 50,
learnSecs: 20,
});
async function computeWeights(): Promise<void> {
if (computing) {
await setWantsAbort({});
return;
}
computing = true;
try {
await runWithBackendProgress(
async () => {
const search = customSearch ?? `preset:"${state.getCurrentName()}"`;
const resp = await computeFsrsWeights({
search,
});
if (computeWeightsProgress) {
computeWeightsProgress.current = computeWeightsProgress.total;
}
$config.fsrsWeights = resp.weights;
},
(progress) => {
if (progress.value.case === "computeWeights") {
computeWeightsProgress = progress.value.value;
}
},
);
} finally {
computing = false;
}
}
async function checkWeights(): Promise<void> {
if (computing) {
await setWantsAbort({});
return;
}
computing = true;
try {
await runWithBackendProgress(
async () => {
const search = customSearch ?? `preset:"${state.getCurrentName()}"`;
const resp = await evaluateWeights({
weights: $config.fsrsWeights,
search,
});
if (computeWeightsProgress) {
computeWeightsProgress.current = computeWeightsProgress.total;
}
setTimeout(
() =>
alert(
`Log loss: ${resp.logLoss.toFixed(
3,
)}, RMSE: ${resp.rmse.toFixed(3)}`,
),
200,
);
},
(progress) => {
if (progress.value.case === "computeWeights") {
computeWeightsProgress = progress.value.value;
}
},
);
} finally {
computing = false;
}
}
async function computeRetention(): Promise<void> {
if (computing) {
await setWantsAbort({});
return;
}
computing = true;
try {
await runWithBackendProgress(
async () => {
computeOptimalRequest.weights = $config.fsrsWeights;
const resp = await computeOptimalRetention(computeOptimalRequest);
$config.desiredRetention = resp.optimalRetention;
if (computeRetentionProgress) {
computeRetentionProgress.current =
computeRetentionProgress.total;
}
},
(progress) => {
if (progress.value.case === "computeRetention") {
computeRetentionProgress = progress.value.value;
}
},
);
} finally {
computing = false;
}
}
$: computeWeightsProgressString = renderWeightProgress(computeWeightsProgress);
$: computeRetentionProgressString = renderRetentionProgress(
computeRetentionProgress,
);
function renderWeightProgress(val: Progress_ComputeWeights | undefined): String {
if (!val || !val.total) {
return "";
}
let pct = ((val.current / val.total) * 100).toFixed(2);
pct = `${pct}%`;
if (val instanceof Progress_ComputeRetention) {
return pct;
} else {
return `${pct} of ${val.revlogEntries} reviews`;
}
}
function renderRetentionProgress(
val: Progress_ComputeRetention | undefined,
): String {
if (!val || !val.total) {
return "";
}
const pct = ((val.current / val.total) * 100).toFixed(2);
return `${pct}%`;
}
</script>
<TitledContainer title={"FSRS"}>
<WeightsInputRow
bind:value={$config.fsrsWeights}
defaultValue={[
0.4, 0.6, 2.4, 5.8, 4.93, 0.94, 0.86, 0.01, 1.49, 0.14, 0.94, 2.18, 0.05,
0.34, 1.26, 0.29, 2.61,
]}
>
<SettingTitle>Weights</SettingTitle>
</WeightsInputRow>
<div>Optimal retention</div>
<ConfigInput>
<input type="number" bind:value={$config.desiredRetention} />
<RevertButton
slot="revert"
bind:value={$config.desiredRetention}
defaultValue={0.9}
/>
</ConfigInput>
<div class="mb-3" />
<div class="bordered">
<b>Optimize weights</b>
<br />
<input
bind:value={customSearch}
placeholder="Search; leave blank for all cards using this preset"
class="w-100 mb-1"
/>
<button
class="btn {computing ? 'btn-warning' : 'btn-primary'}"
on:click={() => computeWeights()}
>
{#if computing}
Cancel
{:else}
Compute
{/if}
</button>
<button
class="btn {computing ? 'btn-warning' : 'btn-primary'}"
on:click={() => checkWeights()}
>
{#if computing}
Cancel
{:else}
Check
{/if}
</button>
<div>{computeWeightsProgressString}</div>
</div>
<div class="bordered">
<b>Calculate optimal retention</b>
<br />
Deck size:
<br />
<input type="number" bind:value={computeOptimalRequest.deckSize} />
<br />
Days to simulate
<br />
<input type="number" bind:value={computeOptimalRequest.daysToSimulate} />
<br />
Max seconds of study per day:
<br />
<input
type="number"
bind:value={computeOptimalRequest.maxSecondsOfStudyPerDay}
/>
<br />
Maximum interval:
<br />
<input type="number" bind:value={computeOptimalRequest.maxInterval} />
<br />
Seconds to recall a card:
<br />
<input type="number" bind:value={computeOptimalRequest.recallSecs} />
<br />
Seconds to forget a card:
<br />
<input type="number" bind:value={computeOptimalRequest.forgetSecs} />
<br />
Seconds to learn a card:
<br />
<input type="number" bind:value={computeOptimalRequest.learnSecs} />
<br />
<button
class="btn {computing ? 'btn-warning' : 'btn-primary'}"
on:click={() => computeRetention()}
>
{#if computing}
Cancel
{:else}
Compute
{/if}
</button>
<div>{computeRetentionProgressString}</div>
</div>
</TitledContainer>
<style>
.bordered {
border: 1px solid #777;
padding: 1em;
margin-bottom: 2px;
}
</style>

View File

@ -0,0 +1,16 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
export let value: number[];
let stringValue: string;
$: stringValue = value.map((v) => v.toFixed(4)).join(", ");
function update(this: HTMLInputElement): void {
value = this.value.split(", ").map((v) => Number(v));
}
</script>
<textarea value={stringValue} on:blur={update} class="w-100" />

View File

@ -0,0 +1,18 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import ConfigInput from "./ConfigInput.svelte";
import RevertButton from "./RevertButton.svelte";
import WeightsInput from "./WeightsInput.svelte";
export let value: any;
export let defaultValue: any;
</script>
<slot />
<ConfigInput>
<WeightsInput bind:value />
<RevertButton slot="revert" bind:value {defaultValue} />
</ConfigInput>

View File

@ -18,7 +18,7 @@ export async function postProto<T>(
const outputBytes = await postProtoInner(path, inputBytes);
return outputType.fromBinary(outputBytes);
} catch (err) {
if (alertOnError) {
if (alertOnError && !(err instanceof Error && err.message === "500: Interrupted")) {
alert(err);
}
throw err;

View File

@ -12,7 +12,9 @@ export async function runWithBackendProgress<T>(
const progress = await latestProgress({});
onUpdate(progress);
}, 100);
const result = await callback();
clearInterval(intervalId);
return result;
try {
return await callback();
} finally {
clearInterval(intervalId);
}
}