Iteration using Loop

One way to perform iteration in Tierkreis is to use GraphBuilder.loop. The first argument to GraphBuilder.loop is a graph that constitutes the loop body. The second is the initial input to the loop.

Graphs

Bounded iteration

As an example to introduce the concepts, we create a bounded iteration that has additional inputs to configure the iteration. The signature of the loop body is:

from typing import NamedTuple
from tierkreis.builder import GraphBuilder
from tierkreis.models import TKR

class LoopBodyInput(NamedTuple):
    i: TKR[int]
    value: TKR[int]
    step: TKR[int]
    bound: TKR[int]

class LoopBodyOutput(NamedTuple):
    i: TKR[int]
    value: TKR[int]
    should_continue: TKR[bool]

loop_body = GraphBuilder(LoopBodyInput, LoopBodyOutput)

The loop body input type consists of all the variables that we want the loop body to have access to. This includes variables that the loop does not modify, like step and bound. The loop body output type consists of the variables that the loop modifies (in this case the counter i and the value) plus a special boolean output called should_continue that tells the loop when to finish.

In our example the loop body increments the counter i, adds step to the value and checks if i has met the bound.

from tierkreis.builtins.stubs import iadd, igt, rand_int

i = loop_body.task(iadd(loop_body.const(1), loop_body.inputs.i))
value = loop_body.task(iadd(loop_body.inputs.step, loop_body.inputs.value))
pred = loop_body.task(igt(loop_body.inputs.bound, i))
loop_body.outputs(LoopBodyOutput(i=i, value=value, should_continue=pred))

The main graph constructs the initial values for the loop and uses GraphBuilder.loop to run the loop.

from tierkreis.models import EmptyModel

f = GraphBuilder(EmptyModel, TKR[int])
init = LoopBodyInput(f.const(0), f.const(0), f.const(2), f.const(10))
loop_output = f.loop(loop_body, init)
f.outputs(loop_output.value)

Repeat until success

In addition to bounded iteration, the GraphBuilder.loop method can also define a ‘repeat until success’ loop. First we need to create the graph that constitutes the body of the loop. As a toy example, we choose a random number between 1 and 10 inclusive and continue if it is less than 10.

from typing import NamedTuple

from tierkreis.builder import GraphBuilder
from tierkreis.builtins.stubs import iadd, igt, rand_int
from tierkreis.models import TKR


class LoopBodyInput(NamedTuple):
    i: TKR[int]


class LoopBodyOutput(NamedTuple):
    i: TKR[int]
    should_continue: TKR[bool]


body = GraphBuilder(LoopBodyInput, LoopBodyOutput)
i = body.task(iadd(body.const(1), body.inputs.i))
a = body.task(rand_int(body.const(0), body.const(10)))
pred = body.task(igt(body.const(10), a))
body.outputs(LoopBodyOutput(i=i, should_continue=pred))

The main graph runs the loop and tells us the iteration on which we found success.

from tierkreis.models import EmptyModel

g = GraphBuilder(EmptyModel, TKR[int])
loop_output = g.loop(body, LoopBodyInput(g.const(0)))
g.outputs(loop_output.i)

Execution

Since we still only use built-in functions, we execute the graph in the same way as before.

from uuid import UUID
from pathlib import Path

from tierkreis import run_graph
from tierkreis.storage import FileStorage, read_outputs
from tierkreis.executor import ShellExecutor

storage = FileStorage(UUID(int=99), name="Nested graphs using Eval")
executor = ShellExecutor(Path("."), logs_path=storage.logs_path)

storage.clean_graph_files()
run_graph(storage, executor, f.get_data(), {})
print(read_outputs(storage))

storage.clean_graph_files()
run_graph(storage, executor, g.get_data(), {})
print(read_outputs(storage))
20
8