Quick Start Guide#

TurboGraph is a Python library for defining and computing dependent computations using a directed acyclic graph (DAG).

πŸš€ Why Use TurboGraph?#

βœ” Automatically determines dependencies & execution order
βœ” Computes only required values
βœ” Allows dynamic overrides of values/functions
βœ” Reuses and modifies graphs without rebuilding

➊ Setting Up: Logging & Debugging#

[1]:
import logging

logging.basicConfig(
    level=logging.INFO,  # Change to DEBUG for more detailed logs
    format="%(name)s - %(levelname)s - %(message)s",
)

βž‹ Understanding the Computation Graph (DAG)#

TurboGraph structures computations as a directed acyclic graph (DAG):

βœ” Vertices (Nodes): Represent computations or values
βœ” Edges (Connections): Define dependencies between computations

Each vertex in the graph can hold:

  • A fixed value

  • A function depending on other values

  • Predecessors (dependencies)

πŸ”Ή Example Computation Graph#

specs = {
    "a": 5,
    "b": 3,
    "add": lambda a, b: a + b,  # Depends on "a" and "b"
    "double": lambda add: add * 2,  # Depends on "add"
}

Graph Structure#

a       b
 \     /
  \   /
  (add)
    |
 (double)

Execution Order#

1️⃣ Compute a and b first (as they have no dependencies).
2️⃣ Compute add using a and b.
3️⃣ Compute double using add.

βœ… TurboGraph ensures computations are executed in the correct order automatically.


➌ Running Computations with compute()#

The compute() function:

βœ” Builds the computation graph (DAG)
βœ” Determines execution order
βœ” Computes only necessary values
βœ” Allows flexible overrides

πŸ”Ή Basic Computation Example#

[2]:
from turbograph import compute

specs = {
    "a": 5,  # Constant value.
    "b": 3,  # Constant value.
    "add": lambda a, b: a + b,  # TurboGraph infers dependencies "a" and "b".
    "double": lambda add: add * 2,  # Depends on "add".
}

compute(specs)
turbograph.run.graphcomputing - INFO - Compute vertices {'b', 'double', 'a', 'add'} in call mode 'args'. Vertices are computed in the following order: ['b', 'a', 'add', 'double']
[2]:
{'b': 3, 'a': 5, 'add': 8, 'double': 16}

βœ… TurboGraph detects dependencies and computes in the correct order!

πŸ”Ή Overriding Values and Functions#

[3]:
compute(
    specs,
    values={"a": 10},  # Override "a".
    funcs={"double": lambda x: x + 1},  # Modify "double".
)
turbograph.run.graphcomputing - INFO - Compute vertices {'b', 'double', 'a', 'add'} in call mode 'args'. Vertices are computed in the following order: ['b', 'a', 'add', 'double']
[3]:
{'b': 3, 'a': 10, 'add': 13, 'double': 14}

βœ… "x" is overridden to 10, and "double" now computes add + 1.

πŸ”Ή Computing Only a Subset#

[4]:
compute(specs, vertices=["add"])
turbograph.run.graphcomputing - INFO - Compute vertices {'add'} in call mode 'args'. Vertices are computed in the following order: ['b', 'a', 'add']
[4]:
{'add': 8}

βœ… Computes only "add", retrieving required dependencies automatically. "final" is not computed.


➍ Defining Computations (Vertex Specifications)#

TurboGraph provides four ways to specify computations.

πŸ”ΉMethod 1: Constant Values#

[5]:
specs = {"a": 1, "b": 2}
compute(specs, vertices=["b"])
turbograph.run.graphcomputing - INFO - Compute vertices {'b'} in call mode 'args'. Vertices are computed in the following order: ['b']
[5]:
{'b': 2}

βœ… "b" is directly returned without computation.

πŸ”Ή Method 2: Functions (Automatic Dependency Inference)#

[6]:
specs = {
    "a": lambda: 7,
    "b": lambda: 8,
    "add": lambda a, b: a + b,
}
compute(specs)
turbograph.run.graphcomputing - INFO - Compute vertices {'b', 'a', 'add'} in call mode 'args'. Vertices are computed in the following order: ['b', 'a', 'add']
[6]:
{'b': 8, 'a': 7, 'add': 15}

βœ… "add" is computed only after "a" and "b" are evaluated.

πŸ”Ή Method 3: Dictionary Specification (Manual Dependencies)#

[7]:
specs = {
    # `"value"` takes precedence over `"func"` during computation.
    "a": {"func": lambda: 0, "value": 7},
    "b": {"value": 8},
    # Dependencies: "a" and "b" explicitly specified.
    "add": {"func": lambda x, y: x + y, "predecessors": ("a", "b")},
}
compute(specs)
turbograph.run.graphcomputing - INFO - Compute vertices {'b', 'a', 'add'} in call mode 'args'. Vertices are computed in the following order: ['b', 'a', 'add']
[7]:
{'b': 8, 'a': 7, 'add': 15}

βœ… "add" is computed after "a" and "b".

πŸ”Ή Method 4: Sequence Specification#

Explicitly define function, dependencies, and values in a list or tuple.

[8]:
specs = {
    "a": [None, (), 3],
    "b": [],  # Uses defaults.
    "diff": [lambda x, y: x - y, ["a", "b"]],
}

compute(specs, values={"b": 5})
turbograph.run.graphcomputing - INFO - Compute vertices {'b', 'a', 'diff'} in call mode 'args'. Vertices are computed in the following order: ['b', 'a', 'diff']
[8]:
{'b': 5, 'a': 3, 'diff': -2}

βœ… "diff" is computed using "a" and "b" as dependencies.


➎ Controlling Function Execution with call_mode#

The call_mode parameter controls how dependency values are passed to functions.

Available call_mode Options#

Mode

Description

Example Function Signature

args

Positional Arguments

def func(a, b): ...

kwargs

Keyword Arguments

def func(a=1, b=2): ...

arg

Single dictionary argument

def func(inputs): ...

The default call_mode is args.

πŸ”Ή args (Positional Arguments)#

Dependencies are passed as positional arguments, matching the function’s parameter order.

[9]:
def add_args(a: int, b: int, *, c: int = 0) -> int:
    """Add three numbers. `"c"` is a keyword-only argument and is therefore ignored."""
    return a + b + c


specs = {"a": lambda: 2, "b": lambda: 3, "add": add_args}
compute(specs, call_mode="args")
turbograph.run.graphcomputing - INFO - Compute vertices {'b', 'a', 'add'} in call mode 'args'. Vertices are computed in the following order: ['b', 'a', 'add']
[9]:
{'b': 3, 'a': 2, 'add': 5}

βœ… "add" is called as add_args(a, b).

πŸ”Ή kwargs (Keyword Arguments)#

Dependencies are passed as named arguments.

[10]:
def add_kwargs(c: int = 1, /, a: int = 2, *, b: int = 0) -> int:
    """Add three numbers. `c` is positional-only, and is therefore ignored."""
    return a + b + c


specs = {"a": lambda: 2, "b": lambda: 3, "add": add_kwargs}
compute(specs, call_mode="kwargs")
turbograph.run.graphcomputing - INFO - Compute vertices {'b', 'a', 'add'} in call mode 'kwargs'. Vertices are computed in the following order: ['b', 'a', 'add']
[10]:
{'b': 3, 'a': 2, 'add': 6}

βœ… "add" is called as add_kwargs(a=2, b=3).

πŸ”Ή arg (Single Dictionary Argument)#

All values are passed as a dictionary.

[11]:
def add_arg(inputs: dict[str, int]) -> int:
    """Add two numbers from a dictionary."""
    return inputs["a"] + inputs["b"]


specs = {
    "a": lambda _: 2,
    "b": lambda _: 3,
    "add": {"func": add_arg, "predecessors": ["a", "b"]},
}
compute(specs, call_mode="arg")
turbograph.run.graphcomputing - INFO - Compute vertices {'b', 'a', 'add'} in call mode 'arg'. Vertices are computed in the following order: ['b', 'a', 'add']
[11]:
{'b': 3, 'a': 2, 'add': 5}

βœ… Function receives all dependencies as a dictionary.


➏ Reusing Graphs#

TurboGraph allows for building, reusing, modifying computation graphs.

Instead of recomputing everything from scratch, you can reuse and modify existing graphs.

πŸ”Ή Building & Computing a Graph with build_graph() and compute_from_graph()#

Instead of directly using compute(), you can manually build a computation graph with build_graph() and compute values later using compute_from_graph().

[12]:
from turbograph import build_graph, compute_from_graph

specs = {
    "input1": lambda: 5,
    "input2": lambda: 3,
    "add": lambda input1, input2: input1 + input2,
    "output": lambda add: add * 2,
}
graph = build_graph(specs)

compute_from_graph(graph, vertices=["output"])
turbograph.run.graphcomputing - INFO - Compute vertices {'output'} in call mode 'args'. Vertices are computed in the following order: ['input1', 'input2', 'add', 'output']
[12]:
{'output': 16}

βœ… The graph is built once and reused multiple times.

πŸ”Ή Computing with Different Values#

You can override specific values without modifying the underlying graph structure.

[13]:
compute_from_graph(graph, ["add"], values={"input1": 10})
turbograph.run.graphcomputing - INFO - Compute vertices {'add'} in call mode 'args'. Vertices are computed in the following order: ['input1', 'input2', 'add']
[13]:
{'add': 13}

βœ… The graph remains unchanged, but "add" is recomputed using the new "input1" value.

πŸ”Ή The Graph Object#

The Graph object serves as a wrapper around a NetworkX or iGraph graph, depending on the selected backend.

It provides a simple interface for constructing, modifying, and querying graphs within TurboGraph computations. This object is mainly intended for internal use.

[14]:
internal_graph = graph.graph  # Access the underlying graph object

print("Internal graph:", internal_graph)
print("Vertices:", graph.vertices)
print("Edges:", graph.edges)
print("Vertex attributes:", graph.get_all_vertex_attributes())
print("Call mode:", graph.call_mode)
Internal graph: _DiGraph with 4 nodes and 3 edges
Vertices: {'input2', 'input1', 'output', 'add'}
Edges: {('add', 'output'), ('input2', 'add'), ('input1', 'add')}
Vertex attributes: {'input1': {'func': <function <lambda> at 0x7f5a81a747c0>, 'value': <NA>, 'predecessors': ()}, 'output': {'func': <function <lambda> at 0x7f5a80374220>, 'value': <NA>, 'predecessors': ('add',)}, 'input2': {'func': <function <lambda> at 0x7f5a81a77ec0>, 'value': <NA>, 'predecessors': ()}, 'add': {'func': <function <lambda> at 0x7f5a80374ea0>, 'value': <NA>, 'predecessors': ('input1', 'input2')}}
Call mode: args

πŸ”Ή Use the .compute() Method#

For convenience, the .compute() method method is available on the graph object. It serves as an alias for compute_from_graph().

[15]:
graph.compute(["add"], {"input1": 10})
turbograph.run.graphcomputing - INFO - Compute vertices {'add'} in call mode 'args'. Vertices are computed in the following order: ['input1', 'input2', 'add']
[15]:
{'add': 13}

➐ Modifying Graphs#

πŸ”Ή Rebuilding a Graph (rebuild_graph())#

To modify specific computations while preserving dependencies, use rebuild_graph().

[16]:
from turbograph import compute_from_graph, rebuild_graph

updated_graph = rebuild_graph(
    graph,
    vertices=["add"],
    funcs={"add": lambda input1, input2: input1 - input2},
)

# Compute using the updated graph.
compute_from_graph(updated_graph)
turbograph.run.graphcomputing - INFO - Compute vertices {'input1', 'input2', 'add'} in call mode 'args'. Vertices are computed in the following order: ['input1', 'input2', 'add']
[16]:
{'input1': 5, 'input2': 3, 'add': 2}
βœ… "add" now computes subtraction instead of addition, while keeping the rest of the graph intact.
βœ… The node "output" is removed since it does not depend on "add".

You can clear precomputed values using turbograph.NA.

[17]:
from turbograph import NA

graph = build_graph(
    {
        "input1": lambda: 5,
        "input2": lambda: 3,
        "add": {
            "func": lambda input1, input2: input1 + input2,
            "predecessors": ["input1", "input2"],
            "value": 3,  # Precomputed value.
        },
    }
)

updated_graph = rebuild_graph(
    graph,
    values={
        "input1": 10,
        "input2": 3,
        "add": NA,  # Reset the value of "add".
    },
)

compute_from_graph(updated_graph)
# equivalent to updated_graph.compute()
turbograph.run.graphcomputing - INFO - Compute vertices {'input1', 'input2', 'add'} in call mode 'args'. Vertices are computed in the following order: ['input1', 'input2', 'add']
[17]:
{'input1': 10, 'input2': 3, 'add': 13}

βœ… "add" is now recalculated instead of using a previous precomputed value 3.

πŸ”Ή Use the .rebuild() Method#

The .rebuild() method allows you to rebuild the graph directly. It’s an alias for rebuild_graph().

[18]:
# Equivalent to the previous example but using the `rebuild` method.
other_updated_graph = graph.rebuild(
    values={
        "input1": 10,
        "input2": 3,
        "add": NA,
    }
)

# Both updated graphs should be identical.
assert updated_graph == other_updated_graph

πŸ”Ή Resetting the Graph with the .reset() Method#

The .reset() removes all precomputed values, functions, and the call_mode in the graph, while preserving the graph structure.

[19]:
from pprint import pprint

graph = build_graph(
    {
        "input1": 2,
        "add": lambda input1, input2: input1 + input2,
    },
    call_mode="args",
)
print("Before reset:")
pprint(graph.get_all_vertex_attributes())
print("Call mode:", graph.call_mode, "\n")

graph.reset()
print("After reset:")
pprint(graph.get_all_vertex_attributes())
print("Call mode:", graph.call_mode)
Before reset:
{'add': {'func': <function <lambda> at 0x7f5a80374180>,
         'predecessors': ('input1', 'input2'),
         'value': <NA>},
 'input1': {'func': None, 'predecessors': (), 'value': 2},
 'input2': {'func': None, 'predecessors': (), 'value': <NA>}}
Call mode: args

After reset:
{'add': {'func': None, 'predecessors': ('input1', 'input2'), 'value': <NA>},
 'input1': {'func': None, 'predecessors': (), 'value': <NA>},
 'input2': {'func': None, 'predecessors': (), 'value': <NA>}}
Call mode: None

✨ Takeaways#

TurboGraph provides a flexible approach to defining and executing dependent computations. It enables you to:

  • Define Computations: Use functions, dictionaries, or sequences to declare computation rules and dependencies.

  • Execute & Modify Computations:

    • Compute full or partial graphs dynamically using compute().

    • Control function argument handling with call_mode.

    • Override values and functions at runtime via values and funcs.

  • Reuse & Update Graphs: