mixle.reason.ontology module

Ontology objects for typed constraints on knowledge graphs.

A knowledge graph without a schema will happily assert (paris, employs, france). An Ontology is the typed contract that rules such triples out structurally: a class hierarchy, relation signatures (employs: Organization -> Person), per-relation axioms (functional, symmetric, asymmetric, irreflexive), and disjoint-class declarations. Ontology.check_triple() names every violation of one assertion; Ontology.check_graph() audits a whole triple set, including the cross-triple axioms (a functional relation asserted with two different tails).

The same contract turns a fitted KG embedding into an ontology-constrained distribution: OntologyConstrainedKG wraps a KnowledgeGraphDistribution and masks the tail posterior to range-conforming entities, renormalizing – so the model literally cannot place probability on a triple the ontology forbids. Constrained extraction or decoding can apply the same mask before accepting generated triples.

Everything is symbolic and dependency-free: entities/relations are strings, entity types are supplied as a {entity: class} map (the entity-linking output). Violations are named, never silent.

Construction is collision- and cycle-safe (MXR-080-0297): Ontology.add_class()/add_relation reject a name that is already registered instead of silently overwriting its definition, and Ontology.replace_class()/replace_relation are the explicit, differently named operations for deliberately redefining one – this codebase’s established reject-then-explicit-replace convention (see e.g. CrossModalModel.add_modality/replace_modality, MXR-080-0276). Decode-time filtering and KG completion validate the FULL contract – per-triple axioms plus cross-triple graph axioms plus the raw model output itself – rather than the partial checks that let a schema-violating or numerically degenerate result through while still claiming to be “ontology-constrained” (MXR-080-0298/0299/0300), and constrained decoding validates its confidence policy and binds its identity to the result (MXR-080-0301).

class Ontology(classes=<factory>, relations=<factory>, axioms=<factory>, disjoint=<factory>, version=None)[source]

Bases: object

A typed schema over knowledge: class hierarchy + relation signatures + axioms + disjointness.

Parameters:
add_class(name, parent=None)[source]

Add a NEW ontology class, optionally under a known parent.

MXR-080-0297: a duplicate name is rejected rather than silently overwriting the existing class’s parent – the prior behavior let a second, differently-parented add_class(name, other_parent) retroactively change what every relation/disjointness declaration mentioning name means, with no trace that a redefinition happened. Use replace_class() to deliberately reparent an existing class.

Raises:

ValueError – name is already registered, or parent is not a known class.

Parameters:
  • name (str)

  • parent (str | None)

Return type:

Ontology

replace_class(name, parent=None)[source]

Deliberately reparent an existing class.

MXR-080-0297: unlike add_class() (which rejects a duplicate name outright), this exists specifically to allow redefining an existing class’s parent – but a parent that IS name, or that is already a descendant of name in the CURRENT hierarchy, is rejected here, before the mutation, rather than silently installed and merely limped around later by is_a()’s cycle guard (a partial-answer, quietly-wrong result).

Raises:
  • KeyError – name is not already registered.

  • ValueError – parent is not a known class, or reparenting would create a self-loop or a cycle.

Parameters:
  • name (str)

  • parent (str | None)

Return type:

Ontology

add_relation(name, domain, range_, *axioms)[source]

Add a NEW relation with domain, range, and optional ontology axioms.

MXR-080-0297: a duplicate name is rejected rather than silently overwriting the existing relation’s signature and axioms. Use replace_relation() to deliberately redefine an existing one.

Raises:

ValueError – name is already registered, domain/range_ is not a known class, or an axiom is not one of AXIOMS.

Parameters:
Return type:

Ontology

replace_relation(name, domain, range_, *axioms)[source]

Deliberately redefine an existing relation’s domain, range, and/or axioms.

MXR-080-0297: the explicit, differently-named counterpart to add_relation()’s reject-on-duplicate.

Raises:
  • KeyError – name is not already registered.

  • ValueError – domain/range_ is not a known class, or an axiom is not one of AXIOMS.

Parameters:
Return type:

Ontology

add_disjoint(a, b)[source]

Declare two classes mutually exclusive.

MXR-080-0297: an unknown class reference, or declaring a class disjoint with ITSELF, is rejected rather than silently accepted – add_disjoint(x, x) previously made check_triple() flag every single instance of x as violating disjointness against itself, and an unknown class reference could never match any real entity, silently defeating the declaration instead of failing loudly at the typo.

Raises:

ValueError – a or b is not a known class, or a == b.

Parameters:
Return type:

Ontology

is_a(cls, ancestor)[source]

Whether cls is ancestor or a descendant of it (walks the parent chain).

MXR-080-0297: construction (add_class()/replace_class()) now rejects every path that could install a cycle, so this walk should never see one. As a defense-in-depth check (e.g. against .classes being mutated directly, bypassing the API) this RAISES if a cycle is nonetheless encountered, rather than silently stopping and returning a partial, possibly wrong answer – a malformed schema must never look like an ordinary “not an ancestor”.

Parameters:
Return type:

bool

check_triple(h, r, t, types)[source]

Every named violation of (h, r, t) given entity types ({} means unconstrained).

Parameters:
Return type:

list[str]

check_graph(triples, types)[source]

Audit a triple set: per-triple violations plus the cross-triple axioms (functional/symmetric/ asymmetric/transitive).

Returns {consistent, n_triples, violations: [{triple, problems}]} – every problem named. Every cross-triple (graph-level) violation also carries an "axiom" key naming which axiom it is (the per-triple violations sourced from check_triple() do not, since one of those can bundle several different kinds of problem for a single triple) – filter_triples() relies on this to apply a deterministic policy per axiom kind (MXR-080-0298).

Parameters:
Return type:

dict[str, Any]

filter_triples(triples, types)[source]

Split triples into (kept, rejected-with-reasons) – the decode mask.

MXR-080-0298: per-triple consistency (check_triple()) alone is not enough – it cannot see that a functional relation was asserted with two different tails, or any other cross-triple axiom violation, so kept could previously contain a set that is globally inconsistent even though every individual triple in it looks fine alone. After the per-triple pass, the accepted set is re-validated AS A GRAPH and any set-level conflict is resolved by _resolve_graph_conflicts()’s deterministic policy; every triple that policy withholds is added to rejected with its reason, same as a per-triple violation.

Parameters:
Return type:

tuple[list[tuple], list[dict[str, Any]]]

class OntologyConstrainedKG(kg, ontology, *, entities, relations, types)[source]

Bases: object

A fitted KG embedding, typed by an ontology: probability mass only on schema-consistent triples.

Wraps a KnowledgeGraphDistribution (entities and relations as integer indices) together with the symbolic ontology and the index<->name maps. The tail posterior is masked to entities that satisfy the FULL triple contract for the completion – range, the head’s domain, irreflexivity, and disjointness (MXR-080-0299) – over a validated, numerically-stable-normalized model output (MXR-080-0300), and renormalized, so completion can never propose an ontology-violating tail nor silently degrade into NaN “probabilities” – Graph(ontology) as a distribution.

Parameters:
tail_posterior(head, relation)[source]

p(tail | head, relation) over ONLY entities satisfying the full triple contract for this completion (renormalized).

MXR-080-0299: previously masked on the candidate tail’s range class alone, so e.g. an irreflexive relation’s self-completion competed for probability mass on equal footing with every genuinely valid tail – for one such relation, self-completion was assigned the LARGEST probability of any candidate. The mask now also checks the head’s domain, irreflexivity, and disjointness.

MXR-080-0300: the raw model output is validated before it is ever normalized – a NaN, +inf, wrong-length, or non-vector tail_log_posterior raises instead of silently producing a dictionary of NaN “probabilities”. Returns {} when the ontology structurally admits no candidate for this completion (an ordinary, expected outcome – e.g. an empty range class), but RAISES when the schema admits candidates and every one of them is nonetheless degenerate (-inf / carries no finite mass): that is an anomalous model/data failure, not a schema fact, and returning {} for both would hide the difference.

Parameters:
Return type:

dict[str, float]

complete(head, relation)[source]

The most probable ontology-consistent tail (or None when the range admits no entity).

Parameters:
Return type:

tuple[str, float] | None

class ConstrainedDecode(facts, rejected, below_floor, n_samples, receipt=<factory>)[source]

Bases: object

The result of ontology-constrained LLM decoding: what survived, what the schema rejected, and why.

Parameters:
asserted()[source]

Return facts that passed constraints and confidence floor.

Return type:

list[Any]

as_dict()[source]

Return constrained decoding results as JSON-compatible data.

Return type:

dict[str, Any]

constrained_decode(llm, prompt, ontology, types, *, n=None, floor=0.5, calibrator=None, calibration_population=None)[source]

Decode only schema-consistent facts above a confidence floor.

Samples llm (a GraphLLM) n times, masks every sampled graph through Ontology.filter_triples() (violating triples are rejected with named reasons), then marginalizes the constrained graphs into a GraphDistribution and keeps only facts whose edge marginal clears floor – the calibrated confidence floor (pass a fitted calibrator from fit_fact_calibrator() to apply the floor on calibrated truth probability rather than the raw marginal). Consistent-but-underconfident facts are reported as withheld, never silently dropped: the decode says what it refused to assert and why.

MXR-080-0298: per-sample filtering only guarantees each INDIVIDUAL sampled graph is internally consistent; the published facts set is built by thresholding each triple’s marginal independently, so two triples that never co-occurred in any single sample (e.g. two different tails of a functional relation, each confident in a disjoint subset of samples) could previously both clear the floor and be asserted together – globally inconsistent while still labeled “ontology-constrained”. facts is re-validated as a graph after confidence-sorting, so Ontology.filter_triples()’s “first in the list wins” conflict policy resolves to “highest-confidence wins” at this aggregate stage; every casualty is named in rejected.

MXR-080-0301: floor and every calibrated confidence must be finite and within [0, 1] – see _require_unit_probability() for why these raise rather than silently clip or misclassify. The result’s receipt binds the calibrator’s identity (its class and, when available, its method), the calibration_population the caller asserts it was fit on (this function cannot recover that from an arbitrary calibrator object, so it is taken on trust from the caller – pass whatever identifies the labeled set used with fit_fact_calibrator()), and ontology.version, so a result can be independently audited instead of trusted blind.

Parameters:
Return type:

ConstrainedDecode