Back to Blog

Load and save graphical models in pyAgrum >2.3.2

Loading and saving graphical models

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.

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.

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:

ModelLoadSave
BayesNetgum.loadBN(filename)gum.saveBN(bn, filename)
InfluenceDiagramgum.loadID(filename)gum.saveID(diag, filename)
MarkovRandomFieldgum.loadMRF(filename)gum.saveMRF(mrf, filename)

The format is selected automatically from the file extension.

Bayesian networks

The generated readme of the pgmrepository contains more complete comparison in size and read/write time among the different formats for 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.

bn_asia = gum.fastBN("Asia->Tuberculosis->TorC->chest XRay;TorC<-Lung cancer<-Smoking->Bronchitis->Dyspnoea<-TorC")
gnb.flow.row(bn_asia, captions=[f"Asia BN — {bn_asia.size()} nodes, {bn_asia.sizeArcs()} arcs"])
G Smoking Smoking Lung cancer Lung cancer Smoking->Lung cancer Bronchitis Bronchitis Smoking->Bronchitis Dyspnoea Dyspnoea TorC TorC Lung cancer->TorC Asia Asia Tuberculosis Tuberculosis Asia->Tuberculosis chest XRay chest XRay Bronchitis->Dyspnoea Tuberculosis->TorC TorC->Dyspnoea TorC->chest XRay
Asia BN — 8 nodes, 8 arcs
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 (unsupported for this model)
dsl (unsupported for this model)
net (unsupported for this model)
bifxml 3593 0.189 0.237 True True True
o3prm (unsupported for this model)
uai 587 0.133 0.107 False False False
xdsl 2342 0.481 0.119 True True False
pkl 1940 0.178 0.104 True True True
jgum 1887 0.115 0.066 True True True
bgum 836 0.098 0.059 True True True

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.

# 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:
Smoker : Labelized
Cancer : Labelized
Temp : Discretized
Age : Range
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.267 0.483 True True True
o3prm (unsupported for this model)
uai 6703 0.307 0.625 False False False
xdsl 7886 0.554 0.519 True True False
pkl 16127 0.255 0.176 True True True
jgum 16074 0.153 0.140 True True True
bgum 5160 0.157 0.074 True True True

Observations for BN:

FormatNotes
bif, dsl, netClassic, widely used, but do not support DiscretizedVariable
bifxml / xdslXML-based; bifxml preserves types, xdsl does not
uaiCompact but loses variable names
o3prmVerbose; requires a full class hierarchy
pklPython pickle; preserves everything but not portable across pyAgrum versions
jgumNative JSON format; preserves all types, human-readable
bgumNative binary format; smallest files, fastest I/O, preserves all types

Influence diagrams

Influence diagrams have fewer supported formats than BNs.

# Classic Oil Wildcatter influence diagram
diag = gum.fastID("$Cost<-*Testing->TestResult<-OilContents->Reward<-*Drilling<-TestResult;Drilling<-Testing")
gnb.flow.row(diag, captions=[f"Oil Wildcatter — {diag.size()} nodes"])
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
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 2664 0.249 0.222 True
bifxml 2664 0.125 0.111 True
xml 2664 0.117 0.097 True
jgum 1299 0.136 0.079 True
bgum 609 0.097 0.058 True
pkl 1360 0.148 0.073 True

Observations for InfluenceDiagram:

FormatNotes
bifxml / xmlbif / xmlThree aliases for the same XML format
pklPortable only within the same pyAgrum version
jgumCompact JSON, fully faithful, human-readable
bgumSmallest files, fastest I/O

Markov random fields

MRFs have the smallest set of supported formats.

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"])
G A A C C D D B B f0#1#2 f0#1#2--A f0#1#2--C f0#1#2--B f1#3 f1#3--D f1#3--B
MRF — 4 nodes, 4 edges
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 280 0.202 0.194 False False
jgum 1029 0.131 0.080 True True
bgum 428 0.103 0.058 True True
pkl 1091 0.157 0.124 True True

Observations for MRF:

FormatNotes
uaiOnly standard MRF format, but loses variable names and labels
pklPortable only within the same pyAgrum version
jgumFull fidelity, JSON, readable
bgumSmallest, 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. Compactbgum 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

# 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:
Smoker : Labelized labels=['yes', 'no']
Cancer : Labelized labels=['yes', 'no']
Temp : Discretized labels=['[36;37[', '[37;38.5[', '[38.5;42]']
Age : Range labels=['20', '21', '22', '23']...
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 (5153 bytes) ---
Smoker : Labelized labels=['yes', 'no']
Cancer : Labelized labels=['yes', 'no']
Temp : Discretized labels=['[36;37[', '[37;38.5[', '[38.5;42]']
Age : Range labels=['20', '21', '22', '23']...
--- jgum (16067 bytes) ---
Smoker : Labelized labels=['yes', 'no']
Cancer : Labelized labels=['yes', 'no']
Temp : Discretized labels=['[36;37[', '[37;38.5[', '[38.5;42]']
Age : Range labels=['20', '21', '22', '23']...
# 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

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

Summary

bif/dsl/netbifxml/xdsluaipkljgumbgum
BayesNetpartial
InfluenceDiagram
MarkovRandomFieldpartial
Preserves all variable typespartial
Preserves variable names
Human-readable
Compact sizemediumlargesmallmediumsmallsmallest
I/O speedmediummediumfastfastfastfastest
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.