Skip to content

pynmms.rdf.rules

pynmms.rdf.rules

Entailment regimes as Horn rules over generalized RDF triples.

The paper's def:entailmentregime: a regime is a set of rules <A, c> with a finite premise set A of triples and a conclusion c that is a triple or ⊥, range-restricted and uniform under substitution. Here a rule is a schema over variables; its instances are the regime's rules.

Shipped regimes:

  • SIMPLE -- the empty regime (simple entailment, cor:simple).
  • RDFS -- the RDF and RDFS entailment patterns of RDF 1.1 Semantics §9.2.1 (rdf1, rdfs2-13 except the literal-typing rules rdfs1 and rdfD1, which need a side condition) with the finite axiomatic triples of §9.2.2 (the container-membership family rdf:_i excluded). See cor:rdfs.

Custom regimes are built from :func:parse_rule lines such as::

?x ex:parentOf ?y -> ?y ex:childOf ?x
?x a ex:Alive, ?x a ex:Dead -> false

OWL2RL_OMITTED = ('eq-ref', 'cls-thing', 'cls-nothing1', 'dt-type1', 'dt-type2', 'dt-eq', 'dt-diff', 'dt-not-type') module-attribute

OWL 2 RL/RDF rules not implemented: the axiomatic-only rules (eq-ref, cls-thing, cls-nothing1 add a triple per term or one fixed triple) and the datatype rules, which need the datatype value spaces. The list-valued rule families are implemented procedurally (OWL2RL_LIST_RULES).

ProceduralRule dataclass

A rule family implemented in Python rather than as one pattern.

triggers are the predicates of triples that can activate it (None for any predicate); fire(t, lookup) yields the conclusions that follow from a triggering triple t given a lookup over the current closure (None for ⊥). Used for the OWL 2 RL rules whose premises range over an rdf:List of arbitrary length, and for rdfD1, whose conclusion is computed from a literal's datatype.

Source code in src/pynmms/rdf/rules.py
@dataclass(frozen=True)
class ProceduralRule:
    """A rule family implemented in Python rather than as one pattern.

    ``triggers`` are the predicates of triples that can activate it (``None``
    for any predicate); ``fire(t, lookup)`` yields the conclusions that follow
    from a triggering triple *t* given a lookup over the current closure
    (``None`` for ⊥). Used for the OWL 2 RL rules whose premises range over an
    ``rdf:List`` of arbitrary length, and for rdfD1, whose conclusion is
    computed from a literal's datatype.
    """

    name: str
    triggers: tuple[Node | None, ...]
    fire: Callable[[Triple, Lookup], Iterator[Triple | None]]

    def __str__(self) -> str:
        return f"{self.name} (procedural)"

Regime dataclass

A named rule set plus axiomatic triples.

Source code in src/pynmms/rdf/rules.py
@dataclass(frozen=True)
class Regime:
    """A named rule set plus axiomatic triples."""

    name: str
    rules: tuple[AnyRule, ...] = ()
    axioms: tuple[Triple, ...] = ()

    def __str__(self) -> str:
        return self.name

Rule dataclass

A Horn rule schema. conclusion is None means ⊥ (false-concluding).

guard is an optional side condition on the bindings (def:entailmentregime admits conditions such as "is a literal" that substitution preserves).

Source code in src/pynmms/rdf/rules.py
@dataclass(frozen=True)
class Rule:
    """A Horn rule schema. ``conclusion is None`` means ⊥ (false-concluding).

    ``guard`` is an optional side condition on the bindings (``def:entailmentregime``
    admits conditions such as "is a literal" that substitution preserves).
    """

    name: str
    premises: tuple[Pattern, ...]
    conclusion: Pattern | None
    guard: Guard | None = None
    #: A value guard (``pynmms.rdf.values.Guard``) written in the rule; when set and
    #: ``guard`` is not, ``guard`` is its Python evaluation, and the store runs it
    #: as a ``FILTER``.
    guard_expr: Any = None

    def __post_init__(self) -> None:
        premise_vars = {t for p in self.premises for t in p if isinstance(t, Var)}
        if self.conclusion is not None:
            for t in self.conclusion:
                if isinstance(t, Var) and t not in premise_vars:
                    raise ValueError(
                        f"Rule {self.name!r} is not range-restricted: {t} not in premises"
                    )
        if self.guard_expr is not None:
            for v in self.guard_expr.variables:
                if v not in premise_vars:
                    raise ValueError(f"Rule {self.name!r}: guard variable {v} not in premises")
            if self.guard is None:
                object.__setattr__(self, "guard", self.guard_expr.evaluate)

    def __str__(self) -> str:
        prem = ", ".join(" ".join(str(t) for t in p) for p in self.premises)
        if self.guard_expr is not None:
            prem += f", [{self.guard_expr}]"
        concl = "false" if self.conclusion is None else " ".join(str(t) for t in self.conclusion)
        return f"{prem} -> {concl}"

Var dataclass

A rule variable.

Source code in src/pynmms/rdf/rules.py
@dataclass(frozen=True, slots=True)
class Var:
    """A rule variable."""

    name: str

    def __str__(self) -> str:
        return f"?{self.name}"

content_lines(text)

The lines of a rules or entries file that carry a rule or an entry.

Blank lines and # comments are skipped; ordering name: a < b lines are consumed (the ordering is declared for rank guards, see :func:pynmms.rdf.values.parse_ordering_line) and not yielded. Lines are stripped.

Source code in src/pynmms/rdf/rules.py
def content_lines(text: str) -> Iterator[str]:
    """The lines of a rules or entries file that carry a rule or an entry.

    Blank lines and ``#`` comments are skipped; ``ordering name: a < b`` lines
    are consumed (the ordering is declared for ``rank`` guards, see
    :func:`pynmms.rdf.values.parse_ordering_line`) and not yielded. Lines
    are stripped.
    """
    from pynmms.rdf.values import parse_ordering_line

    for ln in text.splitlines():
        line = ln.strip()
        if not line or line.startswith("#") or parse_ordering_line(line):
            continue
        yield line

custom(name, rules, axioms=(), *, extends=None)

A user regime, optionally extending a shipped one.

Source code in src/pynmms/rdf/rules.py
def custom(
    name: str,
    rules: tuple[AnyRule, ...] | list[AnyRule],
    axioms: tuple[Triple, ...] = (),
    *,
    extends: Regime | None = None,
) -> Regime:
    """A user regime, optionally extending a shipped one."""
    base_rules = extends.rules if extends else ()
    base_axioms = extends.axioms if extends else ()
    return Regime(name, base_rules + tuple(rules), base_axioms + tuple(axioms))

parse_premises(text, resolver)

Parse a premise list p1, p2, [guard], ... into its patterns and one guard.

Several [...] guards are conjoined; the result's guard is None when there is none (a :class:pynmms.rdf.values.Guard otherwise).

Source code in src/pynmms/rdf/rules.py
def parse_premises(text: str, resolver: Resolver | None) -> tuple[tuple[Pattern, ...], Any]:
    """Parse a premise list ``p1, p2, [guard], ...`` into its patterns and one guard.

    Several ``[...]`` guards are conjoined; the result's guard is ``None``
    when there is none (a :class:`pynmms.rdf.values.Guard` otherwise).
    """
    parts = split_premises(text)
    guards = [p[1:-1].strip() for p in parts if _is_guard(p)]
    patterns = tuple(_parse_pattern(p, resolver) for p in parts if not _is_guard(p))
    guard = None
    if guards:
        from pynmms.rdf.values import parse_guard

        guard = parse_guard(" && ".join(f"({g})" for g in guards) if len(guards) > 1
                            else guards[0])
    return patterns, guard

parse_rule(text, resolver=None, name=None)

Parse p1, p2, ... -> conclusion where conclusion is a pattern or false.

Source code in src/pynmms/rdf/rules.py
def parse_rule(text: str, resolver: Resolver | None = None, name: str | None = None) -> Rule:
    """Parse ``p1, p2, ... -> conclusion`` where conclusion is a pattern or ``false``."""
    if "->" not in text:
        raise ValueError(f"Rule must contain '->': {text!r}")
    left, right = text.split("->", 1)
    premises, guard_expr = parse_premises(left, resolver)
    if not premises:
        raise ValueError(f"Rule has no premises: {text!r}")
    right = right.strip()
    conclusion = None if right.lower() in ("false", "⊥", "bottom") else _parse_pattern(
        right, resolver
    )
    return Rule(name or text.strip(), premises, conclusion, guard_expr=guard_expr)

parse_rules_text(text, resolver=None)

Parse a rules file: one rule per line, # comments, and ordering name: a < b lines declaring orderings for rank(name, x) guards.

Source code in src/pynmms/rdf/rules.py
def parse_rules_text(text: str, resolver: Resolver | None = None) -> list[Rule]:
    """Parse a rules file: one rule per line, ``#`` comments, and ``ordering name: a < b``
    lines declaring orderings for ``rank(name, x)`` guards."""
    return [parse_rule(line, resolver) for line in content_lines(text)]

rdf_list(head, lookup)

Members of the rdf:List starting at head, or None if malformed.

A cell may carry several rdf:first or rdf:rest values once owl:sameAs substitution has run over it (eq-rep-o); the smallest is taken so the walk stays deterministic, and every alternative first value is included as a member.

Source code in src/pynmms/rdf/rules.py
def rdf_list(head: Node, lookup: Lookup) -> list[Node] | None:
    """Members of the rdf:List starting at *head*, or None if malformed.

    A cell may carry several ``rdf:first`` or ``rdf:rest`` values once
    ``owl:sameAs`` substitution has run over it (eq-rep-o); the smallest is
    taken so the walk stays deterministic, and every alternative first value
    is included as a member.
    """
    items: list[Node] = []
    seen: set[Node] = set()
    node = head
    while node != RDF.nil:
        if node in seen:
            return None
        seen.add(node)
        first = sorted((t[2] for t in lookup((node, RDF.first, None))), key=str)
        rest = sorted((t[2] for t in lookup((node, RDF.rest, None))), key=str)
        if not first or not rest:
            return None
        items.extend(first)
        node = rest[0]
    return items

split_premises(text)

Split a premise list on commas outside [guards], (...), "..." and <iri>.

A guard may contain < ([?a < ?b]), so the quoted-atom rule of :func:pynmms.syntax.split_top_level cannot apply inside brackets. Parts are stripped; empty parts are dropped.

Source code in src/pynmms/rdf/rules.py
def split_premises(text: str) -> list[str]:
    """Split a premise list on commas outside ``[guards]``, ``(...)``, ``"..."`` and ``<iri>``.

    A guard may contain ``<`` (``[?a < ?b]``), so the quoted-atom rule of
    :func:`pynmms.syntax.split_top_level` cannot apply inside brackets.
    Parts are stripped; empty parts are dropped.
    """
    parts: list[str] = []
    depth = bracket = 0
    quote = ""
    start = 0
    for i, ch in enumerate(text):
        if quote:
            if ch == quote:
                quote = ""
            continue
        if ch == '"' or (ch == "<" and not bracket):
            quote = '"' if ch == '"' else ">"
        elif ch == "[":
            bracket += 1
        elif ch == "]" and bracket:
            bracket -= 1
        elif ch == "(":
            depth += 1
        elif ch == ")" and depth:
            depth -= 1
        elif ch == "," and not depth and not bracket:
            parts.append(text[start:i])
            start = i + 1
    parts.append(text[start:])
    return [p.strip() for p in parts if p.strip()]