waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ data-pipeline fsrs_referenz.py
| 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)) |