Skip to main content

Edge-Deployable Simulators (EDS)

Introduction

Sedaro's simulator compiles input models and libraries to executable EDS's (Edge Deployable Simulators) for maximal performance in resource-constrained environments. This document describes how to execute and configure a generated EDS. Significant changes to a scenario or its models will require re-generation of the EDS, which is outside the scope of this document. We'll assume the EDS has a filename of eds below.

# Example: View up-to-date eds-specific documentation
eds --help
# Example: Run an eds
eds

Users and drivers like SAFE invoke EDS's with initial state and additional configuration. During the course of execution, data may be exchanged bidirectionally via cosimulation and data frames may be egressed via targets.

Background

An EDS is a simple executable that simulates a scenario for a period of time. It takes initial values that describe the various agents in the simulation and it propagates them through time. At each time step, it outputs a set of Frames to the Target that is configured at runtime. Each Frame encompasses the state of some agent at a given time and Frames are composed into continuous Streams.

The lifecycle of an EDS roughly looks like:

  1. Run an EDS with customer-provided initial values via the interface described in the Patching section
  2. While the EDS process is running. read the output Frames from the Stream files written to disk and deserializes them to read and aggregate instantaneous agent state
  3. After the EDS process exits. read the last output Frame from the Stream files written to disk and deserializes it to read terminal agent state (the state at the end of the simulation)

This procedure can then be run any number of times in parallel and/or serially as an EDS consumer requires.

Quickstart

Run an EDS from MJD 61091 to 61092

eds --start 61091 --end 61092

Extract certain outputs (time and ECI position in this case) and display as CSV

eds --start 61091 --end 61092 \
| jq -r '[.content.enqueue.time, .content.enqueue.frame."root.pointing_error"] + .content.enqueue.frame."root.position" | @csv'

Render data in Excel

eds --start 61091 --end 61092 \
| jq -r '[.content.enqueue.time] + .content.enqueue.frame."root.position" | @csv' \
> tmp.csv && open tmp.csv

Compute stats on an output stream

eds --start 61091 --end 61092 \
| jq -r 'select(.content.enqueue.frame) | .content.enqueue.frame."root.pointing_error"' \
| awk '{sum+=$1; sumsq+=$1*$1; if(NR==1){min=$1;max=$1} if($1<min){min=$1} if($1>max){max=$1}} END{print "Min:", min, "\nMax:", max, "\nMean:", sum/NR, "\nStdDev:", sqrt(sumsq/NR - (sum/NR)^2)}'

Time Control

The start and end times of the simulations can be configured with the --start and --end flags respectively. These both accept a time in MJD (Modified Julian Date) format.

Alternatively, the --duration flag can be used to specify the duration of the simulation in days. If both --end and --duration are specified, the simulation will stop at the earlier of the two times. If neither is specified, the simulation will run indefinitely.

# Example: Run the simulation from MJD 60000 to MJD 60001
eds --start 60000 --end 60001
# Example: Run the simulation from MJD 60000 for 1.5 days
eds --start 60000 --duration 1.5
# Example: Run the simulation from MJD 60000 indefinitely
eds --start 60000

Realtime and Synctime Modes

By default, EDS's run deterministically and as fast as possible. However, two modes are available to control the simulation time relative to real-world time:

Realtime Mode

The --realtime flag enables realtime mode, where the simulation runs at some multiple of real-world time. The flag accepts a floating-point multiplier, where 1.0 means the simulation runs in real time. The simulation will start immediately. If the EDS fails to keep up with real time for a duration of execution, it will run as fast as possible until it catches back up.

# Example: Run the simulation in realtime mode at normal speed
eds --realtime 1.0
# Example: Run the simulation in realtime mode at 10x speed
eds --realtime 10.0

Synctime Mode

The --synctime flag enables sync time mode, where the simulation time is synchronized exactly with real-world time. The simulation will pause between each round of computation until real-world time catches up. If a start time in the future is specified with --start, the simulation will wait until that time to start executing. If a start time in the past is specified, the simulation will start immediately and operate at full speed until it catches up to real time.

# Example: Run the simulation in synctime mode
eds --synctime

Target Configuration

EDS's are built with a given Target implementor compiled in, responsible for the egress of simulated data. Each Target implementation provides a custom configuration format. For example, a logging Target may accept a log level like info or debug, and a gRPC Target may accept a server address and port. Information about a given EDS's target configuration can be obtained by running the EDS with the --help flag. This configuration is passed to the EDS at runtime via the --target-config flag as a string.

# Example: Get information about the EDS's target configuration
eds target
# Example: Print out the EDS's frame type
eds frame
# Example: Run the simulation while logging output at INFO level
eds --target-config info

The details of defining custom Target implementations is outside the scope of this document. If no config is given, the default is used, which is typically JSON. This allows for parsing via jq as demonstrated below:

eds --start 61091 --end 61092 | jq -r '[.content.enqueue.time, .content.enqueue.frame."root.pointing_error"] + .content.enqueue.frame."root.position" | @csv'

All outputs from an eds can be discovered by running:

eds --duration 0.01 | jq -r '.content.enqueue.frame | [paths(scalars)] | map("content.enqueue.frame." + join(".")) | .[]' | sort -u

For programmatic consumption, running the eds frame subcommand will print just the frame type signature to stdout. The --raw flag will print out just the raw unrefined type which is sufficient for serialization. The --width flag can be used to pretty-print the type with a specified width for human readability.

Initialization

EDS's require initialization data to set up the initial state of the simulation. Each EDS requires a specific data type for its initialization data, which can be obtained by running the EDS with the --help flag. The initial value is passed to the EDS at runtime via the --init. The argument to this flag may be one of:

  • A path to a binary file (with .bin extension) containing the initialization data in serialized form
  • A path to a text file (with .txt extension) containing the initialization data in syntactic form
  • A string containing the initialization data in syntactic form
# Example: Print out the EDS's initialization type
eds init
# Example: Run the simulation with initialization data from `init.bin`
eds --init init.bin
# Example: Run the simulation with explicit initial values
eds --init '(9.8, "green")'

For programmatic consumption, running the eds init subcommand will print just the initialization type signature to stdout. The --raw flag will print out just the raw unrefined type which is sufficient for serialization. The --width flag can be used to pretty-print the type with a specified width for human readability.

Patching

A common use case for using EDS, such as in Monte Carlo or Optimization workloads, is to run multiple simulations with slightly different initial conditions. To facilitate this, EDS's support the concept of "patching" initialization data. A patch is a partial initialization data structure that only specifies the fields to be modified from a base initialization data structure. These patches are themselves well-typed. Patches may be provided to the EDS at runtime via the --patch flag which accepts two arguments: a type and a value. The value may be specified in the same way as initialization data (i.e. binary file, text file, or string). The type must be one of:

  • A path to a text file (with .txt extension) containing the patch type in syntactic form
  • A string containing the patch type in syntactic form

When a patch is provided, the EDS will first load the base initialization data specified by --init, then apply the patch to it, and finally use the resulting initialization data to start the simulation.

# Example: Override initial agent position
# Note that `PTnYWzsc2Nhywc8WVS4blm` is the Agent ID.
# This is the address used to patch a specific agent's initial state.
eds -p '(PTnYWzsc2Nhywc8WVS4blm: (gnc: ("root!.position": {(float, float, float) | #, eci},),),)' '((((42000.0, 0, 0),),),)'

# Example: Run the simulation with initialization data from `init.bin` but with the moon's position and velocity patched
eds --init init.bin --patch '(moon: (pos: #[m; 3], vel: #[m/s; 3]),)' patch.bin
# Example: The same as above by with explicit initial values
eds --init init.bin --patch '(moon: (pos: #[m; 3], vel: #[m/s; 3]),)' '((100, 0, 0), (0, 10, 0))'

The type signature of the init interface of an eds can be discovered by running:

eds init --width 200

SedaroTS

SedaroTS is Sedaro's type system for simulation values. Some of its key features are summarized here for reference:

  • Algebraic data types including product types, sum types, lists, maps, and standard primitives
  • Support for dimensional analysis with SI units and unit conversions
  • Support for tensor math with vectors and matrices
  • Additional "refinements" for restricting values to subsets of their base types
  • A syntax for representing types and values in text form
  • A compact binary serialization format for values
  • Language bindings and embeddings for Rust, Python, and WASM
# Example: Use the python bindings to SedaroTS to convert 60 miles per hour to meters per second
>>> Type('mph').td(60).convert(Type('m/s')).py()
26.822333333333333

Inter-Process Communication (IPC)

Sedaro is actively designing a multi modal IPC interface for running EDS processes to communicate with the outside world. This interface will be bi-directional such that external processes can push data into a running simulation and/or pull data out. We expect to implement multiple schemes for data egress, including both polling and streaming. Additionally, multiple transports will be supported, including unix domain and TCP sockets as well as gRPC. This API is under active development. More details to follow in a future release of this document.

Error Handling

If a simulation error occurs during the execution of a simulation, the simulation will eventually exit with a non-zero exit code. Simulators ensure deterministic error handling across asynchronous engines by allowing all engines to run until the simulation time at which the earliest error has been detected. This ensures that the earliest error is always raised, with ties broken lexicographically by agent and engine names.

Note that proper error handling can't be ensured in the case of a runtime panic. These could be caused by either a bug in the simulator or the functions which it invokes, or an OS issue such as insufficient memory. Additionally, determinism isn't guaranteed when cosimulation is used.

CodeMeaningDeterministic
0Sim ran to end time successfully
1Sim ended early w/ unspecified error
2-100Sim ended early w/ specified error*
101Sim panicked (defined by Rust)
128+Sim terminated by OS signal

*Documentation for specified error codes is being developed.

Dev Tools

The documentation below can be helpful for debugging EDS's but is not required for normal operation and is not a stable API.

Logging

For a look under the hood at what an EDS is doing, it can be helpful to enable logging. This is done by setting the RUST_LOG environment variable to one of: error, warn, info, debug, or trace.

# Example: Run the simulation with debug logging enabled
RUST_LOG=debug eds --duration 0.1