fitts

Crates.iofitts
lib.rsfitts
version0.2.1
created_at2025-03-25 04:11:37.303278+00
updated_at2025-12-28 22:09:07.881458+00
descriptionSpaced repetition scheduler using Fitts' Law for difficulty prediction and SM-2 for interval scheduling.
homepagehttps://github.com/brenogonzaga/fitts
repositoryhttps://github.com/brenogonzaga/fitts
max_upload_size
id1604722
size80,868
Breno Gonzaga (brenogonzaga)

documentation

https://docs.rs/fitts

README

Fitts Memory Scheduler

Spaced repetition scheduler combining SM-2 for interval scheduling with Fitts' Law for adaptive difficulty prediction.

Key Innovation: Dual-Signal Capture

Traditional flashcard apps only capture rating (subjective). This library captures both:

Signal Type Source Use
Rating Subjective User input "How well did I recall?" → SM-2 intervals
Response Time Objective Measured "How fast did I recall?" → Fitts calibration

A user might:

  • Respond fast but rate Hard (implicit memory worked, felt unsure)
  • Respond slow but rate Easy (was distracted, knew the answer)

By capturing both, the model personalizes to each user.

Installation

[dependencies]
fitts = "0.1"

Quick Start

use fitts::{FittsScheduler, CardState, Rating, ReviewInput};

fn main() {
    let mut scheduler = FittsScheduler::new();
    let card = CardState::default();

    // Predict difficulty before showing answer
    let (predicted_rt, retrievability) = scheduler.predict(&card);
    println!("Predicted: {:.2}s, Retrievability: {:.0}%", predicted_rt, retrievability * 100.0);

    // User responds... app measures time
    let input = ReviewInput::new(Rating::Good, 2500); // 2.5 seconds

    // Process review with both signals
    let result = scheduler.review(card, input);
    println!("Next review in {} days", result.card.interval_days);

    // See prediction error (model learns from this)
    if let Some(error) = result.prediction_error {
        println!("Prediction error: {:+.2}s", error);
    }
}

Architecture

┌─────────────────────────────────────────────────────────────┐
│                     ReviewInput                              │
│              ┌──────────────┬──────────────┐                │
│              │   Rating     │ Response Time │                │
│              │ (subjective) │  (objective)  │                │
│              └──────┬───────┴───────┬──────┘                │
│                     │               │                        │
│              ┌──────▼─────┐  ┌──────▼──────┐                │
│              │    SM-2    │  │  Fitts Law  │                │
│              │            │  │             │                │
│              │ → interval │  │ → calibrate │                │
│              │ → ease     │  │ → personalize│                │
│              └──────┬─────┘  └──────┬──────┘                │
│                     │               │                        │
│              ┌──────▼───────────────▼──────┐                │
│              │       ReviewResult          │                │
│              │ • Updated card state        │                │
│              │ • Predicted vs actual RT    │                │
│              │ • Calibration results       │                │
│              └─────────────────────────────┘                │
└─────────────────────────────────────────────────────────────┘

Adaptive Calibration

The Fitts model learns from your actual response times using gradient descent:

use fitts::{FittsScheduler, CardState, Rating, ReviewInput};

let mut scheduler = FittsScheduler::with_learning_rate(0.1);
let card = CardState::default();

// Initial prediction
let (initial, _) = scheduler.predict(&card);
println!("Initial prediction: {:.2}s", initial);

// After 15 reviews, the model adapts
// If user consistently responds in ~2 seconds, model learns this
for _ in 0..15 {
    let input = ReviewInput::new(Rating::Good, 2000);
    scheduler.review(card.clone(), input);
}

let (calibrated, _) = scheduler.predict(&card);
println!("Calibrated prediction: {:.2}s", calibrated);
// Prediction now closer to 2 seconds

Algorithms

SM-2 (Wozniak, 1987)

Classic spaced repetition algorithm:

interval(0) = 1 day
interval(1) = 6 days
interval(n) = interval(n-1) × EF

EF' = EF + (0.1 - (5-q) × (0.08 + (5-q) × 0.02))
EF ≥ 1.3

Note: This implementation uses a 0-3 quality scale (Again/Hard/Good/Easy) instead of SM-2's original 0-5 scale. Quality values are linearly mapped to 1-5 internally for ease factor calculation: q_scaled = 1 + q × (4/3).

Fitts' Law (1954) + Memory Adaptation

Original (MacKenzie, 1992): MT = a + b × log₂(D/W + 1)

Memory adaptation:

  • Distance = memory decay = ln(1 + interval) / ease
  • Width = accessibility = stability × ease
  • ID (Index of Difficulty) = log₂(distance/accessibility + 1)

Formula: RT = a + b × ID

Gradient Descent Calibration (Pavlik & Anderson, 2008)

error = RT_actual - RT_predicted
a_new = a + α × error
b_new = b + α × error × ID

Where:

  • α is the learning rate (default: 0.05)
  • ID is the Index of Difficulty (already computed as log₂ term)

Retrievability (Memory Strength)

Uses logistic function based on response time:

R = 1 / (1 + exp((RT - τ) / σ))
  • When RT < τ (threshold): R > 0.5 (likely to recall)
  • When RT = τ: R = 0.5 (50/50 chance)
  • When RT > τ: R < 0.5 (unlikely to recall)

API Reference

ReviewInput

// Full input with both signals
let input = ReviewInput::new(Rating::Good, 2500);

// Rating only (backward compatible)
scheduler.review(card, Rating::Good);

FittsScheduler

// Default scheduler
let mut scheduler = FittsScheduler::new();

// With custom learning rate
let mut scheduler = FittsScheduler::with_learning_rate(0.1);

// Predict difficulty
let (response_time, retrievability) = scheduler.predict(&card);

// Review with adaptation
let result = scheduler.review(card, input);

// Order cards by difficulty (hardest first)
scheduler.order_by_difficulty(&mut cards);

ReviewResult

pub struct ReviewResult {
    pub card: CardState,           // Updated card state
    pub predicted_rt: f64,         // What model predicted
    pub actual_rt: Option<f64>,    // What actually happened
    pub prediction_error: Option<f64>,  // actual - predicted
    pub retrievability: f64,       // Memory strength (0-1)
    pub calibration: Option<CalibrationResult>,
}

Rating & DifficultyLevel

Both use 4 values for consistency:

Rating DifficultyLevel SM-2 Quality Status
Again VeryHard 0 Failure
Hard Hard 1 Success
Good Medium 2 Success
Easy Easy 3 Success

Note: Rating is subjective user input. Only Again resets the card. All others (Hard, Good, Easy) advance the card interval.

Examples

# Basic usage
cargo run --example basic

# SM-2 interval progression
cargo run --example sm2_progression

# Fitts model predictions
cargo run --example fitts_model

# Adaptive calibration demo
cargo run --example adaptive_calibration

Academic References

  • SM-2: Wozniak, P.A. (1990). "Optimization of learning"
  • Fitts' Law: Fitts, P.M. (1954). "The information capacity of the human motor system"
  • ACT-R Memory: Anderson, J.R. (1993). "Rules of the Mind"
  • Adaptive Scheduling: Pavlik, P.I. & Anderson, J.R. (2008). "Using a Model to Compute the Optimal Schedule of Practice"
  • Duolingo HLR: Settles, B. & Meeder, B. (2016). "A Trainable Spaced Repetition Model for Language Learning"

License

MIT OR Apache-2.0

Commit count: 0

cargo fmt