Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: open-spaced-repetition/fsrs-optimizer
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v4.25.2
Choose a base ref
...
head repository: open-spaced-repetition/fsrs-optimizer
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
Loading
Showing with 1,528 additions and 703 deletions.
  1. +8 −0 .github/workflows/ruff.yml
  2. +27 −0 .github/workflows/test.yml
  3. +8 −0 README.md
  4. +13 −3 pyproject.toml
  5. +29 −10 src/fsrs_optimizer/__main__.py
  6. +819 −462 src/fsrs_optimizer/fsrs_optimizer.py
  7. +305 −228 src/fsrs_optimizer/fsrs_simulator.py
  8. 0 tests/__init__.py
  9. +291 −0 tests/model_test.py
  10. +28 −0 tests/simulator_test.py
8 changes: 8 additions & 0 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: Ruff
on: [ push, pull_request ]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1
27 changes: 27 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Test Python

on: [push, pull_request]

jobs:
test:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.12'

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Install the project
run: uv sync --all-extras --dev

- name: Run tests
# For example, using `pytest`
run: uv run pytest tests
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# FSRS Optimizer

[![PyPi](https://img.shields.io/pypi/v/FSRS-Optimizer)](https://pypi.org/project/FSRS-Optimizer/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

The FSRS Optimizer is a Python library capable of utilizing personal spaced repetition review logs to refine the FSRS algorithm. Designed with the intent of delivering a standardized, universal optimizer to various FSRS implementations across numerous programming languages, this tool is set to establish a ubiquitous standard for spaced repetition review logs. By facilitating the uniformity of learning data among different spaced repetition softwares, it guarantees learners consistent review schedules across a multitude of platforms.

Delve into the underlying principles of the FSRS Optimizer's training process at: https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-mechanism-of-optimization
@@ -62,3 +64,9 @@ python -m fsrs_optimizer "revlog.csv"
![image](https://github.com/open-spaced-repetition/fsrs-optimizer/assets/32575846/fad7154a-9667-4eea-b868-d94c94a50912)

![image](https://github.com/open-spaced-repetition/fsrs-optimizer/assets/32575846/f868aac4-2e9e-4101-b8ad-eccc1d9b1bd5)

---

## Alternative

Are you getting tired of installing torch? Try [fsrs-rs-python](https://github.com/open-spaced-repetition/fsrs-rs-python)!
16 changes: 13 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -4,19 +4,29 @@ build-backend = "setuptools.build_meta"

[project]
name = "FSRS-Optimizer"
version = "4.25.2"
version = "5.7.2"
readme = "README.md"
dependencies = [
"matplotlib>=3.7.0",
"numpy>=1.22.4",
"pandas>=1.5.3",
"pytz>=2022.7.1",
"scikit_learn>=1.2.2",
"scikit_learn>=1.4.0",
"torch>=1.13.1",
"tqdm>=4.64.1",
"statsmodels>=0.13.5",
"scipy<1.14.1"
]
requires-python = ">=3.9"
requires-python = ">=3.9,<3.13"

[project.urls]
Homepage = "https://github.com/open-spaced-repetition/fsrs-optimizer"
[tool.ruff.lint]
ignore = ["F405", "F403", "E712", "F541", "E722", "E741"]
[project.optional-dependencies]
test = [
"ruff",
"mypy",
"pytest",
"pytest-cov",
]
39 changes: 29 additions & 10 deletions src/fsrs_optimizer/__main__.py
Original file line number Diff line number Diff line change
@@ -40,13 +40,16 @@ def process(filepath, filter_out_flags: list[int]):
"revlog_start_date": "2006-10-05",
"preview": "y",
"filter_out_suspended_cards": "n",
"enable_short_term": "y",
}

# Prompts the user with the key and then falls back on the last answer given.
def remembered_fallback_prompt(key: str, pretty: str = None):
if pretty is None:
pretty = key
remembered_fallbacks[key] = prompt(f"input {pretty}", remembered_fallbacks[key])
remembered_fallbacks[key] = prompt(
f"input {pretty}", remembered_fallbacks.get(key, None)
)

print("The defaults will switch to whatever you entered last.\n")

@@ -60,11 +63,15 @@ def remembered_fallback_prompt(key: str, pretty: str = None):

remembered_fallback_prompt("next_day", "used next day start hour")
remembered_fallback_prompt(
"revlog_start_date", "the date at which before reviews will be ignored"
"revlog_start_date",
"the date at which before reviews will be ignored | YYYY-MM-DD",
)
remembered_fallback_prompt(
"filter_out_suspended_cards", "filter out suspended cards? (y/n)"
)
remembered_fallback_prompt(
"enable_short_term", "enable short-term component in FSRS model? (y/n)"
)

graphs_input = prompt("Save graphs? (y/n)", remembered_fallbacks["preview"])
else:
@@ -81,8 +88,9 @@ def remembered_fallback_prompt(key: str, pretty: str = None):
json.dump(remembered_fallbacks, f)

save_graphs = graphs_input != "n"
enable_short_term = remembered_fallbacks["enable_short_term"] == "y"

optimizer = fsrs_optimizer.Optimizer()
optimizer = fsrs_optimizer.Optimizer(enable_short_term=enable_short_term)
if filepath.endswith(".apkg") or filepath.endswith(".colpkg"):
optimizer.anki_extract(
f"{filepath}",
@@ -107,18 +115,23 @@ def remembered_fallback_prompt(key: str, pretty: str = None):
for i, f in enumerate(figures):
f.savefig(f"pretrain_{i}.png")
plt.close(f)
figures = optimizer.train(verbose=save_graphs)
figures = optimizer.train(verbose=save_graphs, recency_weight=True)
for i, f in enumerate(figures):
f.savefig(f"train_{i}.png")
plt.close(f)

optimizer.predict_memory_states()
figures = optimizer.find_optimal_retention(verbose=save_graphs)
for i, f in enumerate(figures):
f.savefig(f"find_optimal_retention_{i}.png")
plt.close(f)
try:
figures = optimizer.find_optimal_retention(verbose=save_graphs)
for i, f in enumerate(figures):
f.savefig(f"find_optimal_retention_{i}.png")
plt.close(f)
except Exception as e:
print(e)
print("Failed to find optimal retention")
optimizer.optimal_retention = 0.9

optimizer.preview(optimizer.optimal_retention)
print(optimizer.preview(optimizer.optimal_retention))

profile = f"""{{
// Generated, Optimized anki deck settings
@@ -139,7 +152,13 @@ def remembered_fallback_prompt(key: str, pretty: str = None):
loss_before, loss_after = optimizer.evaluate()
print(f"Loss before training: {loss_before:.4f}")
print(f"Loss after training: {loss_after:.4f}")
metrics, figures = optimizer.calibration_graph()
metrics, figures = optimizer.calibration_graph(verbose=False)
for partition in metrics:
print(f"Last rating = {partition}")
for metric in metrics[partition]:
print(f"{metric}: {metrics[partition][metric]:.4f}")
print()

metrics["Log loss"] = loss_after
if save_graphs:
for i, f in enumerate(figures):
Loading