PropFlow User Guide
This guide presents PropFlow from the top down so you can understand the high-level architecture before working through concrete APIs. Follow the chain from individual agents, through factor graphs and engines, all the way to full-blown simulator runs and analysis tooling.
Top-Down Architecture
PropFlow can be viewed as a layered pipeline. Each layer builds on the previous one, and you can exit early if you only need part of the stack:
Agents (
propflow.core.agents.VariableAgent
,propflow.core.agents.FactorAgent
) exchange messages.Factor graphs (
propflow.bp.factor_graph.FactorGraph
) connect agents and initialize cost tables. Helper builders live inpropflow.utils
.Engines (
propflow.bp.engine_base.BPEngine
and subclasses) run belief propagation, manage convergence policies, and capture history.Simulations (
propflow.simulator.Simulator
) execute batches of engine configurations across many graphs for fair comparisons.Analysis tooling (
propflow.analyzer
) records data and reports metrics for offline inspection.
Agents Layer
Agents are the smallest active components in PropFlow. They inherit from
propflow.core.agents.FGAgent
, which embeds an inbox/outbox, message
history, and a link to a computator
implementing the BP math.
Variable Agents
propflow.core.agents.VariableAgent
models a discrete decision variable.
Important attributes and behaviours:
name
– identifier used in logs and assignments.domain
– number of discrete values this variable may take.belief
– vector computed by the attachedcomputator
(defaults to uniform if messages are missing).curr_assignment
– best value implied by the current belief.compute_messages()
– callscomputator.compute_Q
to prepare messages for neighbouring factors.
Example:
from propflow.core import VariableAgent
temperature = VariableAgent(name="temp_room_a", domain=4)
Factor Agents
propflow.core.agents.FactorAgent
encodes the local relationships
between several variables. Each factor owns a cost table that is lazily created
from a factory function.
Key fields:
cost_table
–numpy.ndarray
scoring each variable assignment tuple.ct_creation_func
/ct_creation_params
– factory for building the table. The factor graph callsinitiate_cost_table()
once the neighbourhood is known.connection_number
– mapping of variable names to axis indices. Maintained automatically when you add edges.compute_messages()
– usescomputator.compute_R
to send responses back to variables.
from propflow.core import FactorAgent
from propflow.configs import create_random_int_table
penalty = FactorAgent(
name="f_xy",
domain=3,
ct_creation_func=create_random_int_table,
param={"low": 0, "high": 10},
)
Message Lifecycle
Agents exchange propflow.core.components.Message
objects stored within
a propflow.core.components.MailHandler
. The handler:
Deduplicates messages per sender.
Seeds zero-messages so every neighbour pair can exchange information on the very first engine iteration.
Stages outgoing messages until the engine triggers delivery.
You seldom interact with messages directly unless you’re implementing new BP variants.
Factor Graph Layer
With agents in hand, propflow.bp.factor_graph.FactorGraph
wires them
into a bipartite structure, initializes cost tables, and exposes convenience
properties such as the graph diameter and current assignments. Most users
should rely on propflow.utils.FGBuilder
to create graphs. The helpers
ensure domain sizes line up, edges are valid, and factors receive their cost
tables automatically.
Using FGBuilder
FGBuilder
covers common topologies so you can focus on experiments instead
of plumbing. The snippet below builds a cycle and runs a plain BP engine:
from propflow import FGBuilder, BPEngine
from propflow.configs import create_random_int_table
fg = FGBuilder.build_cycle_graph(
num_vars=5,
domain_size=3,
ct_factory=create_random_int_table,
ct_params={"low": 0, "high": 10},
)
engine = BPEngine(fg)
engine.run(max_iter=25)
print(engine.assignments)
Other helpers such as propflow.utils.fg_utils.FGBuilder.build_random_graph()
return fully initialised FactorGraph
objects as well.
Config-Driven Graphs
For reproducible benchmarks, create a propflow.utils.create.GraphConfig
and hand it to propflow.utils.create.FactorGraphBuilder
:
from pathlib import Path
from propflow.utils.create import FactorGraphBuilder
cfg_path = Path("configs/factor_graphs/cycle_demo.pkl")
builder = FactorGraphBuilder()
fg = builder.build_and_return(cfg_path)
The builder loads the config, resolves registered graph/cost factories, and
produces a FactorGraph
. Use FactorGraphBuilder.build_and_save()
to persist generated graphs for later reuse.
Manual Graph Assembly
When you need a structure that the helpers do not cover—custom agents, hybrid
domains—build the graph yourself. Provide explicit lists of variables, factors,
and an ordered edges
mapping.
from propflow import FactorGraph, VariableAgent, FactorAgent
from propflow.configs import create_uniform_float_table
x1 = VariableAgent("x1", domain=2)
x2 = VariableAgent("x2", domain=2)
parity = FactorAgent(
name="f12",
domain=2,
ct_creation_func=create_uniform_float_table,
)
fg = FactorGraph(
variable_li=[x1, x2],
factor_li=[parity],
edges={parity: [x1, x2]},
)
Checklist for manual graphs:
Every factor supplied in
factor_li
appears as a key inedges
.Each value in
edges
is an ordered list; the index order defines tensor axes, so be deliberate when mapping variables to dimensions.ct_creation_func
must acceptnum_vars
anddomain_size
arguments; PropFlow passes them automatically.Use deterministic parameters (bounds, seeds) when you want reproducible runs.
Engine Layer
Engines coordinate message passing, convergence behaviour, history tracking,
and optional snapshots. The base propflow.bp.engine_base.BPEngine
implements synchronous belief propagation: variables update first, then factors,
for each iteration.
Core responsibilities:
Assign the chosen
propflow.core.dcop_base.Computator
to every agent.Seed inboxes with zero-messages so computation can start immediately.
Execute
step
loops until convergence or a maximum iteration cap.Record costs, beliefs, and assignments in
propflow.bp.engine_components.History
.Expose hook methods (
pre_factor_compute
etc.) that subclasses override to implement policies.
Selecting a Computator
Computators contain the algorithmic math. PropFlow ships with:
propflow.bp.computators.MinSumComputator
(default)propflow.bp.computators.MaxSumComputator
propflow.bp.computators.SumProductComputator
propflow.bp.computators.MaxProductComputator
Swap variants by passing the desired instance to the engine:
from propflow import BPEngine, MaxSumComputator
engine = BPEngine(fg, computator=MaxSumComputator())
Engine Variants and Policies
Specialised engines extend BPEngine
with additional behaviour:
propflow.bp.engines.DampingEngine
– smooths messages.propflow.bp.engines.SplitEngine
– splits factors to alter dynamics.propflow.bp.engines.CostReductionOnceEngine
– reduces costs once at startup.propflow.bp.engines.MessagePruningEngine
– prunes messages using policies.
Complement engines with policies and utilities:
propflow.policies.convergance.ConvergenceConfig
to define minimum iterations, tolerance, and patience.propflow.policies.normalize_cost.normalize_inbox()
to shift messages and avoid numerical blow-ups.propflow.snapshots.SnapshotsConfig
to capture detailed per-iteration state.
Running a Single Engine
from propflow import BPEngine, MinSumComputator, SnapshotsConfig
snapshots = SnapshotsConfig(compute_cycles=True, retain_last=5)
engine = BPEngine(
factor_graph=fg,
computator=MinSumComputator(),
snapshots_config=snapshots,
)
engine.run(max_iter=100)
final_cost = engine.history.costs[-1]
beliefs = engine.get_beliefs()
Inspect engine.assignments
or engine.history
for detailed
outputs, and call engine.latest_snapshot()
when snapshots are enabled.
Simulation Layer
The propflow.simulator.Simulator
orchestrates multiple engine
configurations running over many graphs—perfect for benchmarking or tuning.
Prepare a configuration dictionary mapping experiment names to engine classes plus keyword arguments.
Build a list of factor graphs (reuse
FGBuilder
helpers or load pickled graphs).Call
Simulator.run_simulations()
to execute everything. The simulator attempts to run in parallel usingmultiprocessing
but falls back to sequential processing if required.Use
Simulator.plot_results()
to visualise mean cost trajectories.
from propflow import Simulator, BPEngine, DampingEngine, FGBuilder
from propflow.configs import create_random_int_table
configs = {
"baseline": {"class": BPEngine},
"damped": {"class": DampingEngine, "damping_factor": 0.85},
}
graphs = [
FGBuilder.build_random_graph(
num_vars=12,
domain_size=3,
ct_factory=create_random_int_table,
ct_params={"low": 0, "high": 15},
density=0.25,
)
for _ in range(4)
]
simulator = Simulator(configs)
aggregated = simulator.run_simulations(graphs, max_iter=150)
simulator.plot_results(verbose=True)
Analysis Layer
Advanced studies often require visibility into per-iteration behaviour. The
propflow.analyzer
package contains tooling for that.
propflow.analyzer.snapshot_recorder
captures snapshots from running engines and stores them on disk. Pair it withpropflow.snapshots.SnapshotsConfig
to decide what to record.propflow.analyzer.snapshot_visualizer
renders saved snapshots.propflow.analyzer.reporting
aggregates metrics and produces summaries.
Example workflow:
from propflow.analyzer.snapshot_recorder import SnapshotRecorder
from propflow import BPEngine, SnapshotsConfig
recorder = SnapshotRecorder(path="results/run_001")
snapshots = SnapshotsConfig(compute_cycles=True, retain_last=20)
engine = BPEngine(fg, snapshots_config=snapshots)
engine.run(max_iter=75)
recorder.save(engine)
# Later: use recorder.load() or snapshot_visualizer utilities for inspection.
Chain of Creation
Use this checklist when building your own experiments:
Choose a graph strategy
Prefer
propflow.utils.FGBuilder
for standard cycles or random graphs.Fall back to manual agent construction when you need custom structures.
Instantiate the factor graph
Pass lists of variable and factor agents plus an ordered
edges
map.Confirm domain sizes match the factor expectations.
Pick an engine configuration
Select a
computator
and, if needed, an engine variant with policies.Enable snapshots or convergence rules to match your evaluation criteria.
Run experiments
Call
BPEngine.run()
for single cases.Use
Simulator
to fan out across many graphs/configurations.
Analyse results
Inspect
engine.history
for costs, beliefs, and assignments.Persist and revisit runs with
propflow.analyzer
.
Custom Graph Checklist
If you bypass FGBuilder
:
Ensure every
propflow.core.agents.FactorAgent
references each neighbouringpropflow.core.agents.VariableAgent
exactly once.Provide cost-table factories that honour the
(num_vars, domain_size)
signature—the FactorGraph constructor will call them for you.Call
FactorGraph
only after all agents exist; it registers edges and triggers cost table creation automatically.Stick to deterministic seeds and bounds inside your cost factories for reproducible results.
Next Steps
Jump to Quick Start Guide for runnable snippets.
Browse Examples for complete demonstrations and notebooks.
Consult API Reference for the full API surface.
Review PropFlow Handbook for deeper dives, patterns, and practices.