Module bento.sim

Expand source code
#
# Bentobox
# SDK - Simulation
# Simulation
#

from typing import Iterable, List, Optional, Set

from bento.client import Client
from bento.ecs.grpc import Component, Entity
from bento.graph.compile import ConvertFn, compile_graph
from bento.protos import sim_pb2
from bento.spec.ecs import ComponentDef, EntityDef, SystemDef
from bento.spec.graph import Graph
from bento.spec.sim import SimulationDef


class Simulation:
    # TODO(mrzzy): Add a more complete usage example into docs.
    """Represents a `Simulation` in running in the Bentobox Engine.

    Example:
        Building and running a simulation::

            # either: define simulation with entities and components
            sim = Simulation(name="sim", entities=[ ... ], components=[ ... ], client=client)
            # or: load/hydrate a predefined simulation from a SimulationDef
            sim = Simulation.from_def(sim_def)

            # use an init graph to initalize attribute values
            @sim.init
            def init_fn():
                # initialize values with:  entity[Component].attribute = value

            # implement systems running in the simulation
            @sim.system
            def system_fn():
                # ...

            # start-end the simulation using with block
            with sim:
                # run the simulation for one step
                sim.step()
                # ...
    """

    def __init__(
        self,
        name: str,
        components: Iterable[ComponentDef],
        entities: Iterable[EntityDef],
        client: Client,
        system_fns: Iterable[ConvertFn] = [],
        init_fn: Optional[ConvertFn] = None,
    ):
        """Create a new simulation with the given entities and component
        Args:
            name: Name of the the Simulation. The name should be unique among
                registered simulation in the Engine.
            entities: List of entities to use in the simulation.
            components: List of component types in use in the simulation.
            client: Client to use to communicate with the Engine when registering
                and interacting with the simulation.
            system_fns: List of `bento.graph.compile.compile_graph()` compilable
                function implemnting the systems to run in the simulation.
            init_fn: The `bento.graph.compile.compile_graph()` compilable
                function containing the init code for the simulation
                that runs the specified Simulation is registered/applied.
        """
        self.name = name
        self.client = client
        self.started = False
        self.component_defs = list(components)
        self.entity_defs = list(entities)
        # (system_fn, system id). 0 to signify unset system id.
        self.system_fns = [(fn, 0) for fn in system_fns]

        self.init_fn = init_fn

        # register sim on engine
        # obtain autogen ids for entities and the engine by recreating from applied proto
        applied_proto = self.client.apply_sim(self.build(include_graphs=False))
        self.entity_defs = [EntityDef.from_proto(e) for e in applied_proto.entities]

        # unpack entity and components from proto
        # unpack Entity protos into grpc backed entities (set(components) -> grpc entity)
        self.entity_map = {
            frozenset(e.components): Entity(
                sim_name=self.name,
                entity_id=e.id,
                components=e.components,
                client=self.client,
            )
            for e in self.entity_defs
        }

    @classmethod
    def from_def(cls, sim_def: SimulationDef, client: Client):
        """
        Create/Hydrate a Simulation from a `bento.spec.SimulationDef`.

        Args:
            sim_def: SimulationDef specification to load the Simulation from.
            client: Client to use to communicate with the Engine.
        """
        return cls(
            name=sim_def.name,
            components=sim_def.component_defs,
            entities=sim_def.entity_defs,
            system_fns=sim_def.system_fns,
            init_fn=sim_def.init_fn,
            client=client,
        )

    def build(self, include_graphs: bool = True) -> sim_pb2.SimulationDef:
        """
        Build a `bento.eachproto.sim_pb2.SimulationDef` Proto from this Simulation.

        Args:
            include_graphs: Whether to compile & include graphs in the returned Proto.
                This requires that id to be set for each entity as entity ids
                are required for graph compilation to work.
        Returns:
            The `bento.proto.sim_pb2.SimulationDef` Proto equivalent of this Simulation.
        """
        # compile graphs if requested to be included
        system_defs, init_graph = [], Graph()
        if include_graphs:
            compile_fn = lambda fn: compile_graph(
                fn, self.entity_defs, self.component_defs
            )
            # compile systems graphs
            system_defs = [
                SystemDef(
                    graph=compile_fn(fn),
                    system_id=system_id,
                )
                for fn, system_id in self.system_fns
            ]
            # compile init graph
            init_graph = (
                compile_fn(self.init_fn) if self.init_fn is not None else Graph()
            )

        return sim_pb2.SimulationDef(
            name=self.name,
            entities=[e.proto for e in self.entity_defs],
            components=[c.proto for c in self.component_defs],
            systems=[s.proto for s in system_defs],
            init_graph=init_graph.proto,
        )

    def start(self):
        """Starts this Simulation on the Engine.

        If already started, calling `start()` again does nothing.
        """
        # do nothing if already started
        if self.started:
            return
        # commit entire simulation to (ie including systems/init graph added) to engine
        applied_proto = self.client.apply_sim(self.build(include_graphs=True))
        # obtain autogen ids for systems from the engine by recreating the applied proto
        current_sys_fns = [system_fn for system_fn, _ in self.system_fns]
        self.system_fns = [
            # update system ids for systems by position
            (fn, system_def.id)
            for fn, system_def in zip(current_sys_fns, applied_proto.systems)
        ]
        self.started = True

    def stop(self):
        """Stops and removes this Simulation from the Engine.
        Raises:
            RuntimeError: If stop() is called on a simulation that has not started yet.
        """
        if not self.started:
            raise RuntimeError("Cannot stop a Simulation that has not started yet.")
        # cleanup by remove simulation from engine
        self.client.remove_sim(self.name)
        self.started = False

    def entity(self, components: Iterable[str]) -> Entity:
        """Lookup the gRPC entity with the components with the game attached.

        Provides access to ECS entity on the Bentobox Engine via gRPC.

        Args:
            components: Set of the names of the component attached that
                should be attached to the retrieved component.
        Raises:
            ValueError: if component names given contains duplicates
            RuntimeError: If Simulation has not yet stated.
        Returns:
            The gRPC entity with the given list of components attached.
        """
        if not self.started:
            raise RuntimeError(
                "Cannot obtain a gRPC Entity from a Simulation that has not started yet."
            )
        comp_set = frozenset([str(c) for c in components])
        # check for duplicates in given components
        if len(comp_set) != len(list(components)):
            raise ValueError("Given component names should not contain duplicates")
        return self.entity_map[comp_set]

    def init(self, init_fn: ConvertFn):
        """Register given init_fn as the init graph for this simulation.

        The init graph allows for the initization of attribute's values,
        running on the simulation first step() call, before any systems run.

        Compiles the `init_fn` into a computational graph and registers the
        result as a init graph for this Simulation.

            Example:
            @sim.system
            def system_fn(g):
                # ... implementation of the system ..

        Args:
            system_fn: Function that contains the implementation of the system.
                Must be compilable by `compile_graph()`.
        """
        self.init_fn = init_fn

    def system(self, system_fn: ConvertFn):
        """Register a ECS system with the given system_fn on this Simulation.

        ECS Systems are run every step of simulation and encapsulate the logic of the simulation.

        Compiles the `system_fn` into a computational graph and registers the
        result as a ECS system to run on this Simulation.

            Example:
            @sim.system
            def system_fn(g):
                # ... implementation of the system ..

        Args:
            system_fn: Function that contains the implementation of the system.
                Must be compilable by `compile_graph()`.
        """
        # 0 to signify unset system id
        self.system_fns.append((system_fn, 0))

    def step(self):
        """Run this simulation for one step

        Runs this simulation's systems in the order they are registered.
        Blocks until all systems of that simulation have finished running.

        The Simulation must have already started before running the simulation with step()`

        Args:
            RuntimeError: If step() is called on a simulation that has not started yet
                or has already been stopped.
        """
        if not self.started:
            raise RuntimeError(
                "Cannot step a simulation that has not started or already stopped."
            )
        self.client.step_sim(self.name)

    @property
    def entities(self) -> List[Entity]:
        """Get gRPC entities to this Simulation.
        Returns:
            List of entities belonging to this Simulation
        """
        return list(self.entity_map.values())

    def __enter__(self):
        self.start()

    def __exit__(self, exc_type, exc_value, traceback):
        self.stop()
        # never suppress exceptions inside with statement
        return False

    def __repr__(self):
        return f"{type(self).__name__}<{self.name}>"

    def __hash__(self):
        return hash(self.name)

Classes

class Simulation (name: str, components: Iterable[ComponentDef], entities: Iterable[EntityDef], client: Client, system_fns: Iterable[Callable[[Plotter], NoneType]] = [], init_fn: Union[Callable[[Plotter], NoneType], NoneType] = None)

Represents a Simulation in running in the Bentobox Engine.

Example

Building and running a simulation::

# either: define simulation with entities and components
sim = Simulation(name="sim", entities=[ ... ], components=[ ... ], client=client)
# or: load/hydrate a predefined simulation from a SimulationDef
sim = Simulation.from_def(sim_def)

# use an init graph to initalize attribute values
@sim.init
def init_fn():
    # initialize values with:  entity[Component].attribute = value

# implement systems running in the simulation
@sim.system
def system_fn():
    # ...

# start-end the simulation using with block
with sim:
    # run the simulation for one step
    sim.step()
    # ...

Create a new simulation with the given entities and component

Args

name
Name of the the Simulation. The name should be unique among registered simulation in the Engine.
entities
List of entities to use in the simulation.
components
List of component types in use in the simulation.
client
Client to use to communicate with the Engine when registering and interacting with the simulation.
system_fns
List of compile_graph() compilable function implemnting the systems to run in the simulation.
init_fn
The compile_graph() compilable function containing the init code for the simulation that runs the specified Simulation is registered/applied.
Expand source code
class Simulation:
    # TODO(mrzzy): Add a more complete usage example into docs.
    """Represents a `Simulation` in running in the Bentobox Engine.

    Example:
        Building and running a simulation::

            # either: define simulation with entities and components
            sim = Simulation(name="sim", entities=[ ... ], components=[ ... ], client=client)
            # or: load/hydrate a predefined simulation from a SimulationDef
            sim = Simulation.from_def(sim_def)

            # use an init graph to initalize attribute values
            @sim.init
            def init_fn():
                # initialize values with:  entity[Component].attribute = value

            # implement systems running in the simulation
            @sim.system
            def system_fn():
                # ...

            # start-end the simulation using with block
            with sim:
                # run the simulation for one step
                sim.step()
                # ...
    """

    def __init__(
        self,
        name: str,
        components: Iterable[ComponentDef],
        entities: Iterable[EntityDef],
        client: Client,
        system_fns: Iterable[ConvertFn] = [],
        init_fn: Optional[ConvertFn] = None,
    ):
        """Create a new simulation with the given entities and component
        Args:
            name: Name of the the Simulation. The name should be unique among
                registered simulation in the Engine.
            entities: List of entities to use in the simulation.
            components: List of component types in use in the simulation.
            client: Client to use to communicate with the Engine when registering
                and interacting with the simulation.
            system_fns: List of `bento.graph.compile.compile_graph()` compilable
                function implemnting the systems to run in the simulation.
            init_fn: The `bento.graph.compile.compile_graph()` compilable
                function containing the init code for the simulation
                that runs the specified Simulation is registered/applied.
        """
        self.name = name
        self.client = client
        self.started = False
        self.component_defs = list(components)
        self.entity_defs = list(entities)
        # (system_fn, system id). 0 to signify unset system id.
        self.system_fns = [(fn, 0) for fn in system_fns]

        self.init_fn = init_fn

        # register sim on engine
        # obtain autogen ids for entities and the engine by recreating from applied proto
        applied_proto = self.client.apply_sim(self.build(include_graphs=False))
        self.entity_defs = [EntityDef.from_proto(e) for e in applied_proto.entities]

        # unpack entity and components from proto
        # unpack Entity protos into grpc backed entities (set(components) -> grpc entity)
        self.entity_map = {
            frozenset(e.components): Entity(
                sim_name=self.name,
                entity_id=e.id,
                components=e.components,
                client=self.client,
            )
            for e in self.entity_defs
        }

    @classmethod
    def from_def(cls, sim_def: SimulationDef, client: Client):
        """
        Create/Hydrate a Simulation from a `bento.spec.SimulationDef`.

        Args:
            sim_def: SimulationDef specification to load the Simulation from.
            client: Client to use to communicate with the Engine.
        """
        return cls(
            name=sim_def.name,
            components=sim_def.component_defs,
            entities=sim_def.entity_defs,
            system_fns=sim_def.system_fns,
            init_fn=sim_def.init_fn,
            client=client,
        )

    def build(self, include_graphs: bool = True) -> sim_pb2.SimulationDef:
        """
        Build a `bento.eachproto.sim_pb2.SimulationDef` Proto from this Simulation.

        Args:
            include_graphs: Whether to compile & include graphs in the returned Proto.
                This requires that id to be set for each entity as entity ids
                are required for graph compilation to work.
        Returns:
            The `bento.proto.sim_pb2.SimulationDef` Proto equivalent of this Simulation.
        """
        # compile graphs if requested to be included
        system_defs, init_graph = [], Graph()
        if include_graphs:
            compile_fn = lambda fn: compile_graph(
                fn, self.entity_defs, self.component_defs
            )
            # compile systems graphs
            system_defs = [
                SystemDef(
                    graph=compile_fn(fn),
                    system_id=system_id,
                )
                for fn, system_id in self.system_fns
            ]
            # compile init graph
            init_graph = (
                compile_fn(self.init_fn) if self.init_fn is not None else Graph()
            )

        return sim_pb2.SimulationDef(
            name=self.name,
            entities=[e.proto for e in self.entity_defs],
            components=[c.proto for c in self.component_defs],
            systems=[s.proto for s in system_defs],
            init_graph=init_graph.proto,
        )

    def start(self):
        """Starts this Simulation on the Engine.

        If already started, calling `start()` again does nothing.
        """
        # do nothing if already started
        if self.started:
            return
        # commit entire simulation to (ie including systems/init graph added) to engine
        applied_proto = self.client.apply_sim(self.build(include_graphs=True))
        # obtain autogen ids for systems from the engine by recreating the applied proto
        current_sys_fns = [system_fn for system_fn, _ in self.system_fns]
        self.system_fns = [
            # update system ids for systems by position
            (fn, system_def.id)
            for fn, system_def in zip(current_sys_fns, applied_proto.systems)
        ]
        self.started = True

    def stop(self):
        """Stops and removes this Simulation from the Engine.
        Raises:
            RuntimeError: If stop() is called on a simulation that has not started yet.
        """
        if not self.started:
            raise RuntimeError("Cannot stop a Simulation that has not started yet.")
        # cleanup by remove simulation from engine
        self.client.remove_sim(self.name)
        self.started = False

    def entity(self, components: Iterable[str]) -> Entity:
        """Lookup the gRPC entity with the components with the game attached.

        Provides access to ECS entity on the Bentobox Engine via gRPC.

        Args:
            components: Set of the names of the component attached that
                should be attached to the retrieved component.
        Raises:
            ValueError: if component names given contains duplicates
            RuntimeError: If Simulation has not yet stated.
        Returns:
            The gRPC entity with the given list of components attached.
        """
        if not self.started:
            raise RuntimeError(
                "Cannot obtain a gRPC Entity from a Simulation that has not started yet."
            )
        comp_set = frozenset([str(c) for c in components])
        # check for duplicates in given components
        if len(comp_set) != len(list(components)):
            raise ValueError("Given component names should not contain duplicates")
        return self.entity_map[comp_set]

    def init(self, init_fn: ConvertFn):
        """Register given init_fn as the init graph for this simulation.

        The init graph allows for the initization of attribute's values,
        running on the simulation first step() call, before any systems run.

        Compiles the `init_fn` into a computational graph and registers the
        result as a init graph for this Simulation.

            Example:
            @sim.system
            def system_fn(g):
                # ... implementation of the system ..

        Args:
            system_fn: Function that contains the implementation of the system.
                Must be compilable by `compile_graph()`.
        """
        self.init_fn = init_fn

    def system(self, system_fn: ConvertFn):
        """Register a ECS system with the given system_fn on this Simulation.

        ECS Systems are run every step of simulation and encapsulate the logic of the simulation.

        Compiles the `system_fn` into a computational graph and registers the
        result as a ECS system to run on this Simulation.

            Example:
            @sim.system
            def system_fn(g):
                # ... implementation of the system ..

        Args:
            system_fn: Function that contains the implementation of the system.
                Must be compilable by `compile_graph()`.
        """
        # 0 to signify unset system id
        self.system_fns.append((system_fn, 0))

    def step(self):
        """Run this simulation for one step

        Runs this simulation's systems in the order they are registered.
        Blocks until all systems of that simulation have finished running.

        The Simulation must have already started before running the simulation with step()`

        Args:
            RuntimeError: If step() is called on a simulation that has not started yet
                or has already been stopped.
        """
        if not self.started:
            raise RuntimeError(
                "Cannot step a simulation that has not started or already stopped."
            )
        self.client.step_sim(self.name)

    @property
    def entities(self) -> List[Entity]:
        """Get gRPC entities to this Simulation.
        Returns:
            List of entities belonging to this Simulation
        """
        return list(self.entity_map.values())

    def __enter__(self):
        self.start()

    def __exit__(self, exc_type, exc_value, traceback):
        self.stop()
        # never suppress exceptions inside with statement
        return False

    def __repr__(self):
        return f"{type(self).__name__}<{self.name}>"

    def __hash__(self):
        return hash(self.name)

Static methods

def from_def(sim_def: SimulationDef, client: Client)

Create/Hydrate a Simulation from a bento.spec.SimulationDef.

Args

sim_def
SimulationDef specification to load the Simulation from.
client
Client to use to communicate with the Engine.
Expand source code
@classmethod
def from_def(cls, sim_def: SimulationDef, client: Client):
    """
    Create/Hydrate a Simulation from a `bento.spec.SimulationDef`.

    Args:
        sim_def: SimulationDef specification to load the Simulation from.
        client: Client to use to communicate with the Engine.
    """
    return cls(
        name=sim_def.name,
        components=sim_def.component_defs,
        entities=sim_def.entity_defs,
        system_fns=sim_def.system_fns,
        init_fn=sim_def.init_fn,
        client=client,
    )

Instance variables

var entities : List[Entity]

Get gRPC entities to this Simulation.

Returns

List of entities belonging to this Simulation

Expand source code
@property
def entities(self) -> List[Entity]:
    """Get gRPC entities to this Simulation.
    Returns:
        List of entities belonging to this Simulation
    """
    return list(self.entity_map.values())

Methods

def build(self, include_graphs: bool = True) ‑> SimulationDef

Build a bento.eachproto.sim_pb2.SimulationDef Proto from this Simulation.

Args

include_graphs
Whether to compile & include graphs in the returned Proto. This requires that id to be set for each entity as entity ids are required for graph compilation to work.

Returns

The bento.proto.sim_pb2.SimulationDef Proto equivalent of this Simulation.

Expand source code
def build(self, include_graphs: bool = True) -> sim_pb2.SimulationDef:
    """
    Build a `bento.eachproto.sim_pb2.SimulationDef` Proto from this Simulation.

    Args:
        include_graphs: Whether to compile & include graphs in the returned Proto.
            This requires that id to be set for each entity as entity ids
            are required for graph compilation to work.
    Returns:
        The `bento.proto.sim_pb2.SimulationDef` Proto equivalent of this Simulation.
    """
    # compile graphs if requested to be included
    system_defs, init_graph = [], Graph()
    if include_graphs:
        compile_fn = lambda fn: compile_graph(
            fn, self.entity_defs, self.component_defs
        )
        # compile systems graphs
        system_defs = [
            SystemDef(
                graph=compile_fn(fn),
                system_id=system_id,
            )
            for fn, system_id in self.system_fns
        ]
        # compile init graph
        init_graph = (
            compile_fn(self.init_fn) if self.init_fn is not None else Graph()
        )

    return sim_pb2.SimulationDef(
        name=self.name,
        entities=[e.proto for e in self.entity_defs],
        components=[c.proto for c in self.component_defs],
        systems=[s.proto for s in system_defs],
        init_graph=init_graph.proto,
    )
def entity(self, components: Iterable[str]) ‑> Entity

Lookup the gRPC entity with the components with the game attached.

Provides access to ECS entity on the Bentobox Engine via gRPC.

Args

components
Set of the names of the component attached that should be attached to the retrieved component.

Raises

ValueError
if component names given contains duplicates
RuntimeError
If Simulation has not yet stated.

Returns

The gRPC entity with the given list of components attached.

Expand source code
def entity(self, components: Iterable[str]) -> Entity:
    """Lookup the gRPC entity with the components with the game attached.

    Provides access to ECS entity on the Bentobox Engine via gRPC.

    Args:
        components: Set of the names of the component attached that
            should be attached to the retrieved component.
    Raises:
        ValueError: if component names given contains duplicates
        RuntimeError: If Simulation has not yet stated.
    Returns:
        The gRPC entity with the given list of components attached.
    """
    if not self.started:
        raise RuntimeError(
            "Cannot obtain a gRPC Entity from a Simulation that has not started yet."
        )
    comp_set = frozenset([str(c) for c in components])
    # check for duplicates in given components
    if len(comp_set) != len(list(components)):
        raise ValueError("Given component names should not contain duplicates")
    return self.entity_map[comp_set]
def init(self, init_fn: Callable[[Plotter], NoneType])

Register given init_fn as the init graph for this simulation.

The init graph allows for the initization of attribute's values, running on the simulation first step() call, before any systems run.

Compiles the init_fn into a computational graph and registers the result as a init graph for this Simulation.

Example:
@sim.system
def system_fn(g):
    # ... implementation of the system ..

Args

system_fn
Function that contains the implementation of the system. Must be compilable by compile_graph().
Expand source code
def init(self, init_fn: ConvertFn):
    """Register given init_fn as the init graph for this simulation.

    The init graph allows for the initization of attribute's values,
    running on the simulation first step() call, before any systems run.

    Compiles the `init_fn` into a computational graph and registers the
    result as a init graph for this Simulation.

        Example:
        @sim.system
        def system_fn(g):
            # ... implementation of the system ..

    Args:
        system_fn: Function that contains the implementation of the system.
            Must be compilable by `compile_graph()`.
    """
    self.init_fn = init_fn
def start(self)

Starts this Simulation on the Engine.

If already started, calling start() again does nothing.

Expand source code
def start(self):
    """Starts this Simulation on the Engine.

    If already started, calling `start()` again does nothing.
    """
    # do nothing if already started
    if self.started:
        return
    # commit entire simulation to (ie including systems/init graph added) to engine
    applied_proto = self.client.apply_sim(self.build(include_graphs=True))
    # obtain autogen ids for systems from the engine by recreating the applied proto
    current_sys_fns = [system_fn for system_fn, _ in self.system_fns]
    self.system_fns = [
        # update system ids for systems by position
        (fn, system_def.id)
        for fn, system_def in zip(current_sys_fns, applied_proto.systems)
    ]
    self.started = True
def step(self)

Run this simulation for one step

Runs this simulation's systems in the order they are registered. Blocks until all systems of that simulation have finished running.

The Simulation must have already started before running the simulation with step()`

Args

RuntimeError
If step() is called on a simulation that has not started yet or has already been stopped.
Expand source code
def step(self):
    """Run this simulation for one step

    Runs this simulation's systems in the order they are registered.
    Blocks until all systems of that simulation have finished running.

    The Simulation must have already started before running the simulation with step()`

    Args:
        RuntimeError: If step() is called on a simulation that has not started yet
            or has already been stopped.
    """
    if not self.started:
        raise RuntimeError(
            "Cannot step a simulation that has not started or already stopped."
        )
    self.client.step_sim(self.name)
def stop(self)

Stops and removes this Simulation from the Engine.

Raises

RuntimeError
If stop() is called on a simulation that has not started yet.
Expand source code
def stop(self):
    """Stops and removes this Simulation from the Engine.
    Raises:
        RuntimeError: If stop() is called on a simulation that has not started yet.
    """
    if not self.started:
        raise RuntimeError("Cannot stop a Simulation that has not started yet.")
    # cleanup by remove simulation from engine
    self.client.remove_sim(self.name)
    self.started = False
def system(self, system_fn: Callable[[Plotter], NoneType])

Register a ECS system with the given system_fn on this Simulation.

ECS Systems are run every step of simulation and encapsulate the logic of the simulation.

Compiles the system_fn into a computational graph and registers the result as a ECS system to run on this Simulation.

Example:
@sim.system
def system_fn(g):
    # ... implementation of the system ..

Args

system_fn
Function that contains the implementation of the system. Must be compilable by compile_graph().
Expand source code
def system(self, system_fn: ConvertFn):
    """Register a ECS system with the given system_fn on this Simulation.

    ECS Systems are run every step of simulation and encapsulate the logic of the simulation.

    Compiles the `system_fn` into a computational graph and registers the
    result as a ECS system to run on this Simulation.

        Example:
        @sim.system
        def system_fn(g):
            # ... implementation of the system ..

    Args:
        system_fn: Function that contains the implementation of the system.
            Must be compilable by `compile_graph()`.
    """
    # 0 to signify unset system id
    self.system_fns.append((system_fn, 0))