Getting Started with FERAL¶
This guide will help you get started with creating a sample simulation using the FERAL framework. By following the step-by-step instructions, you will learn how to build a complex simulation that leverages FERAL's powerful features. With this simulation, you will be able to:
- Validate the system against faulty behavior: Ensure your system behaves correctly even in the presence of faults.
- Simulate network communications: Model and analyze communication protocols and network interactions.
- Connect an external simulation component via the FERAL Co-Simulation API (FCAPI): Integrate external simulation tools to enhance your simulation capabilities.
This tutorial is designed to provide you with a comprehensive understanding of FERAL's capabilities and to empower you to create sophisticated simulations tailored to your specific needs. Let's dive in and start building your first FERAL simulation!
Create an Environment¶
First, we need to set up our development environment. Follow these steps:
Install the Prerequisites:
- Follow the instructions described here to install all necessary prerequisites.
Create a Virtual Environment and Install the FERAL Python API:
- Open your terminal or command prompt.
- Navigate to your project directory.
- Create a virtual environment:
python -m venv feral_env
- Activate the virtual environment:
- On Windows:
.\feral_env\Scripts\activate
- On macOS/Linux:
source feral_env/bin/activate
- On Windows:
- Install the FERAL Python API from a local wheel:
pip install path_to_your_wheel_file.whl
Create the Main Script¶
Now, let's create the main script for our simulation.
Minimal Simulation Script: To verify if the installation was successful, create a script that starts the simulation with minimal configuration.
from feral3gp import feral
feral.start()
Run the script:
python main.py
If the result looks like this:
FERAL (3rd Gen) Version 1.0.0
INFO: starting the simulation...
INFO: simulation finished!
Then the installation was correct and you have successfully run the first FERAL simulation.
Add Simulation Logic¶
The log is relatively empty since there are no events happening. In order to change that, we will add some logic and simulation components.
Add Value Producer and Sink: In this section, we will add a Value Producer and a Sink that receives the messages sent.
Update your main.py
:
from feral3gp import feral
from feral3gp.feral.presets.producer.value_producer import ValueProducer
from feral3gp.feral.presets.sink.sink import Sink
producer = ValueProducer("Sender")
sink = Sink("Receiver")
The ValueProducer
has the default behavior to send an event every second. The Sink
prints out the received value.
Connect Components:
Link the ValueProducer
's output/transmitting port (TxPort) to the Sink
input/receiving port (RxPort).
Update your main.py
:
feral.link(producer, "output", sink, "input")
Adjust Simulation Duration¶
Now, adjust the duration of the simulation.
Update your main.py
:
feral.start(feral.seconds(1))
Info
Note, that the duration is the simulated time in a discrete event simulation, not the wall-clock time. For a detailed description of the time progression, refer to Discrete Event MOCC.
Run the Simulation¶
The script should look like this:
from feral3gp import feral
from feral3gp.feral.presets.producer.value_producer import ValueProducer
from feral3gp.feral.presets.sink.sink import Sink
producer = ValueProducer("Sender")
sink = Sink("Receiver")
feral.link(producer, "output", sink, "input")
feral.start(feral.seconds(1))
Simulation Structure
The Simulation has a default Scenario
and a root director preconfigured, which can be seen in the image above.
ValueProducer
and Sink
are nested inside a Discrete Event Director
.
Finally, run the simulation with:
python main.py
Expected Result:
FERAL (3rd Gen) Version 1.0.0
INFO: starting the simulation...
INFO: (Receiver) Receiving 0
INFO: (Receiver) Receiving 1
INFO: (Receiver) Receiving 2
INFO: (Receiver) Receiving 3
INFO: (Receiver) Receiving 4
INFO: (Receiver) Receiving 5
INFO: (Receiver) Receiving 6
INFO: (Receiver) Receiving 7
INFO: (Receiver) Receiving 8
INFO: (Receiver) Receiving 9
INFO: (Receiver) Receiving 10
INFO: simulation finished!
Congratulations! You have successfully created and run a basic FERAL simulation. In the next sections, we will explore more advanced features and extend this simulation further.
Add a Network Configuration¶
In current state the messages send from the ValueProducer
are immediately received by the Sink
, without any delay.
FERAL offers several Network Simulations out of the box to simulate the communication through these networks.
A very common protocol is the CAN Bus , which we are now adding to the simulation.
Modify you script as below:
from feral3gp import feral
from feral3gp.feral.presets.network.can import two_nodes_can
from feral3gp.feral.presets.producer.value_producer import ValueProducer
from feral3gp.feral.presets.sink.sink import Sink
producer = ValueProducer("producer", interval=feral.seconds(1))
sink = Sink(name="Sink")
can = feral.can(config=two_nodes_can.config)
feral.link(producer, "output", can, "node0_tx")
feral.link(can, "node1_rx", sink, "input")
feral.start(feral.seconds(10))
The new simulation script imports a preconfigured CAN Bus simulation with two nodes.
Then execute again.
Expected Results:
FERAL (3rd Gen) Version 1.0.0
INFO: starting the simulation...
INFO: (CANMedium) CAN Bus (unnamed Network): [268250 ns] - ID:2 | Value: 0
INFO: (Sink) Receiving 0
INFO: (CANMedium) CAN Bus (unnamed Network): [1000248250 ns] - ID:2 | Value: 1
INFO: (Sink) Receiving 1
INFO: (CANMedium) CAN Bus (unnamed Network): [2000248250 ns] - ID:2 | Value: 2
INFO: (Sink) Receiving 2
INFO: (CANMedium) CAN Bus (unnamed Network): [3000248250 ns] - ID:2 | Value: 3
INFO: (Sink) Receiving 3
INFO: (CANMedium) CAN Bus (unnamed Network): [4000248250 ns] - ID:2 | Value: 4
INFO: (Sink) Receiving 4
INFO: (CANMedium) CAN Bus (unnamed Network): [5000248250 ns] - ID:2 | Value: 5
INFO: (Sink) Receiving 5
INFO: (CANMedium) CAN Bus (unnamed Network): [6000248250 ns] - ID:2 | Value: 6
INFO: (Sink) Receiving 6
INFO: (CANMedium) CAN Bus (unnamed Network): [7000248250 ns] - ID:2 | Value: 7
INFO: (Sink) Receiving 7
INFO: (CANMedium) CAN Bus (unnamed Network): [8000248250 ns] - ID:2 | Value: 8
INFO: (Sink) Receiving 8
INFO: (CANMedium) CAN Bus (unnamed Network): [9000248250 ns] - ID:2 | Value: 9
INFO: (Sink) Receiving 9
INFO: simulation finished!
The messages are now passed through the CAN Bus simulation, changing the structure like so:
Create Custom Workers¶
In order to further analyze the simulation we will now add custom workers.
Create two new files: my_sink.py
and my_value_producer.py
and add these implementations:
from feral3gp import feral
class MySink(feral.EventTriggeredWorker):
def __init__(self, name):
super().__init__(name)
def message_received(self, event, rx_port):
rx_value = event.getValue()
self.get_console().info(f"{rx_value}")
The MySink
inherits from the feral.EventTriggeredWorker
and implements just the message_received
method.
It is triggered, when an event is delivered to the worker through any input port (RxPort
)
An event has a value that can be extracted with the .getValue()
method.
from feral3gp import feral
class MyValueProducer(feral.EventTriggeredWorker):
def __init__(self, name: str, parent=feral.Simulation.director_de, interval=feral.millis(100)):
super().__init__(name, parent)
self.interval = interval
def initialize(self):
self.schedule_timer(0, 42)
def timer_expired(self, timed_event):
tx_value = timed_event.getValue()
self.out_port.send(feral.event(tx_value))
self.schedule_delta_timer(self.interval, 42)
The MyValueProducer
also inherits from the feral.EventTriggeredWorker
and implement the initialize
method, that is called when the simulation starts, but before the simulation time starts progressing.
The timer_expired
method is triggered, when a schedule event is ready to be executed.
In this case, an initial event is scheduled in the initialize
call.
Subsequent events are then scheduled with the specified delay
with the schedule_delta_timer
method.
Create Custom CAN Configuration¶
Create a new file for the CAN configuration: can_config.py
:
from feral3gp import feral
node0 = feral.CANNodeConfig(
name="node0",
drop_duplicated_frames=True,
message_types=[
feral.MessageTypeConfig(
type=feral.MessageType.TX,
address="fd:2",
simulated_payload_size=4,
port_name="node0_tx"
)]
)
node1 = feral.CANNodeConfig(
name="node1",
drop_duplicated_frames=True,
message_types=[
feral.MessageTypeConfig(
type=feral.MessageType.RX,
address="fd:2",
simulated_payload_size=4,
port_name="node1_rx"
)]
)
config = feral.CANConfig(
bit_rate=100_000,
bit_stuffing_mode=feral.BitStuffingMode.NONE,
log_transmissions=True,
nodes=[node0, node1]
)
The two nodes that correspond to the workers are defined here with some settings.
Putting it all together, the main.py
now looks like this:
from feral3gp import feral
from my_sink import MySink
from my_value_producer import MyValueProducer
from can_config import config
producer = MyValueProducer("Producer", interval=feral.seconds(1))
sink = MySink(name="Sink")
can = feral.can(config=config)
feral.link(producer, "output", can, "node0_tx")
feral.link(can, "node1_rx", sink, "input")
feral.start(feral.seconds(10))
Configure Fault Injection¶
We are now able to inject faults in the communication bus.
For this, modify the can_config.py
and add a fault_injector
to the node1
config:
from feral3gp import feral
node0 = feral.CANNodeConfig(
name="node0",
drop_duplicated_frames=True,
message_types=[
feral.MessageTypeConfig(
type=feral.MessageType.TX,
address="fd:2",
simulated_payload_size=4,
port_name="node0_tx"
)],
fault_injectors=[
feral.FaultInjectorConfig(
error_model=feral.ErrorModelType.SENDER_LINK_FAILURE,
error_processor=feral.ErrorProcessorType.DROP_FRAME,
start_time=feral.seconds(3),
end_time=feral.seconds(6)
)]
)
node1 = feral.CANNodeConfig(
name="node1",
drop_duplicated_frames=True,
message_types=[
feral.MessageTypeConfig(
type=feral.MessageType.RX,
address="fd:2",
simulated_payload_size=4,
port_name="node1_rx"
)]
)
config = feral.CANConfig(
bit_rate=100_000,
bit_stuffing_mode=feral.BitStuffingMode.NONE,
log_transmissions=True,
nodes=[node0, node1]
)
When you execute the simulation, you will observe, that the messages between the simulation time 2 and 6 are missing:
FERAL (3rd Gen) Version 1.0.0
INFO: starting the simulation...
INFO: (CANMedium) CAN Bus (unnamed Network): [268250 ns] - ID:2 | Value: 42
INFO: (Sink) 42
INFO: (CANMedium) CAN Bus (unnamed Network): [1000248250 ns] - ID:2 | Value: 42
INFO: (Sink) 42
INFO: (CANMedium) CAN Bus (unnamed Network): [2000248250 ns] - ID:2 | Value: 42
INFO: (Sink) 42
INFO: (CANMedium) CAN Bus (unnamed Network): [6000248250 ns] - ID:2 | Value: 42
INFO: (Sink) 42
INFO: (CANMedium) CAN Bus (unnamed Network): [7000248250 ns] - ID:2 | Value: 42
INFO: (Sink) 42
INFO: (CANMedium) CAN Bus (unnamed Network): [8000248250 ns] - ID:2 | Value: 42
INFO: (Sink) 42
INFO: (CANMedium) CAN Bus (unnamed Network): [9000248250 ns] - ID:2 | Value: 42
INFO: (Sink) 42
INFO: simulation finished!
Tip
Try out different settings for the feral.ErrorProcessorType
like RANDOM_DOUBLE
or CHANGE_SIGN_NUMBER
to test out different kinds of faults
Add an External Component¶
With the FERAL Co-Simulation API - FCAPI you can add other simulation components to the scenario.
Create a new file external_system.py
:
from feral3gp import feral
from feral3gp.feral.fcapi.client import FCAPIClient
from feral3gp.feral import seconds
client = FCAPIClient(feral.TransmissionBackend.TCP)
handle = client.connect("localhost", 51899, "localhost", "external")
sync_end_time = seconds(7)
client.sync(handle, seconds(1))
client.sync(handle, seconds(2))
client.sync(handle, seconds(3))
client.sync(handle, seconds(4))
client.sync(handle, seconds(5))
client.sync(handle, seconds(6))
client.sync(handle, seconds(7))
client.start_sim(handle)
client.wait(handle)
while True:
current_time = client.get_sim_time(handle)
client.tx(handle, [42])
client.continue_(handle)
if current_time >= sync_end_time:
break
client.wait(handle)
client.disconnect_all()
Info
The external system uses the FERAL FCAPIClient
to connect to the simulation via the handle
and registers several sync points in the simulation.
This way, the FERAL simulation can schedule the events accordingly.
The simulation waits until it has received the start_sim
signal from the external_system
to begin with the simulation.
Then the client.wait
call blocks the execution of the external_system
until it receives a continue
signal from the simulation, i.e., the previously registered synchronization points have been reached.
The simulation blocks its execution until it receives the client.continue
call.
To run this simulation, modify the main.py
, add a Gateway
and adjust the linking, so the gateway port is connected to the CAN Bus:
from feral3gp import feral
from my_sink import MySink
from can_config import config
sink = MySink(name="Sink")
can = feral.can(config=config)
gateway_config = feral.GatewayConfig(
expected_clients=1,
worker_ports=["external"],
backends=[
feral.BackendConfig(protocol=feral.TransmissionBackend.TCP, port=51899)]
)
gateway = feral.gateway(gateway_config)
feral.link(gateway, "external", can, "node0_tx")
feral.link(can, "node1_rx", sink, "input")
feral.start(feral.seconds(10))
As the event value is now a byte array adjust the message_received
method of the MySink
like this:
from feral3gp import feral
class MySink(feral.EventTriggeredWorker):
def __init__(self, name):
super().__init__(name)
def message_received(self, event, rx_port):
rx_value = event.getValue()
array = []
for value in rx_value:
array.append(value)
self.get_console().info(f"{array}")
Everything is set up now!
Now start the simulation with python main.py
.
Then open another terminal window and run python external_system.py
Success
Congratulations! You have successfully run a distributed simulation, performed fault injection and created a simulated network bus!