Class: FsrsRuby::Algorithm
- Inherits:
-
Object
- Object
- FsrsRuby::Algorithm
- Defined in:
- lib/fsrs_ruby/algorithm.rb
Overview
Core FSRS v6.0 algorithm implementation
Direct Known Subclasses
Instance Attribute Summary collapse
-
#interval_modifier ⇒ Object
readonly
Returns the value of attribute interval_modifier.
-
#parameters ⇒ Object
Returns the value of attribute parameters.
-
#seed ⇒ Object
Returns the value of attribute seed.
Instance Method Summary collapse
-
#apply_fuzz(ivl, elapsed_days) ⇒ Integer
Apply fuzz using Alea PRNG.
-
#calculate_interval_modifier(request_retention) ⇒ Float
Calculate interval modifier from request_retention.
-
#compute_decay_factor(w) ⇒ Hash
Compute decay factor from w.
-
#forgetting_curve(w, elapsed_days, stability) ⇒ Float
Forgetting curve formula.
-
#init_difficulty(g) ⇒ Float
CRITICAL: Exponential difficulty formula (NOT linear!).
-
#init_stability(g) ⇒ Float
Initial stability (simple lookup).
-
#initialize(params = {}) ⇒ Algorithm
constructor
A new instance of Algorithm.
-
#linear_damping(delta_d, old_d) ⇒ Float
NEW IN v6: Linear damping.
-
#mean_reversion(init, current) ⇒ Float
Mean reversion.
-
#next_difficulty(d, g) ⇒ Float
Next difficulty with linear damping.
-
#next_forget_stability(d, s, r) ⇒ Float
Next forget stability (for failed reviews).
-
#next_interval(s, elapsed_days = 0) ⇒ Integer
Calculate next interval.
-
#next_recall_stability(d, s, r, g) ⇒ Float
Next recall stability (for successful reviews).
-
#next_short_term_stability(s, g) ⇒ Float
NEW IN v6: Short-term stability.
-
#next_state(memory_state, t, g, r = nil) ⇒ Hash
Calculate next state of memory.
Constructor Details
#initialize(params = {}) ⇒ Algorithm
Returns a new instance of Algorithm.
9 10 11 12 13 |
# File 'lib/fsrs_ruby/algorithm.rb', line 9 def initialize(params = {}) @parameters = ParameterUtils.generate_parameters(params) @interval_modifier = calculate_interval_modifier(@parameters.request_retention) @seed = nil end |
Instance Attribute Details
#interval_modifier ⇒ Object (readonly)
Returns the value of attribute interval_modifier.
6 7 8 |
# File 'lib/fsrs_ruby/algorithm.rb', line 6 def interval_modifier @interval_modifier end |
#parameters ⇒ Object
Returns the value of attribute parameters.
6 7 8 |
# File 'lib/fsrs_ruby/algorithm.rb', line 6 def parameters @parameters end |
#seed ⇒ Object
Returns the value of attribute seed.
7 8 9 |
# File 'lib/fsrs_ruby/algorithm.rb', line 7 def seed @seed end |
Instance Method Details
#apply_fuzz(ivl, elapsed_days) ⇒ Integer
Apply fuzz using Alea PRNG
145 146 147 148 149 150 151 152 153 |
# File 'lib/fsrs_ruby/algorithm.rb', line 145 def apply_fuzz(ivl, elapsed_days) return ivl.round unless @parameters.enable_fuzz && ivl >= 2.5 prng = @seed ? FsrsRuby.alea(@seed) : FsrsRuby.alea(Time.now.to_i) fuzz_factor = prng.call fuzz_range = Helpers.get_fuzz_range(ivl, elapsed_days, @parameters.maximum_interval) (fuzz_factor * (fuzz_range[:max_ivl] - fuzz_range[:min_ivl] + 1) + fuzz_range[:min_ivl]).floor end |
#calculate_interval_modifier(request_retention) ⇒ Float
Calculate interval modifier from request_retention
44 45 46 47 48 49 |
# File 'lib/fsrs_ruby/algorithm.rb', line 44 def calculate_interval_modifier(request_retention) raise ArgumentError, 'Requested retention rate should be in the range (0,1]' if request_retention <= 0 || request_retention > 1 info = compute_decay_factor(@parameters.w) Helpers.round8((request_retention**(1.0 / info[:decay]) - 1) / info[:factor]) end |
#compute_decay_factor(w) ⇒ Hash
Compute decay factor from w
24 25 26 27 28 |
# File 'lib/fsrs_ruby/algorithm.rb', line 24 def compute_decay_factor(w) decay = w.is_a?(Array) ? -w[20] : -w factor = Math.exp(Math.log(0.9) / decay) - 1.0 { decay: decay, factor: Helpers.round8(factor) } end |
#forgetting_curve(w, elapsed_days, stability) ⇒ Float
Forgetting curve formula
35 36 37 38 39 |
# File 'lib/fsrs_ruby/algorithm.rb', line 35 def forgetting_curve(w, elapsed_days, stability) info = compute_decay_factor(w) result = (1 + (info[:factor] * elapsed_days) / stability)**info[:decay] Helpers.round8(result) end |
#init_difficulty(g) ⇒ Float
CRITICAL: Exponential difficulty formula (NOT linear!)
61 62 63 64 |
# File 'lib/fsrs_ruby/algorithm.rb', line 61 def init_difficulty(g) d = @parameters.w[4] - Math.exp((g - 1) * @parameters.w[5]) + 1 Helpers.round8(d) end |
#init_stability(g) ⇒ Float
Initial stability (simple lookup)
54 55 56 |
# File 'lib/fsrs_ruby/algorithm.rb', line 54 def init_stability(g) [@parameters.w[g - 1], Constants::S_MIN].max end |
#linear_damping(delta_d, old_d) ⇒ Float
NEW IN v6: Linear damping
70 71 72 |
# File 'lib/fsrs_ruby/algorithm.rb', line 70 def linear_damping(delta_d, old_d) Helpers.round8((delta_d * (10 - old_d)) / 9.0) end |
#mean_reversion(init, current) ⇒ Float
Mean reversion
78 79 80 |
# File 'lib/fsrs_ruby/algorithm.rb', line 78 def mean_reversion(init, current) Helpers.round8(@parameters.w[7] * init + (1 - @parameters.w[7]) * current) end |
#next_difficulty(d, g) ⇒ Float
Next difficulty with linear damping
86 87 88 89 90 |
# File 'lib/fsrs_ruby/algorithm.rb', line 86 def next_difficulty(d, g) delta_d = -@parameters.w[6] * (g - 3) next_d = d + linear_damping(delta_d, d) Helpers.clamp(mean_reversion(init_difficulty(Rating::EASY), next_d), 1, 10) end |
#next_forget_stability(d, s, r) ⇒ Float
Next forget stability (for failed reviews)
119 120 121 122 123 124 125 126 127 128 |
# File 'lib/fsrs_ruby/algorithm.rb', line 119 def next_forget_stability(d, s, r) new_s = ( @parameters.w[11] * (d**-@parameters.w[12]) * ((s + 1)**@parameters.w[13] - 1) * Math.exp((1 - r) * @parameters.w[14]) ) Helpers.clamp(Helpers.round8(new_s), Constants::S_MIN, Constants::S_MAX) end |
#next_interval(s, elapsed_days = 0) ⇒ Integer
Calculate next interval
159 160 161 162 163 |
# File 'lib/fsrs_ruby/algorithm.rb', line 159 def next_interval(s, elapsed_days = 0) new_interval = [(s * @interval_modifier).round, 1].max new_interval = [new_interval, @parameters.maximum_interval].min apply_fuzz(new_interval, elapsed_days) end |
#next_recall_stability(d, s, r, g) ⇒ Float
Next recall stability (for successful reviews)
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/fsrs_ruby/algorithm.rb', line 98 def next_recall_stability(d, s, r, g) hard_penalty = g == Rating::HARD ? @parameters.w[15] : 1 easy_bonus = g == Rating::EASY ? @parameters.w[16] : 1 new_s = s * ( 1 + Math.exp(@parameters.w[8]) * (11 - d) * (s**-@parameters.w[9]) * (Math.exp((1 - r) * @parameters.w[10]) - 1) * hard_penalty * easy_bonus ) Helpers.clamp(Helpers.round8(new_s), Constants::S_MIN, Constants::S_MAX) end |
#next_short_term_stability(s, g) ⇒ Float
NEW IN v6: Short-term stability
134 135 136 137 138 139 |
# File 'lib/fsrs_ruby/algorithm.rb', line 134 def next_short_term_stability(s, g) sinc = (s**-@parameters.w[19]) * Math.exp(@parameters.w[17] * (g - 3 + @parameters.w[18])) masked_sinc = g >= Rating::HARD ? [sinc, 1.0].max : sinc Helpers.clamp(Helpers.round8(s * masked_sinc), Constants::S_MIN, Constants::S_MAX) end |
#next_state(memory_state, t, g, r = nil) ⇒ Hash
Calculate next state of memory
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/fsrs_ruby/algorithm.rb', line 171 def next_state(memory_state, t, g, r = nil) d = memory_state ? memory_state[:difficulty] : 0 s = memory_state ? memory_state[:stability] : 0 raise ArgumentError, "Invalid delta_t \"#{t}\"" if t < 0 raise ArgumentError, "Invalid grade \"#{g}\"" if g < 0 || g > 4 # First review if d == 0 && s == 0 return { difficulty: Helpers.clamp(init_difficulty(g), 1, 10), stability: init_stability(g) } end # Manual grade if g == 0 return { difficulty: d, stability: s } end # Validate state if d < 1 || s < Constants::S_MIN raise ArgumentError, "Invalid memory state { difficulty: #{d}, stability: #{s} }" end # Calculate retrievability if not provided r = forgetting_curve(@parameters.w, t, s) if r.nil? # Calculate possible next stabilities s_after_success = next_recall_stability(d, s, r, g) s_after_fail = next_forget_stability(d, s, r) s_after_short_term = next_short_term_stability(s, g) # Select appropriate stability new_s = s_after_success if g == Rating::AGAIN w_17 = @parameters.enable_short_term ? @parameters.w[17] : 0 w_18 = @parameters.enable_short_term ? @parameters.w[18] : 0 next_s_min = s / Math.exp(w_17 * w_18) new_s = Helpers.clamp(Helpers.round8(next_s_min), Constants::S_MIN, s_after_fail) end if t == 0 && @parameters.enable_short_term new_s = s_after_short_term end new_d = next_difficulty(d, g) { difficulty: new_d, stability: new_s } end |