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
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
- .cards_array(cards) ⇒ Object
-
.compute_parameters(items, enable_short_term: true, num_relearning_steps: nil) ⇒ Object
Train 21 FSRS-6 parameters from review history.
-
.current_retrievability(state, days_elapsed, decay = FSRS6_DEFAULT_DECAY) ⇒ Object
Retrievability (recall probability) of a memory state after
days_elapseddays. -
.expected_workload(parameters, desired_retention, config: nil) ⇒ Object
Expected daily workload (seconds) at the given retention.
-
.extract_simulator_config(revlog_entries, day_cutoff, smooth: true) ⇒ Object
Calibrate a SimulatorConfig from Anki-style revlog rows.
- .flatten_items(items) ⇒ Object
- .flatten_reviews(reviews) ⇒ Object
- .memory_pair(state) ⇒ Object
- .memory_pair_or_empty(state) ⇒ Object
-
.optimal_retention(parameters, config: nil) ⇒ Object
Suggested retention (CMRR) minimizing workload for the memorized target.
- .params_array(parameters) ⇒ Object
-
.rating_int(rating) ⇒ Object
── Shared input coercion (also used by Nagori::FSRS) ──.
-
.simulate(parameters, desired_retention, config: nil, seed: nil, existing_cards: nil) ⇒ Object
Project reviews/day and memorized count over the horizon.
- .stringify_keys(hash) ⇒ Object
- .symbolize_keys(hash) ⇒ Object
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| = (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? [, delta_t] end end |
.memory_pair(state) ⇒ Object
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 () value = .is_a?(Symbol) ? RATINGS[] : value = Integer(value) if value unless value&.between?(1, 4) raise ArgumentError, "rating must be 1..4 or #{RATINGS.keys.inspect}, got #{.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 |