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:

  1. Agents (propflow.core.agents.VariableAgent, propflow.core.agents.FactorAgent) exchange messages.

  2. Factor graphs (propflow.bp.factor_graph.FactorGraph) connect agents and initialize cost tables. Helper builders live in propflow.utils.

  3. Engines (propflow.bp.engine_base.BPEngine and subclasses) run belief propagation, manage convergence policies, and capture history.

  4. Simulations (propflow.simulator.Simulator) execute batches of engine configurations across many graphs for fair comparisons.

  5. 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 attached computator (defaults to uniform if messages are missing).

  • curr_assignment – best value implied by the current belief.

  • compute_messages() – calls computator.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_tablenumpy.ndarray scoring each variable assignment tuple.

  • ct_creation_func / ct_creation_params – factory for building the table. The factor graph calls initiate_cost_table() once the neighbourhood is known.

  • connection_number – mapping of variable names to axis indices. Maintained automatically when you add edges.

  • compute_messages() – uses computator.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 in edges.

  • 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 accept num_vars and domain_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:

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.

  1. Prepare a configuration dictionary mapping experiment names to engine classes plus keyword arguments.

  2. Build a list of factor graphs (reuse FGBuilder helpers or load pickled graphs).

  3. Call Simulator.run_simulations() to execute everything. The simulator attempts to run in parallel using multiprocessing but falls back to sequential processing if required.

  4. 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 with propflow.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:

  1. 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.

  2. Instantiate the factor graph

    • Pass lists of variable and factor agents plus an ordered edges map.

    • Confirm domain sizes match the factor expectations.

  3. 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.

  4. Run experiments

    • Call BPEngine.run() for single cases.

    • Use Simulator to fan out across many graphs/configurations.

  5. 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 neighbouring propflow.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