Source code for knowledgespaces.structures.reports
"""Path validation and multi-item differences, distinct from canonical fringes."""
from __future__ import annotations
from collections.abc import Collection, Iterable
from dataclasses import dataclass
from itertools import pairwise
from knowledgespaces.structures.knowledge_structure import KnowledgeStructure
from knowledgespaces.structures.set_family import SetFamily
[docs]
def is_gradation(
structure: KnowledgeStructure,
path: Iterable[Collection[str]],
*,
start: Collection[str] | None = None,
target: Collection[str] | None = None,
) -> bool:
"""Whether a supplied path adds one item per step between its endpoints.
Defaults are ∅ and Q, giving a full gradation. With explicit endpoints
this validates a single-item chain in that interval. All members must
belong to ``structure``; repetition, deletion, or exchanging items is
invalid even if successive cardinalities differ by one. Invalid expected
endpoints raise ``ValueError``; an empty or invalid path returns false.
Equal expected endpoints admit only their singleton path.
"""
first = frozenset() if start is None else frozenset(start)
last = structure.domain if target is None else frozenset(target)
if first not in structure or last not in structure or not first <= last:
raise ValueError("Endpoints must be states with start <= target.")
states = tuple(frozenset(s) for s in path)
return bool(
states
and states[0] == first
and states[-1] == last
and all(s in structure for s in states)
and all(a < b and len(b - a) == 1 for a, b in pairwise(states))
)
[docs]
@dataclass(frozen=True)
class StateChanges:
"""Families of changed-item sets within a symmetric-difference radius.
``all_changes`` corresponds to the generalized ``kstpy.fringe`` output.
``removals`` reach proper subsets, ``additions`` proper supersets, and
``mixed`` incomparable states. These contain sets of items, whereas a
canonical fringe contains individual items. The centre is excluded.
"""
centre: frozenset[str]
distance: int
all_changes: frozenset[frozenset[str]]
removals: frozenset[frozenset[str]]
additions: frozenset[frozenset[str]]
@property
def mixed(self) -> frozenset[frozenset[str]]:
"""Changes that remove some items and add others."""
return self.all_changes - self.removals - self.additions
[docs]
def state_changes(
structure: KnowledgeStructure | SetFamily,
state: Collection[str],
distance: int = 1,
) -> StateChanges:
"""Report multi-item changes to states at distance 1 through ``distance``.
The radius uses set distance, not graph distance; intermediate states
need not exist. ``distance=1`` recovers singleton sets for the canonical
inner/outer fringes. No states, including endpoints, are inserted when
the input is a ``SetFamily``. This enumerates only the supplied family.
"""
centre = frozenset(state)
if centre not in structure:
raise ValueError("The centre must belong to the supplied family.")
if isinstance(distance, bool) or not isinstance(distance, int) or distance < 0:
raise ValueError("distance must be a nonnegative integer.")
neighbours = [s for s in structure if 0 < len(s ^ centre) <= distance]
return StateChanges(
centre,
distance,
frozenset(s ^ centre for s in neighbours),
frozenset(centre - s for s in neighbours if s < centre),
frozenset(s - centre for s in neighbours if centre < s),
)