Source code for knowledgespaces.derivation.inverse

"""Inverse problem-function computations for conjunctive maps and multimaps.

These are finite set computations using p(C) = {q: some A in mu(q) is a
subset of C}. No fringe or floor theorem for conjunctive models is extended
to multimaps. See Falmagne & Doignon (2011), Chapter 6, and the distinction
between exact inversion and positive-item coverage in the user guide.
"""

from __future__ import annotations

from collections.abc import Collection

from knowledgespaces.derivation.skill_map import SkillMap, SkillMultiMap
from knowledgespaces.structures.knowledge_structure import KnowledgeStructure
from knowledgespaces.structures.set_family import SetFamily


[docs] def compatible_competencies( performance: Collection[str], skill_map: SkillMap | SkillMultiMap, competence_structure: KnowledgeStructure | SetFamily, *, exact: bool = True, ) -> frozenset[frozenset[str]]: """Return admissible C with p(C) = performance (default). With ``exact=False``, return C with ``performance <= p(C)``: only the specified mastered items constrain the result. The domain of the supplied competence structure or set family must equal the skill domain. A ``SetFamily`` need not contain either endpoint. An unattainable exact performance yields the empty family, not an invented competence state. """ target = _validate(performance, skill_map, exact) if competence_structure.domain != skill_map.skills: raise ValueError("Competence structure and skill map must have the same skill domain.") return frozenset( c for c in competence_structure if ( skill_map.problem_function(c) == target if exact else target <= skill_map.problem_function(c) ) )
[docs] def minimal_competencies( performance: Collection[str], skill_map: SkillMap | SkillMultiMap, *, competence_structure: KnowledgeStructure | SetFamily | None = None, exact: bool = True, max_candidates: int = 100_000, ) -> frozenset[frozenset[str]]: """All inclusion-minimal compatible competence states. By default compatibility means exact performance, including the absence of mastery of every other item. ``exact=False`` asks instead for minimal skill sets sufficient to solve the specified items, as in the positive coverage interpretation of ``CbKST::cbkst_perf2comp``. If a competence structure or set family is given, minimize within its admissible states; a minimal unrestricted solution may not itself be admissible. Otherwise every subset of the skill domain is admissible. The unrestricted algorithm builds minimal unions of alternative competencies incrementally, without enumerating the entire skill powerset. Its intermediate antichains can nevertheless be exponential. ``max_candidates`` bounds each intermediate family (before minimization), raising rather than returning partial output. Minimal means set inclusion, not smallest cardinality. The empty family means infeasibility; ``frozenset({frozenset()})`` is a valid empty-skill solution. No probability or inference about noisy responses is involved. """ target = _validate(performance, skill_map, exact) if ( isinstance(max_candidates, bool) or not isinstance(max_candidates, int) or max_candidates < 1 ): raise ValueError("max_candidates must be a positive integer.") if competence_structure is not None: candidates = compatible_competencies(target, skill_map, competence_structure, exact=exact) if len(candidates) > max_candidates: raise ValueError("Compatible family exceeds max_candidates.") return _minimal(candidates) multi = ( SkillMultiMap.from_skill_map(skill_map) if isinstance(skill_map, SkillMap) else skill_map ) candidates = frozenset({frozenset()}) for item in sorted(target): combined: set[frozenset[str]] = set() for state in candidates: for alternative in multi.competencies_for(item): union = state | alternative # p is isotone: a forbidden mastered item cannot disappear # after adding more skills, so this pruning is exact. if exact and not multi.problem_function(union) <= target: continue combined.add(union) if len(combined) > max_candidates: raise ValueError("Intermediate competence family exceeds max_candidates.") candidates = _minimal(combined) # Also handles the empty target and items with empty competencies. return frozenset(c for c in candidates if not exact or multi.problem_function(c) == target)
def _validate( performance: Collection[str], skill_map: SkillMap | SkillMultiMap, exact: bool ) -> frozenset[str]: if not isinstance(exact, bool): raise ValueError("exact must be a bool.") target = frozenset(performance) if not target <= set(skill_map.items): raise ValueError("Performance contains unknown items.") return target def _minimal(states: Collection[frozenset[str]]) -> frozenset[frozenset[str]]: result: set[frozenset[str]] = set() for state in sorted(states, key=lambda s: (len(s), sorted(s))): if not any(smaller <= state for smaller in result): result.add(state) return frozenset(result)