Quick Start Guide#
TurboGraph is a Python library for defining and computing dependent computations using a directed acyclic graph (DAG).
π Why Use TurboGraph?#
β 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):
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#
a
and b
first (as they have no dependencies).add
using a
and b
.double
using add
.β TurboGraph ensures computations are executed in the correct order automatically.
β Running Computations with compute()#
The compute() function:
πΉ 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 |
---|---|---|
|
Positional Arguments |
|
|
Keyword Arguments |
|
|
Single dictionary argument |
|
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."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
andfuncs
.
Reuse & Update Graphs:
Construct graphs with build_graph() and execute computations using compute_from_graph().
Modify graphs selectively with rebuild_graph().