Module: Nagori

Defined in:
lib/nagori.rb,
lib/nagori/version.rb

Overview

Ruby bindings for fsrs-rs (the fsrs crate), FSRS-6.

Plain values in and out: reviews are { rating:, delta_t: } hashes (rating 1=Again 2=Hard 3=Good 4=Easy; delta_t = whole days since the previous review, 0 for the first review and same-day re-reviews). Intervals come back as fractional, unrounded f32 days — rounding/fuzz policy lives in the caller.

Defined Under Namespace

Classes: Error, FSRS

Constant Summary collapse

DEFAULT_PARAMETERS =

The 21-float FSRS-6 default weights (fallback when a user has no trained parameters yet).

_default_parameters.freeze
FSRS6_DEFAULT_DECAY =

FSRS-6's default forgetting-curve decay; the default for current_retrievability's decay argument. Equals DEFAULT_PARAMETERS.

0.1542
RATINGS =

Rating encoding, matching Anki / Shirabe's Review::RATINGS.

{ again: 1, hard: 2, good: 3, easy: 4 }.freeze
VERSION =
"0.1.0".freeze

Class Method Summary collapse

Class Method Details

.cards_array(cards) ⇒ Object



141
142
143
# File 'lib/nagori.rb', line 141

def cards_array(cards)
  cards.nil? ? nil : Array(cards).map { |card| stringify_keys(card) }
end

.compute_parameters(items, enable_short_term: true, num_relearning_steps: nil) ⇒ Object

Train 21 FSRS-6 parameters from review history. items is an array of cards, each a chronological array of { rating:, delta_t: } reviews. Returns 21 floats. Releases the GVL — this is CPU-heavy.



39
40
41
42
43
44
45
# File 'lib/nagori.rb', line 39

def compute_parameters(items, enable_short_term: true, num_relearning_steps: nil)
  unless num_relearning_steps.nil?
    num_relearning_steps = Integer(num_relearning_steps)
    raise ArgumentError, "num_relearning_steps must be >= 0" if num_relearning_steps.negative?
  end
  _compute_parameters(flatten_items(items), enable_short_term ? true : false, num_relearning_steps)
end

.current_retrievability(state, days_elapsed, decay = FSRS6_DEFAULT_DECAY) ⇒ Object

Retrievability (recall probability) of a memory state after days_elapsed days. state is { stability:, difficulty: }.



49
50
51
52
# File 'lib/nagori.rb', line 49

def current_retrievability(state, days_elapsed, decay = FSRS6_DEFAULT_DECAY)
  stability, difficulty = memory_pair(state)
  _current_retrievability(stability, difficulty, Float(days_elapsed), Float(decay))
end

.expected_workload(parameters, desired_retention, config: nil) ⇒ Object

Expected daily workload (seconds) at the given retention. Powers the retention-slider preview.



71
72
73
# File 'lib/nagori.rb', line 71

def expected_workload(parameters, desired_retention, config: nil)
  _expected_workload(params_array(parameters), Float(desired_retention), stringify_keys(config))
end

.extract_simulator_config(revlog_entries, day_cutoff, smooth: true) ⇒ Object

Calibrate a SimulatorConfig from Anki-style revlog rows. Each entry is a hash with keys :id, :cid, :usn, :button_chosen, :interval, :last_interval, :ease_factor, :taken_millis, :review_kind (0..4). Returns a config hash suitable for simulate/expected_workload.



85
86
87
88
89
# File 'lib/nagori.rb', line 85

def extract_simulator_config(revlog_entries, day_cutoff, smooth: true)
  entries = Array(revlog_entries).map { |entry| stringify_keys(entry) }
  config = _extract_simulator_config(entries, Integer(day_cutoff), smooth ? true : false)
  symbolize_keys(config)
end

.flatten_items(items) ⇒ Object



113
114
115
# File 'lib/nagori.rb', line 113

def flatten_items(items)
  Array(items).map { |reviews| flatten_reviews(reviews) }
end

.flatten_reviews(reviews) ⇒ Object



103
104
105
106
107
108
109
110
111
# File 'lib/nagori.rb', line 103

def flatten_reviews(reviews)
  Array(reviews).flat_map do |review|
    rating = rating_int(review.fetch(:rating) { review[:rating] })
    delta_t = Integer(review.fetch(:delta_t) { review[:delta_t] })
    raise ArgumentError, "delta_t must be >= 0, got #{delta_t}" if delta_t.negative?

    [rating, delta_t]
  end
end

.memory_pair(state) ⇒ Object

Raises:

  • (ArgumentError)


117
118
119
120
121
# File 'lib/nagori.rb', line 117

def memory_pair(state)
  raise ArgumentError, "memory state is required" if state.nil?

  [Float(state.fetch(:stability)), Float(state.fetch(:difficulty))]
end

.memory_pair_or_empty(state) ⇒ Object



123
124
125
# File 'lib/nagori.rb', line 123

def memory_pair_or_empty(state)
  state.nil? ? [] : memory_pair(state)
end

.optimal_retention(parameters, config: nil) ⇒ Object

Suggested retention (CMRR) minimizing workload for the memorized target. Releases the GVL.



77
78
79
# File 'lib/nagori.rb', line 77

def optimal_retention(parameters, config: nil)
  _optimal_retention(params_array(parameters), stringify_keys(config))
end

.params_array(parameters) ⇒ Object



127
128
129
130
131
# File 'lib/nagori.rb', line 127

def params_array(parameters)
  return [] if parameters.nil?

  Array(parameters).map { |value| Float(value) }
end

.rating_int(rating) ⇒ Object

── Shared input coercion (also used by Nagori::FSRS) ──



93
94
95
96
97
98
99
100
101
# File 'lib/nagori.rb', line 93

def rating_int(rating)
  value = rating.is_a?(Symbol) ? RATINGS[rating] : rating
  value = Integer(value) if value
  unless value&.between?(1, 4)
    raise ArgumentError, "rating must be 1..4 or #{RATINGS.keys.inspect}, got #{rating.inspect}"
  end

  value
end

.simulate(parameters, desired_retention, config: nil, seed: nil, existing_cards: nil) ⇒ Object

Project reviews/day and memorized count over the horizon. Returns a hash of per-day arrays (:memorized_cnt_per_day, :review_cnt_per_day, :learn_cnt_per_day, :cost_per_day, :correct_cnt_per_day, :introduced_cnt_per_day, :average_desired_retention). Releases the GVL.



58
59
60
61
62
63
64
65
66
67
# File 'lib/nagori.rb', line 58

def simulate(parameters, desired_retention, config: nil, seed: nil, existing_cards: nil)
  result = _simulate(
    params_array(parameters),
    Float(desired_retention),
    stringify_keys(config),
    seed.nil? ? nil : Integer(seed),
    cards_array(existing_cards)
  )
  symbolize_keys(result)
end

.stringify_keys(hash) ⇒ Object



133
134
135
# File 'lib/nagori.rb', line 133

def stringify_keys(hash)
  hash&.to_h { |key, value| [key.to_s, value] }
end

.symbolize_keys(hash) ⇒ Object



137
138
139
# File 'lib/nagori.rb', line 137

def symbolize_keys(hash)
  hash.to_h { |key, value| [key.to_sym, value] }
end