hiphopsllm.bayes¶
Fault trees as Bayesian networks: CPTs, exact inference, diagnosis, drawing.
A fault tree and a Bayesian network describe the same object. Building the second from the first — instead of authoring both — buys three things:
an exact top-event probability, where cut sets give only an upper bound;
diagnosis: condition on what a run showed and read the posterior over causes;
learned gates: where per-node outcomes were logged, a gate’s table can be fitted from data rather than assumed to be AND or OR.
Start with fault_tree_to_cpts() if you want the tables, and
BayesianNetwork if you want answers:
from hiphopsllm.bayes import BayesianNetwork
bn = BayesianNetwork.from_fault_tree(report.tree("H2"), report.failure_model)
bn.p_fail() # exact P(top event)
bn.posteriors({"BE-coder-EXECERR": "Fail"}) # posterior over causes
bn.show() # pyAgrum, or matplotlib
State indices are 0 = OK and 1 = Fail throughout.
Conditional probability tables¶
Fault tree to conditional probability tables.
A fault tree and a Bayesian network are two readings of the same object. Every node of the tree becomes a binary variable; every gate becomes a conditional probability table over its inputs; every basic event becomes a root with its failure probability as prior. Converting rather than re-authoring matters for a practical reason: the source notebook hand-wired a pyAgrum network beside its fault tree, and nothing kept the two consistent when the graph changed.
Two things become available once the tree is a set of CPTs.
An exact top-event probability. Cut-set quantification uses the minimal cut upper bound, which over-estimates whenever cut sets share basic events — and in agentic architectures they always do, because every agent depends on the same request and often on the same model snapshot. Exact inference over the CPTs gives the true value, so the pair brackets the answer.
Soft gates. A deterministic OR says an input deviation always propagates. A noisy-OR says each input propagates with its own probability and adds a leak term for causes outside the model. That is usually the more honest statement about an LLM component, and it is a CPT away.
Convention¶
Index 0 is OK and index 1 is Fail, throughout, matching
hiphopsllm.bayes.network. (The HIP-MAS study code uses the opposite
order; CPTSet.to_hipmas_order() converts.)
- hiphopsllm.bayes.cpt.OK = 0¶
State indices.
0 = OK,1 = Fail.
- hiphopsllm.bayes.cpt.FAIL = 1¶
State indices.
0 = OK,1 = Fail.
- hiphopsllm.bayes.cpt.MAX_GATE_INPUTS = 18¶
A deterministic gate table has
2 ** inputsrows. Past this the table stops being a usable representation and the cut sets should be used instead.
- class hiphopsllm.bayes.cpt.GateType[source]¶
-
How a gate’s conditional table is filled in.
- OR = 'OR'¶
- AND = 'AND'¶
- KOFN = 'KOFN'¶
- NOISY_OR = 'NOISY_OR'¶
- NOISY_AND = 'NOISY_AND'¶
- PASS = 'PASS'¶
- __new__(value)¶
- class hiphopsllm.bayes.cpt.CPT[source]¶
Bases:
objectOne conditional probability table, with the provenance of its numbers.
-
kind:
str= 'deterministic'¶ deterministic gate, noisy-OR, prior, house
- Type:
how the table was filled
-
kind:
- class hiphopsllm.bayes.cpt.CPTSet[source]¶
Bases:
objectEvery CPT of one fault tree, in a valid topological order.
orderlists variables parents-first, so the set can be handed to any Bayesian-network library — or to the exact enumeration reference inhiphopsllm.bayes.network— without a further sort.-
event_variable:
Dict[str,str]¶ basic event id -> variable name, for evidence and diagnosis by event name
- to_frame(variable=None)[source]¶
CPTs as a pandas DataFrame — one block per variable, for inspection.
- Parameters:
variable (str | None)
- to_hipmas_order()[source]¶
Tables re-indexed to the HIP-MAS convention, where index 0 is
Fail.The HIP-MAS study code (
hipmas.bn) putsFailfirst. This package putsOKfirst so thattable[..., 1]reads as “probability of failure” everywhere. Use this when handing tables to that code.
- __init__(name, cpts=<factory>, order=<factory>, variable_of=<factory>, node_of=<factory>, top='', event_variable=<factory>, notes=<factory>)¶
-
event_variable:
- class hiphopsllm.bayes.cpt.CPTBuilder[source]¶
Bases:
objectConvert fault trees into
CPTSetobjects, under fixed conventions.- Parameters:
bound (
str) – Which end of an imprecise basic-event probability to use."point"takesBasicEvent.prob;"lower"and"upper"take the ends ofBasicEvent.prob_intervalwhen one is present. Because a coherent (monotone) fault tree’s top-event probability is non-decreasing in every basic-event probability, building at both ends brackets the true value — which is howhiphopsllm.bayes.network.BayesianNetworkreports an imprecise result.soft_gates (
bool) – Replace deterministic OR gates with noisy-OR.False(the default) keeps the classical fault tree reading, in which the network and the cut sets describe exactly the same Boolean function.link_probability (
float) – Per-input propagation probability used whensoft_gatesis on.leak (
float) – Noisy-OR leak: the chance the child fails with every modelled input OK.default_prob (
float) – Prior for a basic event that carries no probability at all. It is deliberately visible in the report rather than silent, because a whole network quietly running on default priors is a failure mode this package exists to prevent.
- probability(event)[source]¶
- Return type:
- Parameters:
event (BasicEvent | None)
- build(tree, failure_model=None, name=None, gate_overrides=None)[source]¶
Convert one fault tree into a
CPTSet.gate_overridesmaps a fault tree node id to a replacement gate:"AND","OR","NOISY_OR", or("KOFN", k)for a voting gate. Use it where the synthesised structure is right but the logic is not — a two-of-three aggregator being the usual case.
- hiphopsllm.bayes.cpt.fault_tree_to_cpts(tree, failure_model=None, *, name=None, bound='point', soft_gates=False, link_probability=0.9, leak=0.01, default_prob=0.05, gate_overrides=None)[source]¶
Convert a fault tree into conditional probability tables.
The functional form of
CPTBuilder, for the common case:cpts = fault_tree_to_cpts(report.tree("H2"), report.failure_model) print(cpts.summary()) cpts.to_frame("BE-react_agent-HALLUC")
See
CPTBuilderfor what each option means.
- hiphopsllm.bayes.cpt.deterministic_gate_cpt(n_parents, gate='OR')[source]¶
Deterministic AND/OR table of shape
(2,) * n_parents + (2,).The child fails with probability one exactly on the parent configurations the Boolean gate makes true, and with probability zero elsewhere. This is the classical fault tree reading, and it is what makes exact inference over the network agree with an exhaustive evaluation of the Boolean function.
- hiphopsllm.bayes.cpt.noisy_or_cpt(link_probabilities, leak=0.0)[source]¶
Noisy-OR table: each failed parent independently tries to cause failure.
P(child = Fail | parents) = 1 - (1 - leak) * prod_{i failed} (1 - p_i)The deterministic OR is the special case where every
p_iis one and the leak is zero. Softening it is the honest option when an input deviation only sometimes propagates — a malformed tool observation that the agent occasionally recovers from, say — and the leak covers causes the tree does not model, which for an LLM component is never an empty set.
- hiphopsllm.bayes.cpt.k_of_n_cpt(n_parents, k)[source]¶
Voting gate: the child fails when at least
kofninputs fail.k = 1reproduces OR andk = nreproduces AND, so this generalises both. It is the right shape for a majority-vote aggregator, where two wrong answers out of three carry the error through and one does not.
Networks and inference¶
Bayesian networks over agentic fault trees.
BayesianNetwork wraps a CPTSet and does
four things a fault tree on its own cannot.
Exact top-event probability. The minimal cut upper bound over-estimates whenever cut sets share basic events. Inference over the network is exact, so
compare_with_cutsets()brackets the answer instead of asserting one end of it.Diagnosis. Condition on what a run actually showed — the router took the error branch, the tool raised — and read the posterior over causes. That is the step run-time monitoring of an agentic system is reaching for.
Imprecision. Basic-event probabilities estimated from a few hundred benchmark items are intervals, not numbers. A coherent fault tree’s top-event probability is monotone in each of them, so evaluating at both ends gives a guaranteed envelope — see
ImpreciseBayesianNetwork.envelope().Independence from pyAgrum. Every quantity is also computable by exact enumeration in NumPy alone, which is what
cross_check()uses to verify the pyAgrum result. pyAgrum stays an optional dependency.
- class hiphopsllm.bayes.network.BayesianNetwork[source]¶
Bases:
objectA discrete two-state Bayesian network built from a fault tree.
Construct it with
from_fault_tree(), or directly from aCPTSetthat was fitted from data.- classmethod from_fault_tree(tree, failure_model=None, *, name=None, bound='point', soft_gates=False, gate_overrides=None, **builder_kwargs)[source]¶
Convert a synthesised fault tree straight into a network.
- Return type:
- Parameters:
- classmethod imprecise_from_fault_tree(tree, failure_model=None, **kwargs)[source]¶
Build the lower and upper networks of an imprecise fault tree.
- Return type:
- Parameters:
tree (FaultTree)
failure_model (FailureModel | None)
kwargs (Any)
- p_fail(target=None, evidence=None, engine='auto')[source]¶
P(target = Fail | evidence);targetdefaults to the top event.engineis"pyagrum","exact"(NumPy enumeration) or"auto", which prefers pyAgrum and falls back to enumeration.
- posterior(target, evidence=None, engine='auto')[source]¶
Posterior
[P(OK), P(Fail)]over one variable.
- posteriors(evidence=None, engine='auto', basic_events_only=True)[source]¶
P(x = Fail | evidence)for every variable, ranked most likely first.With
basic_events_only(the default) the result is a posterior over causes — the diagnostic view. PassingFalsereturns every node, including the intermediate deviations, which is useful for locating where in the architecture a failure most likely entered.
- evidence_posterior(evidence, engine='auto')[source]¶
P(each basic event = Fail | evidence)— run-time diagnosis.
- most_probable_explanation(evidence=None)[source]¶
The single most probable joint assignment to the basic events.
Exhaustive over basic events, so it is exact but exponential; it refuses above 22 basic events rather than running for hours. For larger trees rank causes with
posteriors()instead.
- cross_check(rel_tol=1e-09, abs_tol=1e-12)[source]¶
Compare the pyAgrum and exact-enumeration top-event probabilities.
The two paths share no code, so agreement is real evidence that the conversion is right — and cheap to obtain, before a number reaches a paper.
The comparison is relative rather than absolute. Exact enumeration sums
2 ** (basic events)terms, so on a twenty-leaf tree its rounding error is around1e-12in absolute terms while still being correct to nine significant figures. An absolute threshold would flag that as a disagreement and teach the reader to ignore this check, which is worse than not having it.
- compare_with_cutsets(analysis)[source]¶
Exact network probability against the cut-set bounds.
The minimal cut upper bound is an upper bound on a coherent tree, so
bound_overestimateshould never be negative. A negative value means the tree and the network have drifted apart.- Return type:
- Parameters:
analysis (TreeAnalysis)
- view(**kwargs)[source]¶
A
BayesNetViewover this network.- Parameters:
kwargs (Any)
- class hiphopsllm.bayes.network.ImpreciseBayesianNetwork[source]¶
Bases:
objectLower and upper networks of a fault tree with interval-valued events.
A coherent (monotone) fault tree’s top-event probability is non-decreasing in every basic-event probability. Evaluating at the lower ends of all intervals therefore gives a genuine lower bound and the upper ends a genuine upper bound — no optimisation over the interval box is needed, and no sampling.
This is where HIP-LLM’s imprecise posterior meets the fault tree: the interval on each basic event comes from
EvidenceCalibrator, which derives it from observed outcomes under an operational profile rather than from engineering judgement.-
lower:
BayesianNetwork¶
-
upper:
BayesianNetwork¶
- envelope(target=None, evidence=None, engine='auto')[source]¶
[P_lower, P_upper]for a target, by default the top event.
- posterior_envelopes(evidence=None, engine='auto')[source]¶
Per-basic-event posterior envelopes, ranked by upper bound.
- show(**kwargs)[source]¶
Draw the upper network; the envelope is reported alongside it.
- Parameters:
kwargs (Any)
- __init__(lower, upper, tree=None)¶
- Parameters:
lower (BayesianNetwork)
upper (BayesianNetwork)
tree (FaultTree | None)
- Return type:
None
-
lower:
- class hiphopsllm.bayes.network.Envelope[source]¶
Bases:
objectA
[lower, upper]probability interval with its own arithmetic.
- hiphopsllm.bayes.network.fault_tree_to_bayesnet(tree, failure_model=None, name=None, **kwargs)[source]¶
Build a
BayesianNetworkfrom a fault tree.The functional form of
BayesianNetwork.from_fault_tree():bn = fault_tree_to_bayesnet(report.tree("H2"), report.failure_model) bn.p_fail()
- Return type:
- Parameters:
tree (FaultTree)
failure_model (FailureModel | None)
name (str | None)
kwargs (Any)
- hiphopsllm.bayes.network.exact_top_probability(network)[source]¶
Exact
P(top event)— the reference for the cut-set estimate.- Return type:
- Parameters:
network (BayesianNetwork)
- hiphopsllm.bayes.network.compare_with_cutsets(network, analysis)[source]¶
Exact network probability against the minimal cut upper bound.
- Return type:
- Parameters:
network (BayesianNetwork)
analysis (TreeAnalysis)
Bases:
ImportErrorpyAgrum is not installed, and the requested operation needs it.
Learning tables from data¶
Fitting conditional probability tables from observed agent outcomes.
A synthesised fault tree says an aggregator’s output is wrong when both its inputs are wrong. That is a modelling assumption, and in a multi-agent system it is usually wrong in an interesting direction: a reviewer repairs some upstream errors and introduces others, so the true table is neither AND nor OR. When per-node outcomes have actually been logged, the table can be estimated instead of assumed.
The difference is not academic. In the HIP-MAS synthetic ground-truth study,
with a reviewer repairing 55% of upstream errors, the deterministic AND-series
gate mispredicted held-out failure by +0.386 while the learned-CPT model was
within 0.005.
Two guards are enforced in code rather than by convention:
a CPT is never fitted from rows marked
test—learn_cpt()raises;rows with no observations fall back to the Dirichlet prior mean and are counted, so a report can state how many table rows were prior-dominated rather than implying they were measured.
- class hiphopsllm.bayes.learn.LearnedCPT[source]¶
Bases:
objectA fitted table plus how much data stood behind each row.
- hiphopsllm.bayes.learn.learn_cpt(frame, child, parents=(), *, alpha=1.0, check_split=True)[source]¶
Estimate
P(child | parents)with symmetric Dirichlet(alpha) smoothing.- Parameters:
frame (
Any) – A pandas DataFrame with one row per observed task and one column per node, holding failure indicators (seefit_cpts()).child (
str) – Column names.parentsmay be empty, giving a root prior.parents (
Sequence[str]) – Column names.parentsmay be empty, giving a root prior.alpha (
float) – Dirichlet concentration.alpha = 1is Laplace smoothing and the sensible default: at pilot sample sizes several parent configurations (“both agents wrong and they agree”) are seen a handful of times or not at all, and an unsmoothed MLE would put a hard 0 or 1 in the table and make the network claim a certainty it has not earned.check_split (
bool) – Refuse to fit if the frame carries rows marked as a test split.
- Return type:
- Returns:
LearnedCPT – The table, plus the raw counts, so the report can say how much of it was measured and how much is prior.
- hiphopsllm.bayes.learn.learn_gate(frame, child, parents, *, alpha=1.0, check_split=True)[source]¶
Fit a gate and report how far it is from AND and from OR.
The distances are the mean absolute difference between the fitted
P(Fail | parents)column and the deterministic table, which is a direct answer to “is this aggregator really a voter?”.
- hiphopsllm.bayes.learn.fit_cpts(frame, structure, *, name='learned', alpha=1.0, check_split=True, outcomes_are_failures=True)[source]¶
Fit every CPT of a network whose structure is already known.
structuremaps each variable to its parents, and must be given in a topological order (parents before children) — the same order aCPTSetkeeps.outcomes_are_failuresstates the polarity of the columns explicitly. Benchmark data usually records correctness; passFalseand the columns are inverted once, here, instead of silently everywhere:cpts, fits = fit_cpts( observations, {"react": [], "cot": [], "aggregator": ["react", "cot"]}, outcomes_are_failures=False, # columns hold 1 = correct ) bn = BayesianNetwork(cpts)
- exception hiphopsllm.bayes.learn.CPTLearningError[source]¶
Bases:
ValueErrorA CPT was asked to learn from data it must not see, or cannot use.
Drawing¶
Drawing Bayesian networks, with pyAgrum where it works and without it where it does not.
pyAgrum renders through Graphviz, and Graphviz is a separate native binary, not a
Python package — pip install pydot does not provide it. Whether it is
present varies by environment and by the day: it ships on a current Colab image
and not on many Windows installs, and pyAgrum’s own import prints a warning when
it is missing. A visualisation layer that only works when dot is on the PATH
is a visualisation layer that silently produces nothing exactly when a reader
most needs the picture, so this one does not assume either way —
graphviz_available() runs dot -V and the backend follows the answer.
BayesNetView therefore has two backends and one behaviour:
"pyagrum"—pyagrum.lib.notebook, the richest output: node shading by posterior, inference histograms, side-by-side prior/posterior views;"matplotlib"— a layered DAG drawn directly, which needs nothing beyond matplotlib.
backend="auto" (the default) picks pyAgrum when Graphviz is genuinely
callable and matplotlib otherwise, so bn.show() always draws something.
The matplotlib backend draws the same kind of picture pyAgrum’s
showInference does — each node is a titled box holding one labelled bar per
state — rather than a reduced one. A fallback that says less than the thing it
replaces trains a reader to distrust it, and the two views appearing side by side
across environments should be comparable at a glance. Every bar carries its
percentage as text, so the reading never depends on judging a bar length or on
seeing colour.
- class hiphopsllm.bayes.viz.BayesNetView[source]¶
Bases:
objectA drawable view of a
BayesianNetwork.- Parameters:
network (
BayesianNetwork) – The network to draw.backend (
str) –"auto","pyagrum"or"matplotlib".evidence (
Optional[Mapping[str,Any]]) – Observations to condition on; nodes are then shaded by their posterior and the evidence nodes are outlined. Keys may be variable names, fault tree node ids or basic event ids.show_probabilities (
bool) – Draw the per-state bars inside each node (matplotlib backend). WithFalsethe nodes collapse to compact labelled boxes.max_label (
int) – Truncate node labels to this many characters.annotations (
Optional[Sequence[str]]) – Lines of explanatory text drawn above the graph, the first in green and the rest in a lighter grey-green. Use them to say what the reader is looking at — which hazard, which evidence, which bound.
-
network:
BayesianNetwork¶
- figure(evidence=None)[source]¶
Return the matplotlib
Figurewithout displaying it.show()is for notebooks: inside one it callsdisplay()and returnsNone, because returning the figure as well would render it twice. That leaves no handle for a caller who wants to adjust the figure, embed it in a larger layout, or assert something about it, which is what this is for:fig = bn.view().figure() fig.set_size_inches(14, 9) fig.savefig("bn.pdf")
Always the matplotlib backend, so it never needs Graphviz.
- side_by_side(evidence=None)[source]¶
Structure and inference next to each other (pyAgrum only).
Falls back to a single annotated matplotlib figure when Graphviz is unavailable, rather than raising.
- to_dot()[source]¶
Graphviz source for the network.
Written by hand rather than through pyAgrum so that it works without the
dotbinary — the text is useful on its own, and can be rendered elsewhere.- Return type:
- __init__(network, backend='auto', evidence=None, show_probabilities=True, max_label=28, figsize=None, annotations=None)¶
- hiphopsllm.bayes.viz.graphviz_available()[source]¶
Is the Graphviz
dotbinary actually callable?import pydotsucceeding proves nothing: the Python binding is not the renderer. This runsdot -V.- Return type:
- hiphopsllm.bayes.viz.PALETTE = {'bar': '#8fbc8f', 'basic': '#e8eef7', 'basic_line': '#5b7fa6', 'caption': '#4d7098', 'edge': '#9a9a9a', 'evidence': '#2f6f4f', 'gate': '#f3efe6', 'gate_line': '#a08a5f', 'header_line': '#c9c9c9', 'high': '#f6dcd6', 'low': '#eef4ee', 'muted': '#6b6b6b', 'node_face': '#ffffff', 'node_line': '#b0b0b0', 'note': '#2f7d4f', 'note_muted': '#7e9a89', 'surface': '#fcfcfb', 'text': '#0b0b0b', 'top': '#f7e7e4', 'top_line': '#b26a5c'}¶
Node shading, low failure probability to high. Status colour is never the only signal — every node also carries its probability as text.