Source code for knowledgespaces.query.expert

"""
Expert protocols for the QUERY algorithm.

An expert is any callable that answers prerequisite queries.
This module defines the protocol and provides built-in implementations
for testing and interactive use.

References:
    Koppen, M., & Doignon, J.-P. (1990).
    How to build a knowledge space by querying an expert.
    Journal of Mathematical Psychology, 34, 311-331.
"""

from __future__ import annotations

from collections.abc import Callable, Collection
from typing import Protocol, runtime_checkable

from knowledgespaces.query.types import Query


[docs] @runtime_checkable class Expert(Protocol): """Protocol for expert query functions. An expert receives a Query and returns True (positive) or False (negative). The semantic of a positive answer to Query(A, q) is: 'If a student fails all items in A, they will also fail q.' Equivalently: mastering q requires mastering at least one item in A. """ def __call__(self, query: Query) -> bool: ...
[docs] class PresetExpert: """Expert with predetermined answers, for testing and replay. Parameters ---------- answers : dict[tuple[frozenset[str], str], bool] Mapping from (antecedent, consequent) to answer. default : bool Answer for queries not in the mapping. """ def __init__( self, answers: dict[tuple[frozenset[str], str], bool] | None = None, default: bool = False, ) -> None: self._answers: dict[tuple[frozenset[str], str], bool] = dict(answers or {}) self._default = default def __call__(self, query: Query) -> bool: key = (query.antecedent, query.consequent) return self._answers.get(key, self._default)
[docs] @classmethod def from_relation(cls, relations: Collection[tuple[str, str]]) -> PresetExpert: """Create an expert that answers based on a known surmise relation. A pair query (a -> q) is positive iff (a, q) is in the relation. A group query (A -> q) is positive iff some a in A has (a, q). This simulates an expert who knows the true prerequisite structure. Suitable for testing the query algorithm against a known ground truth. """ rel_set = set(relations) # We don't pre-compute all queries; instead override __call__ expert = cls.__new__(cls) expert._answers = {} expert._default = False expert._relation = rel_set expert.__class__ = _RelationExpert return expert # type: ignore[return-value]
class _RelationExpert: """Expert backed by a known surmise relation (transitive closure).""" _relation: set[tuple[str, str]] def __call__(self, query: Query) -> bool: # Positive iff there exists a in A such that (a, q) in relation # This implements monotonicity: if {a} -> q, then any A containing a -> q return any((a, query.consequent) in self._relation for a in query.antecedent)
[docs] class CallbackExpert: """Expert that delegates to a user-supplied function. Parameters ---------- fn : Callable[[frozenset[str], str], bool] A function that takes (antecedent_set, consequent) and returns bool. """ def __init__(self, fn: Callable[[frozenset[str], str], bool]) -> None: self._fn = fn def __call__(self, query: Query) -> bool: return self._fn(query.antecedent, query.consequent)