Skip to content

Semantics

Every inference variable is a binary proposition. A node X with requires parents R, supports parents S and refutes parents F, of strengths r, s and f, and base b, is true with probability

\[ P(X = 1 \mid \text{parents}) = N \cdot O \cdot I, \]

where

  • \(N = \prod_{i \in R,\ R_i = 0} (1 - r_i)\) is the necessity gate (noisy-AND),
  • \(O = 1 - (1 - b) \prod_{j \in S,\ S_j = 1} (1 - s_j)\) is the sufficiency term (noisy-OR with leak b),
  • \(I = \prod_{k \in F,\ F_k = 1} (1 - f_k)\) is the inhibition by refuters.

So b is the probability of X when every required premise holds and no support or refuter is active; with no parents it is the prior.

compile_graph turns a Graph into an immutable Network:

  • nodes joined by equivalent relations are merged into one variable, and their bases must agree;
  • each exclusive(A, B) relation becomes a constraint variable C with \(P(C = 1 \mid A, B) = 0\) when both are true and 1 otherwise, observed as \(C = 1\) in every query, which changes the marginals of A and B;
  • an inference variable without a base is a compile error: there is no default credence.

A compiled network holds every parameter at its credence mean. Because each parameter enters the joint distribution with degree at most one, the answer at the means is the predictive ratio \(E[P(A, E)] / E[P(E)]\), where both expectations are taken over independent parameter distributions and the exclusive constraints count as evidence on both sides. This is not the average of \(P(A \mid E)\) over the parameters, which generally differs.

Probabilistic semantics: conditional probability tables and compiling a graph into a network.

CompileError

Bases: CredenceGraphError, ValueError

A graph cannot be compiled into a network.

Raised for an inference variable without a base, for equivalent nodes whose bases conflict, and for a cycle that only appears once equivalent nodes are merged.

Source code in src/credencegraph/semantics/errors.py
 8
 9
10
11
12
13
class CompileError(CredenceGraphError, ValueError):
    """A graph cannot be compiled into a network.

    Raised for an inference variable without a ``base``, for ``equivalent`` nodes whose bases
    conflict, and for a cycle that only appears once ``equivalent`` nodes are merged.
    """

Factor dataclass

A non-negative table over binary variables.

Attributes:

Name Type Description
scope tuple[int, ...]

The variable indices, one per table axis.

table ndarray

A read-only array of shape (2,) * len(scope).

Source code in src/credencegraph/semantics/network.py
75
76
77
78
79
80
81
82
83
84
85
@dataclass(frozen=True, slots=True, eq=False)
class Factor:
    """A non-negative table over binary variables.

    Attributes:
        scope: The variable indices, one per table axis.
        table: A read-only array of shape ``(2,) * len(scope)``.
    """

    scope: tuple[int, ...]
    table: np.ndarray

An inferential relation into a proposition variable.

Attributes:

Name Type Description
relation str

The relation id, which is also the id of its strength parameter.

type str

requires, supports or refutes.

parent int

Index of the parent variable.

Source code in src/credencegraph/semantics/network.py
32
33
34
35
36
37
38
39
40
41
42
43
44
@dataclass(frozen=True, slots=True)
class Link:
    """An inferential relation into a proposition variable.

    Attributes:
        relation: The relation id, which is also the id of its strength parameter.
        type: ``requires``, ``supports`` or ``refutes``.
        parent: Index of the parent variable.
    """

    relation: str
    type: str
    parent: int

Network

An immutable Bayesian network compiled from a graph.

The network keeps each parameter's credence and the value each parameter currently takes; the tables are built from those values. A freshly compiled network uses the credence means, which give the point answer of every query. with_parameters and intervene return new networks and leave this one unchanged.

Source code in src/credencegraph/semantics/network.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
class Network:
    """An immutable Bayesian network compiled from a graph.

    The network keeps each parameter's credence and the value each parameter currently takes; the
    tables are built from those values. A freshly compiled network uses the credence means, which
    give the point answer of every query. ``with_parameters`` and ``intervene`` return new networks
    and leave this one unchanged.
    """

    def __init__(
        self,
        variables: tuple[Variable, ...],
        parameters: Mapping[ParameterKey, Credence],
        carried: frozenset[str] = frozenset(),
        values: Mapping[ParameterKey, float] | None = None,
        interventions: Mapping[int, bool] | None = None,
    ) -> None:
        """Build the network's tables.

        Args:
            variables: The variables, with ``variables[i].index == i``.
            parameters: The credence of every parameter the variables refer to.
            carried: Ids of graph nodes that are not inference variables.
            values: Parameter values; a missing parameter takes its credence mean.
            interventions: Variables fixed by intervention, whose incoming relations are cut.

        Raises:
            ValidationError: If a variable is out of place or refers to an unknown parameter, or a
                value names an unknown parameter or is not a probability.
        """
        self._variables = tuple(variables)
        self._parameters = MappingProxyType(dict(parameters))
        self._carried = frozenset(carried)
        given: dict[ParameterKey, float] = {}
        for key, value in (values or {}).items():
            if key not in self._parameters:
                msg = f"network has no parameter {key!r}"
                raise ValidationError(msg)
            given[key] = _probability(value, f"parameter {key.kind} of {key.id!r}")
        self._values = MappingProxyType({key: given.get(key, credence.mean) for key, credence in parameters.items()})
        self._interventions = MappingProxyType(dict(interventions or {}))
        self._node_index: dict[str, int] = {}
        for position, variable in enumerate(self._variables):
            if variable.index != position:
                msg = f"variable {variable.name!r} has index {variable.index}, expected {position}"
                raise ValidationError(msg)
            for node_id in variable.members:
                self._node_index[node_id] = position
        self._factors = tuple(self._factor(variable) for variable in self._variables)

    def _factor(self, variable: Variable) -> Factor:
        """Build the factor of one variable from the current parameter values.

        Args:
            variable: The variable.

        Returns:
            Its conditional probability table, or a point mass if it is intervened on.

        Raises:
            ValidationError: If the variable refers to an unknown parameter.
        """
        if variable.index in self._interventions:
            table = np.zeros(2)
            table[int(self._interventions[variable.index])] = 1.0
            return Factor((variable.index,), _frozen(table))
        if variable.kind == CONSTRAINT:
            table = exclusion_table(len(variable.parents))
        else:
            position = {parent: axis for axis, parent in enumerate(variable.parents)}
            terms = [
                Term(position[link.parent], link.type, self._value(ParameterKey("strength", link.relation)))
                for link in variable.links
            ]
            base = self._value(ParameterKey("base", variable.base or ""))
            table = proposition_table(base, terms, len(variable.parents))
        return Factor((*variable.parents, variable.index), _frozen(table))

    def _value(self, key: ParameterKey) -> float:
        """Look up a parameter's current value.

        Args:
            key: The parameter.

        Returns:
            Its value.

        Raises:
            ValidationError: If the network has no such parameter.
        """
        if key not in self._values:
            msg = f"network has no parameter {key.kind} of {key.id!r}"
            raise ValidationError(msg)
        return self._values[key]

    @property
    def variables(self) -> tuple[Variable, ...]:
        """The variables, propositions first in topological order, then constraints."""
        return self._variables

    @property
    def factors(self) -> tuple[Factor, ...]:
        """One factor per variable: its table given its parents, over ``(*parents, variable)``."""
        return self._factors

    @property
    def parameters(self) -> Mapping[ParameterKey, Credence]:
        """Read-only view of every parameter's credence."""
        return self._parameters

    @property
    def values(self) -> Mapping[ParameterKey, float]:
        """Read-only view of the value every parameter currently takes."""
        return self._values

    @property
    def interventions(self) -> Mapping[int, bool]:
        """Read-only view of the variables fixed by intervention, by index."""
        return self._interventions

    @property
    def constraints(self) -> Mapping[int, bool]:
        """The constraint variables, each observed true in every query."""
        return MappingProxyType({v.index: True for v in self._variables if v.kind == CONSTRAINT})

    def __len__(self) -> int:
        """Return the number of variables, constraints included."""
        return len(self._variables)

    def __repr__(self) -> str:
        """Summarise the network."""
        n_constraints = len(self.constraints)
        return f"Network(propositions={len(self) - n_constraints}, constraints={n_constraints})"

    def index(self, node_id: str) -> int:
        """Find the variable that stands for a node.

        Args:
            node_id: A node id; nodes merged by ``equivalent`` share one variable.

        Returns:
            The variable index.

        Raises:
            ValidationError: If the node is unknown or is not an inference variable.
        """
        if node_id in self._node_index:
            return self._node_index[node_id]
        if node_id in self._carried:
            msg = f"node {node_id!r} is not an inference variable: it has no base and no inferential relation"
            raise ValidationError(msg)
        msg = f"no node {node_id!r} in the network"
        raise ValidationError(msg)

    def with_parameters(self, values: Mapping[ParameterKey, float]) -> Network:
        """Return a copy of the network in which some parameters take the given values.

        The credences are unchanged. Overriding a ``Beta`` parameter moves the point answer of a
        query but not its band, which draws that parameter from its credence; overriding a
        parameter with a ``Point`` credence moves both, since its value is kept in every draw.

        Args:
            values: New values by parameter; every other parameter keeps its current value.

        Returns:
            The new network, with the same interventions.

        Raises:
            ValidationError: If a key is not a parameter of the network or a value is not in [0, 1].
        """
        merged = {**self._values, **values}
        return Network(self._variables, self._parameters, self._carried, merged, self._interventions)

    def intervene(self, assignment: Mapping[str, bool]) -> Network:
        """Return a copy of the network with some propositions fixed by intervention.

        Intervening on a node cuts the relations into its variable and fixes the variable's value.
        Unlike conditioning, it leaves the node's own premises unchanged. A later intervention on the
        same variable replaces an earlier one. ``exclusive`` constraints stay observed.

        Args:
            assignment: Node ids and the value each is set to.

        Returns:
            The intervened network.

        Raises:
            ValidationError: If a node is not an inference variable, a value is not a bool, or two
                merged nodes are set to different values.
        """
        fixed: dict[int, bool] = {}
        for node_id, value in assignment.items():
            if not isinstance(value, bool):
                msg = f"intervention on {node_id!r} must be True or False, got {value!r}"
                raise ValidationError(msg)
            index = self.index(node_id)
            if fixed.get(index, value) != value:
                msg = f"intervention sets {self._variables[index].name!r} both true and false"
                raise ValidationError(msg)
            fixed[index] = value
        return Network(self._variables, self._parameters, self._carried, self._values, {**self._interventions, **fixed})

constraints property

The constraint variables, each observed true in every query.

factors property

One factor per variable: its table given its parents, over (*parents, variable).

interventions property

Read-only view of the variables fixed by intervention, by index.

parameters property

Read-only view of every parameter's credence.

values property

Read-only view of the value every parameter currently takes.

variables property

The variables, propositions first in topological order, then constraints.

__init__(variables, parameters, carried=frozenset(), values=None, interventions=None)

Build the network's tables.

Parameters:

Name Type Description Default
variables tuple[Variable, ...]

The variables, with variables[i].index == i.

required
parameters Mapping[ParameterKey, Credence]

The credence of every parameter the variables refer to.

required
carried frozenset[str]

Ids of graph nodes that are not inference variables.

frozenset()
values Mapping[ParameterKey, float] | None

Parameter values; a missing parameter takes its credence mean.

None
interventions Mapping[int, bool] | None

Variables fixed by intervention, whose incoming relations are cut.

None

Raises:

Type Description
ValidationError

If a variable is out of place or refers to an unknown parameter, or a value names an unknown parameter or is not a probability.

Source code in src/credencegraph/semantics/network.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def __init__(
    self,
    variables: tuple[Variable, ...],
    parameters: Mapping[ParameterKey, Credence],
    carried: frozenset[str] = frozenset(),
    values: Mapping[ParameterKey, float] | None = None,
    interventions: Mapping[int, bool] | None = None,
) -> None:
    """Build the network's tables.

    Args:
        variables: The variables, with ``variables[i].index == i``.
        parameters: The credence of every parameter the variables refer to.
        carried: Ids of graph nodes that are not inference variables.
        values: Parameter values; a missing parameter takes its credence mean.
        interventions: Variables fixed by intervention, whose incoming relations are cut.

    Raises:
        ValidationError: If a variable is out of place or refers to an unknown parameter, or a
            value names an unknown parameter or is not a probability.
    """
    self._variables = tuple(variables)
    self._parameters = MappingProxyType(dict(parameters))
    self._carried = frozenset(carried)
    given: dict[ParameterKey, float] = {}
    for key, value in (values or {}).items():
        if key not in self._parameters:
            msg = f"network has no parameter {key!r}"
            raise ValidationError(msg)
        given[key] = _probability(value, f"parameter {key.kind} of {key.id!r}")
    self._values = MappingProxyType({key: given.get(key, credence.mean) for key, credence in parameters.items()})
    self._interventions = MappingProxyType(dict(interventions or {}))
    self._node_index: dict[str, int] = {}
    for position, variable in enumerate(self._variables):
        if variable.index != position:
            msg = f"variable {variable.name!r} has index {variable.index}, expected {position}"
            raise ValidationError(msg)
        for node_id in variable.members:
            self._node_index[node_id] = position
    self._factors = tuple(self._factor(variable) for variable in self._variables)

__len__()

Return the number of variables, constraints included.

Source code in src/credencegraph/semantics/network.py
246
247
248
def __len__(self) -> int:
    """Return the number of variables, constraints included."""
    return len(self._variables)

__repr__()

Summarise the network.

Source code in src/credencegraph/semantics/network.py
250
251
252
253
def __repr__(self) -> str:
    """Summarise the network."""
    n_constraints = len(self.constraints)
    return f"Network(propositions={len(self) - n_constraints}, constraints={n_constraints})"

index(node_id)

Find the variable that stands for a node.

Parameters:

Name Type Description Default
node_id str

A node id; nodes merged by equivalent share one variable.

required

Returns:

Type Description
int

The variable index.

Raises:

Type Description
ValidationError

If the node is unknown or is not an inference variable.

Source code in src/credencegraph/semantics/network.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def index(self, node_id: str) -> int:
    """Find the variable that stands for a node.

    Args:
        node_id: A node id; nodes merged by ``equivalent`` share one variable.

    Returns:
        The variable index.

    Raises:
        ValidationError: If the node is unknown or is not an inference variable.
    """
    if node_id in self._node_index:
        return self._node_index[node_id]
    if node_id in self._carried:
        msg = f"node {node_id!r} is not an inference variable: it has no base and no inferential relation"
        raise ValidationError(msg)
    msg = f"no node {node_id!r} in the network"
    raise ValidationError(msg)

intervene(assignment)

Return a copy of the network with some propositions fixed by intervention.

Intervening on a node cuts the relations into its variable and fixes the variable's value. Unlike conditioning, it leaves the node's own premises unchanged. A later intervention on the same variable replaces an earlier one. exclusive constraints stay observed.

Parameters:

Name Type Description Default
assignment Mapping[str, bool]

Node ids and the value each is set to.

required

Returns:

Type Description
Network

The intervened network.

Raises:

Type Description
ValidationError

If a node is not an inference variable, a value is not a bool, or two merged nodes are set to different values.

Source code in src/credencegraph/semantics/network.py
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
def intervene(self, assignment: Mapping[str, bool]) -> Network:
    """Return a copy of the network with some propositions fixed by intervention.

    Intervening on a node cuts the relations into its variable and fixes the variable's value.
    Unlike conditioning, it leaves the node's own premises unchanged. A later intervention on the
    same variable replaces an earlier one. ``exclusive`` constraints stay observed.

    Args:
        assignment: Node ids and the value each is set to.

    Returns:
        The intervened network.

    Raises:
        ValidationError: If a node is not an inference variable, a value is not a bool, or two
            merged nodes are set to different values.
    """
    fixed: dict[int, bool] = {}
    for node_id, value in assignment.items():
        if not isinstance(value, bool):
            msg = f"intervention on {node_id!r} must be True or False, got {value!r}"
            raise ValidationError(msg)
        index = self.index(node_id)
        if fixed.get(index, value) != value:
            msg = f"intervention sets {self._variables[index].name!r} both true and false"
            raise ValidationError(msg)
        fixed[index] = value
    return Network(self._variables, self._parameters, self._carried, self._values, {**self._interventions, **fixed})

with_parameters(values)

Return a copy of the network in which some parameters take the given values.

The credences are unchanged. Overriding a Beta parameter moves the point answer of a query but not its band, which draws that parameter from its credence; overriding a parameter with a Point credence moves both, since its value is kept in every draw.

Parameters:

Name Type Description Default
values Mapping[ParameterKey, float]

New values by parameter; every other parameter keeps its current value.

required

Returns:

Type Description
Network

The new network, with the same interventions.

Raises:

Type Description
ValidationError

If a key is not a parameter of the network or a value is not in [0, 1].

Source code in src/credencegraph/semantics/network.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
def with_parameters(self, values: Mapping[ParameterKey, float]) -> Network:
    """Return a copy of the network in which some parameters take the given values.

    The credences are unchanged. Overriding a ``Beta`` parameter moves the point answer of a
    query but not its band, which draws that parameter from its credence; overriding a
    parameter with a ``Point`` credence moves both, since its value is kept in every draw.

    Args:
        values: New values by parameter; every other parameter keeps its current value.

    Returns:
        The new network, with the same interventions.

    Raises:
        ValidationError: If a key is not a parameter of the network or a value is not in [0, 1].
    """
    merged = {**self._values, **values}
    return Network(self._variables, self._parameters, self._carried, merged, self._interventions)

ParameterKey

Bases: NamedTuple

Names one parameter of a network.

Attributes:

Name Type Description
kind Literal['base', 'strength']

"base" for a node's base, "strength" for a relation's strength.

id str

The id of the node or relation that carries the parameter.

Source code in src/credencegraph/semantics/network.py
20
21
22
23
24
25
26
27
28
29
class ParameterKey(NamedTuple):
    """Names one parameter of a network.

    Attributes:
        kind: ``"base"`` for a node's base, ``"strength"`` for a relation's strength.
        id: The id of the node or relation that carries the parameter.
    """

    kind: Literal["base", "strength"]
    id: str

Term dataclass

One inferential relation's contribution to a child's table.

Attributes:

Name Type Description
parent int

Position of the parent among the table's parent axes.

type str

requires, supports or refutes.

strength float

The relation's strength, a probability.

Source code in src/credencegraph/semantics/cpt.py
30
31
32
33
34
35
36
37
38
39
40
41
42
@dataclass(frozen=True, slots=True)
class Term:
    """One inferential relation's contribution to a child's table.

    Attributes:
        parent: Position of the parent among the table's parent axes.
        type: ``requires``, ``supports`` or ``refutes``.
        strength: The relation's strength, a probability.
    """

    parent: int
    type: str
    strength: float

Variable dataclass

A binary variable of the network.

A proposition variable stands for one node, or for several nodes merged by equivalent relations. A constraint variable encodes one exclusive relation and is always observed true.

Attributes:

Name Type Description
index int

Position of the variable in the network.

name str

The representative node id, or exclusive[<relation id>] for a constraint.

kind Literal['proposition', 'constraint']

"proposition" or "constraint".

members tuple[str, ...]

The node ids the variable stands for; empty for a constraint.

parents tuple[int, ...]

Indices of the parent variables, ascending and distinct.

base str | None

The node id whose base is the variable's base; None for a constraint.

links tuple[Link, ...]

The inferential relations into the variable.

relation str | None

The exclusive relation id of a constraint; None for a proposition.

Source code in src/credencegraph/semantics/network.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@dataclass(frozen=True, slots=True)
class Variable:
    """A binary variable of the network.

    A proposition variable stands for one node, or for several nodes merged by ``equivalent``
    relations. A constraint variable encodes one ``exclusive`` relation and is always observed true.

    Attributes:
        index: Position of the variable in the network.
        name: The representative node id, or ``exclusive[<relation id>]`` for a constraint.
        kind: ``"proposition"`` or ``"constraint"``.
        members: The node ids the variable stands for; empty for a constraint.
        parents: Indices of the parent variables, ascending and distinct.
        base: The node id whose ``base`` is the variable's base; ``None`` for a constraint.
        links: The inferential relations into the variable.
        relation: The ``exclusive`` relation id of a constraint; ``None`` for a proposition.
    """

    index: int
    name: str
    kind: Literal["proposition", "constraint"]
    members: tuple[str, ...]
    parents: tuple[int, ...]
    base: str | None = None
    links: tuple[Link, ...] = ()
    relation: str | None = None

compile_graph(graph)

Compile a graph into a network.

Nodes joined by equivalent relations are merged into one variable, named after the earliest of them; their bases must agree. Each exclusive relation becomes a constraint variable that is false only when both of its nodes are true, and that every query observes as true. Nodes that are not inference variables, and annotation relations, are left out.

Parameters:

Name Type Description Default
graph Graph

The graph.

required

Returns:

Type Description
Network

The network, with every parameter at its credence mean.

Raises:

Type Description
CompileError

If an inference variable has no base, merged nodes have conflicting bases, or the relations form a cycle once equivalent nodes are merged.

Source code in src/credencegraph/semantics/compiler.py
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def compile_graph(graph: Graph) -> Network:
    """Compile a graph into a network.

    Nodes joined by ``equivalent`` relations are merged into one variable, named after the earliest
    of them; their bases must agree. Each ``exclusive`` relation becomes a constraint variable that
    is false only when both of its nodes are true, and that every query observes as true. Nodes that
    are not inference variables, and annotation relations, are left out.

    Args:
        graph: The graph.

    Returns:
        The network, with every parameter at its credence mean.

    Raises:
        CompileError: If an inference variable has no base, merged nodes have conflicting bases, or
            the relations form a cycle once ``equivalent`` nodes are merged.
    """
    node_ids = _inference_nodes(graph)
    sets, members = _merge(graph, node_ids)
    bases = _bases(graph, members)

    edges: dict[str, list[tuple[str, str]]] = {}
    links: dict[str, list[tuple[str, str, str]]] = {}
    parameters: dict[ParameterKey, Credence] = {}
    for node_id in bases.values():
        parameters[ParameterKey("base", node_id)] = graph.nodes[node_id].base  # type: ignore[assignment]
    for relation in graph.relations.values():
        if relation.type not in STRENGTH_TYPES:
            continue
        parent, child = sets.find(relation.source), sets.find(relation.target)
        edges.setdefault(parent, []).append((relation.id, child))
        links.setdefault(child, []).append((relation.id, relation.type, parent))
        parameters[ParameterKey("strength", relation.id)] = relation.strength  # type: ignore[assignment]

    order = _topological(list(members), edges)
    index = {root: position for position, root in enumerate(order)}
    variables: list[Variable] = []
    for root in order:
        own = links.get(root, [])
        variables.append(
            Variable(
                index=index[root],
                name=root,
                kind=PROPOSITION,
                members=tuple(members[root]),
                parents=tuple(sorted({index[parent] for _, _, parent in own})),
                base=bases[root],
                links=tuple(Link(relation, rtype, index[parent]) for relation, rtype, parent in own),
            )
        )
    for relation in graph.relations.values():
        if relation.type != EXCLUSIVE:
            continue
        parents = tuple(sorted({index[sets.find(relation.source)], index[sets.find(relation.target)]}))
        variables.append(
            Variable(
                index=len(variables),
                name=f"exclusive[{relation.id}]",
                kind=CONSTRAINT,
                members=(),
                parents=parents,
                relation=relation.id,
            )
        )
    carried = frozenset(graph.nodes) - frozenset(node_ids)
    return Network(tuple(variables), parameters, carried)

exclusion_table(n_parents)

Build the table of the constraint variable C that encodes "not all of these are true".

P(C = 1 | parents) is 0 when every parent is true and 1 otherwise. exclusive(A, B) uses two parents; when A and B were merged as equivalent the constraint has the single parent A, and observing C = 1 then forces A to be false.

Parameters:

Name Type Description Default
n_parents int

The number of parent axes, at least 1.

required

Returns:

Type Description
ndarray

An array of shape (2,) * (n_parents + 1) holding P(C = c | parents).

Raises:

Type Description
ValueError

If n_parents is below 1.

Source code in src/credencegraph/semantics/cpt.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def exclusion_table(n_parents: int) -> np.ndarray:
    """Build the table of the constraint variable C that encodes "not all of these are true".

    ``P(C = 1 | parents)`` is 0 when every parent is true and 1 otherwise. ``exclusive(A, B)`` uses
    two parents; when A and B were merged as ``equivalent`` the constraint has the single parent A,
    and observing C = 1 then forces A to be false.

    Args:
        n_parents: The number of parent axes, at least 1.

    Returns:
        An array of shape ``(2,) * (n_parents + 1)`` holding ``P(C = c | parents)``.

    Raises:
        ValueError: If ``n_parents`` is below 1.
    """
    if n_parents < 1:
        msg = f"an exclusion constraint needs at least one parent, got {n_parents}"
        raise ValueError(msg)
    true = np.ones((2,) * n_parents)
    true[(1,) * n_parents] = 0.0
    return np.stack([1.0 - true, true], axis=-1)

inference_sets(graph)

Group a graph's inference variables into the sets that compile to one variable each.

Parameters:

Name Type Description Default
graph Graph

The graph.

required

Returns:

Type Description
dict[str, list[str]]

The node ids of each set of nodes joined by equivalent relations, keyed by its earliest

dict[str, list[str]]

member, both in insertion order.

Source code in src/credencegraph/semantics/compiler.py
 96
 97
 98
 99
100
101
102
103
104
105
106
def inference_sets(graph: Graph) -> dict[str, list[str]]:
    """Group a graph's inference variables into the sets that compile to one variable each.

    Args:
        graph: The graph.

    Returns:
        The node ids of each set of nodes joined by ``equivalent`` relations, keyed by its earliest
        member, both in insertion order.
    """
    return _merge(graph, _inference_nodes(graph))[1]

proposition_table(base, terms, n_parents)

Build the table of a proposition from its base and inferential relations.

A parent may appear in several terms, for example when two relations from nodes that were merged as equivalent reach the same child; each term then acts as an independent mechanism.

Parameters:

Name Type Description Default
base float

The base probability b.

required
terms Sequence[Term]

The inferential relations into the child.

required
n_parents int

The number of parent axes.

required

Returns:

Type Description
ndarray

An array of shape (2,) * (n_parents + 1) holding P(X = x | parents).

Raises:

Type Description
ValueError

If a term names an unknown relation type or a parent axis out of range.

Source code in src/credencegraph/semantics/cpt.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def proposition_table(base: float, terms: Sequence[Term], n_parents: int) -> np.ndarray:
    """Build the table of a proposition from its base and inferential relations.

    A parent may appear in several terms, for example when two relations from nodes that were
    merged as ``equivalent`` reach the same child; each term then acts as an independent mechanism.

    Args:
        base: The base probability ``b``.
        terms: The inferential relations into the child.
        n_parents: The number of parent axes.

    Returns:
        An array of shape ``(2,) * (n_parents + 1)`` holding ``P(X = x | parents)``.

    Raises:
        ValueError: If a term names an unknown relation type or a parent axis out of range.
    """
    shape = (2,) * n_parents
    grid = np.indices(shape)
    necessity = np.ones(shape)
    no_support = np.ones(shape)
    inhibition = np.ones(shape)
    for term in terms:
        if term.type not in STRENGTH_TYPES:
            msg = f"unknown inferential relation type {term.type!r}"
            raise ValueError(msg)
        if not 0 <= term.parent < n_parents:
            msg = f"term parent {term.parent} out of range for {n_parents} parents"
            raise ValueError(msg)
        parent = grid[term.parent]
        if term.type == REQUIRES:
            necessity = necessity * np.where(parent == 0, 1.0 - term.strength, 1.0)
        elif term.type == SUPPORTS:
            no_support = no_support * np.where(parent == 1, 1.0 - term.strength, 1.0)
        elif term.type == REFUTES:
            inhibition = inhibition * np.where(parent == 1, 1.0 - term.strength, 1.0)
    # 1 - (1 - b) * P rewritten as b * P + (1 - P): two non-negative terms, so a tiny base is not
    # lost to the cancellation of 1 - (1 - b), and with no active support it is exactly b.
    sufficiency = base * no_support + (1.0 - no_support)
    true = necessity * sufficiency * inhibition
    return np.stack([1.0 - true, true], axis=-1)