Source code for knowledgespaces.derivation.skill_map

"""
Skill maps: the mapping from items to required skills.

A skill map μ assigns to each item q the set of skills μ(q) needed
to solve it. The problem function p(C) = {q ∈ Q | μ(q) ⊆ C} maps
a competence state to the set of solvable items.

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

from __future__ import annotations

from collections.abc import Collection, Mapping


[docs] class SkillMap: """Mapping from items to the skills required to solve them. Parameters ---------- items : Collection[str] The domain of items. skills : Collection[str] The set of all skills. mapping : Mapping[str, Collection[str]] For each item, the skills required to solve it: μ(q). Raises ------ ValueError If an item references a skill not in the skills set, or if mapping keys don't match items. """ __slots__ = ("_items", "_mapping", "_skills") def __init__( self, items: Collection[str], skills: Collection[str], mapping: Mapping[str, Collection[str]], ) -> None: self._items: tuple[str, ...] = tuple(items) self._skills: frozenset[str] = frozenset(skills) items_set = set(self._items) extra_keys = set(mapping.keys()) - items_set if extra_keys: raise ValueError(f"Mapping contains keys not in items: {extra_keys}") built: dict[str, frozenset[str]] = {} for item in self._items: if item not in mapping: raise ValueError(f"Item '{item}' has no skill mapping.") required = frozenset(mapping[item]) extra = required - self._skills if extra: raise ValueError(f"Item '{item}' references unknown skills: {set(extra)}") built[item] = required self._mapping: dict[str, frozenset[str]] = built @property def items(self) -> tuple[str, ...]: return self._items @property def skills(self) -> frozenset[str]: return self._skills
[docs] def skills_for(self, item: str) -> frozenset[str]: """Return μ(q): skills required by item q.""" return self._mapping[item]
[docs] def problem_function(self, competence: frozenset[str]) -> frozenset[str]: """Compute p(C) = {q ∈ Q | μ(q) ⊆ C}. An item is solvable iff ALL its required skills are present in the competence state. """ return frozenset(item for item in self._items if self._mapping[item].issubset(competence))
[docs] def to_matrix(self) -> tuple[list[str], list[str], list[list[int]]]: """Return (items, skills, binary matrix). matrix[i][j] = 1 iff skill skills[j] is required by items[i]. """ skills_ordered = sorted(self._skills) skill_idx = {s: j for j, s in enumerate(skills_ordered)} matrix = [] for item in self._items: row = [0] * len(skills_ordered) for s in self._mapping[item]: row[skill_idx[s]] = 1 matrix.append(row) return list(self._items), skills_ordered, matrix
[docs] @classmethod def from_matrix( cls, items: list[str], skills: list[str], matrix: list[list[int]], ) -> SkillMap: """Create from a binary matrix. matrix[i][j] = 1 means items[i] requires skills[j]. Raises ------ ValueError If matrix dimensions don't match items/skills, or if values are not 0 or 1. """ if len(matrix) != len(items): raise ValueError(f"Matrix has {len(matrix)} rows but {len(items)} items.") for i, row in enumerate(matrix): if len(row) != len(skills): raise ValueError(f"Row {i} has {len(row)} columns but {len(skills)} skills.") for j, val in enumerate(row): if val not in (0, 1): raise ValueError(f"Matrix[{i}][{j}] = {val!r}, expected 0 or 1.") mapping: dict[str, frozenset[str]] = {} for i, item in enumerate(items): required = frozenset(skills[j] for j, val in enumerate(matrix[i]) if val) mapping[item] = required return cls(items, skills, mapping)
def __repr__(self) -> str: return f"SkillMap(items={len(self._items)}, skills={len(self._skills)})"