Loading and saving graphical models

Creative Commons License

aGrUM

interactive online version

pyAgrum can read and write graphical models in many file formats. This notebook gives an overview of which formats are available for each model type, what they preserve, and why the native bgum (binary) and jgum (JSON) formats are the best choice for pyAgrum workflows.

In [1]:
import os
import tempfile
import time

import pyagrum as gum
import pyagrum.lib.notebook as gnb

Available formats

The three main model types each have their own set of supported formats.

In [2]:
print(f"BayesNet formats  : {gum.availableBNExts()}")
print(f"InfluenceDiagram  : {gum.availableIDExts()}")
print(f"MarkovRandomField : {gum.availableMRFExts()}")
BayesNet formats  : bif|dsl|net|bifxml|o3prm|uai|xdsl|pkl|jgum|bgum
InfluenceDiagram  : xmlbif|bifxml|xml|jgum|bgum|pkl
MarkovRandomField : uai|jgum|bgum|pkl

The load/save API is uniform across model types:

Model

Load

Save

BayesNet

gum.loadBN(filename)

gum.saveBN(bn, filename)

InfluenceDiagram

gum.loadID(filename)

gum.saveID(diag, filename)

MarkovRandomField

gum.loadMRF(filename)

gum.saveMRF(mrf, filename)

The format is selected automatically from the file extension.

Bayesian networks

A tour of BN formats

Let’s load the classic Asia network and save/reload it in every available format, comparing file size and round-trip speed.

In [3]:
bn_asia = gum.loadBN("res/asia.bgum")
gnb.flow.row(bn_asia, captions=[f"Asia BN — {bn_asia.size()} nodes, {bn_asia.sizeArcs()} arcs"])
Out[3]:
G visit_to_Asia visit_to_Asia tuberculosis tuberculosis visit_to_Asia->tuberculosis lung_cancer lung_cancer tuberculos_or_cancer tuberculos_or_cancer lung_cancer->tuberculos_or_cancer positive_XraY positive_XraY tuberculosis->tuberculos_or_cancer bronchitis bronchitis dyspnoea dyspnoea bronchitis->dyspnoea tuberculos_or_cancer->positive_XraY tuberculos_or_cancer->dyspnoea smoking smoking smoking->lung_cancer smoking->bronchitis
Asia BN — 8 nodes, 8 arcs
In [4]:
def benchmark_bn(bn, exts):
  """Save and reload a BN in every given format, return a comparison table."""
  rows = []
  with tempfile.TemporaryDirectory() as d:
    for ext in exts.split("|"):
      fname = os.path.join(d, f"model.{ext}")
      try:
        t0 = time.perf_counter()
        gum.saveBN(bn, fname)
        t_save = time.perf_counter() - t0
        size = os.path.getsize(fname)
        t0 = time.perf_counter()
        bn2 = gum.loadBN(fname)
        t_load = time.perf_counter() - t0
        names_ok = sorted(bn.names()) == sorted(bn2.names())
        labels_ok = names_ok and all(
          list(bn.variable(n).labels()) == list(bn2.variable(n).labels()) for n in bn.names()
        )
        types_ok = names_ok and all(bn.variable(n).varType() == bn2.variable(n).varType() for n in bn.names())
        rows.append((ext, size, t_save * 1000, t_load * 1000, names_ok, labels_ok, types_ok))
      except Exception as e:
        rows.append((ext, None, None, None, False, False, str(e)[:60]))
  return rows


rows = benchmark_bn(bn_asia, gum.availableBNExts())

header = (
  f"{'ext':8s}  {'size (B)':>9s}  {'save (ms)':>10s}  {'load (ms)':>10s}  {'names':>5s}  {'labels':>6s}  {'types':>5s}"
)
print(header)
print("-" * len(header))
for ext, size, ts, tl, n_ok, l_ok, t_ok in rows:
  if size is not None:
    print(f"{ext:8s}  {size:9d}  {ts:10.3f}  {tl:10.3f}  {str(n_ok):>5s}  {str(l_ok):>6s}  {str(t_ok):>5s}")
  else:
    print(f"{ext:8s}  (unsupported for this model)")
ext        size (B)   save (ms)   load (ms)  names  labels  types
-----------------------------------------------------------------
bif            1554       0.505       1.664   True    True  False
dsl            1872       0.293       0.946   True    True  False
net            2558       0.224       0.929   True    True  False
bifxml         3554       0.206       0.146   True    True   True
o3prm     (unsupported for this model)
uai             430       0.231       1.773  False   False  False
xdsl           2264       0.701       0.167   True    True  False
pkl            1339       0.234       0.153   True    True   True
jgum           1283       0.166       0.083   True    True   True
bgum            808       0.218       0.076   True    True   True
In [5]:
import matplotlib.pyplot as plt
import numpy as np

exts = [r[0] for r in rows if r[1] is not None]
sizes = [r[1] for r in rows if r[1] is not None]
t_saves = [r[2] for r in rows if r[1] is not None]
t_loads = [r[3] for r in rows if r[1] is not None]

x = np.arange(len(exts))
width = 0.35

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

ax1.bar(x, sizes, color="steelblue")
ax1.set_xticks(x)
ax1.set_xticklabels(exts, rotation=45)
ax1.set_ylabel("size (B)")
ax1.set_title("File size per format")

ax2.bar(x - width / 2, t_saves, width, label="save (ms)", color="darkorange")
ax2.bar(x + width / 2, t_loads, width, label="load (ms)", color="seagreen")
ax2.set_xticks(x)
ax2.set_xticklabels(exts, rotation=45)
ax2.set_ylabel("time (ms)")
ax2.set_title("Save / load time per format")
ax2.legend()

fig.tight_layout()
../_images/notebooks_91-Tools-LoadAndSaveGraphicalModels_10_0.svg

Variable type fidelity

Many real-world networks contain variables of heterogeneous types: labelled (LabelizedVariable), integer ranges (RangeVariable) or discretized continuous domains (DiscretizedVariable). Not all formats can represent these faithfully.

In [6]:
# Build a BN that mixes all main variable types
bn_mixed = gum.BayesNet("mixed_types")
bn_mixed.add(gum.LabelizedVariable("Smoker", "Smoker", ["yes", "no"]))
bn_mixed.add(gum.RangeVariable("Age", "Age", 20, 60))
bn_mixed.add(gum.DiscretizedVariable("Temp", "Temp", [36.0, 37.0, 38.5, 42.0]))
bn_mixed.add(gum.LabelizedVariable("Cancer", "Cancer", ["yes", "no"]))
bn_mixed.addArc("Smoker", "Cancer")
bn_mixed.addArc("Age", "Cancer")
bn_mixed.addArc("Temp", "Cancer")
bn_mixed.cpt("Smoker").fillWith([0.3, 0.7])
bn_mixed.cpt("Age").fillWith(1).normalize()
bn_mixed.cpt("Temp").fillWith(1).normalize()
bn_mixed.cpt("Cancer").fillWith(1).normalize()

print("Variable types in bn_mixed:")
type_names = {
  gum.VarType_LABELIZED: "Labelized",
  gum.VarType_DISCRETIZED: "Discretized",
  gum.VarType_RANGE: "Range",
  gum.VarType_INTEGER: "Integer",
  gum.VarType_NUMERICAL: "Numerical",
}
for n in bn_mixed.names():
  v = bn_mixed.variable(n)
  print(f"  {n:10s}: {type_names.get(v.varType(), f'type={v.varType()}')}")
Variable types in bn_mixed:
  Temp      : Discretized
  Smoker    : Labelized
  Cancer    : Labelized
  Age       : Range
In [7]:
rows = benchmark_bn(bn_mixed, gum.availableBNExts())

header = (
  f"{'ext':8s}  {'size (B)':>9s}  {'save (ms)':>10s}  {'load (ms)':>10s}  {'names':>5s}  {'labels':>6s}  {'types':>5s}"
)
print(header)
print("-" * len(header))
for ext, size, ts, tl, n_ok, l_ok, t_ok in rows:
  if size is not None:
    print(f"{ext:8s}  {size:9d}  {ts:10.3f}  {tl:10.3f}  {str(n_ok):>5s}  {str(l_ok):>6s}  {str(t_ok):>5s}")
  else:
    print(f"{ext:8s}  (unsupported for this model)")
ext        size (B)   save (ms)   load (ms)  names  labels  types
-----------------------------------------------------------------
bif       (unsupported for this model)
dsl       (unsupported for this model)
net       (unsupported for this model)
bifxml         8951       0.500       0.274   True    True   True
o3prm     (unsupported for this model)
uai            6703       0.481      21.306  False   False  False
xdsl           7882       1.272       0.283   True    True  False
pkl           12187       0.359       0.197   True    True   True
jgum          12131       0.294       0.161   True    True   True
bgum           5158       0.245       0.102   True    True   True

Observations for BN:

Format

Notes

bif, dsl, net

Classic, widely used, but do not support DiscretizedVariable

bifxml / xdsl

XML-based; bifxml preserves types, xdsl does not

uai

Compact but loses variable names

o3prm

Verbose; requires a full class hierarchy

pkl

Python pickle; preserves everything but not portable across pyAgrum versions

``jgum``

Native JSON format; preserves all types, human-readable

``bgum``

Native binary format; smallest files, fastest I/O, preserves all types

Influence diagrams

Influence diagrams have fewer supported formats than BNs.

In [8]:
# Classic Oil Wildcatter influence diagram
diag = gum.loadID("res/OilWildcatter.bgum")
gnb.flow.row(diag, captions=[f"Oil Wildcatter — {diag.size()} nodes"])
Out[8]:
TestResult TestResult Drilling Drilling TestResult->Drilling OilContents OilContents OilContents->TestResult Reward Reward OilContents->Reward Testing Testing Testing->TestResult Testing->Drilling Cost Cost Testing->Cost Drilling->Reward
Oil Wildcatter — 6 nodes
In [9]:
def benchmark_id(diag, exts):
  rows = []
  with tempfile.TemporaryDirectory() as d:
    for ext in exts.split("|"):
      fname = os.path.join(d, f"model.{ext}")
      try:
        t0 = time.perf_counter()
        gum.saveID(diag, fname)
        t_save = time.perf_counter() - t0
        size = os.path.getsize(fname)
        t0 = time.perf_counter()
        diag2 = gum.loadID(fname)
        t_load = time.perf_counter() - t0
        names_ok = sorted(diag.names()) == sorted(diag2.names())
        rows.append((ext, size, t_save * 1000, t_load * 1000, names_ok))
      except Exception as e:
        rows.append((ext, None, None, None, str(e)[:60]))
  return rows


rows = benchmark_id(diag, gum.availableIDExts())

header = f"{'ext':8s}  {'size (B)':>9s}  {'save (ms)':>10s}  {'load (ms)':>10s}  {'names':>5s}"
print(header)
print("-" * len(header))
for ext, size, ts, tl, n_ok in rows:
  if size is not None:
    print(f"{ext:8s}  {size:9d}  {ts:10.3f}  {tl:10.3f}  {str(n_ok):>5s}")
  else:
    print(f"{ext:8s}  (unsupported for this model)")
ext        size (B)   save (ms)   load (ms)  names
--------------------------------------------------
xmlbif         2730       0.591       0.215   True
bifxml         2730       0.235       0.131   True
xml            2730       0.285       0.108   True
jgum            751       0.412       0.112   True
bgum            711       0.232       0.081   True
pkl             828       0.978       0.318   True

Observations for InfluenceDiagram:

Format

Notes

bifxml / xmlbif / xml

Three aliases for the same XML format

pkl

Portable only within the same pyAgrum version

``jgum``

Compact JSON, fully faithful, human-readable

``bgum``

Smallest files, fastest I/O

Markov random fields

MRFs have the smallest set of supported formats.

In [10]:
mrf = gum.fastMRF("A{yes|no}--B{low|mid|high}--C{yes|no}--A;B--D{yes|no}")
gnb.flow.row(mrf, captions=[f"MRF — {mrf.size()} nodes, {mrf.sizeEdges()} edges"])
Out[10]:
G B B D D C C A A f0#1#2 f0#1#2--B f0#1#2--C f0#1#2--A f1#3 f1#3--B f1#3--D
MRF — 4 nodes, 4 edges
In [11]:
def benchmark_mrf(mrf, exts):
  rows = []
  with tempfile.TemporaryDirectory() as d:
    for ext in exts.split("|"):
      fname = os.path.join(d, f"model.{ext}")
      try:
        t0 = time.perf_counter()
        gum.saveMRF(mrf, fname)
        t_save = time.perf_counter() - t0
        size = os.path.getsize(fname)
        t0 = time.perf_counter()
        mrf2 = gum.loadMRF(fname)
        t_load = time.perf_counter() - t0
        names_ok = sorted(mrf.names()) == sorted(mrf2.names())
        labels_ok = names_ok and all(
          list(mrf.variable(n).labels()) == list(mrf2.variable(n).labels()) for n in mrf.names()
        )
        rows.append((ext, size, t_save * 1000, t_load * 1000, names_ok, labels_ok))
      except Exception as e:
        rows.append((ext, None, None, None, False, str(e)[:60]))
  return rows


rows = benchmark_mrf(mrf, gum.availableMRFExts())

header = f"{'ext':8s}  {'size (B)':>9s}  {'save (ms)':>10s}  {'load (ms)':>10s}  {'names':>5s}  {'labels':>6s}"
print(header)
print("-" * len(header))
for ext, size, ts, tl, n_ok, l_ok in rows:
  if size is not None:
    print(f"{ext:8s}  {size:9d}  {ts:10.3f}  {tl:10.3f}  {str(n_ok):>5s}  {str(l_ok):>6s}")
  else:
    print(f"{ext:8s}  (unsupported for this model)")
ext        size (B)   save (ms)   load (ms)  names  labels
----------------------------------------------------------
uai             279       0.381       0.948  False   False
jgum            666       0.351       0.133   True    True
bgum            426       0.222       0.090   True    True
pkl             747       1.158       0.400   True    True

Observations for MRF:

Format

Notes

uai

Only standard MRF format, but loses variable names and labels

pkl

Portable only within the same pyAgrum version

``jgum``

Full fidelity, JSON, readable

``bgum``

Smallest, fastest, full fidelity

Why bgum and jgum are the best choice for pyAgrum

The bgum and jgum formats are the native aGrUM formats, designed specifically for all model types supported by pyAgrum. They share the same advantages:

  1. Universal — same format works for BayesNet, InfluenceDiagram and MarkovRandomField.

  2. Full fidelity — all variable types (LabelizedVariable, RangeVariable, DiscretizedVariable, IntegerVariable) are preserved exactly.

  3. Fast — both I/O are among the fastest of all formats.

  4. Compact — bgum typically produces the smallest files; jgum is still compact while remaining human-readable.

  5. No external dependencies — no need for third-party parsers.

The only difference between the two is readability:

  • ``bgum`` (binary) — optimal for production workflows, automated pipelines, storing large models.

  • ``jgum`` (JSON) — easier to inspect, diff, or version-control.

Quick demo: round-trip with bgum and jgum

In [12]:
# Build a BN with mixed variable types to stress-test fidelity
bn_demo = gum.BayesNet("demo")
bn_demo.add(gum.LabelizedVariable("Smoker", "Smoker", ["yes", "no"]))
bn_demo.add(gum.RangeVariable("Age", "Age", 20, 60))
bn_demo.add(gum.DiscretizedVariable("Temp", "Temp", [36.0, 37.0, 38.5, 42.0]))
bn_demo.add(gum.LabelizedVariable("Cancer", "Cancer", ["yes", "no"]))
bn_demo.addArc("Smoker", "Cancer")
bn_demo.addArc("Age", "Cancer")
bn_demo.addArc("Temp", "Cancer")
bn_demo.cpt("Smoker").fillWith([0.3, 0.7])
bn_demo.cpt("Age").fillWith(1).normalize()
bn_demo.cpt("Temp").fillWith(1).normalize()
bn_demo.cpt("Cancer").fillWith(1).normalize()

type_names = {
  gum.VarType_LABELIZED: "Labelized",
  gum.VarType_DISCRETIZED: "Discretized",
  gum.VarType_RANGE: "Range",
  gum.VarType_INTEGER: "Integer",
  gum.VarType_NUMERICAL: "Numerical",
}

print("Variable types before save:")
for n in bn_demo.names():
  v = bn_demo.variable(n)
  print(
    f"  {n:8s}: {type_names.get(v.varType(), f'type={v.varType()}'):12s}  labels={list(v.labels()[:4])}{'...' if v.domainSize() > 4 else ''}"
  )
Variable types before save:
  Temp    : Discretized   labels=['[36;37[', '[37;38.5[', '[38.5;42]']
  Smoker  : Labelized     labels=['yes', 'no']
  Cancer  : Labelized     labels=['yes', 'no']
  Age     : Range         labels=['20', '21', '22', '23']...
In [13]:
with tempfile.TemporaryDirectory() as d:
  for ext in ("bgum", "jgum"):
    fname = os.path.join(d, f"demo.{ext}")
    gum.saveBN(bn_demo, fname)
    bn2 = gum.loadBN(fname)
    print(f"\n--- {ext} ({os.path.getsize(fname)} bytes) ---")
    for n in bn2.names():
      v = bn2.variable(n)
      print(
        f"  {n:8s}: {type_names.get(v.varType(), f'type={v.varType()}'):12s}  labels={list(v.labels()[:4])}{'...' if v.domainSize() > 4 else ''}"
      )

--- bgum (5151 bytes) ---
  Temp    : Discretized   labels=['[36;37[', '[37;38.5[', '[38.5;42]']
  Smoker  : Labelized     labels=['yes', 'no']
  Cancer  : Labelized     labels=['yes', 'no']
  Age     : Range         labels=['20', '21', '22', '23']...

--- jgum (12124 bytes) ---
  Temp    : Discretized   labels=['[36;37[', '[37;38.5[', '[38.5;42]']
  Smoker  : Labelized     labels=['yes', 'no']
  Cancer  : Labelized     labels=['yes', 'no']
  Age     : Range         labels=['20', '21', '22', '23']...
In [14]:
# jgum is plain JSON — easy to inspect
import json

with tempfile.TemporaryDirectory() as d:
  fname = os.path.join(d, "demo.jgum")
  gum.saveBN(bn_demo, fname)
  with open(fname) as f:
    data = json.load(f)
  # Show just the variable descriptions
  print(json.dumps(data.get("variables", data.get("nodes", {})), indent=2))
[
  "Smoker{yes|no}",
  "Age[20,60]",
  "Temp[36,37,38.5,42]",
  "Cancer{yes|no}"
]

bgum and jgum work identically for all model types

In [15]:
with tempfile.TemporaryDirectory() as d:
  # BayesNet
  gum.saveBN(bn_asia, os.path.join(d, "asia.bgum"))
  bn_rt = gum.loadBN(os.path.join(d, "asia.bgum"))
  print(f"BN  round-trip via bgum: names match = {sorted(bn_asia.names()) == sorted(bn_rt.names())}")

  # InfluenceDiagram
  gum.saveID(diag, os.path.join(d, "oil.bgum"))
  diag_rt = gum.loadID(os.path.join(d, "oil.bgum"))
  print(f"ID  round-trip via bgum: names match = {sorted(diag.names()) == sorted(diag_rt.names())}")

  # MarkovRandomField
  gum.saveMRF(mrf, os.path.join(d, "mrf.bgum"))
  mrf_rt = gum.loadMRF(os.path.join(d, "mrf.bgum"))
  print(f"MRF round-trip via bgum: names match = {sorted(mrf.names()) == sorted(mrf_rt.names())}")
BN  round-trip via bgum: names match = True
ID  round-trip via bgum: names match = True
MRF round-trip via bgum: names match = True

I/O Summary

bif/dsl/net

bifxml/xdsl

uai

pkl

``jgum``

``bgum``

BayesNet

✓

✓

partial

✓

✓

✓

InfluenceDiagram

✗

✓

✗

✓

✓

✓

MarkovRandomField

✗

✗

partial

✓

✓

✓

Preserves all variable types

✗

partial

✗

✓

✓

✓

Preserves variable names

✓

✓

✗

✓

✓

✓

Human-readable

✓

✓

✓

✗

✓

✗

Compact size

medium

large

small

medium

small

smallest

I/O speed

medium

medium

fast

fast

fast

fastest

Version-stable

✓

✓

✓

✗

✓

✓

Recommendation:

  • Use ``bgum`` whenever storage size or I/O speed matters, or when working with non-BN models.

  • Use ``jgum`` when the file needs to be inspected, diffed, or stored in version control.

  • Use ``bif``/``bifxml`` only when interoperability with other tools (GeNIe, Netica, …) is required.

In [ ]: