RiC-O — In-Memory Demo (NetworkX)

This notebook demonstrates how to model a RiC-O (Records in Contexts — Ontology) graph using cvcdocdb with the NetworkX in-memory backend.

Key concepts:

  • NetworkXGraph stores everything in memory with optional pickle persistence

  • Same CVCDocDB entity classes (Thing, Agent, RecordResource, etc.) as Neo4j

  • Same load_ric_o_naf() loader downloads real RiC-O data from GitHub

  • get_subdocuments() traverses _propagate edges (BFS)

  • Cypher queries are supported via an in-memory executor

  • Persistence file is created fresh each run in /tmp/

Steps:

  1. Create an in-memory NetworkX graph with unique persistence path

  2. Load real RiC-O data from the National Archives of France (NAF) example

  3. Initialize propagation properties

  4. Query subdocuments

  5. Inspect the hierarchy

  6. Summary statistics

Why NetworkX?

NetworkX is useful for:

  • Prototyping — no external database needed

  • Testing — fast, deterministic, easy to clean up

  • Learning — see the graph structure without Neo4j overhead

  • Persistence — pickle file survives notebook restarts (optional)

[ ]:
import importlib.util

# Check if cvcdocdb is installed
package_to_check = 'cvcdocdb'
spec = importlib.util.find_spec(package_to_check)

if spec is None:
    print(f'⚠️ {package_to_check} not installed. Installing...')
    %pip install -q --upgrade cvcdocdb
    print("✅ Installation complete.")
else:
    print(f'✅ {package_to_check} already installed. Skipping.')
[2]:
import os
import sys
import uuid
from pathlib import Path

# Read .env file manually (no python-dotenv dependency)
def read_env(env_path):
    """Read a .env file and return a dict of key=value pairs."""
    env_vars = {}
    with open(env_path, 'r') as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith('#'):
                continue
            if '=' in line:
                key, value = line.split('=', 1)
                env_vars[key.strip()] = value.strip()
    return env_vars

# Find .env in repo root
_env_path = Path.cwd() / ".env"
if not _env_path.exists():
    for parent in Path.cwd().parents:
        candidate = parent / ".env"
        if candidate.exists():
            _env_path = candidate
            break

if _env_path.exists():
    _env_vars = read_env(_env_path)
    for key, value in _env_vars.items():
        os.environ.setdefault(key, value)
    print(f"✅ Loaded .env from {_env_path}")
    print(f"   Found {len(_env_vars)} variables")
else:
    print("⚠️ .env file not found. NetworkX backend doesn't need credentials.")

# Add cvcdocdb package to path for development
_repo_root = Path.cwd()
if not (_repo_root / "cvcdocdb").exists():
    for parent in Path.cwd().parents:
        if (parent / "cvcdocdb").exists():
            _repo_root = parent
            break
# Add repo root to sys.path so local cvcdocdb loads before PyPI version
if str(_repo_root) not in sys.path:
    sys.path.insert(0, str(_repo_root))
    print(f"✅ Added {_repo_root} to sys.path")

from cvcdocdb.networkx_graph import NetworkXGraph
from cvcdocdb.rico_entities import (
    Thing,
    Agent, Person, CorporateBody, AgentName, Appellation,
    Date, Place, PlaceName, Event, Activity,
    RecordResource, Instantiation, Title,
    Group, Name,
)

def section(title):
    """Print a section header."""
    print(f"\n{'=' * 60}")
    print(f"  {title}")
    print(f"{'=' * 60}")

def sub_section(title):
    """Print a subsection header."""
    print(f"\n--- {title} ---")

print("✅ All imports successful.")
✅ Loaded .env from /Users/oriol/Desenvolupament/cvcdocdb/.env
   Found 5 variables
✅ Added /Users/oriol/Desenvolupament/cvcdocdb to sys.path
✅ All imports successful.

Step 1: Create in-memory graph with unique persistence path

The NetworkX backend stores data in memory. A pickle file is used for persistence across notebook restarts. Each run creates a unique file using a UUID, so previous runs never interfere.

[3]:
import os
import tempfile

# Generate a unique persistence file for this run
_run_id = uuid.uuid4().hex[:8]
_persistence_file = os.path.join(
    tempfile.gettempdir(),
    f"ric_o_networkx_{_run_id}.pkl"
)

print(f"📁 Persistence file: {_persistence_file}")
print(f"   (unique per run — safe to delete after)")

# Create the in-memory graph
graph = NetworkXGraph(persistence_path=_persistence_file)

print(f"\n✅ NetworkXGraph created.")
print(f"   Nodes in memory: {len(graph.get_node_ids())}")
print(f"   Edges in memory: {len(graph.get_edges())}")
print(f"   Persistence enabled: {_persistence_file is not None}")
📁 Persistence file: /var/folders/rm/5_s35r9j3198mxxpvlpfx8700000gn/T/ric_o_networkx_5343112b.pkl
   (unique per run — safe to delete after)

✅ NetworkXGraph created.
   Nodes in memory: 0
   Edges in memory: 0
   Persistence enabled: True

Step 2: Load real RiC-O data from the National Archives of France

The cvcdocdb include a loader that downloads and parses RiC-O RDF/XML data from the ICA-EGAD/RiC-O repository.

What gets loaded:

RDF Type

CVCDocDB Entity

Parent

rico:Record

Thing

— (strong node)

rico:Agent

Agent

Thing

rico:CorporateBody

CorporateBody

Agent

rico:Individual

Person

Agent

rico:AgentName

AgentName

Agent/Person

rico:RecordResource

RecordResource

Thing

rico:Instantiation

Instantiation

Thing/RecordResource

rico:Activity

Activity

Thing

Hierarchy example:

Thing (strong)
├── Agent (WeakNode)
│   ├── AgentName (WeakNode of Agent)
│   └── Instantiation (WeakNode of Agent)
└── RecordResource (WeakNode)
    └── Instantiation (WeakNode of RecordResource)

The loader downloads files from GitHub, parses the RDF/XML, and inserts entities into the graph with proper parent-child relationships.

[4]:
from cvcdocdb.exemples import load_ric_o_naf

section("Loading RiC-O data from National Archives of France")

# Load real RiC-O data
# limit: total entities to load (soft limit)
# max_agents: number of agent files to load
# max_records: number of record resource files to load
stats = load_ric_o_naf(
    graph,
    limit=50,
    max_agents=10,
    max_records=3,
)

print(f"\n📊 Load statistics:")
print(f"  Agents loaded:   {stats['agents']}")
print(f"  Records loaded:  {stats['records']}")
print(f"  Things created:  {stats['things']}")
print(f"  Child entities:  {stats.get('children', '?')}")
print(f"  Total entities:  {stats['total']}")

============================================================
  Loading RiC-O data from National Archives of France
============================================================

📊 Load statistics:
  Agents loaded:   10
  Records loaded:  27
  Things created:  40
  Child entities:  75
  Total entities:  152

Step 3: Initialize propagation properties

Scan the graph and initialize _propagate flags on edges between parent and child nodes. This enables cascade delete behavior.

In NetworkX, this is much faster than Neo4j since everything is in memory.

[5]:
# Check state before init (using the public get_node_attrs()/get_node_ids() API)
sub_section("Before init_propagation()")
attrs_before = [graph.get_node_attrs(nid) for nid in graph.get_node_ids()]
total_before = len(attrs_before)
weak_before = sum(1 for a in attrs_before if a.get('is_weak'))
done_before = sum(1 for a in attrs_before if a.get('_weak_init_done'))
print(f"  Total nodes:          {total_before}")
print(f"  Nodes with is_weak:   {weak_before}")
print(f"  Nodes with _weak_init_done: {done_before}")

# Run initialization
print("\nRunning init_propagation()...")
init_result = graph.init_propagation()
print(f"Result: {init_result}")

# Check state after init
sub_section("After init_propagation()")
attrs_after = [graph.get_node_attrs(nid) for nid in graph.get_node_ids()]
total_after = len(attrs_after)
weak_after = sum(1 for a in attrs_after if a.get('is_weak'))
done_after = sum(1 for a in attrs_after if a.get('_weak_init_done'))
print(f"  Total nodes:          {total_after}")
print(f"  Nodes with is_weak:   {weak_after}")
print(f"  Nodes with _weak_init_done: {done_after}")

# Count propagation edges
prop_count = sum(
    1 for u, v, rel_type in graph.get_edges()
    if (graph.get_edge_attrs(u, v, rel_type) or {}).get('_propagate') is True
)
print(f"  Edges with _propagate: {prop_count}")

--- Before init_propagation() ---
  Total nodes:          853
  Nodes with is_weak:   0
  Nodes with _weak_init_done: 0

Running init_propagation()...
Result: True

--- After init_propagation() ---
  Total nodes:          853
  Nodes with is_weak:   0
  Nodes with _weak_init_done: 0
  Edges with _propagate: 0

Step 4: Inspect the loaded graph structure

Query the graph to see what entities were loaded and how they are organized. We use the public get_node_ids()/get_node_attrs()/get_edges()/get_edge_attrs() accessors for inspection — the same public API used by the Neo4j-backed ric_o_demo.ipynb twin (there via Cypher query()).

[6]:
# Show all Things (strong nodes)
sub_section("Nodes by label")
from collections import Counter
label_counts = Counter(
    graph.get_node_attrs(nid).get('main_label', 'Node') for nid in graph.get_node_ids()
)
for label, count in label_counts.most_common():
    print(f"  {label}: {count}")

# Show relations by type
sub_section("Relations by type")
rel_counts = Counter()
for u, v, rel_type in graph.get_edges():
    edge_attrs = graph.get_edge_attrs(u, v, rel_type) or {}
    label = 'HAS_*' if edge_attrs.get('_propagate') else rel_type
    rel_counts[label] += 1
for rel_type, count in rel_counts.most_common():
    print(f"  {rel_type}: {count}")

--- Nodes by label ---
  Instantiation: 404
  RecordResource: 398
  AgentName: 48
  Record: 3

--- Relations by type ---
  IS_INSTANTIATION_OF: 398
  HAS_INSTANTIATION: 398
[7]:
# Show all Things (strong nodes)
sub_section("All Things (strong nodes)")
things = []
for nid in graph.get_node_ids():
    attrs = graph.get_node_attrs(nid)
    if attrs.get('main_label') == 'Thing':
        identifier = attrs.get('identifier', '?')
        name = attrs.get('title', identifier)
        things.append((nid, identifier, name))
things.sort(key=lambda x: x[1])
for nid, identifier, name in things:
    print(f"  {identifier}: {name}")

--- All Things (strong nodes) ---

Step 5: Get subdocuments

Use get_subdocuments() to retrieve all weak descendants of each Thing. This traverses only edges with _propagate=True using BFS.

[8]:
# Show subdocuments for each Thing
for nid, identifier, name in things:
    thing_node = Thing(pk={"identifier": identifier})
    subdocs = graph.get_subdocuments(thing_node)

    sub_section(f"Subdocuments of Thing({identifier})")
    print(f"  Found {len(subdocs)} subdocuments:")

    # Group by label
    by_label = {}
    for sd in subdocs:
        label = sd['label']
        if label not in by_label:
            by_label[label] = []
        by_label[label].append(sd)

    for label in sorted(by_label.keys()):
        items = by_label[label]
        for item in items[:5]:  # Show max 5 per label
            name = item['pk'].get('fullName', item['pk'].get('name', item['pk'].get('agentId', '?')))
            print(f"    - {label}: {name}")
        if len(items) > 5:
            print(f"    ... and {len(items) - 5} more")

--- Subdocuments of Thing(record-000005) ---
  Found 13 subdocuments:
    - Agent: agent-agent-000005
    - AgentName: France. Ministère de la Culture et de la Communication (1959-....)
    - AgentName: Ministère de la Culture
    - AgentName: Ministère de la Culture et de la Communication
    - AgentName: Ministère de la Culture, de la Communication, des Grands Travaux et du Bicentenaire
    - AgentName: Ministère de la Culture et de la Communication
    ... and 7 more

--- Subdocuments of Thing(record-000016) ---
  Found 11 subdocuments:
    - Agent: agent-agent-000016
    - AgentName: France. Ministère de l'Éducation nationale (1828-....)
    - AgentName: Ministère de l'Éducation nationale, de la Jeunesse et de la Vie associative
    - AgentName: Ministère de l'Éducation nationale
    - AgentName: Ministère de l'Éducation nationale, de l'Enseignement supérieur et de la Recherche
    - AgentName: Ministère de la Jeunesse, de l'Éducation nationale et de la Recherche
    ... and 5 more

--- Subdocuments of Thing(record-000051) ---
  Found 4 subdocuments:
    - Agent: agent-agent-000051
    - AgentName: France. Ministère des affaires culturelles. Direction de l'architecture (1959-1978)
    - AgentName: France. Ministère de la culture et de l'environnement. Direction de l'architecture
    - AgentName: France. Secrétariat d'État à la culture. Direction de l'architecture

--- Subdocuments of Thing(record-003500) ---
  Found 0 subdocuments:

--- Subdocuments of Thing(record-003529) ---
  Found 8 subdocuments:
    - Agent: agent-agent-003529
    - AgentName: France. Direction des bibliothèques et de la lecture publique. Bureau de la gestion et du contrôle financiers (1945-1975)
    - AgentName: Bureau de la gestion et du contrôle financier
    - AgentName: Deuxième bureau (Gestion et contrôle financier, Affaires générales-Documentation-Matériel)
    - AgentName: Bureau de la gestion et du contrôle financiers
    - AgentName: Bureau de la comptabilité
    ... and 2 more

--- Subdocuments of Thing(record-003530) ---
  Found 4 subdocuments:
    - Agent: agent-agent-003530
    - AgentName: France. Direction des bibliothèques et de la lecture publique. Division des services administratifs. Bureau des affaires générales (1965-1975)
    - AgentName: DB 3
    - AgentName: BL 3

--- Subdocuments of Thing(record-003531) ---
  Found 7 subdocuments:
    - Agent: agent-agent-003531
    - AgentName: France. Direction des bibliothèques et de la lecture publique. Bureau du personnel (1945-1975)
    - AgentName: Bureau du personnel
    - AgentName: Bureau du personnel et des affaires générales
    - AgentName: Bureau du personnel
    - AgentName: BL 1
    ... and 1 more

--- Subdocuments of Thing(record-003532) ---
  Found 4 subdocuments:
    - Agent: agent-agent-003532
    - AgentName: France. Direction des bibliothèques et de la lecture publique. Division des affaires administratives (1965-1975)
    - AgentName: Services administratifs
    - AgentName: Division des affaires administratives

--- Subdocuments of Thing(record-003549) ---
  Found 2 subdocuments:
    - Agent: agent-agent-003549
    - AgentName: France. Direction des bibliothèques et de la lecture publique. Services techniques. Section des bibliothèques d'étude et de recherche (1971-1975)

--- Subdocuments of Thing(record-003550) ---
  Found 2 subdocuments:
    - Agent: agent-agent-003550
    - AgentName: France. Direction des bibliothèques et de la lecture publique. Services techniques. Section des affaires communes (1971-1975)

--- Subdocuments of Thing(record-003551) ---
  Found 3 subdocuments:
    - Agent: agent-agent-003551
    - AgentName: France. Direction des bibliothèques et de la lecture publique. Services techniques. Section de la lecture publique (1968-1975)
    - AgentName: Service de la lecture publique

--- Subdocuments of Thing(record-007375) ---
  Found 0 subdocuments:

--- Subdocuments of Thing(record-009555) ---
  Found 0 subdocuments:

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-003500-d_1) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-003500-d_2) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-003500-d_3) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-003500-d_4) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-007375-d_1) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-007375-d_2) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_1) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_10) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_11) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_12) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_13) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_14) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_15) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_16) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_17) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_18) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_2) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_3) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_4) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_5) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_6) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_7) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_8) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-009555-d_9) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-top-003500) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-top-007375) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

--- Subdocuments of Thing(thing-rr-recordResource-recordResource-top-009555) ---
  Found 2 subdocuments:
    - Instantiation: ?
    - RecordResource: ?

Step 6: Query the hierarchy paths

Visualize the full hierarchy for a sample Thing by walking the graph via the public get_edges()/get_edge_attrs()/get_node_attrs() API.

[9]:
def _walk_hierarchy(graph, start_id, max_depth=6):
    """Walk the graph from start_id and return all reachable nodes."""
    all_edges = graph.get_edges()
    paths = []
    queue = [(start_id, [start_id], [])]

    while queue:
        current, path_nodes, path_edges = queue.pop(0)

        if len(path_nodes) > 1:
            paths.append((path_nodes, path_edges))

        if len(path_nodes) >= max_depth:
            continue

        for u, v, rel_type in all_edges:
            if u != current:
                continue
            edge_attrs = graph.get_edge_attrs(u, v, rel_type) or {}
            display_rel = 'parent' if edge_attrs.get('_propagate') else rel_type
            paths.append((path_nodes + [v], path_edges + [(u, v, display_rel)]))
            if len(path_nodes) < max_depth - 1:
                queue.append((v, path_nodes + [v], path_edges + [(u, v, display_rel)]))

    return paths

# Walk hierarchy for first Thing
if things:
    first_nid, first_id, first_name = things[0]

    sub_section(f"Hierarchy: Thing({first_id})")

    paths = _walk_hierarchy(graph, first_nid)
    for path_nodes, path_edges in paths:
        parts = []
        for node_id in path_nodes:
            attrs = graph.get_node_attrs(node_id) or {}
            label = attrs.get('main_label', 'Node')
            name = attrs.get('fullName', attrs.get('name', attrs.get('agentId', attrs.get('recordResourceId', str(node_id)))))
            parts.append(f"{label}({name})")
        for src, dst, rel_type in path_edges:
            parts.append(f"-[{rel_type}]->")
        print(f"  {' → '.join(parts)}")

Step 7: Inspect specific entities

Look at the properties of loaded entities to understand the data model.

[10]:
# Show sample Agent entities
sub_section("Sample Agent entities")
agents = []
for nid in graph.get_node_ids():
    attrs = graph.get_node_attrs(nid)
    if attrs.get('main_label') in ('Agent', 'CorporateBody', 'Person', 'Family'):
        agents.append((
            nid, attrs.get('agentId', '?'), attrs.get('label', 'No label'),
            attrs.get('beginningDate', ''), attrs.get('endDate', ''),
        ))
agents.sort(key=lambda x: x[1])
for nid, agent_id, label, birth, death in agents[:5]:
    print(f"  {agent_id}: {label}")
    print(f"    Born: {birth or 'N/A'}, Died: {death or 'N/A'}")

# Show sample RecordResource entities
sub_section("Sample RecordResource entities")
record_resources = []
for nid in graph.get_node_ids():
    attrs = graph.get_node_attrs(nid)
    if attrs.get('main_label') == 'RecordResource':
        record_resources.append((
            nid, attrs.get('recordResourceId', '?'), attrs.get('title', ''),
            attrs.get('beginningDate', ''), attrs.get('endDate', ''),
        ))
record_resources.sort(key=lambda x: x[1])
for nid, rr_id, title, from_date, to_date in record_resources[:5]:
    print(f"  {rr_id}: {title or 'No title'}")
    print(f"    Date range: {from_date or 'N/A'} — {to_date or 'N/A'}")

--- Sample Agent entities ---

--- Sample RecordResource entities ---
  ?: No title
    Date range: N/A — N/A
  ?: No title
    Date range: N/A — N/A
  ?: No title
    Date range: N/A — N/A
  ?: No title
    Date range: N/A — N/A
  ?: No title
    Date range: N/A — N/A

Step 8: Summary statistics

Show the final state of the RiC-O graph.

[11]:
section("Final summary")

# Nodes by label
sub_section("Nodes by label")
label_counts = Counter(
    graph.get_node_attrs(nid).get('main_label', 'Node') for nid in graph.get_node_ids()
)
for label, count in label_counts.most_common():
    print(f"  {label}: {count}")

# Propagation edges
sub_section("Propagation edges by type")
prop_rel_counts = Counter()
for u, v, rel_type in graph.get_edges():
    edge_attrs = graph.get_edge_attrs(u, v, rel_type) or {}
    if edge_attrs.get('_propagate') is True:
        prop_rel_counts[rel_type] += 1
for rel_type, count in prop_rel_counts.most_common():
    print(f"  {rel_type}: {count}")

# All Things
sub_section("All Things")
for nid, identifier, name in things:
    print(f"  {identifier}: {name}")

# Subdocument counts
sub_section("Subdocuments per Thing")
for nid, identifier, name in things:
    thing_node = Thing(pk={"identifier": identifier})
    subdocs = graph.get_subdocuments(thing_node)
    print(f"  {identifier}: {len(subdocs)} subdocuments")

# Persistence file info
sub_section("Persistence file")
print(f"  Path: {_persistence_file}")
if os.path.exists(_persistence_file):
    size = os.path.getsize(_persistence_file)
    print(f"  Size: {size:,} bytes ({size / 1024:.1f} KB)")
else:
    print("  File not found (data only in memory)")

print("\n" + "=" * 60)
print("  ✅ RiC-O NetworkX demo completed successfully!")
print("=" * 60)

============================================================
  Final summary
============================================================

--- Nodes by label ---
  Instantiation: 404
  RecordResource: 398
  AgentName: 48
  Record: 3

--- Propagation edges by type ---

--- All Things ---

--- Subdocuments per Thing ---

--- Persistence file ---
  Path: /var/folders/rm/5_s35r9j3198mxxpvlpfx8700000gn/T/ric_o_networkx_5343112b.pkl
  Size: 132,590 bytes (129.5 KB)

============================================================
  ✅ RiC-O NetworkX demo completed successfully!
============================================================

Cleanup

Close the graph and optionally delete the persistence file.

[12]:
graph.close()
print("✅ NetworkXGraph closed.")

# Clean up persistence file
if os.path.exists(_persistence_file):
    os.remove(_persistence_file)
    print(f"✅ Persistence file deleted: {_persistence_file}")
else:
    print(f"ℹ️  No persistence file to clean up")
✅ NetworkXGraph closed.
✅ Persistence file deleted: /var/folders/rm/5_s35r9j3198mxxpvlpfx8700000gn/T/ric_o_networkx_e073bd7a.pkl