Skip to content

Simulator

phyelds.simulator is a small discrete-event simulator for running aggregate programs on many devices in one process. It is enough for experiments and teaching; for physics or robotics, see integration.

The pieces

Piece Module Role
Simulator phyelds.simulator event queue and clock; run(until)
Environment, Node phyelds.simulator the devices: position, data (sensors and results)
deployments phyelds.simulator.deployments place nodes: grid_generation, deformed_lattice, random_in_circle, random_walk
neighborhoods phyelds.simulator.neighborhood who is a neighbor: radius_neighborhood(r), k_nearest_neighbors(k), full_neighborhood
runner phyelds.simulator.runner schedule_program_for_all(simulator, start, frequency, program)
actions phyelds.simulator.actions recurring movements: move_with_velocity, gaussian_movement
monitors phyelds.simulator / .render observe the run: Monitor, RenderMonitor
exporter phyelds.simulator.exporter csv_exporter writes aggregated outputs to CSV

A run, step by step

from phyelds.simulator import Simulator
from phyelds.simulator.deployments import random_in_circle
from phyelds.simulator.neighborhood import radius_neighborhood
from phyelds.simulator.runner import schedule_program_for_all

simulator = Simulator()
random_in_circle(simulator, num_nodes=100, radius=10)
simulator.environment.set_neighborhood_function(radius_neighborhood(2.0))
simulator.environment.add_data_for_all_nodes({"source": False})   # sensors
simulator.environment.nodes[0].data["source"] = True

schedule_program_for_all(simulator, start=0.0, frequency=1.0, program=program)
simulator.run(50)

Each node's data is both its sensors (read with sense(name); position and time are added by the runner) and where the runner writes, after each round:

Key Content
result the value returned by the program
outputs the values written with store(name, value)
messages, state the runner's bookkeeping: do not edit

To change a sensor during a run, schedule an event that writes it:

def turn_off_source(node):
    node.data["source"] = False

simulator.schedule_event(25.0, turn_off_source, simulator.environment.nodes[0])

Warning

node.update(new_data=...) replaces the whole data dictionary, including the program's state. To change one sensor, assign the key.

Watching a run

RenderMonitor draws the network with a list of effects, and either shows it live (RenderMode.SHOW), or saves an animation (RenderMode.SAVE) plus a final PNG. A .gif file name is enough to get an animated GIF; .mp4 needs ffmpeg. Showing it live inside a Jupyter notebook needs the jupyter extra (pip install "phyelds[jupyter]").

from phyelds.simulator.effects import DrawEdges, DrawNodes, DrawValues, RenderConfig, RenderMode
from phyelds.simulator.render import RenderMonitor

RenderMonitor(simulator, RenderConfig(
    effects=[
        DrawEdges(alpha=0.3),
        DrawNodes(color_from="result", cmap="plasma_r", vmin=0, vmax=20),
        DrawValues(value_from="result", precision=1),
    ],
    dt=1.0,                  # simulated time between two frames
    mode=RenderMode.SAVE,
    save_as="run.gif",
    show_axis=False,
    title="My field",
))
Effect Draws
DrawEdges(alpha) the links between neighbors
DrawNodes(color_from, cmap, vmin, vmax, missing_color) the nodes; with cmap, coloured by the number in data[color_from] (missing or infinite values in missing_color); without it, data[color_from] is used as a colour
DrawValues(value_from, precision, offset) the value next to each node (∞ for infinity)
DrawIDs() the node ids

Fix vmin and vmax when you animate a field: otherwise the colour scale follows the values in each frame, and the same colour means different values over time.

A RenderConfig is a pydantic model: config.save(path) and RenderConfig.load(path) store it as JSON.

Exporting metrics

csv_exporter is an event that, every time_delta, aggregates the outputs named in metrics over all nodes (with mean, min or max) and appends a row to <output_directory><experiment_name>.csv. The directory is joined as a string, so end it with /:

from phyelds.simulator.exporter import ExporterConfig, csv_exporter

config = ExporterConfig(
    output_directory="results/",
    experiment_name="gradient",
    metrics=["distance"],             # names written with store(...)
    aggregators=["mean", "max"],
    precision=3,
)
simulator.schedule_event(1.0, csv_exporter, simulator, 1.0, config)