Protection Alarm Diagnosis

This example builds a small protection-alarm diagnosis model. A control center receives relay, breaker, and voltage indications from a feeder section. The variables are finite-state health or indication states, and the factors encode domain likelihoods. The graph contains cycles, so iterative discrete belief propagation is the natural schedule.


Variable Nodes

The latent fault location, breaker state, relay indications, and voltage alarm are modeled as discrete variables:

using FactorGraph

variables = [
    DiscreteVariable(:fault, 3; label = "fault", states = [:none, :line12, :line23]),
    DiscreteVariable(:breaker, 2; label = "breaker", states = [:closed, :open]),
    DiscreteVariable(:relay12, 2; label = "relay12", states = [:quiet, :trip]),
    DiscreteVariable(:relay23, 2; label = "relay23", states = [:quiet, :trip]),
    DiscreteVariable(:voltage, 2; label = "voltage", states = [:normal, :low])
]

Prior and Sensor Model

The fault prior is a unary factor. Pairwise factors encode how likely each relay or voltage alarm is under each fault state:

f1 = DiscreteFactor(:fault, [0.92, 0.05, 0.03]; label = "prior_fault", initialize = true)
f2 = DiscreteFactor(:fault, :relay12, [0.9 0.1; 0.1 0.9; 0.7 0.2]; label = "fault_relay12")
f3 = DiscreteFactor(:fault, :relay23, [0.9 0.1; 0.7 0.3; 0.1 0.9]; label = "fault_relay23")
f4 = DiscreteFactor(:fault, :voltage, [0.9 0.1; 0.2 0.7; 0.2 0.8]; label = "fault_voltage")

Operational Coupling

The breaker and measured voltage are not independent: an open breaker makes a low-voltage indication more likely. Relay 12 can also be affected by breaker operation, which creates a cycle in the factor graph:

f5 = DiscreteFactor(:breaker, :voltage, [0.9 0.1; 0.2 0.8]; label = "breaker_voltage")
f6 = DiscreteFactor(:breaker, :relay12, [0.8 0.1; 0.3 0.7]; label = "breaker_relay12")

Observed Evidence

Observed SCADA indications are represented as unary likelihood factors. Here, relay 12 trips, relay 23 stays quiet, the breaker is open, and the voltage is low:

f7 = DiscreteFactor(:relay12, [0.05, 0.95]; label = "obs_relay12")
f8 = DiscreteFactor(:relay23, [0.95, 0.05]; label = "obs_relay23")
f9 = DiscreteFactor(:breaker, [0.05, 0.95]; label = "obs_breaker")
f10 = DiscreteFactor(:voltage, [0.10, 0.90]; label = "obs_voltage")

Factor Graph Construction

Collect the factors and build the factor graph:

factors = [f1, f2, f3, f4, f5, f6, f7, f8, f9, f10]
graph = factorGraph(variables, factors)

The graph can be rendered as an SVG factor graph figure:

saveGraphFigure("../pad.svg", graph; label = (showEdgeIds = true, tooltipDetail = :full))

Running Belief Propagation

Run damped sum-product belief propagation on the graph:

inference = sumproduct(graph)

gbp!(graph, inference; iterations = 60, tolerance = 1e-8, damping = true)

Results

Inspect the posterior probabilities of the fault location and breaker state:

printMarginal(graph, inference; variable = :fault)
printMarginal(graph, inference; variable = :breaker)
Marginal for variable node "fault" (sum-product form):
  probability = [0.35237168132009944, 0.6264104593559505, 0.021217859323950147]
Marginal for variable node "breaker" (sum-product form):
  probability = [0.0070990270421943304, 0.9929009729578058]

Alarm Update

If a later relay 23 indication changes to trip, update the evidence factor and continue from the current messages:

updateFactor!(graph, inference; factor = "obs_relay23", table = [0.05, 0.95])
gbp!(graph, inference; iterations = 40, tolerance = 1e-8, damping = true)

printMarginal(graph, inference; variable = :fault)
Marginal for variable node "fault" (sum-product form):
  probability = [0.11826022217322593, 0.6117454178492046, 0.26999435997756954]

The same inference object is reused, so the messages from the previous run act as a warm start.