"""Erzeugt die Sollwerte, gegen die `app/tests/fsrs.test.ts` prüft. Die Formeln sind wörtlich aus der Referenzimplementierung übernommen: `open-spaced-repetition/py-fsrs`, Datei `fsrs/scheduler.py` (MIT). Bewusst ein zweiter, unabhängiger Weg zum selben Ergebnis – ein Tippfehler in der TypeScript-Fassung fällt dadurch auf, statt sich still in die Wiedervorlagetermine fortzusetzen. Dieses Skript gehört nicht zur ausgelieferten Anwendung. Es läuft nur, wenn die Sollwerte neu erzeugt werden müssen, etwa nach einer neuen FSRS-Version: python data-pipeline/fsrs_referenz.py > app/tests/fixtures/fsrs-referenz.json Die Sollwerte dürfen niemals von Hand angepasst werden. Wird der Test rot, ist zuerst zu klären, ob sich die Referenz geändert hat oder die Umsetzung falsch ist – nicht, welche Zahl den Test wieder grün macht. """ import math, json w = (0.212,1.2931,2.3065,8.2956,6.4133,0.8334,3.0194,0.001,1.8722,0.1666, 0.796,1.4835,0.0614,0.2629,1.6483,0.6014,1.8729,0.5425,0.0912,0.0658,0.1542) DECAY = -w[20] FACTOR = 0.9 ** (1 / DECAY) - 1 S_MIN = 0.001 def R(s, t): return (1 + FACTOR * max(0, t) / s) ** DECAY def interval(s, dr): return (s / FACTOR) * ((dr ** (1 / DECAY)) - 1) def d0(g, clamp=True): d = w[4] - (math.e ** (w[5] * (g - 1))) + 1 return min(max(d, 1.0), 10.0) if clamp else d def s0(g): return max(w[g - 1], S_MIN) def next_d(d, g): arg1 = d0(4, clamp=False) dd = -(w[6] * (g - 3)) arg2 = d + (10.0 - d) * dd / 9.0 return min(max(w[7] * arg1 + (1 - w[7]) * arg2, 1.0), 10.0) def s_recall(d, s, r, g): hard = w[15] if g == 2 else 1 easy = w[16] if g == 4 else 1 return s * (1 + (math.e ** w[8]) * (11 - d) * (s ** -w[9]) * ((math.e ** ((1 - r) * w[10])) - 1) * hard * easy) def s_forget(d, s, r): lt = w[11] * (d ** -w[12]) * (((s + 1) ** w[13]) - 1) * (math.e ** ((1 - r) * w[14])) st = s / (math.e ** (w[17] * w[18])) return min(lt, st) def s_short(s, g): inc = (math.e ** (w[17] * (g - 3 + w[18]))) * (s ** -w[19]) if g in (2, 3, 4): inc = max(inc, 1.0) return max(s * inc, S_MIN) def naechster(stand, g, abstand): if stand is None: return {"stabilitaet": s0(g), "schwierigkeit": d0(g)} d = next_d(stand["schwierigkeit"], g) if abstand < 1: return {"stabilitaet": s_short(stand["stabilitaet"], g), "schwierigkeit": d} r = R(stand["stabilitaet"], abstand) s = (s_forget(stand["schwierigkeit"], stand["stabilitaet"], r) if g == 1 else s_recall(stand["schwierigkeit"], stand["stabilitaet"], r, g)) return {"stabilitaet": max(s, S_MIN), "schwierigkeit": d} out = { "konstanten": {"ABFALL": DECAY, "STRECKUNG": FACTOR}, "erstStand": {str(g): naechster(None, g, 0) for g in (1,2,3,4)}, "kurve": [{"s": s, "t": t, "r": R(s, t)} for s, t in ((1,1),(2.3065,2.3065),(10,0),(10,5),(10,30),(50,100),(0.212,1))], "intervall": [{"s": s, "ziel": q, "tage": interval(s, q)} for s, q in ((2.3065,0.9),(10,0.9),(10,0.95),(10,0.8),(100,0.97))], "folgen": [], } # Mehrschrittige Verlaeufe - der harte Test, weil sich Fehler aufschaukeln. for name, grade, abstaende in ( ("nur_gut", [3,3,3,3,3], [0,3,8,20,50]), ("mit_fehlern", [3,1,3,2,4], [0,2,1,5,12]), ("nur_leicht", [4,4,4], [0,10,60]), ("selber_tag", [3,3,2,1], [0,0,0,0]), ("dauerfehler", [1,1,1,1], [0,1,1,1]), ): stand, schritte = None, [] for g, a in zip(grade, abstaende): stand = naechster(stand, g, a) schritte.append({"grad": g, "abstand": a, **stand}) out["folgen"].append({"name": name, "schritte": schritte}) print(json.dumps(out, indent=1))