Skip to content

Inference

Four query functions answer questions about a compiled network, each returning an Answer:

Query Meaning
marginal(network, "x") P(x)
joint(network, {"x": True, "y": False}) P(x and not y)
conditional(network, "x", {"y": True}) P(x | y)
intervene(network, "x", set={"y": False}) P(x) after cutting the relations into y and fixing y false

Conditioning on a node also updates what the node rests on; intervening does not. "What does the conclusion rest on?" is an interventional question.

Two exact engines are provided:

  • VariableElimination, the default, eliminates variables in a min-fill order. If an intermediate factor would exceed max_factor_size entries (default 2²²), the query fails with ProblemTooLargeError; it never falls back to an approximation on its own.
  • Enumeration sums over every world. It is exponential in the number of free variables (limited to 20 by default) and serves as the independent reference in tests.

Parameter uncertainty

Every base and strength is treated as independent of the others. An Answer carries:

  • point: the probability with every parameter at its credence mean. Because each parameter enters a joint probability with degree at most one, this is exactly the predictive probability E[P(A, E)] / E[P(E)] over the parameters' distributions, computed without sampling.
  • band: the 5%, 50% and 95% quantiles (q05, q50, q95) of the exact answer over Monte Carlo draws of the Beta parameters. It shows how far the answer would move if the inputs were different.
  • mean_over_draws: the average of those per-draw answers. The draws do not update the parameters on the evidence, so this generally differs from point and is reported under its own name.
  • draws: the number of draws behind the band.

The band is omitted (band and mean_over_draws are None) when every parameter is a Point, or when draws=0 is passed. Queries make DEFAULT_DRAWS (1000) draws by default; pass rng (a seed or a numpy.random.Generator) for reproducible bands.

Overriding a Beta parameter with Network.with_parameters moves point but not the band. The point answer uses the values the network holds, while every Beta parameter is drawn from its stored credence, including one that has been set to another value; a network whose Beta parameters have all been set still gets a band. To hold a parameter fixed in the band, give it a Point credence in the graph instead. Overriding a parameter whose credence is a Point moves the band as well as point, because its overridden value is kept in every draw.

Parameters are independent by construction, so correlated inputs (two measurements sharing an unknown bias, say) cannot be expressed as correlated strengths. Model the shared cause as a proposition of its own, such as "the instrument is calibrated", with a relation into each.

Inference: exact engines, the marginal, joint, conditional and interventional queries, and their bands.

Answer dataclass

The answer to a query.

Attributes:

Name Type Description
point float

The point answer: the probability with every parameter at its credence mean. Because the network is multilinear in its independent parameters, this equals the predictive ratio E[P(A, E)] / E[P(E)], where each expectation is taken over the parameters' distributions and the exclusive constraints count as evidence on both sides. It is not the average of P(A | E) over the parameters, which generally differs.

band Band | None

The 5%, 50% and 95% quantiles of the answer over parameter draws, or None when no draws were made because every parameter is a Point or draws was 0.

mean_over_draws float | None

The average of the per-draw answers, reported separately from point because it is a different quantity; None exactly when band is.

draws int

The number of parameter draws behind band and mean_over_draws; 0 when none.

Source code in src/credencegraph/inference/queries.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@dataclass(frozen=True, slots=True)
class Answer:
    """The answer to a query.

    Attributes:
        point: The point answer: the probability with every parameter at its credence mean. Because
            the network is multilinear in its independent parameters, this equals the predictive
            ratio ``E[P(A, E)] / E[P(E)]``, where each expectation is taken over the parameters'
            distributions and the ``exclusive`` constraints count as evidence on both sides. It is
            not the average of ``P(A | E)`` over the parameters, which generally differs.
        band: The 5%, 50% and 95% quantiles of the answer over parameter draws, or ``None`` when no
            draws were made because every parameter is a ``Point`` or ``draws`` was 0.
        mean_over_draws: The average of the per-draw answers, reported separately from ``point``
            because it is a different quantity; ``None`` exactly when ``band`` is.
        draws: The number of parameter draws behind ``band`` and ``mean_over_draws``; 0 when none.
    """

    point: float
    band: Band | None = None
    mean_over_draws: float | None = None
    draws: int = 0

    def __post_init__(self) -> None:
        """Check that the band, its mean and the draw count come together or not at all.

        Raises:
            ValidationError: If some but not all of them are given.
        """
        if (self.band is None) != (self.mean_over_draws is None) or (self.band is None) != (self.draws == 0):
            msg = "band, mean_over_draws and a positive draws count must be given together or not at all"
            raise ValidationError(msg)

    def to_dict(self) -> dict[str, object]:
        """Return the answer as JSON-ready data.

        Returns:
            ``{"point": ...}``, plus ``"band"``, ``"mean_over_draws"`` and ``"draws"`` when there is a band.
        """
        if self.band is None:
            return {"point": self.point}
        return {
            "point": self.point,
            "band": self.band.to_dict(),
            "mean_over_draws": self.mean_over_draws,
            "draws": self.draws,
        }

__post_init__()

Check that the band, its mean and the draw count come together or not at all.

Raises:

Type Description
ValidationError

If some but not all of them are given.

Source code in src/credencegraph/inference/queries.py
42
43
44
45
46
47
48
49
50
def __post_init__(self) -> None:
    """Check that the band, its mean and the draw count come together or not at all.

    Raises:
        ValidationError: If some but not all of them are given.
    """
    if (self.band is None) != (self.mean_over_draws is None) or (self.band is None) != (self.draws == 0):
        msg = "band, mean_over_draws and a positive draws count must be given together or not at all"
        raise ValidationError(msg)

to_dict()

Return the answer as JSON-ready data.

Returns:

Type Description
dict[str, object]

{"point": ...}, plus "band", "mean_over_draws" and "draws" when there is a band.

Source code in src/credencegraph/inference/queries.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def to_dict(self) -> dict[str, object]:
    """Return the answer as JSON-ready data.

    Returns:
        ``{"point": ...}``, plus ``"band"``, ``"mean_over_draws"`` and ``"draws"`` when there is a band.
    """
    if self.band is None:
        return {"point": self.point}
    return {
        "point": self.point,
        "band": self.band.to_dict(),
        "mean_over_draws": self.mean_over_draws,
        "draws": self.draws,
    }

Band dataclass

Quantiles of a query's answer over parameter draws.

Attributes:

Name Type Description
q05 float

The 5% quantile.

q50 float

The median.

q95 float

The 95% quantile.

Source code in src/credencegraph/inference/uncertainty.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@dataclass(frozen=True, slots=True)
class Band:
    """Quantiles of a query's answer over parameter draws.

    Attributes:
        q05: The 5% quantile.
        q50: The median.
        q95: The 95% quantile.
    """

    q05: float
    q50: float
    q95: float

    def to_dict(self) -> dict[str, float]:
        """Return the band as JSON-ready data.

        Returns:
            ``{"q05": ..., "q50": ..., "q95": ...}``.
        """
        return {"q05": self.q05, "q50": self.q50, "q95": self.q95}

to_dict()

Return the band as JSON-ready data.

Returns:

Type Description
dict[str, float]

{"q05": ..., "q50": ..., "q95": ...}.

Source code in src/credencegraph/inference/uncertainty.py
51
52
53
54
55
56
57
def to_dict(self) -> dict[str, float]:
    """Return the band as JSON-ready data.

    Returns:
        ``{"q05": ..., "q50": ..., "q95": ...}``.
    """
    return {"q05": self.q05, "q50": self.q50, "q95": self.q95}

Engine

Bases: ABC

An inference engine.

An engine only has to compute the probability of a partial assignment. query turns that into conditional and interventional answers, and observes every exclusive constraint as true.

Source code in src/credencegraph/inference/engine.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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
class Engine(ABC):
    """An inference engine.

    An engine only has to compute the probability of a partial assignment. ``query`` turns that into
    conditional and interventional answers, and observes every ``exclusive`` constraint as true.
    """

    @abstractmethod
    def probability(self, network: Network, assignment: Mapping[int, int]) -> float:
        """Compute the probability that the given variables take the given values.

        Args:
            network: The network.
            assignment: Variable indices and values (0 or 1); every other variable is summed out.

        Returns:
            The probability.
        """

    def query(self, network: Network, query: Query) -> float:
        """Answer a query.

        Args:
            network: The network, at the parameter values the answer should use.
            query: The query.

        Returns:
            ``P(target | evidence, constraints)`` in the network after the interventions.

        Raises:
            ValidationError: If the query names a node that is not an inference variable, or the
                interventions set merged nodes to different values.
            ZeroProbabilityError: If the evidence and constraints have probability zero.
        """
        if query.interventions:
            network = network.intervene(query.interventions)
        evidence = _combine(_resolve(network, query.evidence), {i: int(v) for i, v in network.constraints.items()})
        target = _resolve(network, query.target)
        if evidence is None:
            denominator = 0.0
        elif evidence:
            denominator = self.probability(network, evidence)
        else:
            denominator = 1.0
        if denominator <= 0.0 or evidence is None:
            msg = "the evidence, together with the exclusive constraints, has probability zero"
            raise ZeroProbabilityError(msg)
        joint = _combine(evidence, target)
        if joint is None:
            return 0.0
        if len(joint) == len(evidence):
            return 1.0
        answer = self.probability(network, joint) / denominator
        # Rounding can carry the ratio a few ulps past the unit interval.
        return min(1.0, max(0.0, answer))

probability(network, assignment) abstractmethod

Compute the probability that the given variables take the given values.

Parameters:

Name Type Description Default
network Network

The network.

required
assignment Mapping[int, int]

Variable indices and values (0 or 1); every other variable is summed out.

required

Returns:

Type Description
float

The probability.

Source code in src/credencegraph/inference/engine.py
107
108
109
110
111
112
113
114
115
116
117
@abstractmethod
def probability(self, network: Network, assignment: Mapping[int, int]) -> float:
    """Compute the probability that the given variables take the given values.

    Args:
        network: The network.
        assignment: Variable indices and values (0 or 1); every other variable is summed out.

    Returns:
        The probability.
    """

query(network, query)

Answer a query.

Parameters:

Name Type Description Default
network Network

The network, at the parameter values the answer should use.

required
query Query

The query.

required

Returns:

Type Description
float

P(target | evidence, constraints) in the network after the interventions.

Raises:

Type Description
ValidationError

If the query names a node that is not an inference variable, or the interventions set merged nodes to different values.

ZeroProbabilityError

If the evidence and constraints have probability zero.

Source code in src/credencegraph/inference/engine.py
119
120
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
def query(self, network: Network, query: Query) -> float:
    """Answer a query.

    Args:
        network: The network, at the parameter values the answer should use.
        query: The query.

    Returns:
        ``P(target | evidence, constraints)`` in the network after the interventions.

    Raises:
        ValidationError: If the query names a node that is not an inference variable, or the
            interventions set merged nodes to different values.
        ZeroProbabilityError: If the evidence and constraints have probability zero.
    """
    if query.interventions:
        network = network.intervene(query.interventions)
    evidence = _combine(_resolve(network, query.evidence), {i: int(v) for i, v in network.constraints.items()})
    target = _resolve(network, query.target)
    if evidence is None:
        denominator = 0.0
    elif evidence:
        denominator = self.probability(network, evidence)
    else:
        denominator = 1.0
    if denominator <= 0.0 or evidence is None:
        msg = "the evidence, together with the exclusive constraints, has probability zero"
        raise ZeroProbabilityError(msg)
    joint = _combine(evidence, target)
    if joint is None:
        return 0.0
    if len(joint) == len(evidence):
        return 1.0
    answer = self.probability(network, joint) / denominator
    # Rounding can carry the ratio a few ulps past the unit interval.
    return min(1.0, max(0.0, answer))

Enumeration

Bases: Engine

Exhaustive enumeration of every world consistent with the assignment.

Each world's probability is the product of one entry from every table, and the answer is the sum over worlds. It shares no code with variable elimination, which makes it the independent reference that engine is tested against. The cost doubles with every variable left free.

Source code in src/credencegraph/inference/enumeration.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class Enumeration(Engine):
    """Exhaustive enumeration of every world consistent with the assignment.

    Each world's probability is the product of one entry from every table, and the answer is the sum
    over worlds. It shares no code with variable elimination, which makes it the independent
    reference that engine is tested against. The cost doubles with every variable left free.
    """

    def __init__(self, max_free_variables: int = DEFAULT_MAX_FREE_VARIABLES) -> None:
        """Configure the engine.

        Args:
            max_free_variables: The largest number of summed-out variables allowed; a query needing
                more raises ``ProblemTooLargeError``.
        """
        self.max_free_variables = max_free_variables

    def probability(self, network: Network, assignment: Mapping[int, int]) -> float:
        """Sum the joint probability of every world that agrees with ``assignment``.

        Args:
            network: The network.
            assignment: Variable indices and values (0 or 1).

        Returns:
            The probability.

        Raises:
            ProblemTooLargeError: If more than ``max_free_variables`` variables would be summed out.
        """
        n = len(network)
        free = [index for index in range(n) if index not in assignment]
        if len(free) > self.max_free_variables:
            msg = (
                f"enumeration would sum over {len(free)} free variables, above the limit of "
                f"{self.max_free_variables} (max_free_variables); use variable elimination"
            )
            raise ProblemTooLargeError(msg, len(free), self.max_free_variables)
        worlds = np.zeros((2 ** len(free), n), dtype=np.intp)
        codes = np.arange(2 ** len(free))
        for bit, index in enumerate(free):
            worlds[:, index] = (codes >> bit) & 1
        for index, value in assignment.items():
            worlds[:, index] = value
        weight = np.ones(len(worlds))
        for factor in network.factors:
            weight = weight * factor.table[tuple(worlds[:, index] for index in factor.scope)]
        return float(weight.sum())

__init__(max_free_variables=DEFAULT_MAX_FREE_VARIABLES)

Configure the engine.

Parameters:

Name Type Description Default
max_free_variables int

The largest number of summed-out variables allowed; a query needing more raises ProblemTooLargeError.

DEFAULT_MAX_FREE_VARIABLES
Source code in src/credencegraph/inference/enumeration.py
24
25
26
27
28
29
30
31
def __init__(self, max_free_variables: int = DEFAULT_MAX_FREE_VARIABLES) -> None:
    """Configure the engine.

    Args:
        max_free_variables: The largest number of summed-out variables allowed; a query needing
            more raises ``ProblemTooLargeError``.
    """
    self.max_free_variables = max_free_variables

probability(network, assignment)

Sum the joint probability of every world that agrees with assignment.

Parameters:

Name Type Description Default
network Network

The network.

required
assignment Mapping[int, int]

Variable indices and values (0 or 1).

required

Returns:

Type Description
float

The probability.

Raises:

Type Description
ProblemTooLargeError

If more than max_free_variables variables would be summed out.

Source code in src/credencegraph/inference/enumeration.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def probability(self, network: Network, assignment: Mapping[int, int]) -> float:
    """Sum the joint probability of every world that agrees with ``assignment``.

    Args:
        network: The network.
        assignment: Variable indices and values (0 or 1).

    Returns:
        The probability.

    Raises:
        ProblemTooLargeError: If more than ``max_free_variables`` variables would be summed out.
    """
    n = len(network)
    free = [index for index in range(n) if index not in assignment]
    if len(free) > self.max_free_variables:
        msg = (
            f"enumeration would sum over {len(free)} free variables, above the limit of "
            f"{self.max_free_variables} (max_free_variables); use variable elimination"
        )
        raise ProblemTooLargeError(msg, len(free), self.max_free_variables)
    worlds = np.zeros((2 ** len(free), n), dtype=np.intp)
    codes = np.arange(2 ** len(free))
    for bit, index in enumerate(free):
        worlds[:, index] = (codes >> bit) & 1
    for index, value in assignment.items():
        worlds[:, index] = value
    weight = np.ones(len(worlds))
    for factor in network.factors:
        weight = weight * factor.table[tuple(worlds[:, index] for index in factor.scope)]
    return float(weight.sum())

query(network, query)

Answer a query.

Parameters:

Name Type Description Default
network Network

The network, at the parameter values the answer should use.

required
query Query

The query.

required

Returns:

Type Description
float

P(target | evidence, constraints) in the network after the interventions.

Raises:

Type Description
ValidationError

If the query names a node that is not an inference variable, or the interventions set merged nodes to different values.

ZeroProbabilityError

If the evidence and constraints have probability zero.

Source code in src/credencegraph/inference/engine.py
119
120
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
def query(self, network: Network, query: Query) -> float:
    """Answer a query.

    Args:
        network: The network, at the parameter values the answer should use.
        query: The query.

    Returns:
        ``P(target | evidence, constraints)`` in the network after the interventions.

    Raises:
        ValidationError: If the query names a node that is not an inference variable, or the
            interventions set merged nodes to different values.
        ZeroProbabilityError: If the evidence and constraints have probability zero.
    """
    if query.interventions:
        network = network.intervene(query.interventions)
    evidence = _combine(_resolve(network, query.evidence), {i: int(v) for i, v in network.constraints.items()})
    target = _resolve(network, query.target)
    if evidence is None:
        denominator = 0.0
    elif evidence:
        denominator = self.probability(network, evidence)
    else:
        denominator = 1.0
    if denominator <= 0.0 or evidence is None:
        msg = "the evidence, together with the exclusive constraints, has probability zero"
        raise ZeroProbabilityError(msg)
    joint = _combine(evidence, target)
    if joint is None:
        return 0.0
    if len(joint) == len(evidence):
        return 1.0
    answer = self.probability(network, joint) / denominator
    # Rounding can carry the ratio a few ulps past the unit interval.
    return min(1.0, max(0.0, answer))

InferenceError

Bases: CredenceGraphError

Base class for errors raised while answering a query.

Source code in src/credencegraph/inference/errors.py
8
9
class InferenceError(CredenceGraphError):
    """Base class for errors raised while answering a query."""

ProblemTooLargeError

Bases: InferenceError

An exact engine would need more memory than its configured limit allows.

Exact engines never fall back to an approximation on their own: they raise this error instead.

Attributes:

Name Type Description
required

The size the query would need, in the unit limit is expressed in.

limit

The configured limit.

Source code in src/credencegraph/inference/errors.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ProblemTooLargeError(InferenceError):
    """An exact engine would need more memory than its configured limit allows.

    Exact engines never fall back to an approximation on their own: they raise this error instead.

    Attributes:
        required: The size the query would need, in the unit ``limit`` is expressed in.
        limit: The configured limit.
    """

    def __init__(self, message: str, required: int, limit: int) -> None:
        """Store the sizes alongside the message.

        Args:
            message: Human-readable description naming the limit.
            required: The size the query would need.
            limit: The configured limit.
        """
        super().__init__(message)
        self.required = required
        self.limit = limit

__init__(message, required, limit)

Store the sizes alongside the message.

Parameters:

Name Type Description Default
message str

Human-readable description naming the limit.

required
required int

The size the query would need.

required
limit int

The configured limit.

required
Source code in src/credencegraph/inference/errors.py
26
27
28
29
30
31
32
33
34
35
36
def __init__(self, message: str, required: int, limit: int) -> None:
    """Store the sizes alongside the message.

    Args:
        message: Human-readable description naming the limit.
        required: The size the query would need.
        limit: The configured limit.
    """
    super().__init__(message)
    self.required = required
    self.limit = limit

Query dataclass

A probability query: P(target | evidence) after the interventions are applied.

Attributes:

Name Type Description
target Mapping[str, bool]

Node ids and the values whose joint probability is asked for.

evidence Mapping[str, bool]

Node ids and the observed values conditioned on.

interventions Mapping[str, bool]

Node ids and the values they are set to; relations into them are cut.

Source code in src/credencegraph/inference/engine.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@dataclass(frozen=True, slots=True)
class Query:
    """A probability query: ``P(target | evidence)`` after the interventions are applied.

    Attributes:
        target: Node ids and the values whose joint probability is asked for.
        evidence: Node ids and the observed values conditioned on.
        interventions: Node ids and the values they are set to; relations into them are cut.
    """

    target: Mapping[str, bool]
    evidence: Mapping[str, bool] = field(default_factory=dict)
    interventions: Mapping[str, bool] = field(default_factory=dict)

    def __post_init__(self) -> None:
        """Validate the three assignments and store read-only copies."""
        object.__setattr__(self, "target", _frozen_assignment(self.target, "target"))
        object.__setattr__(self, "evidence", _frozen_assignment(self.evidence, "evidence"))
        object.__setattr__(self, "interventions", _frozen_assignment(self.interventions, "interventions"))

__post_init__()

Validate the three assignments and store read-only copies.

Source code in src/credencegraph/inference/engine.py
52
53
54
55
56
def __post_init__(self) -> None:
    """Validate the three assignments and store read-only copies."""
    object.__setattr__(self, "target", _frozen_assignment(self.target, "target"))
    object.__setattr__(self, "evidence", _frozen_assignment(self.evidence, "evidence"))
    object.__setattr__(self, "interventions", _frozen_assignment(self.interventions, "interventions"))

VariableElimination

Bases: Engine

Exact inference by variable elimination with a min-fill order.

The order depends only on the factor scopes once the assignment is applied, so it is computed once for each distinct set of scopes and reused by later queries, including those on networks that differ only in their parameter values.

Before any table is built, the engine works out the scope of every intermediate factor the order produces. If the largest would hold more than max_factor_size entries, the query fails with ProblemTooLargeError; the engine never falls back to an approximation.

Source code in src/credencegraph/inference/elimination.py
110
111
112
113
114
115
116
117
118
119
120
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
class VariableElimination(Engine):
    """Exact inference by variable elimination with a min-fill order.

    The order depends only on the factor scopes once the assignment is applied, so it is computed
    once for each distinct set of scopes and reused by later queries, including those on networks
    that differ only in their parameter values.

    Before any table is built, the engine works out the scope of every intermediate factor the
    order produces. If the largest would hold more than ``max_factor_size`` entries, the query fails
    with ``ProblemTooLargeError``; the engine never falls back to an approximation.
    """

    def __init__(self, max_factor_size: int = DEFAULT_MAX_FACTOR_SIZE) -> None:
        """Configure the engine.

        Args:
            max_factor_size: The largest intermediate factor allowed, in table entries.
        """
        self.max_factor_size = max_factor_size

    def probability(self, network: Network, assignment: Mapping[int, int]) -> float:
        """Compute the probability of ``assignment`` by summing out every other variable.

        Args:
            network: The network.
            assignment: Variable indices and values (0 or 1).

        Returns:
            The probability.

        Raises:
            ProblemTooLargeError: If an intermediate factor would exceed ``max_factor_size`` entries.
        """
        factors = [_reduce(factor.scope, factor.table, assignment) for factor in network.factors]
        order = _order(tuple(scope for scope, _ in factors))
        self._check_size(factors, order)
        for variable in order:
            factors, new = _eliminate(factors, variable)
            factors.append(new)
        result = 1.0
        for _, table in factors:
            result *= float(table)
        return result

    def _check_size(self, factors: Sequence[_Factor], order: Sequence[int]) -> None:
        """Raise if eliminating in ``order`` would build a factor above the limit.

        Args:
            factors: The factors after the assignment is applied.
            order: The elimination order.

        Raises:
            ProblemTooLargeError: If an intermediate factor would exceed ``max_factor_size`` entries.
        """
        scopes = [set(scope) for scope, _ in factors]
        for variable in order:
            touching = [scope for scope in scopes if variable in scope]
            union = set().union(*touching)
            size = 2 ** len(union)
            if size > self.max_factor_size:
                msg = (
                    f"variable elimination would build an intermediate factor over {len(union)} variables "
                    f"({size} entries), above the limit of {self.max_factor_size} entries (max_factor_size). "
                    "Raise max_factor_size to run the query exactly; exact inference does not fall back to "
                    "an approximation on its own"
                )
                raise ProblemTooLargeError(msg, size, self.max_factor_size)
            scopes = [scope for scope in scopes if variable not in scope]
            scopes.append(union - {variable})

__init__(max_factor_size=DEFAULT_MAX_FACTOR_SIZE)

Configure the engine.

Parameters:

Name Type Description Default
max_factor_size int

The largest intermediate factor allowed, in table entries.

DEFAULT_MAX_FACTOR_SIZE
Source code in src/credencegraph/inference/elimination.py
122
123
124
125
126
127
128
def __init__(self, max_factor_size: int = DEFAULT_MAX_FACTOR_SIZE) -> None:
    """Configure the engine.

    Args:
        max_factor_size: The largest intermediate factor allowed, in table entries.
    """
    self.max_factor_size = max_factor_size

probability(network, assignment)

Compute the probability of assignment by summing out every other variable.

Parameters:

Name Type Description Default
network Network

The network.

required
assignment Mapping[int, int]

Variable indices and values (0 or 1).

required

Returns:

Type Description
float

The probability.

Raises:

Type Description
ProblemTooLargeError

If an intermediate factor would exceed max_factor_size entries.

Source code in src/credencegraph/inference/elimination.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def probability(self, network: Network, assignment: Mapping[int, int]) -> float:
    """Compute the probability of ``assignment`` by summing out every other variable.

    Args:
        network: The network.
        assignment: Variable indices and values (0 or 1).

    Returns:
        The probability.

    Raises:
        ProblemTooLargeError: If an intermediate factor would exceed ``max_factor_size`` entries.
    """
    factors = [_reduce(factor.scope, factor.table, assignment) for factor in network.factors]
    order = _order(tuple(scope for scope, _ in factors))
    self._check_size(factors, order)
    for variable in order:
        factors, new = _eliminate(factors, variable)
        factors.append(new)
    result = 1.0
    for _, table in factors:
        result *= float(table)
    return result

query(network, query)

Answer a query.

Parameters:

Name Type Description Default
network Network

The network, at the parameter values the answer should use.

required
query Query

The query.

required

Returns:

Type Description
float

P(target | evidence, constraints) in the network after the interventions.

Raises:

Type Description
ValidationError

If the query names a node that is not an inference variable, or the interventions set merged nodes to different values.

ZeroProbabilityError

If the evidence and constraints have probability zero.

Source code in src/credencegraph/inference/engine.py
119
120
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
def query(self, network: Network, query: Query) -> float:
    """Answer a query.

    Args:
        network: The network, at the parameter values the answer should use.
        query: The query.

    Returns:
        ``P(target | evidence, constraints)`` in the network after the interventions.

    Raises:
        ValidationError: If the query names a node that is not an inference variable, or the
            interventions set merged nodes to different values.
        ZeroProbabilityError: If the evidence and constraints have probability zero.
    """
    if query.interventions:
        network = network.intervene(query.interventions)
    evidence = _combine(_resolve(network, query.evidence), {i: int(v) for i, v in network.constraints.items()})
    target = _resolve(network, query.target)
    if evidence is None:
        denominator = 0.0
    elif evidence:
        denominator = self.probability(network, evidence)
    else:
        denominator = 1.0
    if denominator <= 0.0 or evidence is None:
        msg = "the evidence, together with the exclusive constraints, has probability zero"
        raise ZeroProbabilityError(msg)
    joint = _combine(evidence, target)
    if joint is None:
        return 0.0
    if len(joint) == len(evidence):
        return 1.0
    answer = self.probability(network, joint) / denominator
    # Rounding can carry the ratio a few ulps past the unit interval.
    return min(1.0, max(0.0, answer))

ZeroProbabilityError

Bases: InferenceError, ValueError

The evidence of a query, together with the exclusive constraints, has probability zero.

Source code in src/credencegraph/inference/errors.py
12
13
class ZeroProbabilityError(InferenceError, ValueError):
    """The evidence of a query, together with the ``exclusive`` constraints, has probability zero."""

answers_over_draws(network, query, engine, samples)

Answer a query exactly at every parameter draw.

Parameters:

Name Type Description Default
network Network

The network.

required
query Query

The query.

required
engine Engine

The exact engine used at each draw.

required
samples Mapping[ParameterKey, ndarray]

Values by parameter, one array entry per draw, as from sample_parameters.

required

Returns:

Type Description
ndarray

The answer at each draw.

Raises:

Type Description
ValidationError

If the sample arrays differ in length, or a key or value is invalid.

ZeroProbabilityError

If the evidence has probability zero at some draw.

Source code in src/credencegraph/inference/uncertainty.py
120
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
def answers_over_draws(
    network: Network, query: Query, engine: Engine, samples: Mapping[ParameterKey, np.ndarray]
) -> np.ndarray:
    """Answer a query exactly at every parameter draw.

    Args:
        network: The network.
        query: The query.
        engine: The exact engine used at each draw.
        samples: Values by parameter, one array entry per draw, as from ``sample_parameters``.

    Returns:
        The answer at each draw.

    Raises:
        ValidationError: If the sample arrays differ in length, or a key or value is invalid.
        ZeroProbabilityError: If the evidence has probability zero at some draw.
    """
    lengths = {len(values) for values in samples.values()}
    if len(lengths) > 1:
        msg = f"every parameter needs the same number of draws, got lengths {sorted(lengths)}"
        raise ValidationError(msg)
    count = lengths.pop() if lengths else 0
    answers = np.empty(count)
    for draw in range(count):
        drawn = network.with_parameters({key: float(values[draw]) for key, values in samples.items()})
        try:
            answers[draw] = engine.query(drawn, query)
        except ZeroProbabilityError as error:
            msg = f"parameter draw {draw}: {error}"
            raise ZeroProbabilityError(msg) from error
    return answers

conditional(network, target, given, *, engine=None, draws=DEFAULT_DRAWS, rng=None)

Compute P(target | given).

Conditioning on a node also updates the node's own premises: observing that a conclusion is false is evidence against what supports it.

Parameters:

Name Type Description Default
network Network

The network.

required
target str | Mapping[str, bool]

A node id, asked to be true, or an assignment.

required
given Mapping[str, bool]

The observed values.

required
engine Engine | None

The inference engine; variable elimination by default.

None
draws int

The number of parameter draws behind the band. Each draw answers the query exactly once more, so the default band costs DEFAULT_DRAWS exact inferences; 0 skips the band and gives the point answer alone.

DEFAULT_DRAWS
rng Seed

A random generator, or a seed for one, for the draws; None draws fresh entropy.

None

Returns:

Type Description
Answer

The answer.

Source code in src/credencegraph/inference/queries.py
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
def conditional(  # noqa: PLR0913 - the options after the query itself are keyword-only
    network: Network,
    target: str | Mapping[str, bool],
    given: Mapping[str, bool],
    *,
    engine: Engine | None = None,
    draws: int = DEFAULT_DRAWS,
    rng: Seed = None,
) -> Answer:
    """Compute ``P(target | given)``.

    Conditioning on a node also updates the node's own premises: observing that a conclusion is
    false is evidence against what supports it.

    Args:
        network: The network.
        target: A node id, asked to be true, or an assignment.
        given: The observed values.
        engine: The inference engine; variable elimination by default.
        draws: The number of parameter draws behind the band. Each draw answers the query exactly once
            more, so the default band costs ``DEFAULT_DRAWS`` exact inferences; 0 skips the band and
            gives the point answer alone.
        rng: A random generator, or a seed for one, for the draws; ``None`` draws fresh entropy.

    Returns:
        The answer.
    """
    return _run(network, Query(_as_assignment(target, "target"), given), engine, draws, rng)

has_spread(network)

Tell whether any parameter of the network is uncertain.

Parameters:

Name Type Description Default
network Network

The network.

required

Returns:

Type Description
bool

True if at least one parameter has a Beta credence, False if every one is a Point.

Source code in src/credencegraph/inference/uncertainty.py
60
61
62
63
64
65
66
67
68
69
def has_spread(network: Network) -> bool:
    """Tell whether any parameter of the network is uncertain.

    Args:
        network: The network.

    Returns:
        ``True`` if at least one parameter has a ``Beta`` credence, ``False`` if every one is a ``Point``.
    """
    return any(isinstance(credence, Beta) for credence in network.parameters.values())

intervene(network, target, set, *, given=None, engine=None, draws=DEFAULT_DRAWS, rng=None)

Compute P(target | do(set), given): cut the relations into the set nodes and fix them.

This answers "suppose this premise is simply wrong": unlike conditioning, it does not update the set node's own premises.

Parameters:

Name Type Description Default
network Network

The network.

required
target str | Mapping[str, bool]

A node id, asked to be true, or an assignment.

required
set Mapping[str, bool]

The nodes to set and their values.

required
given Mapping[str, bool] | None

Values observed after the intervention, if any.

None
engine Engine | None

The inference engine; variable elimination by default.

None
draws int

The number of parameter draws behind the band. Each draw answers the query exactly once more, so the default band costs DEFAULT_DRAWS exact inferences; 0 skips the band and gives the point answer alone.

DEFAULT_DRAWS
rng Seed

A random generator, or a seed for one, for the draws; None draws fresh entropy.

None

Returns:

Type Description
Answer

The answer.

Source code in src/credencegraph/inference/queries.py
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
def intervene(  # noqa: PLR0913 - the options after the query itself are keyword-only
    network: Network,
    target: str | Mapping[str, bool],
    set: Mapping[str, bool],  # noqa: A002 - the documented keyword: intervene(X, set={Y: False})
    *,
    given: Mapping[str, bool] | None = None,
    engine: Engine | None = None,
    draws: int = DEFAULT_DRAWS,
    rng: Seed = None,
) -> Answer:
    """Compute ``P(target | do(set), given)``: cut the relations into the set nodes and fix them.

    This answers "suppose this premise is simply wrong": unlike conditioning, it does not update the
    set node's own premises.

    Args:
        network: The network.
        target: A node id, asked to be true, or an assignment.
        set: The nodes to set and their values.
        given: Values observed after the intervention, if any.
        engine: The inference engine; variable elimination by default.
        draws: The number of parameter draws behind the band. Each draw answers the query exactly once
            more, so the default band costs ``DEFAULT_DRAWS`` exact inferences; 0 skips the band and
            gives the point answer alone.
        rng: A random generator, or a seed for one, for the draws; ``None`` draws fresh entropy.

    Returns:
        The answer.
    """
    return _run(network, Query(_as_assignment(target, "target"), given or {}, set), engine, draws, rng)

joint(network, assignment, *, engine=None, draws=DEFAULT_DRAWS, rng=None)

Compute the probability of a joint assignment, P(X = x and Y = y and ...).

Parameters:

Name Type Description Default
network Network

The network.

required
assignment Mapping[str, bool] | Iterable[str]

Node ids and values, or an iterable of node ids that are all asked to be true.

required
engine Engine | None

The inference engine; variable elimination by default.

None
draws int

The number of parameter draws behind the band. Each draw answers the query exactly once more, so the default band costs DEFAULT_DRAWS exact inferences; 0 skips the band and gives the point answer alone.

DEFAULT_DRAWS
rng Seed

A random generator, or a seed for one, for the draws; None draws fresh entropy.

None

Returns:

Type Description
Answer

The answer.

Source code in src/credencegraph/inference/queries.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def joint(
    network: Network,
    assignment: Mapping[str, bool] | Iterable[str],
    *,
    engine: Engine | None = None,
    draws: int = DEFAULT_DRAWS,
    rng: Seed = None,
) -> Answer:
    """Compute the probability of a joint assignment, ``P(X = x and Y = y and ...)``.

    Args:
        network: The network.
        assignment: Node ids and values, or an iterable of node ids that are all asked to be true.
        engine: The inference engine; variable elimination by default.
        draws: The number of parameter draws behind the band. Each draw answers the query exactly once
            more, so the default band costs ``DEFAULT_DRAWS`` exact inferences; 0 skips the band and
            gives the point answer alone.
        rng: A random generator, or a seed for one, for the draws; ``None`` draws fresh entropy.

    Returns:
        The answer.
    """
    return _run(network, Query(_as_assignment(assignment, "assignment")), engine, draws, rng)

marginal(network, node, value=True, *, engine=None, draws=DEFAULT_DRAWS, rng=None)

Compute P(node = value).

Parameters:

Name Type Description Default
network Network

The network.

required
node str

A node id.

required
value bool

The value asked about.

True
engine Engine | None

The inference engine; variable elimination by default.

None
draws int

The number of parameter draws behind the band. Each draw answers the query exactly once more, so the default band costs DEFAULT_DRAWS exact inferences; 0 skips the band and gives the point answer alone.

DEFAULT_DRAWS
rng Seed

A random generator, or a seed for one, for the draws; None draws fresh entropy.

None

Returns:

Type Description
Answer

The answer.

Source code in src/credencegraph/inference/queries.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def marginal(  # noqa: PLR0913 - the options after the query itself are keyword-only
    network: Network,
    node: str,
    value: bool = True,
    *,
    engine: Engine | None = None,
    draws: int = DEFAULT_DRAWS,
    rng: Seed = None,
) -> Answer:
    """Compute ``P(node = value)``.

    Args:
        network: The network.
        node: A node id.
        value: The value asked about.
        engine: The inference engine; variable elimination by default.
        draws: The number of parameter draws behind the band. Each draw answers the query exactly once
            more, so the default band costs ``DEFAULT_DRAWS`` exact inferences; 0 skips the band and
            gives the point answer alone.
        rng: A random generator, or a seed for one, for the draws; ``None`` draws fresh entropy.

    Returns:
        The answer.
    """
    return _run(network, Query({node: value}), engine, draws, rng)

min_fill_order(scopes)

Choose an elimination order by the greedy min-fill heuristic.

At each step the variable whose elimination adds the fewest new edges to the interaction graph is eliminated next. Ties go to the variable with fewer neighbours, then to the lower index.

Parameters:

Name Type Description Default
scopes Iterable[Iterable[int]]

The scopes of the factors; two variables interact when they share a scope.

required

Returns:

Type Description
list[int]

Every variable that appears in a scope, in elimination order.

Source code in src/credencegraph/inference/elimination.py
40
41
42
43
44
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
def min_fill_order(scopes: Iterable[Iterable[int]]) -> list[int]:
    """Choose an elimination order by the greedy min-fill heuristic.

    At each step the variable whose elimination adds the fewest new edges to the interaction graph is
    eliminated next. Ties go to the variable with fewer neighbours, then to the lower index.

    Args:
        scopes: The scopes of the factors; two variables interact when they share a scope.

    Returns:
        Every variable that appears in a scope, in elimination order.
    """
    neighbours: dict[int, set[int]] = {}
    for scope in scopes:
        members = set(scope)
        for variable in members:
            neighbours.setdefault(variable, set()).update(members - {variable})
    order: list[int] = []
    while neighbours:

        def cost(variable: int) -> tuple[int, int, int]:
            around = sorted(neighbours[variable])
            fill = sum(1 for i, a in enumerate(around) for b in around[i + 1 :] if b not in neighbours[a])
            return fill, len(around), variable

        chosen = min(neighbours, key=cost)
        around = neighbours.pop(chosen)
        for variable in around:
            neighbours[variable].discard(chosen)
            neighbours[variable].update(around - {variable})
        order.append(chosen)
    return order

sample_parameters(network, draws, rng=None)

Draw every Beta parameter of a network independently from its credence.

Parameters are drawn in the order network.parameters lists them, so a given seed always yields the same draws for the same network. Point parameters are left out: they keep their value in every draw. A Beta parameter is drawn from its credence even when Network.with_parameters has set it to another value.

Parameters:

Name Type Description Default
network Network

The network.

required
draws int

The number of draws.

required
rng Generator | int | None

A random generator, or a seed for one; None draws fresh entropy.

None

Returns:

Type Description
dict[ParameterKey, ndarray]

For each Beta parameter, an array of draws values inside the open unit interval.

Raises:

Type Description
ValidationError

If draws is not a non-negative integer.

Source code in src/credencegraph/inference/uncertainty.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def sample_parameters(
    network: Network, draws: int, rng: np.random.Generator | int | None = None
) -> dict[ParameterKey, np.ndarray]:
    """Draw every ``Beta`` parameter of a network independently from its credence.

    Parameters are drawn in the order ``network.parameters`` lists them, so a given seed always
    yields the same draws for the same network. ``Point`` parameters are left out: they keep their
    value in every draw. A ``Beta`` parameter is drawn from its credence even when
    ``Network.with_parameters`` has set it to another value.

    Args:
        network: The network.
        draws: The number of draws.
        rng: A random generator, or a seed for one; ``None`` draws fresh entropy.

    Returns:
        For each ``Beta`` parameter, an array of ``draws`` values inside the open unit interval.

    Raises:
        ValidationError: If ``draws`` is not a non-negative integer.
    """
    count = _draw_count(draws)
    generator = np.random.default_rng(rng)
    return {
        key: np.clip(generator.beta(credence.alpha, credence.beta, size=count), _LOWEST, _HIGHEST)
        for key, credence in network.parameters.items()
        if isinstance(credence, Beta)
    }

spread(network, query, engine, draws=DEFAULT_DRAWS, rng=None)

Estimate how a query's answer spreads over the parameters' credences.

Every Beta parameter is drawn from its credence, including one that Network.with_parameters has set to another value: the band describes the Beta credences, not the values the network holds for them, so overriding a Beta parameter moves the point answer but not the band. A Point parameter keeps the value the network holds in every draw, so overriding one moves both. The network does not record which values were overridden, so it cannot hold them fixed; give a parameter a Point credence to keep it out of the draws.

Parameters:

Name Type Description Default
network Network

The network.

required
query Query

The query.

required
engine Engine

The exact engine used at each draw.

required
draws int

The number of Monte Carlo draws; 0 skips the estimate.

DEFAULT_DRAWS
rng Generator | int | None

A random generator, or a seed for one; None draws fresh entropy.

None

Returns:

Type Description
tuple[Band, float] | None

The band and the mean of the per-draw answers, or None when draws is 0 or every

tuple[Band, float] | None

parameter is a Point, since the answer then cannot move. Overridden values do not count:

tuple[Band, float] | None

a network whose every Beta parameter has been set still gets a band.

Raises:

Type Description
ValidationError

If draws is not a non-negative integer.

ZeroProbabilityError

If the evidence has probability zero at some draw.

Source code in src/credencegraph/inference/uncertainty.py
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
def spread(
    network: Network,
    query: Query,
    engine: Engine,
    draws: int = DEFAULT_DRAWS,
    rng: np.random.Generator | int | None = None,
) -> tuple[Band, float] | None:
    """Estimate how a query's answer spreads over the parameters' credences.

    Every ``Beta`` parameter is drawn from its credence, including one that ``Network.with_parameters``
    has set to another value: the band describes the ``Beta`` credences, not the values the network
    holds for them, so overriding a ``Beta`` parameter moves the point answer but not the band. A
    ``Point`` parameter keeps the value the network holds in every draw, so overriding one moves
    both. The network does not record which values were overridden, so it cannot hold them fixed;
    give a parameter a ``Point`` credence to keep it out of the draws.

    Args:
        network: The network.
        query: The query.
        engine: The exact engine used at each draw.
        draws: The number of Monte Carlo draws; 0 skips the estimate.
        rng: A random generator, or a seed for one; ``None`` draws fresh entropy.

    Returns:
        The band and the mean of the per-draw answers, or ``None`` when ``draws`` is 0 or every
        parameter is a ``Point``, since the answer then cannot move. Overridden values do not count:
        a network whose every ``Beta`` parameter has been set still gets a band.

    Raises:
        ValidationError: If ``draws`` is not a non-negative integer.
        ZeroProbabilityError: If the evidence has probability zero at some draw.
    """
    count = _draw_count(draws)
    if count == 0 or not has_spread(network):
        return None
    answers = answers_over_draws(network, query, engine, sample_parameters(network, count, rng))
    q05, q50, q95 = (float(q) for q in np.quantile(answers, (0.05, 0.5, 0.95)))
    return Band(q05, q50, q95), float(answers.mean())