Expand description
§gwr-components
Simulation components are the basic building blocks of any GWR model.
The GWR Engine runs components that are connected together using ports.
The gwr_components library provides a collection of connectable component
primitives to be used when building models.
§Creating New Components
Components are designed to be composable and connectable simulation blocks. When creating a new one it is important to consider all of the following steps:
- Design the component
- Create a struct
- Add ports
- Create subcomponents
- Implement any custom functionality
- Provide default implementations for other methods.
This documentation will take you through designing a custom component that will be used to drop a random number of objects that pass through it.
§Design the Component
There are a number of things to consider when designing a new simulation component. The two main aspects are
§Component Interfaces
An interface will comprise one or more ports and define how a component connects to and interacts with other components.
So it is first essential to define the types of interfaces a component will have and how many of each there will be. Then, the required ports can be created.
A port has a flow of data. The general naming convention is:
- Where data flows in to a component it is a receive port (
rx). - Where data flows out of a component it is a transmit port (
tx).
§Component Functionality
Some components are simply collections of other components plugged together. In
most cases, however, it will be necessary to define custom functionality for the
port. This includes how the ports handle data they send/receive as well as
general activity that can be spawned in the run() function.
§Create a Struct
The first thing to define when creating a component is to create the structs that define the component.
All components should contain an Entity which is used to configure the logging
and also to give a unique location within the model hierarchy. The Entity will
be wrapped in std::rc::Rc so that it can be shared.
GWR components run on the engine’s single-threaded async executor, so shared
component state is normally built from Rc, RefCell, and Cell rather than
Arc and locks. This is intentional: it lets setup code, spawned tasks, and
small helper objects share local mutable state without adding the overhead of
thread synchronisation to the simulation model.
#[derive(EntityGet, EntityDisplay)]
struct MyComponent<T>
where
T: SimObject
{
entity: Rc<Entity>,
// Any component-specific state
}§Ports
A component will have a number of ports which provide its interfaces to other components.
§Output / Input
Ports can either be “output” or “input”. A connection must always be made between one output and one input port.
§Data Types
The type of the port is specialised by the data type that it carries. Ports have to be of the same type to be connected together.
§Component Ports
Components provide functions that allow the connection of their ports. Ports can either be connected directly to a component or to a subcomponent. It is therefore up to the component writer to provide the relevant functions and connect the ports as required.
Ports are commonly stored as RefCell<Option<InPort<T>>> or
RefCell<Option<OutPort<T>>>. During setup, connection methods borrow the
component through &self and connect the port state. When the component’s
run() task starts, it takes the port out of the Option and owns it for the
life of that task. Connectivity is checked separately when the task first uses
the port. If the Option is already empty, the port has previously been taken,
usually because run() was invoked more than once or ownership was transferred
too early.
There are two forms of port connections: those that name a single port and those that take an index into a port array. Each function has a unique name based on the port name and the direction of data flow.
Input port functions are of the form port_<NAME> or port_<NAME>_i for ports
that expose an array of connections. Output port functions are of the form
connect_port_<NAME> and connect_port_<NAME>_i.
Some examples are provided below.
§Input Ports
The method for an input port will return a shared reference to a shared state that is then passed to the output to complete the connection.
Here are a few examples:
A component with a single input port called rx will have:
pub fn port_rx(&self) -> PortStateResult<T>A component with an array of input ports called in will have:
pub fn port_in_i(&self, i: usize) -> PortStateResult<T>§Output Ports
Output ports are connected by passing in the shared state that both sides of the
interface use. If the port is already connected then a panic! will be raised.
A component with a single output port called tx will have:
pub fn connect_port_tx(&self, port_state: PortStateResult<T>) -> SimResultA component with an array of output ports called out will have:
pub fn connect_port_out_i(&self, i: usize, port_state: PortStateResult<T>) -> SimResult§Connecting Ports
Connections are always made in the direction of flow of data (tx -> rx). For
example:
let mut source = Source::new_and_register(&engine, engine.top(), "source", option_box_repeat!(0x123 ; num_puts));
let sink = Sink::new_and_register(&engine, &clock, engine.top(), "sink");
connect_port!(source, tx => sink, rx)
.expect("should be able to connect `Source` to `Sink`");
}§Errors
If attempting to connect ports that don’t exist on the source/dest components then there will be a compile error.
let mut source = Source::new_and_register(&engine, engine.top(), "source", option_box_repeat!(0x123 ; num_puts));
let sink = Sink::new_and_register(&engine, &clock, engine.top(), "sink");
connect_port!(source, tx => sink, invalid)
.expect("should be able to connect `Source` to `Sink`");§Create Subcomponents
A component is the building block of models. It will have ports and usually comprise subcomponents and some extra logic.
The examples/flaky-with-delay gives an example of a simple component that uses an existing subcomponent.
§Implement Custom Functionality
Each component must implement the Runnable trait which allows it to be
registered with the Engine to ensure that it is run when the simulation
starts.
The async run(&self) method is defined by all components that provide custom
functionality.
Currently this relies on the #[async_trait(?Send)] support for async traits.
The (?Send) decoration indicating that only single-threaded support is
required.
#[async_trait(?Send)]
impl<T> Runnable for MyComponent<T> where T: SimObject {
async fn run(&self) -> SimResult {
// Implement custom-functionality
// Return result - Ok unless there is an error to raise
Ok(())
}
}The examples/flaky-with-delay gives an example of a component that uses
custom run() functionality.
§Default Functionality
If the new component does not need to have any custom behaviour and is simply
connecting a collection of sub-components then it can implement just use the
default Runnable provided by the library with a derive statement.
#[derive(Runnable)]
struct MyComponent<T>
where
T: SimObject
{
// Component members
}§Testing
Components can be tested by connecting them into a small simulation and driving
their ports directly. For simple cases this can be done by hand with
OutPort/InPort, but most component tests need the same testbench structure:
- Create an engine and the device under test (DUT).
- Connect driver ports to DUT input ports.
- Connect receiver ports to DUT output ports.
- Run a sequence of sends, expects, delays, and no-traffic checks.
The build_component_harness! macro will generate the repeated testbench code.
It generates the harness struct, Port/Step enums, helper macros, etc.
Harnesses are usually declared inside a small test module. This keeps generated
names such as Port, Step, and the helper macros local to the harness and
avoids clashes with other harnesses in the same test file.
For example, the harness around a Delay component is created and used below:
mod delay_harness {
use std::rc::Rc;
use gwr_components::build_component_harness;
use gwr_components::delay::Delay;
use gwr_engine::test_helpers::start_test;
build_component_harness! {
harness DelayHarness<T> {
component: delay: Rc<Delay<T>>,
rx ports: {
Rx<T> => rx,
},
tx ports: {
Tx<T> => tx,
},
}
}
#[test]
fn delay_forwards_values() {
let mut engine = start_test(file!());
let clock = engine.default_clock();
let delay = Delay::new_and_register(&engine, &clock, engine.top(), "delay", 5).unwrap();
let mut harness = DelayHarness::new(engine, delay);
harness.run_steps([
send_rx!(10),
expect_no_traffic!(&[Port::Tx], 4),
expect_tx!(10),
]);
}
}The macro supports scalar RX/TX ports and RX/TX port arrays. Each port section
is optional, so a source-only component can define only tx ports and a
sink-only component can define only rx ports.
Step can be a send, expect, delay, no-traffic check, Seq(Vec<Step<...>>)
that runs child steps in order, or Par(Vec<Step<...>>) that runs child steps
concurrently and waits for all of them before moving on. The generated seq!
and par! helper macros build those recursive control structures and record
their source location, so tests can express parallel sequences on different
ports while keeping error messages tied to the call site.
The harness checks that each step is used on a compatible port; for example, using an expect step on an RX port or a send step on a TX port will fail the test.
Use run_steps([Step<...>]) for fixed test sequences and
run_step_generator(iterator) for stateful generators that yield steps as the
test progresses.
Modules§
- arbiter
- Perform arbitration between a number of interfaces.
- capacity_
allocator - Capacity accounting and scoped reservations.
- cli
- connect
- Helper connection macros.
- delay
- A component that adds
delay_ticksbetween receiving anything and sending it on to its output. - flow_
controls - Components used for flow-control.
- queue
- Generic queues.
- router
- Perform routing between an input interface and a number number of outputs.
- sink
- A data sink.
- source
- A data source.
- state_
machine - State-machine builders.
- store
- A data store.
- test_
helpers - types
- Shared types.
Macros§
- borrow_
option - Get a reference to a variable stored in a
RefCell<Option<>>. - borrow_
option_ mut - Get a mutable reference to a variable stored in a
RefCell<Option<>>. - build_
component_ harness - Build a simulation test harness around a component.
- connect_
dummy_ rx - Create and connect a dummy RX port.
- connect_
dummy_ tx - Create and connect a dummy TX port.
- connect_
port - Connect an OutPort port to an InPort
- connect_
tx - Connect a tx port for a subcomponent.
- connect_
tx_ i - Connect a tx port for a subcomponent where the port is one of an array.
- create_
state_ machine - Build a state enum, transition metadata, runtime transition checks, and typestate proof helpers.
- option_
box_ chain - option_
box_ repeat - option_
rc_ limiter - port_rx
- Access rx port for a subcomponent.
- port_
rx_ i - Access an individual index of an rx port array for a subcomponent.
- rc_
limiter - Create a RateLimiter wrapped in an Rc.
- take_
option - Take a variable out of a
RefCell<Option<>>.