Source code for knowledgespaces.derivation.cbkst

"""
Competence-Based Knowledge Space Theory (CB-KST) derivation.

Given a skill map μ (items → skills) and a surmise relation on skills,
derives the knowledge structure on items through the problem function.

The pipeline:
1. Build competence structure C from skill prerequisites.
2. Apply problem function p(C) to each competence state.
3. Collect unique knowledge states → knowledge structure K.

Additionally provides conversion from skill prerequisites to item
prerequisites (surmise relation on items).

References:
    Doignon, J.-P., & Falmagne, J.-C. (1999).
    Knowledge Spaces, Chapter 7. Springer-Verlag.
"""

from __future__ import annotations

from dataclasses import dataclass

from knowledgespaces.derivation.skill_map import SkillMap
from knowledgespaces.structures.knowledge_structure import KnowledgeStructure
from knowledgespaces.structures.relations import SurmiseRelation


[docs] @dataclass class CBKSTResult: """Result of a CB-KST derivation.""" competence_structure: KnowledgeStructure knowledge_structure: KnowledgeStructure mapping: dict[frozenset[str], frozenset[str]] skill_map: SkillMap skill_relation: SurmiseRelation
def _validate_skill_domains(skill_map: SkillMap, skill_relation: SurmiseRelation) -> None: """Ensure the skill relation covers all skills used by the skill map.""" missing = skill_map.skills - skill_relation.items if missing: raise ValueError(f"SkillMap uses skills not present in the SurmiseRelation: {missing}")
[docs] def derive_knowledge_structure( skill_map: SkillMap, skill_relation: SurmiseRelation, ) -> CBKSTResult: """Derive a knowledge structure from a competence model. Parameters ---------- skill_map : SkillMap The mapping μ: items → required skills. skill_relation : SurmiseRelation Prerequisite relation on skills (will be transitively closed). Must cover all skills referenced by the skill map. Returns ------- CBKSTResult Contains the competence structure C, knowledge structure K, and the mapping from competence states to knowledge states. Raises ------ ValueError If skill_map references skills not in skill_relation's domain. """ _validate_skill_domains(skill_map, skill_relation) closure = skill_relation.transitive_closure() # 1. Competence structure: downsets of the skill order competence = KnowledgeStructure.from_surmise_relation(closure) # 2. Apply problem function to each competence state c_to_k: dict[frozenset[str], frozenset[str]] = {} knowledge_states: set[frozenset[str]] = set() for comp_state in competence.states: know_state = skill_map.problem_function(comp_state) knowledge_states.add(know_state) c_to_k[comp_state] = know_state # 3. Build knowledge structure on items knowledge = KnowledgeStructure(skill_map.items, knowledge_states) return CBKSTResult( competence_structure=competence, knowledge_structure=knowledge, mapping=c_to_k, skill_map=skill_map, skill_relation=closure, )
[docs] def skill_to_item_relation( skill_map: SkillMap, skill_relation: SurmiseRelation, ) -> SurmiseRelation: """Derive an item surmise relation from skills. Item p is a prerequisite of item q iff every skill required by p is "covered" by q — meaning the skill is either directly required by q or is a prerequisite of a skill required by q. Formally: covers(q) = μ(q) ∪ {s : ∃t ∈ μ(q), (s,t) ∈ closure} p ≺ q iff μ(p) ⊆ covers(q) Note: the result is already transitively closed. Parameters ---------- skill_map : SkillMap The mapping μ: items → required skills. skill_relation : SurmiseRelation Prerequisite relation on skills. Returns ------- SurmiseRelation Surmise relation on items. Raises ------ ValueError If skill_map references skills not in skill_relation's domain. """ _validate_skill_domains(skill_map, skill_relation) closure = skill_relation.transitive_closure() # Precompute covers(q) for each item q covers: dict[str, frozenset[str]] = {} for item in skill_map.items: mu_q = skill_map.skills_for(item) # All skills required by q, plus all their prerequisites covered: set[str] = set(mu_q) for skill in mu_q: covered |= set(closure.prerequisites_of(skill)) covered.add(skill) # reflexive covers[item] = frozenset(covered) # p ≺ q iff μ(p) ⊆ covers(q) and p ≠ q items = list(skill_map.items) relations: set[tuple[str, str]] = set() for p in items: mu_p = skill_map.skills_for(p) for q in items: if p != q and mu_p.issubset(covers[q]): relations.add((p, q)) return SurmiseRelation(items, relations)