forked from open-spaced-repetition/fsrs4anki-helper
-
Notifications
You must be signed in to change notification settings - Fork 0
/
optimizer.py
237 lines (179 loc) · 7.79 KB
/
optimizer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
from .utils import *
from .configuration import *
from anki.exporting import AnkiPackageExporter
from anki.decks import DeckManager
from aqt.qt import QProcess, QThreadPool, QRunnable, QObject, pyqtSignal
from aqt.utils import showInfo, showCritical, askUserDialog
import os
import time
import sys
import platform
config = Config()
class Progress(QObject):
progress = pyqtSignal(int, int)
@staticmethod
def tooltip(n, total):
tooltip(f"{_stage}: {n}/{total} {100 * n/total}%")
update_period = 0.1 # how long the progress tooltips are refreshed in seconds
_progress = Progress()
_progress.progress.connect(Progress.tooltip)
_stage = "Error"
def optimize(did: int):
try: # This code is here so that when it fails the popup can show immediately rather than after the after the cancel prompt
# Progress bar -> tooltip
from tqdm import tqdm, cli
from tqdm.notebook import tqdm_notebook
# orig = tqdm.update
last_print = time.time()
def update(self, n=1):
nonlocal last_print
#orig(self,n)
self.n += n
if last_print + update_period < time.time():
_progress.progress.emit(self.n, self.total)
last_print = time.time()
noop = lambda *args, **kwargs: noop
orig_init = tqdm.__init__
def new_init(self, *args, **kwargs):
kwargs["file"] = sys.stdout
orig_init(self, *args, **kwargs)
tqdm.__init__ = new_init
orig_notebook_init = tqdm_notebook.__init__
def new_notebook_init(self, *args, **kwargs):
kwargs["display"] = False
orig_notebook_init(self, *args, **kwargs)
tqdm_notebook.__init__ = new_notebook_init
tqdm.update = update
tqdm.close = noop
from fsrs4anki_optimizer import Optimizer
except ImportError:
showCritical(
"""
You need to have the optimizer installed in order to optimize your decks using this option.
Please run Tools>FSRS4Anki helper>Install local optimizer.
Alternatively, use a different method of optimizing (https://github.com/open-spaced-repetition/fsrs4anki/releases)
""")
return
exporter = AnkiPackageExporter(mw.col)
manager = DeckManager(mw.col)
deck = manager.get(did)
assert deck
name = deck["name"]
dir_path = os.path.expanduser("~/.fsrs4ankiHelper")
tmp_dir_path = f"{dir_path}/tmp"
exporter.did = did
exporter.includeMedia = False
exporter.includeSched = True
export_file_path = f"{tmp_dir_path}/{did}.apkg"
if not os.path.isdir(dir_path):
os.mkdir(dir_path)
if not os.path.isdir(tmp_dir_path):
os.mkdir(tmp_dir_path)
preferences = mw.col.get_preferences()
# https://stackoverflow.com/questions/1111056/get-time-zone-information-of-the-system-in-python/10854983#10854983
offset = time.timezone if (time.localtime().tm_isdst == 0) else time.altzone
offset = offset / 60 / 60 * -1
timezone = f"Etc/GMT{'+' if offset >= 0 else ''}{int(offset)}" # Maybe make this overridable?
print(timezone)
revlog_start_date = "2000-01-01" # TODO: implement this as a config option
rollover = preferences.scheduling.rollover
diag = askUserDialog("Find optimal retention? (This takes an extra long time)", ["Yes", "No", "Cancel"])
diag.setDefault(1)
resp = diag.run()
if resp == "Cancel": # If they hit cancel
tooltip("Optimization cancelled")
return
else:
get_optimal_retention = resp == "Yes" # If they didn't hit cancel convert answer to bool
class OptimizeWorker(QRunnable):
class Events(QObject):
finished = pyqtSignal(dict)
stage = pyqtSignal(str)
events = Events()
def run(self):
optimizer = Optimizer()
self.events.stage.emit("Exporting deck")
exporter.exportInto(export_file_path) # This is simply quicker than somehow making it so that anki doesn't zip the export
optimizer.anki_extract(export_file_path)
self.events.stage.emit("Training model")
try:
optimizer.create_time_series(timezone, revlog_start_date, rollover)
except ValueError as e:
showCritical(
"""You got a value error, This usually happens when the deck has no or very few reviews.
You have to do some reviews on the deck before you optimize it!""")
raise e
optimizer.define_model()
optimizer.train()
DEFAULT_RETENTION = 0.8
if get_optimal_retention:
self.events.stage.emit("Finding optimal retention (Ignore right number)")
optimizer.predict_memory_states()
optimizer.find_optimal_retention(False)
else:
optimizer.optimal_retention = DEFAULT_RETENTION
result = {
# Calculated
"name": name,
"w": optimizer.w,
REQUEST_RETENTION: optimizer.optimal_retention,
RETENTION_IS_NOT_OPTIMIZED: not get_optimal_retention,
# Defaults
MAX_INTERVAL: 36500,
EASY_BONUS: 1.3,
HARD_INTERVAL: 1.2
}
self.events.finished.emit(result)
def on_complete(result: dict[str]):
config.load()
saved_results = config.saved_optimized
saved_results[did] = result
config.saved_optimized = saved_results
showInfo(config.results_string())
# shutil.rmtree(tmp_dir_path)
# Uses workers to avoid blocking main thread
worker = OptimizeWorker()
worker.events.finished.connect(on_complete)
def on_stage(stage):
global _stage
_stage = stage
worker.events.stage.connect(on_stage)
QThreadPool.globalInstance().start(worker)
downloader = QProcess()
downloader.setProcessChannelMode(QProcess.ProcessChannelMode.ForwardedChannels)
def install(_):
global downloader
confirmed = askUser(
"""This will install the optimizer onto your system.
You will need to install python or at least pip for this to work.
You may also need to install git.
This will occupy about 1GB of space and can take some time.
Please dont close anki until the popup arrives telling you its complete.
I reccomend that you launch anki with command line (anki-console.bat on windows) as otherwise there is no progress bar.
There are other options if you just need to optimize a few decks
(consult https://github.com/open-spaced-repetition/fsrs4anki/releases).
Proceed?""",
title="Install local optimizer?")
if confirmed:
# Not everyone is going to have git installed but works for testing.
PACKAGE = 'fsrs4anki_optimizer @ git+https://github.com/open-spaced-repetition/fsrs4anki@v3.18.1#subdirectory=package'
if platform.system() == "Windows": # For windows
anki_path = sys.executable
anki_lib_path = os.path.dirname(anki_path)
anki_lib_path = os.path.join(anki_lib_path, "lib")
print(anki_lib_path)
# https://stackoverflow.com/a/2916320
downloader.start("pip", ["install", f'--target={anki_lib_path}', PACKAGE])
else: # For linux (mac untested)
downloader.start(sys.executable, ["-m", "pip", "install", PACKAGE])
tooltip("Installing optimizer")
def finished(exitCode, exitStatus):
if exitCode == 0:
showInfo("Optimizer installed successfully, restart for it to take effect")
else:
showCritical(
f"""Optimizer wasn't installed. For more information, run anki in console mode. (on windows anki-console.bat)
Error code: '{exitCode}', Error status '{exitStatus}'
"""
)
downloader.finished.connect(finished)