Skip to content

MaterialBase

pynmms.base.MaterialBase dataclass

A material base B = for propositional NMMS.

Parameters:

Name Type Description Default
language set[str] | frozenset[str] | None

Set of atomic sentence strings comprising L_B.

None
consequences set[Sequent] | set[tuple[frozenset[str], frozenset[str]]] | None

Set of (antecedent, consequent) sequent pairs comprising |~_B.

None
Source code in pynmms/base.py
@dataclass
class MaterialBase:
    """A material base B = <L_B, |~_B> for propositional NMMS.

    Parameters:
        language: Set of atomic sentence strings comprising L_B.
        consequences: Set of (antecedent, consequent) sequent pairs comprising |~_B.
    """

    _language: set[str] = field(default_factory=set)
    _consequences: set[Sequent] = field(default_factory=set)

    def __init__(
        self,
        language: set[str] | frozenset[str] | None = None,
        consequences: (
            set[Sequent]
            | set[tuple[frozenset[str], frozenset[str]]]
            | None
        ) = None,
        annotations: dict[str, str] | None = None,
    ) -> None:
        self._language: set[str] = set(language) if language else set()
        self._consequences: set[Sequent] = set()
        self._annotations: dict[str, str] = dict(annotations) if annotations else {}

        # Validate all language atoms
        for s in self._language:
            _validate_atomic(s, "Material base language")

        # Validate and store consequences
        if consequences:
            for gamma, delta in consequences:
                for s in gamma | delta:
                    _validate_atomic(s, "Material base consequence")
                self._consequences.add((gamma, delta))

        logger.debug(
            "MaterialBase created: %d atoms, %d consequences",
            len(self._language),
            len(self._consequences),
        )

    # --- Read-only properties ---

    @property
    def language(self) -> frozenset[str]:
        """The atomic language L_B (read-only view)."""
        return frozenset(self._language)

    @property
    def consequences(self) -> frozenset[Sequent]:
        """The base consequence relation |~_B (read-only view)."""
        return frozenset(self._consequences)

    @property
    def annotations(self) -> dict[str, str]:
        """Atom annotations (read-only view)."""
        return dict(self._annotations)

    # --- Mutation ---

    def add_atom(self, s: str) -> None:
        """Add an atomic sentence to the language L_B."""
        _validate_atomic(s, "add_atom")
        self._language.add(s)
        logger.debug("Added atom: %s", s)

    def annotate(self, atom: str, description: str) -> None:
        """Attach a natural-language description to an atom."""
        self._annotations[atom] = description
        logger.debug("Annotated atom %s: %s", atom, description)

    def add_consequence(self, antecedent: frozenset[str], consequent: frozenset[str]) -> None:
        """Add a base consequence Gamma |~_B Delta.

        All sentences in *antecedent* and *consequent* must be atomic. They are
        also implicitly added to the language.
        """
        for s in antecedent | consequent:
            _validate_atomic(s, "add_consequence")
            self._language.add(s)
        self._consequences.add((antecedent, consequent))
        logger.debug("Added consequence: %s |~ %s", set(antecedent), set(consequent))

    # --- Axiom check ---

    def is_axiom(self, gamma: frozenset[str], delta: frozenset[str]) -> bool:
        """Check if Gamma => Delta is an axiom of NMMS_B.

        Ax1 (Containment): Gamma ∩ Delta ≠ ∅.
        Ax2 (Base consequence): (Gamma, Delta) ∈ |~_B exactly.

        No Weakening: the base relation uses exact syntactic match.
        """
        # Ax1: Containment
        if gamma & delta:
            return True
        # Ax2: Explicit base consequence (exact match)
        if (gamma, delta) in self._consequences:
            return True
        return False

    # --- Serialization ---

    def to_dict(self) -> dict:
        """Serialize to a JSON-compatible dict."""
        d: dict = {
            "language": sorted(self._language),
            "consequences": [
                {
                    "antecedent": sorted(gamma),
                    "consequent": sorted(delta),
                }
                for gamma, delta in sorted(
                    self._consequences, key=lambda s: (sorted(s[0]), sorted(s[1]))
                )
            ],
        }
        if self._annotations:
            d["annotations"] = dict(sorted(self._annotations.items()))
        return d

    @classmethod
    def from_dict(cls, data: dict) -> MaterialBase:
        """Deserialize from a dict (as produced by ``to_dict``)."""
        language = set(data.get("language", []))
        consequences: set[Sequent] = set()
        for entry in data.get("consequences", []):
            gamma = frozenset(entry["antecedent"])
            delta = frozenset(entry["consequent"])
            consequences.add((gamma, delta))
        annotations = data.get("annotations", {})
        return cls(language=language, consequences=consequences, annotations=annotations)

    def to_file(self, path: str | Path) -> None:
        """Write the base to a JSON file."""
        with open(path, "w") as f:
            json.dump(self.to_dict(), f, indent=2)
        logger.debug("Saved base to %s", path)

    @classmethod
    def from_file(cls, path: str | Path) -> MaterialBase:
        """Load a base from a JSON file."""
        with open(path) as f:
            data = json.load(f)
        logger.debug("Loaded base from %s", path)
        return cls.from_dict(data)

language property

The atomic language L_B (read-only view).

consequences property

The base consequence relation |~_B (read-only view).

__init__(language=None, consequences=None, annotations=None)

Source code in pynmms/base.py
def __init__(
    self,
    language: set[str] | frozenset[str] | None = None,
    consequences: (
        set[Sequent]
        | set[tuple[frozenset[str], frozenset[str]]]
        | None
    ) = None,
    annotations: dict[str, str] | None = None,
) -> None:
    self._language: set[str] = set(language) if language else set()
    self._consequences: set[Sequent] = set()
    self._annotations: dict[str, str] = dict(annotations) if annotations else {}

    # Validate all language atoms
    for s in self._language:
        _validate_atomic(s, "Material base language")

    # Validate and store consequences
    if consequences:
        for gamma, delta in consequences:
            for s in gamma | delta:
                _validate_atomic(s, "Material base consequence")
            self._consequences.add((gamma, delta))

    logger.debug(
        "MaterialBase created: %d atoms, %d consequences",
        len(self._language),
        len(self._consequences),
    )

add_atom(s)

Add an atomic sentence to the language L_B.

Source code in pynmms/base.py
def add_atom(self, s: str) -> None:
    """Add an atomic sentence to the language L_B."""
    _validate_atomic(s, "add_atom")
    self._language.add(s)
    logger.debug("Added atom: %s", s)

add_consequence(antecedent, consequent)

Add a base consequence Gamma |~_B Delta.

All sentences in antecedent and consequent must be atomic. They are also implicitly added to the language.

Source code in pynmms/base.py
def add_consequence(self, antecedent: frozenset[str], consequent: frozenset[str]) -> None:
    """Add a base consequence Gamma |~_B Delta.

    All sentences in *antecedent* and *consequent* must be atomic. They are
    also implicitly added to the language.
    """
    for s in antecedent | consequent:
        _validate_atomic(s, "add_consequence")
        self._language.add(s)
    self._consequences.add((antecedent, consequent))
    logger.debug("Added consequence: %s |~ %s", set(antecedent), set(consequent))

is_axiom(gamma, delta)

Check if Gamma => Delta is an axiom of NMMS_B.

Ax1 (Containment): Gamma ∩ Delta ≠ ∅. Ax2 (Base consequence): (Gamma, Delta) ∈ |~_B exactly.

No Weakening: the base relation uses exact syntactic match.

Source code in pynmms/base.py
def is_axiom(self, gamma: frozenset[str], delta: frozenset[str]) -> bool:
    """Check if Gamma => Delta is an axiom of NMMS_B.

    Ax1 (Containment): Gamma ∩ Delta ≠ ∅.
    Ax2 (Base consequence): (Gamma, Delta) ∈ |~_B exactly.

    No Weakening: the base relation uses exact syntactic match.
    """
    # Ax1: Containment
    if gamma & delta:
        return True
    # Ax2: Explicit base consequence (exact match)
    if (gamma, delta) in self._consequences:
        return True
    return False

to_dict()

Serialize to a JSON-compatible dict.

Source code in pynmms/base.py
def to_dict(self) -> dict:
    """Serialize to a JSON-compatible dict."""
    d: dict = {
        "language": sorted(self._language),
        "consequences": [
            {
                "antecedent": sorted(gamma),
                "consequent": sorted(delta),
            }
            for gamma, delta in sorted(
                self._consequences, key=lambda s: (sorted(s[0]), sorted(s[1]))
            )
        ],
    }
    if self._annotations:
        d["annotations"] = dict(sorted(self._annotations.items()))
    return d

from_dict(data) classmethod

Deserialize from a dict (as produced by to_dict).

Source code in pynmms/base.py
@classmethod
def from_dict(cls, data: dict) -> MaterialBase:
    """Deserialize from a dict (as produced by ``to_dict``)."""
    language = set(data.get("language", []))
    consequences: set[Sequent] = set()
    for entry in data.get("consequences", []):
        gamma = frozenset(entry["antecedent"])
        delta = frozenset(entry["consequent"])
        consequences.add((gamma, delta))
    annotations = data.get("annotations", {})
    return cls(language=language, consequences=consequences, annotations=annotations)

to_file(path)

Write the base to a JSON file.

Source code in pynmms/base.py
def to_file(self, path: str | Path) -> None:
    """Write the base to a JSON file."""
    with open(path, "w") as f:
        json.dump(self.to_dict(), f, indent=2)
    logger.debug("Saved base to %s", path)

from_file(path) classmethod

Load a base from a JSON file.

Source code in pynmms/base.py
@classmethod
def from_file(cls, path: str | Path) -> MaterialBase:
    """Load a base from a JSON file."""
    with open(path) as f:
        data = json.load(f)
    logger.debug("Loaded base from %s", path)
    return cls.from_dict(data)