| Crates.io | fitts |
| lib.rs | fitts |
| version | 0.2.1 |
| created_at | 2025-03-25 04:11:37.303278+00 |
| updated_at | 2025-12-28 22:09:07.881458+00 |
| description | Spaced repetition scheduler using Fitts' Law for difficulty prediction and SM-2 for interval scheduling. |
| homepage | https://github.com/brenogonzaga/fitts |
| repository | https://github.com/brenogonzaga/fitts |
| max_upload_size | |
| id | 1604722 |
| size | 80,868 |
Spaced repetition scheduler combining SM-2 for interval scheduling with Fitts' Law for adaptive difficulty prediction.
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:
By capturing both, the model personalizes to each user.
[dependencies]
fitts = "0.1"
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);
}
}
┌─────────────────────────────────────────────────────────────┐
│ ReviewInput │
│ ┌──────────────┬──────────────┐ │
│ │ Rating │ Response Time │ │
│ │ (subjective) │ (objective) │ │
│ └──────┬───────┴───────┬──────┘ │
│ │ │ │
│ ┌──────▼─────┐ ┌──────▼──────┐ │
│ │ SM-2 │ │ Fitts Law │ │
│ │ │ │ │ │
│ │ → interval │ │ → calibrate │ │
│ │ → ease │ │ → personalize│ │
│ └──────┬─────┘ └──────┬──────┘ │
│ │ │ │
│ ┌──────▼───────────────▼──────┐ │
│ │ ReviewResult │ │
│ │ • Updated card state │ │
│ │ • Predicted vs actual RT │ │
│ │ • Calibration results │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
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
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).
Original (MacKenzie, 1992): MT = a + b × log₂(D/W + 1)
Memory adaptation:
ln(1 + interval) / easestability × easelog₂(distance/accessibility + 1)Formula: RT = a + b × ID
error = RT_actual - RT_predicted
a_new = a + α × error
b_new = b + α × error × ID
Where:
Uses logistic function based on response time:
R = 1 / (1 + exp((RT - τ) / σ))
// Full input with both signals
let input = ReviewInput::new(Rating::Good, 2500);
// Rating only (backward compatible)
scheduler.review(card, Rating::Good);
// 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);
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>,
}
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.
# 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
MIT OR Apache-2.0