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.
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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
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]:
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.442 1.324 True True False
dsl 1872 0.269 0.966 True True False
net 2558 0.241 0.977 True True False
bifxml 3554 0.217 0.187 True True True
o3prm 694 0.163 3.380 True True True
uai 430 0.213 3.029 False False False
xdsl 2264 1.525 0.191 True True False
pkl 1911 0.317 0.159 True True True
jgum 1858 0.197 0.099 True True True
bgum 808 0.179 0.211 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.
In [5]:
# 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:
Cancer : Labelized
Age : Range
Temp : Discretized
Smoker : Labelized
In [6]:
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.450 0.271 True True True
o3prm (unsupported for this model)
uai 6703 0.554 19.874 False False False
xdsl 7882 1.725 0.310 True True False
pkl 16125 0.394 0.210 True True True
jgum 16072 0.304 0.177 True True True
bgum 5158 0.232 0.096 True True True
Observations for BN:
Format |
Notes |
|---|---|
|
Classic, widely used, but do not support |
|
XML-based; |
|
Compact but loses variable names |
|
Verbose; requires a full class hierarchy |
|
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 [7]:
# Classic Oil Wildcatter influence diagram
diag = gum.loadID("res/OilWildcatter.bgum")
gnb.flow.row(diag, captions=[f"Oil Wildcatter — {diag.size()} nodes"])
Out[7]:
In [8]:
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.739 0.381 True
bifxml 2730 0.382 0.231 True
xml 2730 0.842 0.217 True
jgum 1215 0.302 0.108 True
bgum 711 0.172 0.084 True
pkl 1276 0.202 0.101 True
Observations for InfluenceDiagram:
Format |
Notes |
|---|---|
|
Three aliases for the same XML format |
|
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 [9]:
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[9]:
In [10]:
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 276 0.299 1.010 False False
jgum 1028 0.178 0.095 True True
bgum 426 0.128 0.067 True True
pkl 1090 1.014 0.515 True True
Observations for MRF:
Format |
Notes |
|---|---|
|
Only standard MRF format, but loses variable names and labels |
|
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:
Universal — same format works for
BayesNet,InfluenceDiagramandMarkovRandomField.Full fidelity — all variable types (
LabelizedVariable,RangeVariable,DiscretizedVariable,IntegerVariable) are preserved exactly.Fast — both I/O are among the fastest of all formats.
Compact —
bgumtypically produces the smallest files;jgumis still compact while remaining human-readable.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 [11]:
# 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:
Cancer : Labelized labels=['yes', 'no']
Age : Range labels=['20', '21', '22', '23']...
Temp : Discretized labels=['[36;37[', '[37;38.5[', '[38.5;42]']
Smoker : Labelized labels=['yes', 'no']
In [12]:
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) ---
Cancer : Labelized labels=['yes', 'no']
Age : Range labels=['20', '21', '22', '23']...
Temp : Discretized labels=['[36;37[', '[37;38.5[', '[38.5;42]']
Smoker : Labelized labels=['yes', 'no']
--- jgum (16065 bytes) ---
Cancer : Labelized labels=['yes', 'no']
Age : Range labels=['20', '21', '22', '23']...
Temp : Discretized labels=['[36;37[', '[37;38.5[', '[38.5;42]']
Smoker : Labelized labels=['yes', 'no']
In [13]:
# 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 [14]:
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
|
|
|
|
``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 [ ]:

