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.