Skip to content

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).

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).

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)
}

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.

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).

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_MULTIPLIER

The distance matrix determines which skills are related. When the answer is correct, propagation flows to parent skills; when incorrect, to child skills.

After both updates, knowledge is clipped to [KL, KU] (default [-5.0, 5.0]).

Heuristics determine how valuable it is to test each skill, combining knowledge state with graph structure.

Two modes compute how much “room” each skill has for knowledge change:

Parent mode (correct answer propagation):

  • 0 ≤ k < KUKU - |k| (room to grow upward)
  • KL < k ≤ 01 (constant weight)
  • |k| ≥ KU0 (saturated)

Child mode (incorrect answer propagation):

  • KL < k ≤ 0|k| - KL (room to grow downward)
  • 0 ≤ k < KU1 (constant weight)
  • |k| ≥ KU0 (saturated)
1. Compute parent and child knowledge heuristics
2. Multiply each by their graph heuristic matrix (transposed)
3. Average where both are non-zero → final heuristic vector

Full pipeline: heuristic → normalize → multiply → filter → argmax.

  1. Heuristic vector — compute combined heuristic from knowledge + graph data
  2. QSS matrix — build S×Q question-skill-score matrix from question pool, clip values to [0, 1]
  3. Normalize — divide each column by its skill sum (so multi-skill questions don’t dominate)
  4. Scorenormalized_qss.T × heuristic → score per question
  5. Filter — zero out already-prompted questions
  6. 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 index

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.

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.

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)
  • Student — how the engine integrates with the submission flow
  • Overview — assessment architecture
  • gRPC — how bundles are fetched from the diagnostics service