Source code for knowledgespaces.derivation.competence
"""Observable learning for conjunctive competence models.
Definitions and direct finite-set algorithms from Stefanutti & de Chiusole
(2017), On the assessment of learning in competence based knowledge space
theory, Journal of Mathematical Psychology 80, 22-32,
https://doi.org/10.1016/j.jmp.2017.08.003, Sections 3-6.
"""
from __future__ import annotations
from collections.abc import Collection
from knowledgespaces.derivation.skill_map import SkillMap
from knowledgespaces.structures.knowledge_structure import KnowledgeStructure
[docs]
class CompetenceModel:
"""A conjunctive skill function on an explicit competence structure.
Unlike derivation from skill prerequisites, ``structure`` may be any
finite competence structure, including a non-ordinal one. Skills and
items remain distinct domains. All item requirements must be nonempty
(the skill-function assumptions in Section 2 of the reference).
Equivalence classes enumerate the supplied competence states, so their
cost scales with the explicit structure, not an additional powerset.
No stochastic learning or ability to observe latent skills is assumed.
"""
__slots__ = ("_classes", "_skill_map", "_structure")
def __init__(self, structure: KnowledgeStructure, skill_map: SkillMap) -> None:
if not isinstance(skill_map, SkillMap):
raise TypeError("CompetenceModel requires a conjunctive SkillMap.")
if structure.domain != skill_map.skills:
raise ValueError("Competence structure domain must equal the skill map's skills.")
if not skill_map.items or any(not skill_map.skills_for(q) for q in skill_map.items):
raise ValueError("A skill function needs items with nonempty skill requirements.")
self._structure = structure
self._skill_map = skill_map
classes: dict[frozenset[str], set[frozenset[str]]] = {}
for state in structure.states:
classes.setdefault(skill_map.problem_function(state), set()).add(state)
self._classes = {k: frozenset(v) for k, v in classes.items()}
@property
def structure(self) -> KnowledgeStructure:
"""The supplied competence structure (skills as its domain)."""
return self._structure
@property
def skill_map(self) -> SkillMap:
"""The supplied conjunctive skill function."""
return self._skill_map
def _state(self, state: Collection[str]) -> frozenset[str]:
state = frozenset(state)
if state not in self.structure:
raise ValueError("Not a state of the competence structure.")
return state
@property
def knowledge_structure(self) -> KnowledgeStructure:
"""The exact image p(C), without adding unattained performance states."""
return KnowledgeStructure(self.skill_map.items, self._classes)
[docs]
def compatible_states(self, performance: Collection[str]) -> frozenset[frozenset[str]]:
"""All competence states that delineate the given performance state.
An unattainable performance state raises ``ValueError``; an empty
response pattern is a performance state here, not missing data.
"""
try:
return self._classes[frozenset(performance)]
except KeyError as error:
raise ValueError("Performance state is not delineated by this model.") from error
[docs]
def equivalence_class(self, state: Collection[str]) -> frozenset[frozenset[str]]:
"""The class [C]_C of states with the same p(C) (Section 3)."""
state = self._state(state)
return self.compatible_states(self.skill_map.problem_function(state))
[docs]
def effective_outer_fringe(self, state: Collection[str]) -> frozenset[str]:
"""C+ = {s in C° : p(C) is a proper subset of p(C + s)} (4.1).
The returned elements are skills. A single effective skill may
unlock several items, even when the item-level outer fringe is empty.
"""
state = self._state(state)
performance = self.skill_map.problem_function(state)
return frozenset(
s
for s in self.structure.outer_fringe(state)
if performance < self.skill_map.problem_function(state | {s})
)
[docs]
def collective_outer_fringe(self, performance: Collection[str]) -> frozenset[str]:
"""Intersection of outer fringes across p^{-1}(performance).
Definition 2 and Proposition 5: each returned skill is available
and effective for every compatible competence state. This need
not recover every effective skill of an individual latent state.
"""
states = self.compatible_states(performance)
return frozenset.intersection(*(self.structure.outer_fringe(c) for c in states))
[docs]
def floor(self, state: Collection[str]) -> frozenset[str]:
"""Intersection of [C]_C (Definition 1).
A floor need not belong to the competence structure. Check
``is_floor_inclusive`` before treating all floors as admissible states.
"""
return frozenset.intersection(*self.equivalence_class(state))
@property
def floors(self) -> frozenset[frozenset[str]]:
"""The family C_p of floors, preserving its exact domain and members.
This is a family of sets, not automatically a KnowledgeStructure
on all declared skills: unused skills need not occur in any floor.
"""
return frozenset(frozenset.intersection(*states) for states in self._classes.values())
@property
def is_floor_inclusive(self) -> bool:
"""True if every class contains its floor (Definition 1)."""
return all(frozenset.intersection(*states) in states for states in self._classes.values())
[docs]
def floor_fringe(self, state: Collection[str]) -> frozenset[str]:
"""Outer fringe of the floor in the family C_p, computed directly.
Requires floor-inclusiveness. This is distinct from the effective
fringe of the floor in the original competence structure, and from
the collective fringe of its equivalence class. Floor-inclusiveness
and union closure alone do not make all three quantities equal.
``is_compatible`` is a sufficient condition for equality; see the
derivation guide for the hypotheses and argument.
"""
floor = self.floor(state)
if not self.is_floor_inclusive:
raise ValueError("Floor fringes require a floor-inclusive competence structure.")
floors = self.floors
return frozenset(s for s in self.structure.domain - floor if floor | {s} in floors)
@property
def is_compatible(self) -> bool:
"""Compatibility of a competence space with the skill function (Def. 5).
On a union-closed family the atom condition is equivalent to every
item's required skill set being a state. Returns False if the
competence structure is not a knowledge space.
"""
return self.structure.is_knowledge_space and all(
self.skill_map.skills_for(q) in self.structure for q in self.skill_map.items
)