Open cashpw opened 1 year ago
This seems very difficult to do right and I'm not sure how useful an inaccurate variant would be.
The simulation loop would need to compute the probabilities of each of the four ratings for each card. If we had such an algorithm, we could easily use it to predict the recall probability (like ebisu) for each card and use it to schedule reviews.
A simpler version could use the same probabilities for each card, computed from the past reviews. Below is some code that does this with some hard-coded numbers.
At the moment I'm not diligent enough to review all due cards each day (or even multiple times each day) to judge how my real review counts differ from the predicted ones.
(require 'time-date)
(defun off-flatten (card)
(unless (plist-get card :suspended)
(mapcar
(lambda (pos)
(list
:due (plist-get pos :due)
:ease (plist-get pos :ease)
:box (plist-get pos :box)
:interval (plist-get pos :interval)))
(plist-get card :positions))))
(defun off-insert (pos buckets)
(let* ((due (plist-get pos :due))
(bucket (-find (lambda (b) (time-less-p due (plist-get b :end))) buckets)))
(when bucket
(push pos (plist-get bucket :positions)))))
(defun off-rate-pos (pos rating)
(cl-destructuring-bind (ease box interval)
(org-fc-algo-sm2-next-parameters
(plist-get pos :ease)
(plist-get pos :box)
(plist-get pos :interval)
rating)
(plist-put pos :ease ease)
(plist-put pos :box box)
(plist-put pos :interval interval)
(plist-put pos :due
(encode-time
(decoded-time-add
(decode-time
(plist-get pos :due))
(make-decoded-time :second
(*
interval
60 60 24)))))))
(defun off-rating ()
(let ((r (cl-random 1.0)))
(cond
((< r 0.10) 'again)
((< r 0.15) 'hard)
((< r 0.60) 'good)
(t 'easy))))
(defun off-step (buckets)
(let ((first-non-empty
(-find (lambda (b) (not (null (plist-get b :positions))))
buckets)))
(when first-non-empty
(let* ((poss (plist-get first-non-empty :positions))
(poss_
(mapcar (lambda (pos)
(off-rate-pos pos (off-rating))) poss)))
(cl-incf (plist-get first-non-empty :reviews)
(length poss))
(plist-put first-non-empty :positions '())
(dolist (pos poss_)
(off-insert pos buckets)))
buckets)))
(defun off-iterate (buckets)
(while (off-step buckets))
buckets)
(defun off-forecast (context)
(interactive (list (org-fc-select-context)))
(let* ((now (encode-time (decode-time)))
(poss
;; To simplify the code, we'll assume cards are reviewed on
;; exactly when they become due. For cards that are already due,
;; we'll rewrite the due date to the current time.
;; (mapcan #'off-flatten (org-fc-index context))
(mapcar
(lambda (pos)
(if (time-less-p (plist-get pos :due) now)
(plist-put pos :due now)
pos))
(mapcan #'off-flatten (org-fc-index context))))
(buckets (off-buckets 90 0)))
(dolist (pos poss) (off-insert pos buckets))
(off-iterate buckets)))
(defun off-show (context)
(interactive (list (org-fc-select-context)))
(with-current-buffer (get-buffer-create "*org-fc Forecast*")
(let* ((org-fc-algorithm 'sm2-v2)
(buckets (off-forecast context)))
(goto-char (point-min))
(erase-buffer)
(dolist (buk buckets)
(insert (format "%s - %s\n"
(plist-get buk :end)
(plist-get buk :reviews))))
(insert
(format "Total reviews: %s\n"
(cl-loop for buk in buckets summing
(plist-get buk :reviews))))
(switch-to-buffer (current-buffer)))))
(defun off-buckets (n &optional new-per-day)
(setq new-per-day (or new-per-day 0))
(let* ((cur (decode-time (current-time)))
(day-end
(make-decoded-time
:second 59
:minute 59
:hour 23
:day (nth 3 cur)
:month (nth 4 cur)
:year (nth 5 cur))))
(cl-loop
for d from 0 to (1- n) collecting
(let ((time
(encode-time
(decoded-time-add day-end (make-decoded-time :day d))))
(start
(encode-time
(decoded-time-add day-end (make-decoded-time :day (1- d))))))
(list :end time
:reviews 0
:positions
(cl-loop for i from 0 below new-per-day collecting
(list
:due start
:ease 2.5
:box 0
:interval 0)))))))
I'm thinking of something like the graph generated by the Anki Simulator add-on.