diff --git a/docs/source/configuration/simulation.rst b/docs/source/configuration/simulation.rst index bd66914d..48b857d9 100644 --- a/docs/source/configuration/simulation.rst +++ b/docs/source/configuration/simulation.rst @@ -108,31 +108,23 @@ This is an integer value specifying the allowed bandwidth across the connection. ``airspace`` ------------ -This section configures settings specific to the wireless network's virtual airspace. It defines how wireless interfaces within the simulation will interact and perform under various environmental conditions. +This section configures settings specific to the wireless network's virtual airspace. -``airspace_environment_type`` +``frequency_max_capacity_mbps`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This setting specifies the environmental conditions of the airspace which affect the propagation and interference characteristics of wireless signals. Changing this environment type impacts how signal noise and interference are calculated, thus affecting the overall network performance, including data transmission rates and signal quality. +This setting allows the user to override the default maximum bandwidth capacity set for each frequency. The key should +be the AirSpaceFrequency name and the value be the desired maximum bandwidth capacity in mbps (megabits per second) for +a single timestep. -**Configurable Options** +The below example would permit 123.45 megabits to be transmit across the WiFi 2.4 GHz frequency in a single timestep. +Setting a frequencies max capacity to 0.0 blocks that frequency on the airspace. -- **rural**: A rural environment offers clear channel conditions due to low population density and minimal electronic device presence. +.. code-block:: yaml -- **outdoor**: Outdoor environments like parks or fields have minimal electronic interference. - -- **suburban**: Suburban environments strike a balance with fewer electronic interferences than urban but more than rural. - -- **office**: Office environments have moderate interference from numerous electronic devices and overlapping networks. - -- **urban**: Urban environments are characterized by tall buildings and a high density of electronic devices, leading to significant interference. - -- **industrial**: Industrial areas face high interference from heavy machinery and numerous electronic devices. - -- **transport**: Environments such as subways and buses where metal structures and high mobility create complex interference patterns. - -- **dense_urban**: Dense urban areas like city centers have the highest level of signal interference due to the very high density of buildings and devices. - -- **jamming_zone**: A jamming zone environment where signals are actively interfered with, typically through the use of signal jammers or scrambling devices. This represents the environment with the highest level of interference. - -- **blocked**: A jamming zone environment with total levels of interference. Airspace is completely blocked. + simulation: + network: + airspace: + frequency_max_capacity_mbps: + WIFI_2_4: 123.45 + WIFI_5: 0.0 diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index b976e55f..9eadc360 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -16,6 +16,7 @@ from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT +from primaite.simulator.network.airspace import AirSpaceFrequency from primaite.simulator.network.hardware.base import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC @@ -237,6 +238,11 @@ class PrimaiteGame: simulation_config = cfg.get("simulation", {}) network_config = simulation_config.get("network", {}) airspace_cfg = network_config.get("airspace", {}) + frequency_max_capacity_mbps_cfg = airspace_cfg.get("frequency_max_capacity_mbps", {}) + + frequency_max_capacity_mbps_cfg = {AirSpaceFrequency[k]: v for k, v in frequency_max_capacity_mbps_cfg.items()} + + net.airspace.frequency_max_capacity_mbps_ = frequency_max_capacity_mbps_cfg nodes_cfg = network_config.get("nodes", []) links_cfg = network_config.get("links", []) diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 5019385a..9c736383 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from prettytable import MARKDOWN, PrettyTable from pydantic import BaseModel, Field @@ -59,6 +59,15 @@ class AirSpaceFrequency(Enum): @property def maximum_data_rate_bps(self) -> float: + """ + Retrieves the maximum data transmission rate in bits per second (bps) for the frequency. + + The maximum rates are predefined for known frequencies: + - For WIFI_2_4, it returns 100,000,000 bps (100 Mbps). + - For WIFI_5, it returns 500,000,000 bps (500 Mbps). + + :return: The maximum data rate in bits per second. If the frequency is not recognized, returns 0.0. + """ if self == AirSpaceFrequency.WIFI_2_4: return 100_000_000.0 # 100 Megabits per second if self == AirSpaceFrequency.WIFI_5: @@ -67,6 +76,14 @@ class AirSpaceFrequency(Enum): @property def maximum_data_rate_mbps(self) -> float: + """ + Retrieves the maximum data transmission rate in megabits per second (Mbps). + + This is derived by converting the maximum data rate from bits per second, as defined + in `maximum_data_rate_bps`, to megabits per second. + + :return: The maximum data rate in megabits per second. + """ return self.maximum_data_rate_bps / 1_000_000.0 @@ -84,28 +101,33 @@ class AirSpace(BaseModel): default_factory=lambda: {} ) bandwidth_load: Dict[AirSpaceFrequency, float] = Field(default_factory=lambda: {}) - frequency_max_capacity_mbps: Dict[AirSpaceFrequency, float] = Field(default_factory=lambda: {}) + frequency_max_capacity_mbps_: Dict[AirSpaceFrequency, float] = Field(default_factory=lambda: {}) - def model_post_init(self, __context: Any) -> None: + def get_frequency_max_capacity_mbps(self, frequency: AirSpaceFrequency) -> float: """ - Initialize the airspace metadata after instantiation. + Retrieves the maximum data transmission capacity for a specified frequency. - This method is called to set up initial configurations like the maximum capacity of each frequency. + This method checks a dictionary holding custom maximum capacities. If the frequency is found, it returns the + custom set maximum capacity. If the frequency is not found in the dictionary, it defaults to the standard + maximum data rate associated with that frequency. - :param __context: Contextual data or settings, typically used for further initializations beyond - the basic constructor. - """ - self.set_frequency_max_capacity_mbps() + :param frequency: The frequency for which the maximum capacity is queried. - def set_frequency_max_capacity_mbps(self, capacity_config: Optional[Dict[AirSpaceFrequency, float]] = None): + :return: The maximum capacity in Mbps for the specified frequency. """ - Set the maximum channel capacity in Mbps for each frequency. + if frequency in self.frequency_max_capacity_mbps_: + return self.frequency_max_capacity_mbps_[frequency] + return frequency.maximum_data_rate_mbps + + def set_frequency_max_capacity_mbps(self, cfg: Dict[AirSpaceFrequency, float]): """ - if capacity_config is None: - capacity_config = {} - for frequency in AirSpaceFrequency: - max_capacity = capacity_config.get(frequency, frequency.maximum_data_rate_mbps) - self.frequency_max_capacity_mbps[frequency] = max_capacity + Sets custom maximum data transmission capacities for multiple frequencies. + + :param cfg: A dictionary mapping frequencies to their new maximum capacities in Mbps. + """ + self.frequency_max_capacity_mbps_ = cfg + for freq, mbps in cfg.items(): + print(f"Overriding {freq} max capacity as {mbps:.3f} mbps") def show_bandwidth_load(self, markdown: bool = False): """ @@ -117,8 +139,6 @@ class AirSpace(BaseModel): :param markdown: Flag indicating if output should be in markdown format. """ - if not self.frequency_max_capacity_mbps: - self.set_frequency_max_capacity_mbps() headers = ["Frequency", "Current Capacity (%)", "Maximum Capacity (Mbit)"] table = PrettyTable(headers) if markdown: @@ -126,8 +146,8 @@ class AirSpace(BaseModel): table.align = "l" table.title = "Airspace Frequency Channel Loads" for frequency, load in self.bandwidth_load.items(): - maximum_capacity = self.frequency_max_capacity_mbps[frequency] - load_percent = load / maximum_capacity + maximum_capacity = self.get_frequency_max_capacity_mbps(frequency) + load_percent = load / maximum_capacity if maximum_capacity > 0 else 0.0 if load_percent > 1.0: load_percent = 1.0 table.add_row([format_hertz(frequency.value), f"{load_percent:.0%}", f"{maximum_capacity:.3f}"]) @@ -152,7 +172,7 @@ class AirSpace(BaseModel): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"Devices on Air Space" + table.title = "Devices on Air Space" for interface in self.wireless_interfaces.values(): status = "Enabled" if interface.enabled else "Disabled" @@ -235,14 +255,11 @@ class AirSpace(BaseModel): relevant frequency and its current bandwidth load. :return: True if the frame can be transmitted within the bandwidth limit, False if it would exceed the limit. """ - if not self.frequency_max_capacity_mbps: - self.set_frequency_max_capacity_mbps() if sender_network_interface.frequency not in self.bandwidth_load: self.bandwidth_load[sender_network_interface.frequency] = 0.0 - return ( - self.bandwidth_load[sender_network_interface.frequency] + frame.size_Mbits - <= self.frequency_max_capacity_mbps[sender_network_interface.frequency] - ) + return self.bandwidth_load[ + sender_network_interface.frequency + ] + frame.size_Mbits <= self.get_frequency_max_capacity_mbps(sender_network_interface.frequency) def transmit(self, frame: Frame, sender_network_interface: WirelessNetworkInterface): """ @@ -255,9 +272,7 @@ class AirSpace(BaseModel): excluded from the list of receivers to prevent it from receiving its own transmission. """ self.bandwidth_load[sender_network_interface.frequency] += frame.size_Mbits - for wireless_interface in self.wireless_interfaces_by_frequency.get( - sender_network_interface.frequency, [] - ): + for wireless_interface in self.wireless_interfaces_by_frequency.get(sender_network_interface.frequency, []): if wireless_interface != sender_network_interface and wireless_interface.enabled: wireless_interface.receive_frame(frame) diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 5ded993e..3cb4c515 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -265,9 +265,7 @@ class WirelessRouter(Router): ip_address = cfg["wireless_access_point"]["ip_address"] subnet_mask = cfg["wireless_access_point"]["subnet_mask"] frequency = AirSpaceFrequency[cfg["wireless_access_point"]["frequency"]] - router.configure_wireless_access_point( - ip_address=ip_address, subnet_mask=subnet_mask, frequency=frequency - ) + router.configure_wireless_access_point(ip_address=ip_address, subnet_mask=subnet_mask, frequency=frequency) if "acl" in cfg: for r_num, r_cfg in cfg["acl"].items(): diff --git a/tests/assets/configs/wireless_wan_network_config.yaml b/tests/assets/configs/wireless_wan_network_config.yaml index 7172f66d..c8f61bad 100644 --- a/tests/assets/configs/wireless_wan_network_config.yaml +++ b/tests/assets/configs/wireless_wan_network_config.yaml @@ -9,8 +9,6 @@ game: simulation: network: - airspace: - airspace_environment_type: urban nodes: - type: computer hostname: pc_a diff --git a/tests/assets/configs/wireless_wan_wifi_5_80_channel_width_urban.yaml b/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml similarity index 92% rename from tests/assets/configs/wireless_wan_wifi_5_80_channel_width_urban.yaml rename to tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml index d2e64720..a327b0f5 100644 --- a/tests/assets/configs/wireless_wan_wifi_5_80_channel_width_urban.yaml +++ b/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml @@ -10,7 +10,9 @@ game: simulation: network: airspace: - airspace_environment_type: urban + frequency_max_capacity_mbps: + WIFI_2_4: 123.45 + WIFI_5: 0.0 nodes: - type: computer hostname: pc_a @@ -37,7 +39,7 @@ simulation: wireless_access_point: ip_address: 192.168.1.1 subnet_mask: 255.255.255.0 - frequency: WIFI_5 + frequency: WIFI_2_4 acl: 1: action: PERMIT @@ -58,7 +60,7 @@ simulation: wireless_access_point: ip_address: 192.168.1.2 subnet_mask: 255.255.255.0 - frequency: WIFI_5 + frequency: WIFI_2_4 acl: 1: action: PERMIT diff --git a/tests/assets/configs/wireless_wan_wifi_5_80_channel_width_blocked.yaml b/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml similarity index 92% rename from tests/assets/configs/wireless_wan_wifi_5_80_channel_width_blocked.yaml rename to tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml index 5aed49cb..ff048c92 100644 --- a/tests/assets/configs/wireless_wan_wifi_5_80_channel_width_blocked.yaml +++ b/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml @@ -10,7 +10,9 @@ game: simulation: network: airspace: - airspace_environment_type: blocked + frequency_max_capacity_mbps: + WIFI_2_4: 0.0 + WIFI_5: 0.0 nodes: - type: computer hostname: pc_a @@ -37,7 +39,7 @@ simulation: wireless_access_point: ip_address: 192.168.1.1 subnet_mask: 255.255.255.0 - frequency: WIFI_5 + frequency: WIFI_2_4 acl: 1: action: PERMIT @@ -58,7 +60,7 @@ simulation: wireless_access_point: ip_address: 192.168.1.2 subnet_mask: 255.255.255.0 - frequency: WIFI_5 + frequency: WIFI_2_4 acl: 1: action: PERMIT diff --git a/tests/integration_tests/network/test_airspace_config.py b/tests/integration_tests/network/test_airspace_config.py new file mode 100644 index 00000000..78d00b47 --- /dev/null +++ b/tests/integration_tests/network/test_airspace_config.py @@ -0,0 +1,44 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import yaml + +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.airspace import AirSpaceFrequency +from tests import TEST_ASSETS_ROOT + + +def test_override_freq_max_capacity_mbps(): + config_path = TEST_ASSETS_ROOT / "configs" / "wireless_wan_network_config_freq_max_override.yaml" + + with open(config_path, "r") as f: + config_dict = yaml.safe_load(f) + network = PrimaiteGame.from_config(cfg=config_dict).simulation.network + + assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency.WIFI_2_4) == 123.45 + assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency.WIFI_5) == 0.0 + + pc_a = network.get_node_by_hostname("pc_a") + pc_b = network.get_node_by_hostname("pc_b") + + assert pc_a.ping(pc_b.network_interface[1].ip_address), "PC A should be able to ping PC B" + assert pc_b.ping(pc_a.network_interface[1].ip_address), "PC B should be able to ping PC A" + + network.airspace.show() + + +def test_override_freq_max_capacity_mbps_blocked(): + config_path = TEST_ASSETS_ROOT / "configs" / "wireless_wan_network_config_freq_max_override_blocked.yaml" + + with open(config_path, "r") as f: + config_dict = yaml.safe_load(f) + network = PrimaiteGame.from_config(cfg=config_dict).simulation.network + + assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency.WIFI_2_4) == 0.0 + assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency.WIFI_5) == 0.0 + + pc_a = network.get_node_by_hostname("pc_a") + pc_b = network.get_node_by_hostname("pc_b") + + assert not pc_a.ping(pc_b.network_interface[1].ip_address), "PC A should not be able to ping PC B" + assert not pc_b.ping(pc_a.network_interface[1].ip_address), "PC B should not be able to ping PC A" + + network.airspace.show()