Skip to content

State, URI, and source-management demos

Overview

These scripts highlight the ways dynlib exposes the compiled artifacts, externalizes simulation state, and resolves models from different URI schemes. Use them when you want to checkpoint a run, save or reload presets, export generated Python source for inspection, or understand how setup() picks a model from inline text, files, or config-based tags.

Example scripts

Snapshots & export/import

# examples/snapshot_demo.py
"""
Demonstration of snapshot export/import functionality in dynlib.

This example shows how to:
1. Export current simulation state to disk (including workspaces)
2. Export named snapshots to disk
3. Import snapshots from disk
4. Inspect snapshot metadata
5. Demonstrate workspace persistence across sessions
"""

from pathlib import Path
import tempfile
import tomllib
from dynlib import setup


def create_model():
    """Create a simple exponential decay model for demonstration."""
    model_uri = """
inline:
[model]
type = "ode"
name = "Demo Exponential Decay"
dtype = "float64"

[states]
x = 1.0

[params]
decay_rate = 0.1

[equations.rhs]
x = "-decay_rate * x"

[sim]
stepper = "euler"
dt = 0.01
t0 = 0.0
t_end = 10.0
record = true
"""

    return setup(model_uri, jit=True)
def main():
    print("=== Dynlib Snapshot Export/Import Demo ===\n")

    # Create simulation
    sim = create_model()
    print("✓ Created simulation with exponential decay model")

    # Run to some intermediate state
    sim.run(T=2.0)
    print(f"✓ Ran simulation to t=2.0, current state: x={sim.state_vector()[0]:.4f}")

    # Create an in-memory snapshot
    sim.create_snapshot("checkpoint_1", "After running to t=2.0")
    print("✓ Created in-memory snapshot 'checkpoint_1'")

    # Continue running
    sim.run(T=5.0, resume=True)
    print(f"✓ Continued to t=5.0, current state: x={sim.state_vector()[0]:.4f}")

    with tempfile.TemporaryDirectory() as tmp_dir:
        tmp_path = Path(tmp_dir)

        # Export current state
        current_snap_path = tmp_path / "current_state.npz"
        sim.export_snapshot(current_snap_path, source="current")
        print(f"✓ Exported current state to {current_snap_path.name}")

        # Export the named snapshot
        named_snap_path = tmp_path / "checkpoint_1.npz"
        sim.export_snapshot(named_snap_path, source="snapshot", name="checkpoint_1")
        print(f"✓ Exported named snapshot to {named_snap_path.name}")

        # Inspect the snapshots without loading them
        print("\n--- Snapshot Inspection ---")
        for path in [current_snap_path, named_snap_path]:
            meta = sim.inspect_snapshot(path)
            print(f"{path.name}:")
            print(f"  Schema: {meta['schema']}")
            print(f"  Name: {meta['name']}")
            print(f"  Time: {meta['t_curr']:.3f}")
            print(f"  State: x={meta['state_names'][0]}")
            print(f"  Workspace signature: {meta['pins']['workspace_sig']}")
            print(f"  Created: {meta['created_at']}")
            print()

        # Continue simulation further
        sim.run(T=8.0, resume=True)
        current_x = sim.state_vector()[0]
        print(f"✓ Continued to t=8.0, current state: x={current_x:.4f}")

        # Import the earlier snapshot
        print("\n--- Importing Snapshot ---")
        sim.import_snapshot(named_snap_path)
        restored_x = sim.state_vector()[0]
        restored_t = sim.session_state_summary()["t"]
        print(f"✓ Imported snapshot from t=2.0")
        print(f"  Restored state: t={restored_t:.3f}, x={restored_x:.4f}")

        # Verify results are cleared
        try:
            results = sim.raw_results()
            print("✗ ERROR: Results should be cleared after import!")
        except RuntimeError as e:
            print(f"✓ Results correctly cleared: {e}")

        # Can continue from restored state
        sim.run(T=3.0, resume=True)
        final_x = sim.state_vector()[0]
        print(f"✓ Resumed from restored state to t=3.0, x={final_x:.4f}")

        # Demonstrate workspace persistence
        print("\n--- Workspace Persistence ---")
        # Create a new simulation to show workspace restoration
        sim2 = create_model()
        sim2.run(T=1.0)  # Run to different state

        # Export current state with workspace
        ws_snap_path = tmp_path / "workspace_demo.npz"
        sim.export_snapshot(ws_snap_path, source="current")
        print("✓ Exported simulation state with workspace")

        # Import into new simulation
        sim2.import_snapshot(ws_snap_path)
        print("✓ Imported state with workspace into new simulation")

        # Verify state was restored
        restored_state = sim2.session_state_summary()
        print(f"  Restored time: {restored_state['t']:.3f}")
        print(f"  Restored state: x={sim2.state_vector()[0]:.4f}")
        print(f"  Can resume: {restored_state['can_resume']}")

    print("\n=== Demo Complete ===")


if __name__ == "__main__":
    main()
Builds a simple exponential-decay model, runs to a few time points, and then demonstrates sim.create_snapshot, sim.export_snapshot, and sim.import_snapshot. The script prints snapshot metadata, shows how workspaces survive across exports, and verifies that sim.results() is cleared after restoring a snapshot so you can safely resume from arbitrary points.

Exporting the compiled sources

# examples/export_sources_demo.py
"""
Demonstration of compiled model source code export functionality.

This example shows how to:
1. Build a compiled model using setup()
2. Export the generated Python source code for inspection
3. Verify the compilation is correct
"""

from pathlib import Path
import tempfile
from dynlib import setup

def main():
    # Use an existing test model
    model_path = Path(__file__).parent.parent / "tests" / "data" / "models" / "decay.toml"

    # Setup simulation with JIT compilation
    print(f"Building model from: {model_path.name}")
    sim = setup(str(model_path), stepper="euler", jit=True, disk_cache=False)

    # Check if source code is available
    print(f"\nSource code availability:")
    print(f"  RHS source: {'✓' if sim.model.rhs_source else '✗'}")
    print(f"  Events pre source: {'✓' if sim.model.events_pre_source else '✗'}")
    print(f"  Events post source: {'✓' if sim.model.events_post_source else '✗'}")
    print(f"  Stepper source: {'✓' if sim.model.stepper_source else '✗'}")

    # Export sources to a temporary directory
    with tempfile.TemporaryDirectory() as tmpdir:
        export_dir = Path(tmpdir) / "compiled_model"
        print(f"\nExporting sources to: {export_dir}")

        exported_files = sim.model.export_sources(export_dir)

        print(f"\nExported files:")
        for component, filepath in exported_files.items():
            size = filepath.stat().st_size if filepath.exists() else 0
            print(f"  {component}: {filepath.name} ({size} bytes)")

        # Display the RHS source code
        if "rhs" in exported_files:
            print(f"\n{'='*60}")
            print("RHS Function Source Code:")
            print('='*60)
            rhs_content = exported_files["rhs"].read_text()
            print(rhs_content)
            print('='*60)

        # Display the stepper source code if available
        if "stepper" in exported_files:
            print(f"\n{'='*60}")
            print("Stepper Function Source Code:")
            print('='*60)
            stepper_content = exported_files["stepper"].read_text()
            print(stepper_content)
            print('='*60)

    print("\n✓ Export demo completed successfully!")

if __name__ == "__main__":
    main()
Builds a JIT-enabled simulation from tests/data/models/decay.toml, toggles disk_cache=False, and calls sim.model.export_sources() into a temporary directory. The demo checks that the RHS, events, and stepper sources exist, prints their sizes, and shows the contents so you can review what dynlib emitted for debugging or compliance.

Preset workflows

#!/usr/bin/env python3
"""
Demo of the presets feature in dynlib v2.

Shows:
- Inline presets defined in the model DSL
- Listing and applying presets
- Loading presets from external files
- Saving presets to files
- Capturing runtime presets with add_preset
- Round-trip preservation
"""

import tempfile
from pathlib import Path
import tomllib


from dynlib.dsl.parser import parse_model_v2
from dynlib.dsl.spec import build_spec
from dynlib.compiler.build import build
from dynlib.runtime.sim import Sim


# Model with inline presets
MODEL_TOML = """
[model]
type = "ode"
name = "Izhikevich Neuron (Simple)"

[states]
v = -65.0
u = -13.0

[params]
a = 0.02
b = 0.2
c = -65.0
d = 8.0
I = 10.0

[equations.rhs]
v = "0.04*v*v + 5*v + 140 - u + I"
u = "a*(b*v - u)"

[events.spike]
phase = "post"
cond = "v >= 30"
log = ["t"]

[events.spike.action]
v = "c"
u = "u + d"

[sim]
t0 = 0.0
t_end = 200.0
dt = 0.25
stepper = "euler"
record = true

# Inline presets for different neuron behaviors
[presets.regular_spiking.params]
a = 0.02
b = 0.2
c = -65.0
d = 8.0
I = 10.0

[presets.fast_spiking.params]
a = 0.1
b = 0.2
c = -65.0
d = 2.0
I = 10.0

[presets.bursting.params]
a = 0.02
b = 0.2
c = -50.0
d = 2.0
I = 15.0

[presets.bursting.states]
v = -70.0
u = -14.0
"""


def main():
    # Load model
    print("=== Loading model ===")
    doc = tomllib.loads(MODEL_TOML)
    spec = build_spec(parse_model_v2(doc))
    model = build(spec, stepper=spec.sim.stepper, jit=True)
    sim = Sim(model)

    # List inline presets
    print("\n=== Inline presets ===")
    presets = sim.list_presets()
    print(f"Available presets: {presets}")

    # Apply a preset and run
    print("\n=== Applying 'regular_spiking' preset ===")
    sim.apply_preset("regular_spiking")
    sim.run(T=100.0)
    results = sim.results()
    event_counts = results.event.summary()
    n_spikes = event_counts.get('spike', 0)
    print(f"Regular spiking: {n_spikes} spikes in 100ms")

    # Apply different preset
    print("\n=== Applying 'fast_spiking' preset ===")
    sim.reset()  # Reset to initial state
    sim.apply_preset("fast_spiking")
    sim.run(T=100.0)
    results = sim.results()
    event_counts = results.event.summary()
    n_spikes = event_counts.get('spike', 0)
    print(f"Fast spiking: {n_spikes} spikes in 100ms")

    # Save presets to file
    with tempfile.TemporaryDirectory() as tmpdir:
        preset_file = Path(tmpdir) / "neuron_presets.toml"

        print(f"\n=== Saving presets to {preset_file.name} ===")
        sim.reset()
        sim.apply_preset("regular_spiking")
        sim.add_preset("regular_spiking", overwrite=True)
        sim.save_preset("regular_spiking", preset_file)

        sim.apply_preset("fast_spiking")
        sim.add_preset("fast_spiking", overwrite=True)
        sim.save_preset("fast_spiking", preset_file)

        # Check file contents
        with open(preset_file, "rb") as f:
            doc = tomllib.load(f)
        print(f"File contains: {list(doc['presets'].keys())}")

        # Create a new sim and load from file
        print("\n=== Loading presets from file ===")
        sim2 = Sim(model)
        count = sim2.load_preset("*", preset_file, on_conflict="replace") # warning should be raised
        print(f"Loaded {count} presets")
        print(f"Available: {sim2.list_presets()}")

        # Verify they work
        sim2.apply_preset("regular_spiking")
        sim2.run(T=100.0)
        results2 = sim2.results()
        event_counts2 = results2.event.summary()
        n_spikes2 = event_counts2.get('spike', 0)
        print(f"Loaded preset produces {n_spikes2} spikes (should match original)")

    # Demonstrate glob patterns
    print("\n=== Glob pattern matching ===")
    print(f"All presets: {sim.list_presets('*')}")
    print(f"Presets starting with 'fast': {sim.list_presets('fast_*')}")
    print(f"Presets ending with 'spiking': {sim.list_presets('*_spiking')}")

    print("\n=== Demo complete ===")


if __name__ == "__main__":
    main()
Builds an inline Izhikevich neuron and then lists/apply the inline regular_spiking, fast_spiking, and bursting presets. It saves the presets to a temporary file, loads them back into a second Sim, and proves that the loaded presets reproduce the spike counts while showcasing glob-style preset matching.

URI and path resolution

#!/usr/bin/env python3
"""
This script demonstrates the new path resolution and URI system,
showing various ways to load and run models.
"""
from dynlib import setup


def demo_inline_model():
    """Demo 1: Load a model from inline TOML."""
    print("=" * 60)
    print("Demo 1: Inline Model Definition")
    print("=" * 60)

    inline_model = """
    inline:
    [model]
    type = "ode"

    [states]
    x = 1.0

    [params]
    a = 1.0

    [equations.rhs]
    x = "-a * x"

    [sim]
    t0 = 0.0
    t_end = 2.0
    dt = 0.1
    stepper = "euler"
    """

    uri = inline_model
    sim = setup(uri, stepper="euler", jit=False)

    print(f"Model kind: {sim.model.spec.kind}")
    print(f"States: {sim.model.spec.states}")
    print(f"Stepper: {sim.model.stepper_name}")

    # Run simulation
    sim.run(T=2.0)
    results = sim.results()

    print(f"Simulation ran {len(results)} steps")
    print(f"Initial x: {results['x'][0]:.6f}")
    print(f"Final x: {results['x'][-1]:.6f}")
    print()


def demo_file_loading():
    """Demo 2: Load a model from existing test file."""
    print("=" * 60)
    print("Demo 2: File-based Model Loading")
    print("=" * 60)

    from pathlib import Path
    data_dir = Path(__file__).parent.parent / "tests" / "data" / "models"
    model_path = data_dir / "decay.toml"

    if not model_path.exists():
        print(f"Test data not found: {model_path}")
        return

    # Load using absolute path
    sim = setup(str(model_path), stepper="euler", jit=False)

    print(f"Loaded from: {model_path}")
    print(f"Model kind: {sim.model.spec.kind}")
    print(f"States: {sim.model.spec.states}")
    print(f"Default stepper: {sim.model.stepper_name}")
    print()


def demo_uri_schemes():
    """Demo 3: Show different URI schemes."""
    print("=" * 60)
    print("Demo 3: URI Scheme Examples")
    print("=" * 60)

    examples = [
        ("inline: [model]\\ntype='ode'", "Inline TOML definition"),
        ("/abs/path/model.toml", "Absolute file path"),
        ("relative/model.toml", "Relative path from cwd"),
        ("model", "Extensionless (tries model.toml)"),
        ("proj://model.toml", "TAG resolution from config"),
        ("model.toml#mod=fast", "Fragment selector for mods"),
        ("TAG://path/to/model.toml#mod=variant", "Combined TAG + fragment"),
    ]

    print("Supported URI schemes:")
    for uri, description in examples:
        print(f"  {uri:40s} - {description}")
    print()

    print("Configuration:")
    print("  Config file: ~/.config/dynlib/config.toml (Linux)")
    print("  Or: ~/Library/Application Support/dynlib/config.toml (macOS)")
    print("  Or: %APPDATA%\\dynlib\\config.toml (Windows)")
    print()
    print("  Environment overrides:")
    print("    DYNLIB_CONFIG=/custom/config.toml")
    print("    DYN_MODEL_PATH=proj=/extra/path,/another")
    print()


def main():
    """Run all demos."""
    print("\n")
    print("╔" + "═" * 58 + "╗")
    print("║" + " " * 10 + "DYNLIB URI System Demo" + " " * 26 + "║")
    print("╚" + "═" * 58 + "╝")
    print()

    demo_inline_model()
    demo_file_loading()
    demo_uri_schemes()

    print("=" * 60)
    print("Demo Complete!")
    print("=" * 60)


if __name__ == "__main__":
    main()
Covers the URI resolver itself by: - running inline TOML via the inline: scheme, - loading from absolute and relative filesystem paths, - showing how to use extensionless names, fragments (#mod=), and proj:///TAG:// references, - and reminding you where config files live (~/.config/dynlib/config.toml, %APPDATA% on Windows, etc.). The script prints what each invocation loaded to make debug easier when a model path is ambiguous.