ergodicDocs
Products: switch from ergodic to another product
Get started

ReferenceDomain knowledge

Domain knowledge

Domain knowledge: prior constraints on causal structure. Import from ergodic.knowledge.

DomainKnowledge records what a user knows before looking at the data, the edges that must or must not exist, the temporal order of variables, which variables are roots or sinks, and which pairs share a hidden confounder. A skeleton restriction (restrict_to) caps which pairs may be adjacent at all. It is an immutable value object built up with small methods, and two knowledge objects combine with & (more knowledge means fewer admissible graphs).

Every builder but confound also takes confidence=. Left out (or 1), the statement is a hard constraint, stored as before. A value in [0.5, 1) makes it soft: decode() weighs it against a model's edge probabilities instead of enforcing it. The confidence applies to each pair the statement covers: soft tiers over 16 cross-tier pairs act as 16 beliefs of that confidence, not one belief that the whole tiering holds. Soft statements live in soft; every constraint projection (conflicts, violations, permits_edge, edge_mask, apply_to) reads the hard ones only, and the discovery searches refuse soft statements rather than drop them (hard() removes them). log_odds(), log_prior() and energy() read the soft ones.

On one pair there are three exclusive states (no edge, a -> b, b -> a), and a soft statement with confidence c multiplies the chance of each state by c when the state makes it hold and by 1 - c when it makes it fail. Against "no edge" that is a likelihood ratio on each directed state: c / (1 - c) for a state that makes the statement hold where "no edge" fails it, (1 - c) / c for the reverse, and 1 otherwise. Different statements multiply; the same statement made twice is one statement (soft is a set). A soft order (order, tiers) is weighed pair by pair, on the direct edge against it, and is not closed transitively, as a hard one is.

Also exported from ergodic: InconsistentKnowledgeError, KnowledgeConflictWarning, DomainKnowledge.

InconsistentKnowledgeError#

Domain knowledge contradicts itself (for example a required edge cycle).

KnowledgeConflictWarning#

The model strongly disagrees with a knowledge statement.

decode() raises it once per decode when some pair a statement covers gets a model probability below 0.1 for the statement's part on that pair. Filter it by category, for example warnings.simplefilter("ignore", KnowledgeConflictWarning).

KnowledgeStatement#

python
class KnowledgeStatement(
    kind: str,
    args: tuple[Any, ...],
    confidence: float | None = None,
) -> None

One knowledge statement.

kind names the builder: "require", "forbid", "forbid_adjacency", "order" and "confound" take two names in args; "forbid_between" two groups of names; "tiers" the tiers in order, each a tuple of names; "root" and "sink" one name (a builder call naming several makes one statement per name); "restrict_to" the skeleton pairs. The builders sort groups, tiers and pairs. confidence is None for a hard statement and in [0.5, 1) for a soft one; confound is hard only. Everything is checked on construction.

Attribute Description
kind The builder that made the statement.
args Its arguments, as above.
confidence None (hard) or the soft statement's confidence.
Field Type Default
kind str required
args tuple[Any, ...] required
confidence float | None None

KnowledgeStatement.is_soft#

python
property KnowledgeStatement.is_soft: bool

Whether the statement is soft (it has a confidence below 1).

KnowledgeStatement.variables#

python
property KnowledgeStatement.variables: frozenset[str]

Every variable the statement names.

DomainKnowledge#

python
class DomainKnowledge(
    variables: frozenset[str] = <factory>,
    required: frozenset[tuple[str, str]] = <factory>,
    forbidden: frozenset[tuple[str, str]] = <factory>,
    precedes: frozenset[tuple[str, str]] = <factory>,
    roots: frozenset[str] = <factory>,
    sinks: frozenset[str] = <factory>,
    confounded: frozenset[frozenset[str]] = <factory>,
    allowed_adjacencies: frozenset[frozenset[str]] | None = None,
    soft: frozenset[KnowledgeStatement] = <factory>,
    nonadjacent: frozenset[frozenset[str]] = <factory>,
) -> None

Prior constraints on a causal graph over named variables.

The hard constraints live in the set fields (required, forbidden, nonadjacent, precedes, roots, sinks, confounded, allowed_adjacencies), the soft statements in soft. forbid_adjacency() records its pair in nonadjacent as well as both directions in forbidden: an adjacency ban rules out a bidirected edge too, while two directed bans (forbid(a, b) and forbid(b, a)) do not, so confound(a, b) with them says "confounded, and neither causes the other".

Field Type Default
variables frozenset[str] factory
required frozenset[tuple[str, str]] factory
forbidden frozenset[tuple[str, str]] factory
precedes frozenset[tuple[str, str]] factory
roots frozenset[str] factory
sinks frozenset[str] factory
confounded frozenset[frozenset[str]] factory
allowed_adjacencies frozenset[frozenset[str]] | None None
soft frozenset[KnowledgeStatement] factory
nonadjacent frozenset[frozenset[str]] factory

DomainKnowledge.hard#

python
def DomainKnowledge.hard() -> DomainKnowledge

This knowledge without its soft statements.

The discovery searches read hard constraints only and refuse soft statements rather than drop them silently; pass knowledge.hard() to run one with the hard part. The variables stay listed.

DomainKnowledge.with_variables#

python
def DomainKnowledge.with_variables(*names: str) -> DomainKnowledge

Return knowledge that also lists names in variables.

Every builder records the names it mentions there; this adds names without adding a constraint.

DomainKnowledge.require#

python
def DomainKnowledge.require(
    cause: str,
    effect: str,
    *,
    confidence: float | None = None,
) -> DomainKnowledge

Require a directed edge cause -> effect.

Parameter Description
cause The edge's source.
effect The edge's target.
confidence None or 1 for a hard constraint; a value in [0.5, 1) makes it soft (0.5 changes nothing).

DomainKnowledge.forbid#

python
def DomainKnowledge.forbid(
    cause: str,
    effect: str,
    *,
    confidence: float | None = None,
) -> DomainKnowledge

Forbid the directed edge cause -> effect (the reverse stays allowed).

confidence makes it soft, as for require(). Forbidding both directions bans every edge but a bidirected one, so it can sit beside confound().

DomainKnowledge.forbid_adjacency#

python
def DomainKnowledge.forbid_adjacency(
    a: str,
    b: str,
    *,
    confidence: float | None = None,
) -> DomainKnowledge

Forbid any edge between two variables, bidirected included.

confidence makes it soft, as for require(); a soft ban weighs the two directed edges (decoders see no other edge type).

DomainKnowledge.order#

python
def DomainKnowledge.order(
    before: str,
    after: str,
    *,
    confidence: float | None = None,
) -> DomainKnowledge

Record that before precedes after in time, so after cannot cause it.

confidence makes it soft, as for require(). A soft order weighs only the direct edge after -> before and is not closed transitively.

DomainKnowledge.with_tiers#

python
def DomainKnowledge.with_tiers(
    tiers: Sequence[Iterable[str]],
    *,
    confidence: float | None = None,
) -> DomainKnowledge

Add a tiered order: no variable in a later tier causes one in an earlier tier.

confidence makes it soft, as for require(): one statement that weighs every edge from a later tier into an earlier one, each with that confidence. It says nothing about pairs within a tier, as the hard form does not.

DomainKnowledge.root#

python
def DomainKnowledge.root(
    *names: str,
    confidence: float | None = None,
) -> DomainKnowledge

Mark variables as roots: no incoming directed edges.

confidence makes it soft, as for require(), with one statement per name, weighing every edge into it.

DomainKnowledge.sink#

python
def DomainKnowledge.sink(
    *names: str,
    confidence: float | None = None,
) -> DomainKnowledge

Mark variables as sinks: no outgoing directed edges.

confidence makes it soft, as for require(), with one statement per name, weighing every edge out of it.

DomainKnowledge.confound#

python
def DomainKnowledge.confound(
    a: str,
    b: str,
    *,
    confidence: float | None = None,
) -> DomainKnowledge

Record that a and b share a hidden common cause.

A confounder is hard only: confidence must be None or 1. A hidden common cause is not an edge state, so edge probabilities cannot weigh it. Add forbid(a, b) and forbid(b, a) to say that neither causes the other as well.

DomainKnowledge.forbid_between#

python
def DomainKnowledge.forbid_between(
    group_a: Iterable[str],
    group_b: Iterable[str],
    *,
    confidence: float | None = None,
) -> DomainKnowledge

Forbid every directed edge from group_a into group_b.

confidence makes it soft, as for require(): one statement that weighs every such edge, each with that confidence.

DomainKnowledge.restrict_to#

python
def DomainKnowledge.restrict_to(
    skeleton: MixedGraph | Iterable[tuple[str, str]],
    *,
    confidence: float | None = None,
) -> DomainKnowledge

Allow adjacencies only on the pairs of skeleton.

skeleton is a MixedGraph (its edges name the pairs, whatever the marks) or an iterable of (u, v) pairs. A pair outside the allow-list is banned in every form: directed either way, bidirected, or undirected. Restricting an already restricted object keeps the intersection, so a second call only narrows the support. An empty iterable allows no adjacencies at all.

confidence makes it soft, as for require(): one statement that weighs both directed edges of every pair outside the skeleton (among the variables being decoded), each pair with that confidence. Soft restrictions do not intersect; each is its own statement.

DomainKnowledge.combine#

python
def DomainKnowledge.combine(other: DomainKnowledge) -> DomainKnowledge

Combine two knowledge objects by taking the union of their constraints.

Skeleton allow-lists intersect: an adjacency survives only when both sides allow it, and None (unrestricted) defers to the other side. Soft statements are pooled as a set, so a statement both sides make (same kind, arguments and confidence) counts once.

DomainKnowledge.statements#

python
def DomainKnowledge.statements() -> tuple[KnowledgeStatement, ...]

Every statement, read from what is stored: the hard ones, then the soft ones.

Hard constraints are listed one per stored item, so equal knowledge lists equal statements however it was built: a required pair (require), an adjacency ban (forbid_adjacency), any other forbidden direction (forbid; forbid_between and two-way forbids show up this way), a precedence pair (order; tiers show up as their cross-tier pairs), a root, a sink, a confounded pair, and one restrict_to for the allow-list. Soft statements are listed as made, sorted by kind, then arguments.

DomainKnowledge.conflicts#

python
def DomainKnowledge.conflicts() -> list[str]

Return a list of internal contradictions among the hard constraints.

Empty when consistent. Soft statements never conflict: they are weighed, not enforced. A confounded pair under an adjacency ban (forbid_adjacency()) or outside a skeleton restriction is a contradiction; a confounded pair with both directed edges forbidden is not (confounded, and neither causes the other).

DomainKnowledge.validate#

python
def DomainKnowledge.validate() -> None

Raise InconsistentKnowledgeError if the hard constraints contradict.

Soft statements are never checked: they are weighed, not enforced.

DomainKnowledge.permits_edge#

python
def DomainKnowledge.permits_edge(cause: str, effect: str) -> bool

Return True if a directed edge cause -> effect is allowed.

Precedence is read transitively: with A before B and B before C, an edge C -> A is not permitted. Under a skeleton restriction, a pair outside the allow-list fails in both directions. Only hard constraints count; soft statements permit everything.

DomainKnowledge.requires_edge#

python
def DomainKnowledge.requires_edge(cause: str, effect: str) -> bool

Return True if cause -> effect was required with require().

Only the stated pair counts; nothing is inferred from tiers or other constraints. A soft require does not count.

DomainKnowledge.precedence#

python
def DomainKnowledge.precedence() -> frozenset[tuple[str, str]]

Every (before, after) pair, with the order read transitively.

The projection discovery reads to orient: (a, b) here means b cannot be an ancestor of a, which in a latent-variable search (FCI) pins an arrowhead at b on any surviving adjacency.

DomainKnowledge.violations#

python
def DomainKnowledge.violations(graph: MixedGraph) -> list[str]

Return the ways graph breaks the hard constraints, empty when consistent.

Soft statements are not checked. A node in graph.latent is read as a hidden cause, not as one of the variables: a latent node with two or more observed children joins each pair of them as a <-> b would (the way a bow, a -> b plus a <-> b, is written), for confounded pairs, adjacency bans and a skeleton restriction alike, and its edges count against neither a root nor a sink. Two directed bans on one pair (forbid(a, b) and forbid(b, a)) rule out every edge there but a bidirected one; forbid_adjacency() rules out that too.

DomainKnowledge.is_consistent#

python
def DomainKnowledge.is_consistent(graph: MixedGraph) -> bool

Return True if graph satisfies every hard constraint.

DomainKnowledge.apply_to#

python
def DomainKnowledge.apply_to(graph: MixedGraph) -> MixedGraph

Enforce the hard constraints on graph: drop forbidden, add required edges.

A pair under an adjacency ban (forbid_adjacency()) loses its edge whatever the marks; a pair forbidden in both directions keeps a bidirected edge (a hidden common cause, neither causing the other) and loses any other; a single-direction ban only drops the matching directed edge. Under a skeleton restriction, every edge on a pair outside the allow-list is dropped too. Edges left undirected or with a circle are oriented when only one direction stays permitted. The result is returned as a new graph. Soft statements are not applied.

DomainKnowledge.edge_mask#

python
def DomainKnowledge.edge_mask(
    variables: Sequence[str] | None = None,
) -> tuple[npt.NDArray[np.bool_], list[str]]

Return a boolean matrix of permitted directed edges and its variable order.

mask[i, j] is True when an edge from variable i to variable j is allowed. The diagonal is False, and a skeleton restriction zeroes both directions of every pair outside its allow-list. Useful as a search mask for score-based and continuous-optimization discovery. Only hard constraints shape the mask; log_odds() reads the soft ones.

DomainKnowledge.log_odds#

python
def DomainKnowledge.log_odds(
    variables: Sequence[str],
) -> npt.NDArray[np.float64]

The soft statements' summed log likelihood ratios on each directed edge.

out[i, j] is log of the product, over every soft statement, of the ratio it puts on the state variables[i] -> variables[j] against "no edge" on that pair: log(c / (1 - c)) when the edge makes the statement hold and no edge fails it, log((1 - c) / c) the other way round, and 0 otherwise. For example require(a, b) adds log(c / (1 - c)) at [a, b] and nothing at [b, a]; root(x) adds log((1 - c) / c) at every [v, x]. The diagonal is zero and hard constraints are left out (they are enforced, not weighed). decode() multiplies the pair states by exp(out) and renormalizes each pair.

Parameter Description
variables The variable order of the rows and columns. Every variable a soft statement names must be among them; root, sink and restrict_to range over all of them.

Returns: A (d, d) float matrix.

Raises Description
ValueError A soft statement names a variable outside variables, or a name repeats.

DomainKnowledge.log_prior#

python
def DomainKnowledge.log_prior(graph: MixedGraph) -> float

The log prior of graph under this knowledge, up to a constant.

Minus infinity when graph breaks a hard constraint (violations()). Otherwise the sum, over every soft statement and every pair it speaks about, of log c when the pair's state in graph satisfies the statement and log(1 - c) when it does not. That is the reading log_odds() and the decoders use, so a multi-pair statement (tiers, a root, a sink, restrict_to, forbid_between) counts once per pair, and an order is read as "no direct edge against it". Hard knowledge alone gives 0 for every consistent graph.

Parameter Description
graph A graph with directed (and, ignored here, bidirected) edges, such as a DAG or an ADMG. Latent nodes are not variables: root, sink and restrict_to range over the other nodes.
Raises Description
ValueError graph has an undirected or circle-marked edge, or a soft statement names a variable that is not an observed node.

DomainKnowledge.energy#

python
def DomainKnowledge.energy(graph: MixedGraph) -> float

Minus log_prior(): plus infinity when a hard constraint is broken.

Suggest an edit
Documentation

Search ergodic 0.3.0

Search ergodic by task, function or error.