waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ data-pipeline fsrs_referenz.py

3,6 KB Rohdatei
data-pipeline/fsrs_referenz.py — 97 Zeilen
1 """Erzeugt die Sollwerte, gegen die `app/tests/fsrs.test.ts` prüft.
2
3 Die Formeln sind wörtlich aus der Referenzimplementierung übernommen:
4 `open-spaced-repetition/py-fsrs`, Datei `fsrs/scheduler.py` (MIT).
5 Bewusst ein zweiter, unabhängiger Weg zum selben Ergebnis – ein Tippfehler in
6 der TypeScript-Fassung fällt dadurch auf, statt sich still in die
7 Wiedervorlagetermine fortzusetzen.
8
9 Dieses Skript gehört nicht zur ausgelieferten Anwendung. Es läuft nur, wenn
10 die Sollwerte neu erzeugt werden müssen, etwa nach einer neuen FSRS-Version:
11
12 python data-pipeline/fsrs_referenz.py > app/tests/fixtures/fsrs-referenz.json
13
14 Die Sollwerte dürfen niemals von Hand angepasst werden. Wird der Test rot, ist
15 zuerst zu klären, ob sich die Referenz geändert hat oder die Umsetzung falsch
16 ist – nicht, welche Zahl den Test wieder grün macht.
17 """
18
19 import math, json
20
21 w = (0.212,1.2931,2.3065,8.2956,6.4133,0.8334,3.0194,0.001,1.8722,0.1666,
22 0.796,1.4835,0.0614,0.2629,1.6483,0.6014,1.8729,0.5425,0.0912,0.0658,0.1542)
23 DECAY = -w[20]
24 FACTOR = 0.9 ** (1 / DECAY) - 1
25 S_MIN = 0.001
26
27 def R(s, t):
28 return (1 + FACTOR * max(0, t) / s) ** DECAY
29
30 def interval(s, dr):
31 return (s / FACTOR) * ((dr ** (1 / DECAY)) - 1)
32
33 def d0(g, clamp=True):
34 d = w[4] - (math.e ** (w[5] * (g - 1))) + 1
35 return min(max(d, 1.0), 10.0) if clamp else d
36
37 def s0(g):
38 return max(w[g - 1], S_MIN)
39
40 def next_d(d, g):
41 arg1 = d0(4, clamp=False)
42 dd = -(w[6] * (g - 3))
43 arg2 = d + (10.0 - d) * dd / 9.0
44 return min(max(w[7] * arg1 + (1 - w[7]) * arg2, 1.0), 10.0)
45
46 def s_recall(d, s, r, g):
47 hard = w[15] if g == 2 else 1
48 easy = w[16] if g == 4 else 1
49 return s * (1 + (math.e ** w[8]) * (11 - d) * (s ** -w[9])
50 * ((math.e ** ((1 - r) * w[10])) - 1) * hard * easy)
51
52 def s_forget(d, s, r):
53 lt = w[11] * (d ** -w[12]) * (((s + 1) ** w[13]) - 1) * (math.e ** ((1 - r) * w[14]))
54 st = s / (math.e ** (w[17] * w[18]))
55 return min(lt, st)
56
57 def s_short(s, g):
58 inc = (math.e ** (w[17] * (g - 3 + w[18]))) * (s ** -w[19])
59 if g in (2, 3, 4):
60 inc = max(inc, 1.0)
61 return max(s * inc, S_MIN)
62
63 def naechster(stand, g, abstand):
64 if stand is None:
65 return {"stabilitaet": s0(g), "schwierigkeit": d0(g)}
66 d = next_d(stand["schwierigkeit"], g)
67 if abstand < 1:
68 return {"stabilitaet": s_short(stand["stabilitaet"], g), "schwierigkeit": d}
69 r = R(stand["stabilitaet"], abstand)
70 s = (s_forget(stand["schwierigkeit"], stand["stabilitaet"], r) if g == 1
71 else s_recall(stand["schwierigkeit"], stand["stabilitaet"], r, g))
72 return {"stabilitaet": max(s, S_MIN), "schwierigkeit": d}
73
74 out = {
75 "konstanten": {"ABFALL": DECAY, "STRECKUNG": FACTOR},
76 "erstStand": {str(g): naechster(None, g, 0) for g in (1,2,3,4)},
77 "kurve": [{"s": s, "t": t, "r": R(s, t)}
78 for s, t in ((1,1),(2.3065,2.3065),(10,0),(10,5),(10,30),(50,100),(0.212,1))],
79 "intervall": [{"s": s, "ziel": q, "tage": interval(s, q)}
80 for s, q in ((2.3065,0.9),(10,0.9),(10,0.95),(10,0.8),(100,0.97))],
81 "folgen": [],
82 }
83 # Mehrschrittige Verlaeufe - der harte Test, weil sich Fehler aufschaukeln.
84 for name, grade, abstaende in (
85 ("nur_gut", [3,3,3,3,3], [0,3,8,20,50]),
86 ("mit_fehlern", [3,1,3,2,4], [0,2,1,5,12]),
87 ("nur_leicht", [4,4,4], [0,10,60]),
88 ("selber_tag", [3,3,2,1], [0,0,0,0]),
89 ("dauerfehler", [1,1,1,1], [0,1,1,1]),
90 ):
91 stand, schritte = None, []
92 for g, a in zip(grade, abstaende):
93 stand = naechster(stand, g, a)
94 schritte.append({"grad": g, "abstand": a, **stand})
95 out["folgen"].append({"name": name, "schritte": schritte})
96
97 print(json.dumps(out, indent=1))