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)})"