Skip to content

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
      
  • 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:

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:

main.py
feral.link(producer, "output", sink, "input")


Adjust Simulation Duration

Now, adjust the duration of the simulation.

Update your main.py:

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:

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")

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:

main.py
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:

CAN Communication

Simulation with CAN Bus

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:

my_sink.py
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.EventTriggeredWorkerand 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.

my_value_producer.py
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:

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:

main.py
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:

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"
        )],
    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:

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:

main.py
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:

my_sink.py
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!