Download backend/app/engine/cat_engine.py from YVES28/lerningOs: direct link, hf CLI and curl.
- Browser
- Download file 5.62 kB
-
https://huggingface.co/spaces/YVES28/lerningOs/resolve/main/backend/app/engine/cat_engine.py
- Command line
-
hf download hf://spaces/YVES28/lerningOs/backend/app/engine/cat_engine.py
-
curl -L -o cat_engine.py https://huggingface.co/spaces/YVES28/lerningOs/resolve/main/backend/app/engine/cat_engine.py
5.62 kB
| """ | |
| IRT 2PL (Two-Parameter Logistic) engine for Computerized Adaptive Testing (CAT). | |
| Provides: | |
| - IRT2PLEngine.estimate_theta_eap(responses) -> (theta, sem) | |
| - IRT2PLEngine.select_next_item(theta, candidate_items, administered_ids) -> item | None | |
| The EAP (Expected A Posteriori) estimator uses a standard-normal prior and | |
| 41-point Gauss-Legendre quadrature over [-4, +4]. Item selection uses | |
| Maximum Fisher Information, which for the 2PL model is maximised when the | |
| item difficulty b is close to the current ability estimate theta. | |
| """ | |
| from functools import lru_cache | |
| import math | |
| class IRT2PLEngine: | |
| """Core IRT 2PL engine: ability estimation + adaptive item selection.""" | |
| # 41-point quadrature nodes over [-4, +4] | |
| QUAD_NODES = [-4.0 + i * (8.0 / 40.0) for i in range(41)] | |
| # ------------------------------------------------------------------ # | |
| # Probability helpers | |
| # ------------------------------------------------------------------ # | |
| def _probability(theta: float, a: float, b: float) -> float: | |
| """P(correct | theta, a, b) under the 2PL model.""" | |
| return 1.0 / (1.0 + math.exp(-a * (theta - b))) | |
| def probability(theta: float, a: float, b: float) -> float: | |
| """Public wrapper around the cached probability.""" | |
| return IRT2PLEngine._probability(round(theta, 4), round(a, 4), round(b, 4)) | |
| # ------------------------------------------------------------------ # | |
| # Ability estimation (EAP with standard-normal prior) | |
| # ------------------------------------------------------------------ # | |
| def estimate_theta_eap(responses) -> tuple: | |
| """ | |
| Estimate ability (theta) and standard error of measurement (SEM) | |
| via Expected A Posteriori estimation. | |
| Parameters | |
| ---------- | |
| responses : list[tuple] | |
| Each tuple is (a, b, is_correct) where | |
| a = discrimination parameter | |
| b = difficulty parameter | |
| is_correct = bool whether the response was correct | |
| Returns | |
| ------- | |
| (theta, sem) : tuple[float, float] | |
| """ | |
| if not responses: | |
| return 0.0, 1.0 | |
| nodes = IRT2PLEngine.QUAD_NODES | |
| weights = [] | |
| for x in nodes: | |
| prior = (1.0 / math.sqrt(2 * math.pi)) * math.exp(-0.5 * (x ** 2)) | |
| likelihood = 1.0 | |
| for a, b, is_correct in responses: | |
| p = IRT2PLEngine._probability(round(x, 4), round(a, 4), round(b, 4)) | |
| likelihood *= p if is_correct else (1.0 - p) | |
| weights.append(prior * likelihood) | |
| sum_w = sum(weights) | |
| if sum_w == 0: | |
| return 0.0, 1.0 | |
| norm_weights = [w / sum_w for w in weights] | |
| eap_theta = sum(w * x for w, x in zip(norm_weights, nodes)) | |
| variance = sum( | |
| w * ((x - eap_theta) ** 2) for w, x in zip(norm_weights, nodes) | |
| ) | |
| return round(eap_theta, 4), round(math.sqrt(variance), 4) | |
| # ------------------------------------------------------------------ # | |
| # Adaptive item selection (Maximum Fisher Information) | |
| # ------------------------------------------------------------------ # | |
| def fisher_information(theta: float, a: float, b: float) -> float: | |
| """ | |
| Fisher information for a single 2PL item at ability theta. | |
| I(theta) = a^2 * P * (1 - P) | |
| """ | |
| p = IRT2PLEngine.probability(theta, a, b) | |
| return (a ** 2) * p * (1.0 - p) | |
| def select_next_item( | |
| theta: float = None, | |
| candidate_items=None, | |
| administered_ids=None, | |
| *, | |
| current_theta: float = None, | |
| administered_item_ids=None, | |
| ): | |
| """ | |
| Select the next item that maximises Fisher Information at the | |
| current ability estimate, excluding already-administered items. | |
| Accepts both ``theta``/``administered_ids`` and the aliases | |
| ``current_theta``/``administered_item_ids`` for backward compatibility. | |
| Parameters | |
| ---------- | |
| theta / current_theta : float | |
| Current ability estimate for the dimension. | |
| candidate_items : list | |
| ORM AssessmentItem objects with .id, .a_parameter, .b_parameter. | |
| administered_ids / administered_item_ids : set | |
| IDs of items already administered in this session/dimension. | |
| Returns | |
| ------- | |
| The AssessmentItem with the highest Fisher Information, or None if | |
| no candidates remain. | |
| """ | |
| # Resolve aliases | |
| if theta is None: | |
| theta = current_theta | |
| if administered_ids is None: | |
| administered_ids = administered_item_ids | |
| if theta is None: | |
| theta = 0.0 | |
| if administered_ids is None: | |
| administered_ids = set() | |
| if candidate_items is None: | |
| candidate_items = [] | |
| best_item = None | |
| best_info = -1.0 | |
| for item in candidate_items: | |
| if item.id in administered_ids: | |
| continue | |
| a = getattr(item, "a_parameter", None) | |
| b = getattr(item, "b_parameter", None) | |
| if a is None or b is None: | |
| continue | |
| info = IRT2PLEngine.fisher_information(theta, a, b) | |
| if info > best_info: | |
| best_info = info | |
| best_item = item | |
| return best_item | |
| # Backward-compatible alias (some modules import OptimizedIRT2PLEngine) | |
| OptimizedIRT2PLEngine = IRT2PLEngine | |