Skip to content

Visualization Guide

A feature showcase for gds-viz, demonstrating all 6 view types, 5 built-in Mermaid themes, and cross-DSL visualization. Every diagram renders in GitHub, GitLab, VS Code, Obsidian, and mermaid.live.

The 6 GDS Views

Every GDS model can be visualized from 6 complementary perspectives. Each view answers a different question about the system's structure.

# View Input Question Answered
1 Structural SystemIR What is the compiled block topology?
2 Canonical CanonicalGDS What is the formal h = f . g decomposition?
3 Architecture by Role GDSSpec How are blocks organized by GDS role?
4 Architecture by Domain GDSSpec How are blocks organized by domain ownership?
5 Parameter Influence GDSSpec If I change a parameter, what is affected?
6 Traceability GDSSpec What could cause this state variable to change?

View 1: Structural

The compiled block graph from SystemIR. Shows composition topology with role-based shapes and wiring types.

Shape conventions:

  • Stadium ([...]) = BoundaryAction (exogenous input, no forward_in)
  • Double-bracket [[...]] = terminal Mechanism (state sink, no forward_out)
  • Rectangle [...] = Policy or other block with both inputs and outputs

Arrow conventions:

  • Solid arrow --> = covariant forward flow
  • Dashed arrow -.-> = temporal loop (cross-timestep)
  • Thick arrow ==> = feedback (within-timestep, contravariant)

API: system_to_mermaid(system)

View 2: Canonical GDS

The mathematical decomposition: X_t --> U --> g --> f --> X_{t+1}. Shows the abstract dynamical system with state (X), input (U), policy (g), mechanism (f), and parameter space (Theta).

API: canonical_to_mermaid(canonical)

View 3: Architecture by Role

Blocks grouped by GDS role: Boundary (U), Policy (g), Mechanism (f). Entity cylinders show which state variables each mechanism writes.

API: spec_to_mermaid(spec)

View 4: Architecture by Domain

Blocks grouped by domain tag. Shows organizational ownership of blocks. Blocks without the tag go into "Ungrouped".

API: spec_to_mermaid(spec, group_by="domain")

View 5: Parameter Influence

Theta --> blocks --> entities causal map. Answers: "if I change parameter X, which state variables are affected?" Shows parameter hexagons, the blocks they feed, and the entities those blocks transitively update.

API: params_to_mermaid(spec)

View 6: Traceability

Backwards trace from one state variable. Answers: "what blocks and parameters could cause this variable to change?" Direct mechanisms get thick arrows, transitive dependencies get normal arrows, and parameter connections get dashed arrows.

API: trace_to_mermaid(spec, entity, variable)

Generating All Views

from gds.canonical import project_canonical
from gds_viz import (
    canonical_to_mermaid,
    params_to_mermaid,
    spec_to_mermaid,
    system_to_mermaid,
    trace_to_mermaid,
)

# From any model's build functions:
spec = build_spec()
system = build_system()
canonical = project_canonical(spec)

views = {
    "structural": system_to_mermaid(system),
    "canonical": canonical_to_mermaid(canonical),
    "architecture_by_role": spec_to_mermaid(spec),
    "architecture_by_domain": spec_to_mermaid(spec, group_by="domain"),
    "parameter_influence": params_to_mermaid(spec),
    "traceability": trace_to_mermaid(spec, "Susceptible", "count"),
}

Theme Customization

Every gds-viz view function accepts a theme= parameter. There are 5 built-in Mermaid themes that adjust node fills, strokes, text colors, and subgraph backgrounds.

Theme Best for
neutral Light backgrounds (GitHub, docs) -- default
default Mermaid's blue-toned Material style
dark Dark-mode renderers
forest Green-tinted, earthy
base Minimal chrome, very light

Usage

from gds_viz import system_to_mermaid

# Apply any theme to any view
mermaid_str = system_to_mermaid(system, theme="dark")

All Views Support Themes

Themes work with every view function:

from gds_viz import (
    system_to_mermaid,
    canonical_to_mermaid,
    spec_to_mermaid,
    params_to_mermaid,
    trace_to_mermaid,
)

system_to_mermaid(system, theme="forest")
canonical_to_mermaid(canonical, theme="dark")
spec_to_mermaid(spec, theme="base")
params_to_mermaid(spec, theme="default")
trace_to_mermaid(spec, "Entity", "variable", theme="neutral")

Neutral vs Dark Comparison

The two most common choices:

  • Neutral (default): muted gray canvas with saturated node fills. Best for light-background rendering (GitHub, VS Code light mode, documentation sites).
  • Dark: dark canvas with saturated fills and light text. Optimized for dark-mode renderers.

Cross-DSL Views

The gds-viz API is DSL-neutral -- it operates on GDSSpec and SystemIR, which every compilation path produces. Regardless of how a model is built (raw GDS blocks, stockflow DSL, control DSL, or games DSL), the same view functions work unchanged.

Example: Hand-Built vs DSL-Compiled

# Hand-built model (SIR Epidemic)
from sir_epidemic.model import build_spec, build_system
sir_spec = build_spec()
sir_system = build_system()
sir_structural = system_to_mermaid(sir_system)

# DSL-compiled model (Double Integrator via gds-control)
from double_integrator.model import build_spec, build_system
di_spec = build_spec()
di_system = build_system()
di_structural = system_to_mermaid(di_system)

# Same API, same function, different models -- works identically

Both models decompose into the same h = f . g structure, but with different dimensionalities. The SIR model has parameters (Theta); the double integrator may not. The visualization layer does not care about the construction path -- it only sees the compiled IR.

Supported DSL Sources

Source Path to GDSSpec Path to SystemIR
Raw GDS Manual build_spec() compile_system(name, root)
gds-stockflow stockflow.dsl.compile.compile_model() stockflow.dsl.compile.compile_to_system()
gds-control gds_control.dsl.compile.compile_model() gds_control.dsl.compile.compile_to_system()
gds-games ogs.dsl.spec_bridge.compile_pattern_to_spec() via PatternIR.to_system_ir()

API Quick Reference

All functions live in gds_viz and return Mermaid strings.

Function Input View
system_to_mermaid(system) SystemIR Structural
canonical_to_mermaid(canonical) CanonicalGDS Canonical
spec_to_mermaid(spec) GDSSpec By role
spec_to_mermaid(spec, group_by=...) GDSSpec By domain
params_to_mermaid(spec) GDSSpec Parameters
trace_to_mermaid(spec, ent, var) GDSSpec Traceability

All accept an optional theme= parameter: "neutral", "default", "dark", "forest", "base".

Usage Pattern

from gds_viz import system_to_mermaid
from my_model import build_system

system = build_system()
mermaid_str = system_to_mermaid(system, theme="dark")
# Paste into GitHub markdown, mermaid.live, or mo.mermaid()

Interactive Notebook

Source code for packages/gds-examples/notebooks/visualization.py

Tip: paste this code into an empty cell, and the marimo editor will create cells for you

"""GDS Visualization Guide — Interactive Marimo Notebook.

Explore all 6 gds-viz view types, 5 Mermaid themes, and cross-DSL
visualization using interactive controls. Every diagram renders live
as you change selections.

Run interactively:
    uv run marimo edit notebooks/visualization.py

Run as read-only app:
    uv run marimo run notebooks/visualization.py
"""
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "gds-examples",
#     "marimo>=0.20.0",
# ]
# ///

import marimo

__generated_with = "0.20.2"
app = marimo.App(width="medium", app_title="GDS Visualization Guide")


# ── Setup ────────────────────────────────────────────────────


@app.cell
def imports():
    import marimo as mo

    return (mo,)


@app.cell
def header(mo):
    mo.md(
        """
        # GDS Visualization Guide

        The `gds-viz` package provides **6 complementary views** of any GDS
        model. Each view answers a different question about the system's
        structure, from compiled topology to parameter traceability.

        All views produce **Mermaid** diagrams that render in GitHub, GitLab,
        VS Code, Obsidian, and here in marimo via `mo.mermaid()`.

        This notebook is organized into three sections:

        1. **All 6 Views** — explore every view type on the SIR Epidemic model
        2. **Theme Customization** — see how the 5 built-in themes change the palette
        3. **Cross-DSL Views** — same API works on hand-built and DSL-compiled models
        """
    )
    return ()


# ── Section 1: Build the SIR model ──────────────────────────


@app.cell
def build_sir():
    import sys
    from pathlib import Path

    # Add stockflow/ and control/ to path for model imports
    _examples_root = Path(__file__).resolve().parent.parent
    for _subdir in ("stockflow", "control"):
        _path = str(_examples_root / _subdir)
        if _path not in sys.path:
            sys.path.insert(0, _path)

    from sir_epidemic.model import build_spec as _sir_build_spec
    from sir_epidemic.model import build_system as _sir_build_system

    from gds.canonical import project_canonical as _sir_project_canonical

    sir_spec = _sir_build_spec()
    sir_system = _sir_build_system()
    sir_canonical = _sir_project_canonical(sir_spec)
    return sir_canonical, sir_spec, sir_system


# ── Section 2: All 6 Views ──────────────────────────────────


@app.cell
def section_all_views_header(mo):
    mo.md(
        """
        ---

        ## Section 1: The 6 GDS Views

        Select a view from the dropdown to see it rendered live.
        Each view uses a different `gds-viz` function and shows a
        different perspective on the same SIR Epidemic model.
        """
    )
    return ()


@app.cell
def view_selector(mo):
    view_dropdown = mo.ui.dropdown(
        options={
            "Structural (SystemIR)": "structural",
            "Canonical GDS (h = f . g)": "canonical",
            "Architecture by Role": "role",
            "Architecture by Domain": "domain",
            "Parameter Influence": "params",
            "Traceability": "trace",
        },
        value="Structural (SystemIR)",
        label="Select view",
    )
    return (view_dropdown,)


@app.cell
def render_selected_view(mo, view_dropdown, sir_spec, sir_system, sir_canonical):
    from gds_viz import (
        canonical_to_mermaid,
        params_to_mermaid,
        spec_to_mermaid,
        system_to_mermaid,
        trace_to_mermaid,
    )

    _view_id = view_dropdown.value

    _descriptions = {
        "structural": (
            "### View 1: Structural\n\n"
            "Compiled block graph from `SystemIR`. Shows composition "
            "topology with role-based shapes and wiring types.\n\n"
            "- **Stadium** `([...])` = BoundaryAction (exogenous input)\n"
            "- **Double-bracket** `[[...]]` = terminal Mechanism (state sink)\n"
            "- **Solid arrow** = forward covariant flow\n\n"
            "**API:** `system_to_mermaid(system)`"
        ),
        "canonical": (
            "### View 2: Canonical GDS\n\n"
            "Mathematical decomposition: "
            "X_t → U → g → f → X_{t+1}.\n\n"
            "Shows the abstract dynamical system with state (X), "
            "input (U), policy (g), mechanism (f), and parameter "
            "space (Θ).\n\n"
            "**API:** `canonical_to_mermaid(canonical)`"
        ),
        "role": (
            "### View 3: Architecture by Role\n\n"
            "Blocks grouped by GDS role: Boundary (U), Policy (g), "
            "Mechanism (f). Entity cylinders show which state variables "
            "each mechanism writes.\n\n"
            "**API:** `spec_to_mermaid(spec)`"
        ),
        "domain": (
            "### View 4: Architecture by Domain\n\n"
            "Blocks grouped by domain tag (Observation, Decision, "
            "State Update). Shows organizational ownership.\n\n"
            "**API:** `spec_to_mermaid(spec, group_by='domain')`"
        ),
        "params": (
            "### View 5: Parameter Influence\n\n"
            "Θ → blocks → entities causal map. "
            "Answers: *if I change parameter X, which state variables "
            "are affected?*\n\n"
            "**API:** `params_to_mermaid(spec)`"
        ),
        "trace": (
            "### View 6: Traceability\n\n"
            "Traces `Susceptible.count` (S) backwards through the block "
            "graph. Answers: *what blocks and parameters could cause "
            "this variable to change?*\n\n"
            "**API:** `trace_to_mermaid(spec, entity, variable)`"
        ),
    }

    _mermaid_generators = {
        "structural": lambda: system_to_mermaid(sir_system),
        "canonical": lambda: canonical_to_mermaid(sir_canonical),
        "role": lambda: spec_to_mermaid(sir_spec),
        "domain": lambda: spec_to_mermaid(sir_spec, group_by="domain"),
        "params": lambda: params_to_mermaid(sir_spec),
        "trace": lambda: trace_to_mermaid(sir_spec, "Susceptible", "count"),
    }

    _mermaid_str = _mermaid_generators[_view_id]()

    mo.vstack(
        [
            mo.md(_descriptions[_view_id]),
            mo.mermaid(_mermaid_str),
        ]
    )
    return ()


# ── All 6 views at once (tabs) ──────────────────────────────


@app.cell
def all_views_tabs_header(mo):
    mo.md(
        """
        ---

        ### All 6 Views at a Glance

        Use the tabs below to quickly compare all views side-by-side.
        """
    )
    return ()


@app.cell
def all_views_tabs(mo, sir_spec, sir_system, sir_canonical):
    from gds_viz import (
        canonical_to_mermaid as _canonical_to_mermaid,
    )
    from gds_viz import (
        params_to_mermaid as _params_to_mermaid,
    )
    from gds_viz import (
        spec_to_mermaid as _spec_to_mermaid,
    )
    from gds_viz import (
        system_to_mermaid as _system_to_mermaid,
    )
    from gds_viz import (
        trace_to_mermaid as _trace_to_mermaid,
    )

    _tabs = mo.ui.tabs(
        {
            "1. Structural": mo.mermaid(_system_to_mermaid(sir_system)),
            "2. Canonical": mo.mermaid(_canonical_to_mermaid(sir_canonical)),
            "3. By Role": mo.mermaid(_spec_to_mermaid(sir_spec)),
            "4. By Domain": mo.mermaid(_spec_to_mermaid(sir_spec, group_by="domain")),
            "5. Parameters": mo.mermaid(_params_to_mermaid(sir_spec)),
            "6. Traceability": mo.mermaid(
                _trace_to_mermaid(sir_spec, "Susceptible", "count")
            ),
        }
    )
    return (_tabs,)


# ── Section 3: Theme Customization ──────────────────────────


@app.cell
def section_themes_header(mo):
    mo.md(
        """
        ---

        ## Section 2: Theme Customization

        Every `gds-viz` view function accepts a `theme=` parameter.
        There are **5 built-in Mermaid themes** — select one below
        to see how it changes the palette.

        Themes affect node fills, strokes, text colors, and subgraph
        backgrounds. Choose based on your rendering context:

        | Theme | Best for |
        |-------|----------|
        | `neutral` | Light backgrounds (GitHub, docs) |
        | `default` | Mermaid's blue-toned Material style |
        | `dark` | Dark-mode renderers |
        | `forest` | Green-tinted, earthy |
        | `base` | Minimal chrome, very light |
        """
    )
    return ()


@app.cell
def theme_controls(mo):
    theme_dropdown = mo.ui.dropdown(
        options=["neutral", "default", "dark", "forest", "base"],
        value="neutral",
        label="Theme",
    )
    theme_view_dropdown = mo.ui.dropdown(
        options={
            "Structural": "structural",
            "Architecture by Role": "role",
        },
        value="Structural",
        label="View",
    )
    mo.hstack([theme_dropdown, theme_view_dropdown], justify="start", gap=1)
    return theme_dropdown, theme_view_dropdown


@app.cell
def render_themed_view(mo, theme_dropdown, theme_view_dropdown, sir_spec, sir_system):
    from gds_viz import spec_to_mermaid as _spec_to_mermaid
    from gds_viz import system_to_mermaid as _system_to_mermaid

    _theme = theme_dropdown.value
    _view = theme_view_dropdown.value

    if _view == "structural":
        _mermaid = _system_to_mermaid(sir_system, theme=_theme)
    else:
        _mermaid = _spec_to_mermaid(sir_spec, theme=_theme)

    mo.vstack(
        [
            mo.md(f"**Theme: `{_theme}`** | **View: {_view}**"),
            mo.mermaid(_mermaid),
        ]
    )
    return ()


@app.cell
def theme_comparison_header(mo):
    mo.md(
        """
        ### Side-by-Side: Neutral vs Dark

        The two most common choices compared on the structural view.
        """
    )
    return ()


@app.cell
def theme_side_by_side(mo, sir_system):
    from gds_viz import system_to_mermaid as _system_to_mermaid

    _neutral = _system_to_mermaid(sir_system, theme="neutral")
    _dark = _system_to_mermaid(sir_system, theme="dark")

    mo.hstack(
        [
            mo.vstack([mo.md("**Neutral**"), mo.mermaid(_neutral)]),
            mo.vstack([mo.md("**Dark**"), mo.mermaid(_dark)]),
        ],
        widths="equal",
    )
    return ()


# ── Section 4: Cross-DSL Views ──────────────────────────────


@app.cell
def section_cross_dsl_header(mo):
    mo.md(
        """
        ---

        ## Section 3: Cross-DSL Views

        The `gds-viz` API is **DSL-neutral** — it operates on `GDSSpec`
        and `SystemIR`, which every compilation path produces.

        Compare the **SIR Epidemic** (hand-built with GDS primitives)
        against the **Double Integrator** (built via the `gds-control`
        DSL). The same view functions work unchanged on both.
        """
    )
    return ()


@app.cell
def build_double_integrator():
    from double_integrator.model import build_spec as _di_build_spec
    from double_integrator.model import build_system as _di_build_system

    from gds.canonical import project_canonical as _di_project_canonical

    di_spec = _di_build_spec()
    di_system = _di_build_system()
    di_canonical = _di_project_canonical(di_spec)
    return di_canonical, di_spec, di_system


@app.cell
def cross_dsl_controls(mo):
    model_dropdown = mo.ui.dropdown(
        options={
            "SIR Epidemic (hand-built)": "sir",
            "Double Integrator (control DSL)": "di",
        },
        value="SIR Epidemic (hand-built)",
        label="Model",
    )
    cross_view_dropdown = mo.ui.dropdown(
        options={
            "Structural": "structural",
            "Canonical": "canonical",
            "Architecture by Role": "role",
            "Parameter Influence": "params",
            "Traceability": "trace",
        },
        value="Structural",
        label="View",
    )
    mo.hstack([model_dropdown, cross_view_dropdown], justify="start", gap=1)
    return cross_view_dropdown, model_dropdown


@app.cell
def render_cross_dsl_view(
    mo,
    model_dropdown,
    cross_view_dropdown,
    sir_spec,
    sir_system,
    sir_canonical,
    di_spec,
    di_system,
    di_canonical,
):
    from gds_viz import (
        canonical_to_mermaid as _canonical_to_mermaid,
    )
    from gds_viz import (
        params_to_mermaid as _params_to_mermaid,
    )
    from gds_viz import (
        spec_to_mermaid as _spec_to_mermaid,
    )
    from gds_viz import (
        system_to_mermaid as _system_to_mermaid,
    )
    from gds_viz import (
        trace_to_mermaid as _trace_to_mermaid,
    )

    _model = model_dropdown.value
    _view = cross_view_dropdown.value

    if _model == "sir":
        _spec, _system, _canonical = sir_spec, sir_system, sir_canonical
        _trace_entity, _trace_var = "Susceptible", "count"
        _label = "SIR Epidemic"
    else:
        _spec, _system, _canonical = di_spec, di_system, di_canonical
        _trace_entity, _trace_var = "position", "value"
        _label = "Double Integrator"

    _generators = {
        "structural": lambda: _system_to_mermaid(_system),
        "canonical": lambda: _canonical_to_mermaid(_canonical),
        "role": lambda: _spec_to_mermaid(_spec),
        "params": lambda: _params_to_mermaid(_spec),
        "trace": lambda: _trace_to_mermaid(_spec, _trace_entity, _trace_var),
    }

    mo.vstack(
        [
            mo.md(f"**{_label}** | **{_view}**"),
            mo.mermaid(_generators[_view]()),
        ]
    )
    return ()


# ── Canonical Comparison ─────────────────────────────────────


@app.cell
def canonical_comparison_header(mo):
    mo.md(
        """
        ### Canonical Comparison

        Both models decompose into the same `h = f . g` structure,
        but with different dimensionalities. The SIR model has
        parameters (Θ); the double integrator does not.
        """
    )
    return ()


@app.cell
def canonical_comparison(mo, sir_canonical, di_canonical):
    from gds_viz import canonical_to_mermaid as _canonical_to_mermaid

    mo.hstack(
        [
            mo.vstack(
                [
                    mo.md(f"**SIR Epidemic** — `{sir_canonical.formula()}`"),
                    mo.mermaid(_canonical_to_mermaid(sir_canonical)),
                ]
            ),
            mo.vstack(
                [
                    mo.md(f"**Double Integrator** — `{di_canonical.formula()}`"),
                    mo.mermaid(_canonical_to_mermaid(di_canonical)),
                ]
            ),
        ],
        widths="equal",
    )
    return ()


# ── API Reference ────────────────────────────────────────────


@app.cell
def api_reference(mo):
    mo.md(
        """
        ---

        ## API Quick Reference

        All functions live in `gds_viz` and return Mermaid strings.

        | Function | Input | View |
        |----------|-------|------|
        | `system_to_mermaid(system)` | `SystemIR` | Structural |
        | `canonical_to_mermaid(canonical)` | `CanonicalGDS` | Canonical |
        | `spec_to_mermaid(spec)` | `GDSSpec` | By role |
        | `spec_to_mermaid(spec, group_by=...)` | `GDSSpec` | By domain |
        | `params_to_mermaid(spec)` | `GDSSpec` | Parameters |
        | `trace_to_mermaid(spec, ent, var)` | `GDSSpec` | Traceability |

        All accept an optional `theme=` parameter:
        `"neutral"`, `"default"`, `"dark"`, `"forest"`, `"base"`.

        ### Usage Pattern

        ```python
        from gds_viz import system_to_mermaid
        from my_model import build_system

        system = build_system()
        mermaid_str = system_to_mermaid(system, theme="dark")
        # Paste into GitHub markdown, mermaid.live, or mo.mermaid()
        ```
        """
    )
    return ()


if __name__ == "__main__":
    app.run()

To run the notebook locally:

uv run marimo run packages/gds-examples/notebooks/visualization.py

Run the test suite:

uv run --package gds-examples pytest packages/gds-examples/tests/test_visualization_guide.py -v

Source Files

File Purpose
all_views_demo.py All 6 view types on the SIR model
theme_customization.py 5 built-in theme demos
cross_dsl_views.py Cross-DSL visualization comparison
visualization.py Interactive marimo notebook