Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/fuzz #23

Merged
merged 1 commit into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions area.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package fsrs

import (
"math"
"strconv"
"time"
)

type state struct {
C float64
S0 float64
S1 float64
S2 float64
}

type alea struct {
c float64
s0 float64
s1 float64
s2 float64
}

func NewAlea(seed interface{}) *alea {
mash := Mash()
a := &alea{
c: 1,
s0: mash(" "),
s1: mash(" "),
s2: mash(" "),
}

if seed == nil {
seed = time.Now().UnixNano()
}

seedStr := ""
switch s := seed.(type) {
case int:
seedStr = strconv.Itoa(s)
case string:
seedStr = s
}

a.s0 -= mash(seedStr)
if a.s0 < 0 {
a.s0 += 1
}
a.s1 -= mash(seedStr)
if a.s1 < 0 {
a.s1 += 1
}
a.s2 -= mash(seedStr)
if a.s2 < 0 {
a.s2 += 1
}

return a
}

func (a *alea) Next() float64 {
t := 2091639*a.s0 + a.c*2.3283064365386963e-10 // 2^-32
a.s0 = a.s1
a.s1 = a.s2
a.s2 = t - math.Floor(t)
a.c = math.Floor(t)
return a.s2
}

func (a *alea) SetState(state state) {
a.c = state.C
a.s0 = state.S0
a.s1 = state.S1
a.s2 = state.S2
}

func (a *alea) GetState() state {
return state{
C: a.c,
S0: a.s0,
S1: a.s1,
S2: a.s2,
}
}

func Mash() func(string) float64 {
n := uint32(0xefc8249d)
return func(data string) float64 {
for i := 0; i < len(data); i++ {
n += uint32(data[i])
h := 0.02519603282416938 * float64(n)
n = uint32(h)
h -= float64(n)
h *= float64(n)
n = uint32(h)
h -= float64(n)
n += uint32(h * 0x100000000) // 2^32
}
return float64(n) * 2.3283064365386963e-10 // 2^-32
}
}

type PRNG func() float64

func Alea(seed interface{}) PRNG {
xg := NewAlea(seed)
prng := func() float64 {
return xg.Next()
}

return prng
}

func (prng PRNG) Int32() int32 {
return int32(prng() * 0x100000000)
}

func (prng PRNG) Double() float64 {
return prng() + float64(uint32(prng()*0x200000))*1.1102230246251565e-16 // 2^-53
}

func (prng PRNG) State(xg *alea) state {
return xg.GetState()
}

func (prng PRNG) ImportState(xg *alea, state state) PRNG {
xg.SetState(state)
return prng
}
2 changes: 1 addition & 1 deletion fsrs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func TestNextInterval(t *testing.T) {
var ivlList []float64
for i := 1; i <= 10; i++ {
fsrs.RequestRetention = float64(i) / 10
ivlList = append(ivlList, fsrs.nextInterval(1))
ivlList = append(ivlList, fsrs.nextInterval(1, 0))
}
wantIvlList := []float64{422, 102, 43, 22, 13, 8, 4, 2, 1, 1}
if !reflect.DeepEqual(ivlList, wantIvlList) {
Expand Down
53 changes: 51 additions & 2 deletions parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type Parameters struct {
Decay float64 `json:"Decay"`
Factor float64 `json:"Factor"`
EnableShortTerm bool `json:"EnableShortTerm"`
EnableFuzz bool `json:"EnableFuzz"`
seed string
}

func DefaultParam() Parameters {
Expand All @@ -23,6 +25,7 @@ func DefaultParam() Parameters {
Decay: Decay,
Factor: Factor,
EnableShortTerm: true,
EnableFuzz: false,
}
}

Expand All @@ -37,13 +40,26 @@ func (p *Parameters) initDifficulty(r Rating) float64 {
return constrainDifficulty(p.W[4] - math.Exp(p.W[5]*float64(r-1)) + 1)
}

func (p *Parameters) ApplyFuzz(ivl float64, elapsedDays float64, enableFuzz bool) float64 {
if !enableFuzz || ivl < 2.5 {
return ivl
}

generator := Alea(p.seed)
fuzzFactor := generator.Double()

minIvl, maxIvl := getFuzzRange(ivl, elapsedDays, p.MaximumInterval)

return math.Floor(fuzzFactor*float64(maxIvl-minIvl+1)) + float64(minIvl)
}

func constrainDifficulty(d float64) float64 {
return math.Min(math.Max(d, 1), 10)
}

func (p *Parameters) nextInterval(s float64) float64 {
func (p *Parameters) nextInterval(s, elapsedDays float64) float64 {
newInterval := s / p.Factor * (math.Pow(p.RequestRetention, 1/p.Decay) - 1)
return math.Max(math.Min(math.Round(newInterval), p.MaximumInterval), 1)
return p.ApplyFuzz(math.Max(math.Min(math.Round(newInterval), p.MaximumInterval), 1), elapsedDays, p.EnableFuzz)
}

func (p *Parameters) nextDifficulty(d float64, r Rating) float64 {
Expand Down Expand Up @@ -85,3 +101,36 @@ func (p *Parameters) nextForgetStability(d float64, s float64, r float64) float6
(math.Pow(s+1, p.W[13]) - 1) *
math.Exp((1-r)*p.W[14])
}

type FuzzRange struct {
Start float64
End float64
Factor float64
}

var FUZZ_RANGES = []FuzzRange{
{Start: 2.5, End: 7.0, Factor: 0.15},
{Start: 7.0, End: 20.0, Factor: 0.1},
{Start: 20.0, End: math.Inf(1), Factor: 0.05},
}

func getFuzzRange(interval, elapsedDays, maximumInterval float64) (minIvl, maxIvl int) {
delta := 1.0
for _, r := range FUZZ_RANGES {
delta += r.Factor * math.Max(math.Min(interval, r.End)-r.Start, 0.0)
}

interval = math.Min(interval, maximumInterval)
minIvlFloat := math.Max(2, math.Round(interval-delta))
maxIvlFloat := math.Min(math.Round(interval+delta), maximumInterval)

if interval > elapsedDays {
minIvlFloat = math.Max(minIvlFloat, elapsedDays+1)
}
minIvlFloat = math.Min(minIvlFloat, maxIvlFloat)

minIvl = int(minIvlFloat)
maxIvl = int(maxIvlFloat)

return minIvl, maxIvl
}
9 changes: 9 additions & 0 deletions scheduler.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fsrs

import (
"fmt"
"math"
"time"
)
Expand Down Expand Up @@ -45,6 +46,13 @@ func (s *Scheduler) Review(grade Rating) SchedulingInfo {
return item
}

func (s *Scheduler) initSeed() {
time := s.now
reps := s.current.Reps
mul := s.current.Difficulty * s.current.Stability
s.parameters.seed = fmt.Sprintf("%d_%d_%f", time.Unix(), reps, mul)
}

func (s *Scheduler) buildLog(rating Rating) ReviewLog {
return ReviewLog{
Rating: rating,
Expand All @@ -71,6 +79,7 @@ func (p *Parameters) newScheduler(card Card, now time.Time, newImpl func(s *Sche
s.current.LastReview = s.now
s.current.ElapsedDays = uint64(interval)
s.current.Reps++
s.initSeed()

s.impl = newImpl(s)

Expand Down
18 changes: 10 additions & 8 deletions scheduler_basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func (bs basicScheduler) newState(grade Rating) SchedulingInfo {
case Easy:
easyInterval := bs.parameters.nextInterval(
next.Stability,
float64(next.ElapsedDays),
)
next.ScheduledDays = uint64(easyInterval)
next.Due = bs.now.Add(time.Duration(easyInterval) * 24 * time.Hour)
Expand All @@ -64,6 +65,7 @@ func (bs basicScheduler) learningState(grade Rating) SchedulingInfo {
}

next := bs.current
interval := float64(bs.current.ElapsedDays)
next.Difficulty = bs.parameters.nextDifficulty(bs.last.Difficulty, grade)
next.Stability = bs.parameters.shortTermStability(bs.last.Stability, grade)

Expand All @@ -77,15 +79,15 @@ func (bs basicScheduler) learningState(grade Rating) SchedulingInfo {
next.Due = bs.now.Add(10 * time.Minute)
next.State = bs.last.State
case Good:
goodInterval := bs.parameters.nextInterval(next.Stability)
goodInterval := bs.parameters.nextInterval(next.Stability, interval)
next.ScheduledDays = uint64(goodInterval)
next.Due = bs.now.Add(time.Duration(goodInterval) * 24 * time.Hour)
next.State = Review
case Easy:
goodStability := bs.parameters.shortTermStability(bs.last.Stability, Good)
goodInterval := bs.parameters.nextInterval(goodStability)
goodInterval := bs.parameters.nextInterval(goodStability, interval)
easyInterval := math.Max(
bs.parameters.nextInterval(next.Stability),
bs.parameters.nextInterval(next.Stability, interval),
float64(goodInterval)+1,
)
next.ScheduledDays = uint64(easyInterval)
Expand Down Expand Up @@ -118,7 +120,7 @@ func (bs basicScheduler) reviewState(grade Rating) SchedulingInfo {
nextEasy := bs.current

bs.nextDs(&nextAgain, &nextHard, &nextGood, &nextEasy, difficulty, stability, retrievability)
bs.nextInterval(&nextAgain, &nextHard, &nextGood, &nextEasy)
bs.nextInterval(&nextAgain, &nextHard, &nextGood, &nextEasy, interval)
bs.nextState(&nextAgain, &nextHard, &nextGood, &nextEasy)
nextAgain.Lapses++

Expand Down Expand Up @@ -149,13 +151,13 @@ func (bs basicScheduler) nextDs(nextAgain, nextHard, nextGood, nextEasy *Card, d
nextEasy.Stability = bs.parameters.nextRecallStability(difficulty, stability, retrievability, Easy)
}

func (bs basicScheduler) nextInterval(nextAgain, nextHard, nextGood, nextEasy *Card) {
hardInterval := bs.parameters.nextInterval(nextHard.Stability)
goodInterval := bs.parameters.nextInterval(nextGood.Stability)
func (bs basicScheduler) nextInterval(nextAgain, nextHard, nextGood, nextEasy *Card, elapsedDays float64) {
hardInterval := bs.parameters.nextInterval(nextHard.Stability, elapsedDays)
goodInterval := bs.parameters.nextInterval(nextGood.Stability, elapsedDays)
hardInterval = math.Min(hardInterval, goodInterval)
goodInterval = math.Max(goodInterval, hardInterval+1)
easyInterval := math.Max(
bs.parameters.nextInterval(nextEasy.Stability),
bs.parameters.nextInterval(nextEasy.Stability, elapsedDays),
goodInterval+1,
)

Expand Down
14 changes: 7 additions & 7 deletions scheduler_longterm.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (lts longTermScheduler) newState(grade Rating) SchedulingInfo {

lts.initDs(&nextAgain, &nextHard, &nextGood, &nextEasy)

lts.nextInterval(&nextAgain, &nextHard, &nextGood, &nextEasy)
lts.nextInterval(&nextAgain, &nextHard, &nextGood, &nextEasy, 0)
lts.nextState(&nextAgain, &nextHard, &nextGood, &nextEasy)
lts.updateNext(&nextAgain, &nextHard, &nextGood, &nextEasy)

Expand Down Expand Up @@ -75,7 +75,7 @@ func (lts longTermScheduler) reviewState(grade Rating) SchedulingInfo {
nextEasy := lts.current

lts.nextDs(&nextAgain, &nextHard, &nextGood, &nextEasy, difficulty, stability, retrievability)
lts.nextInterval(&nextAgain, &nextHard, &nextGood, &nextEasy)
lts.nextInterval(&nextAgain, &nextHard, &nextGood, &nextEasy, interval)
lts.nextState(&nextAgain, &nextHard, &nextGood, &nextEasy)
nextAgain.Lapses++

Expand All @@ -97,11 +97,11 @@ func (lts longTermScheduler) nextDs(nextAgain, nextHard, nextGood, nextEasy *Car
nextEasy.Stability = lts.parameters.nextRecallStability(difficulty, stability, retrievability, Easy)
}

func (lts longTermScheduler) nextInterval(nextAgain, nextHard, nextGood, nextEasy *Card) {
againInterval := lts.parameters.nextInterval(nextAgain.Stability)
hardInterval := lts.parameters.nextInterval(nextHard.Stability)
goodInterval := lts.parameters.nextInterval(nextGood.Stability)
easyInterval := lts.parameters.nextInterval(nextEasy.Stability)
func (lts longTermScheduler) nextInterval(nextAgain, nextHard, nextGood, nextEasy *Card, elapsedDays float64) {
againInterval := lts.parameters.nextInterval(nextAgain.Stability, elapsedDays)
hardInterval := lts.parameters.nextInterval(nextHard.Stability, elapsedDays)
goodInterval := lts.parameters.nextInterval(nextGood.Stability, elapsedDays)
easyInterval := lts.parameters.nextInterval(nextEasy.Stability, elapsedDays)

againInterval = math.Min(againInterval, hardInterval)
hardInterval = math.Max(hardInterval, againInterval+1)
Expand Down
Loading