Skip to content

Commit

Permalink
Feat/fuzz
Browse files Browse the repository at this point in the history
  • Loading branch information
L-M-Sherlock committed Sep 10, 2024
1 parent cfc70c7 commit a8e6a99
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 18 deletions.
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

0 comments on commit a8e6a99

Please sign in to comment.