Adaptive Algorithm
The adaptive algorithm engine drives question selection in StudentAssessment. It maintains a knowledge state vector, propagates updates through a skill graph, and scores candidate questions using heuristics.
Ported from the Python diagnostics service (progresses.py, heuristics.py, algorithms.py).
AlgorithmEngine
Section titled “AlgorithmEngine”pub struct AlgorithmEngine { pub distance_matrix: DMatrix<f32>, // S×S graph distances pub heuristic_matrix: DMatrix<f32>, // S×S graph heuristics pub question_pool: Vec<QuestionData>, // all available questions pub config: AlgorithmConfig,}Built from an AlgorithmBundle fetched from the server (see gRPC).
AlgorithmConfig
Section titled “AlgorithmConfig”pub struct AlgorithmConfig { pub skill_count: usize, pub ku: f32, // upper knowledge threshold (default 5.0) pub kl: f32, // lower knowledge threshold (default -5.0) pub direct_multiplier: f32, // direct update weight (default 4.0) pub indirect_multiplier: f32, // indirect propagation weight (default 2.5)}AlgorithmBundle
Section titled “AlgorithmBundle”Server-provided data blob containing sparse matrices and the question pool:
pub struct AlgorithmBundle { pub skill_count: usize, pub distance_indices: Vec<usize>, // sparse distance matrix (row-major) pub distance_values: Vec<f32>, pub heuristic_indices: Vec<usize>, // sparse heuristic matrix (row-major) pub heuristic_values: Vec<f32>, pub questions: Vec<QuestionData>, pub config: Option<AlgorithmConfig>, // overrides defaults}AlgorithmBundle::into_engine() converts sparse representations to dense nalgebra matrices.
Knowledge Updates
Section titled “Knowledge Updates”Direct Knowledge
Section titled “Direct Knowledge”Immediate impact from answering a question on tested skills:
updated[i] = knowledge[i] + (±1 × score_factor × DIRECT_MULTIPLIER)Only skills in the question’s skill_indicator are affected. The sign depends on correctness (+1 correct, -1 incorrect).
Indirect Knowledge
Section titled “Indirect Knowledge”Propagation along graph edges to skills not directly tested:
For each untested skill c: delta[c] = Σ_r (distance[r][c] != 0) × question_skill_score[r] × INDIRECT_MULTIPLIERThe distance matrix determines which skills are related. When the answer is correct, propagation flows to parent skills; when incorrect, to child skills.
Clipping
Section titled “Clipping”After both updates, knowledge is clipped to [KL, KU] (default [-5.0, 5.0]).
Heuristic Scoring
Section titled “Heuristic Scoring”Heuristics determine how valuable it is to test each skill, combining knowledge state with graph structure.
Knowledge Heuristics
Section titled “Knowledge Heuristics”Two modes compute how much “room” each skill has for knowledge change:
Parent mode (correct answer propagation):
0 ≤ k < KU→KU - |k|(room to grow upward)KL < k ≤ 0→1(constant weight)|k| ≥ KU→0(saturated)
Child mode (incorrect answer propagation):
KL < k ≤ 0→|k| - KL(room to grow downward)0 ≤ k < KU→1(constant weight)|k| ≥ KU→0(saturated)
Combined Heuristic
Section titled “Combined Heuristic”1. Compute parent and child knowledge heuristics2. Multiply each by their graph heuristic matrix (transposed)3. Average where both are non-zero → final heuristic vectorNext Question Selection
Section titled “Next Question Selection”Full pipeline: heuristic → normalize → multiply → filter → argmax.
Pipeline Steps
Section titled “Pipeline Steps”- Heuristic vector — compute combined heuristic from knowledge + graph data
- QSS matrix — build S×Q question-skill-score matrix from question pool, clip values to
[0, 1] - Normalize — divide each column by its skill sum (so multi-skill questions don’t dominate)
- Score —
normalized_qss.T × heuristic → score per question - Filter — zero out already-prompted questions
- Argmax — select highest-scoring question
Returns None if no valid questions remain.
pub fn compute_next_question( engine: &AlgorithmEngine, knowledge: &DVector<f32>, indicator: &DVector<f32>, qss_matrix: &DMatrix<f32>, prompted_indices: &[usize],) -> Option<usize> // question pool indexProgress Mapping
Section titled “Progress Mapping”Converts knowledge values to [0, 1] progress for display:
pub struct Progress { pub correct: f32, // positive knowledge ratio pub wrong: f32, // negative knowledge ratio}Mapping:
- Correct:
k ≥ KU → 1.0,0 < k < KU → k/KU,k ≤ 0 → 0.0 - Wrong:
k ≤ KL → 1.0,KL < k < 0 → k/KL,k ≥ 0 → 0.0
knowledge_to_progress() averages across skills weighted by the indicator vector.
Precomputed Branches
Section titled “Precomputed Branches”To minimize latency, the engine precomputes next questions for both outcomes while the student is answering:
pub enum StudentAssessmentFeedback { PrecomputedBranch { if_correct: Option<String>, if_incorrect: Option<String>, }, NextQuestionComputed { question_id: String },}When the student submits, the correct branch is used immediately without waiting for recomputation.
Test Coverage
Section titled “Test Coverage”The algorithm module has 400+ tests covering:
- Direct and indirect knowledge updates (15 tests)
- Parent/child heuristics and boundary cases (10 tests)
- QSS normalization, filtering, and full pipeline (10 tests)
- Progress vector computation and aggregation (10 tests)
- Sparse/dense conversion roundtrips and bundle deserialization (11 tests)