Skip to content

Commit

Permalink
Merge branch '197-fix-trigger-realization-pseudo-randomness' into 'ma…
Browse files Browse the repository at this point in the history
…ster'

Fix Trigger Realization Pseudo Randomness

Closes #197

See merge request barkhauseninstitut/wicon/hermespy!181
  • Loading branch information
adlerjan committed Apr 28, 2024
2 parents 39d4fed + 8782378 commit 68fa7b1
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 53 deletions.
1 change: 0 additions & 1 deletion hermespy/core/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,6 @@ def transmit_devices(self) -> Sequence[DeviceTransmission]:
Returns: List of generated information transmitted by each device.
"""

# Note that devices are required to cache so that the leaking signal is available during reception
transmissions = [device.transmit() for device in self.devices]
return transmissions

Expand Down
21 changes: 15 additions & 6 deletions hermespy/simulation/simulated_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,15 @@ def devices(self) -> Set[SimulatedDevice]:
return self.__devices

@abstractmethod
def realize(self) -> TriggerRealization:
def realize(self, rng: np.random.Generator | None = None) -> TriggerRealization:
"""Realize a triggering of all controlled devices.
Args:
rng (np.random.Generator, optional):
Random number generator used to realize this trigger model.
If not specified, the object's internal generator will be queried.
Returns: Realization of the trigger model.
"""
... # pragma: no cover
Expand All @@ -203,7 +209,7 @@ class StaticTrigger(TriggerModel, Serializable):

yaml_tag = "StaticTrigger"

def realize(self) -> TriggerRealization:
def realize(self, rng: np.random.Generator | None = None) -> TriggerRealization:
if self.num_devices < 1:
sampling_rate = 1.0

Expand Down Expand Up @@ -251,7 +257,7 @@ def num_offset_samples(self, value: int) -> None:

self.__num_offset_samples = value

def realize(self) -> TriggerRealization:
def realize(self, rng: np.random.Generator | None = None) -> TriggerRealization:
if self.num_devices < 1:
raise RuntimeError(
"Realizing a static trigger requires the trigger to control at least one device"
Expand Down Expand Up @@ -299,7 +305,7 @@ def offset(self, value: float) -> None:

self.__offset = value

def realize(self) -> TriggerRealization:
def realize(self, rng: np.random.Generator | None = None) -> TriggerRealization:
if self.num_devices < 1:
raise RuntimeError(
"Realizing a static trigger requires the trigger to control at least one device"
Expand All @@ -316,7 +322,7 @@ class RandomTrigger(TriggerModel, Serializable):

yaml_tag = "RandomTrigger"

def realize(self) -> TriggerRealization:
def realize(self, rng: np.random.Generator | None = None) -> TriggerRealization:
if self.num_devices < 1:
raise RuntimeError(
"Realizing a random trigger requires the trigger to control at least one device"
Expand All @@ -341,7 +347,10 @@ def realize(self) -> TriggerRealization:
if max_trigger_delay == 0:
return TriggerRealization(0, sampling_rate)

trigger_delay = self._rng.integers(0, max_trigger_delay)
# Select the proper random number generator
_rng = self._rng if rng is None else rng

trigger_delay = _rng.integers(0, max_trigger_delay)
return TriggerRealization(trigger_delay, sampling_rate)


Expand Down
110 changes: 87 additions & 23 deletions hermespy/simulation/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from hermespy.core import (
DeviceInput,
Drop,
Transmission,
Serializable,
Pipeline,
Verbosity,
Expand Down Expand Up @@ -50,6 +51,7 @@
TriggerRealization,
ProcessedSimulatedDeviceInput,
SimulatedDevice,
SimulatedDeviceOutput,
SimulatedDeviceTransmission,
SimulatedDeviceReception,
)
Expand Down Expand Up @@ -435,41 +437,88 @@ def noise_model(self, value: NoiseModel | None) -> None:
if value is not None:
self.__noise_model.random_mother = self

def transmit_devices(self, cache: bool = True) -> Sequence[SimulatedDeviceTransmission]:
"""Generate simulated device transmissions of all registered devices.
def realize_triggers(self) -> Sequence[TriggerRealization]:
"""Realize the trigger models of all registered devices.
Devices sharing trigger models will be triggered simultaneously.
Devices sharing trigger models will be triggered simulatenously.
Args:
cache (bool, optional):
Cache the generated transmissions at the respective devices.
Enabled by default.
Returns:
Sequence of simulated simulated device transmissions.
Returns: A sequence of trigger model realizations.
"""

# Collect unique triggers
triggers: List[TriggerModel] = []
trigger_realizations: List[TriggerRealization] = []
transmissions: List[SimulatedDeviceTransmission] = []
unique_realizations: List[TriggerRealization] = []
device_realizations: List[TriggerRealization] = []

for device in self.devices:
trigger_realization: TriggerRealization
device_realization: TriggerRealization

if device.trigger_model not in triggers:
trigger_realization = device.trigger_model.realize()
device_realization = device.trigger_model.realize(self._rng)

triggers.append(device.trigger_model)
trigger_realizations.append(trigger_realization)
unique_realizations.append(device_realization)

else:
trigger_realization = trigger_realizations[triggers.index(device.trigger_model)]
device_realization = unique_realizations[triggers.index(device.trigger_model)]

device_realizations.append(device_realization)

transmission = device.transmit(cache=cache, trigger_realization=trigger_realization)
transmissions.append(transmission)
return device_realizations

def generate_outputs(
self,
transmissions: List[List[Transmission]] | None = None,
trigger_realizations: Sequence[TriggerRealization] | None = None,
) -> Sequence[SimulatedDeviceOutput]:
# Assume cached operator transmissions if none were provided
_transmissions: List[None] | List[List[Transmission]] = (
[None] * self.num_devices if not transmissions else transmissions
)

if len(_transmissions) != self.num_devices:
raise ValueError(
f"Number of device transmissions ({len(_transmissions)}) does not match number of registered devices ({self.num_devices}"
)

_trigger_realizations = (
self.realize_triggers() if trigger_realizations is None else trigger_realizations
)

if len(_trigger_realizations) != self.num_devices:
raise ValueError(
f"Number of trigger realizations ({len(_trigger_realizations)}) does not match number of registered devices ({self.num_devices}"
)

outputs = [
d.generate_output(t, True, tr)
for d, t, tr in zip(self.devices, _transmissions, _trigger_realizations)
]
return outputs

def transmit_devices(self, cache: bool = True) -> Sequence[SimulatedDeviceTransmission]:
"""Generate simulated device transmissions of all registered devices.
Devices sharing trigger models will be triggered simultaneously.
Args:
cache (bool, optional):
Cache the generated transmissions at the respective devices.
Enabled by default.
Returns:
Sequence of simulated simulated device transmissions.
"""

# Realize triggers
trigger_realizations = self.realize_triggers()

# Transmit devices
transmissions: List[SimulatedDeviceTransmission] = [
d.transmit(cache=cache, trigger_realization=t)
for d, t in zip(self.devices, trigger_realizations)
]
return transmissions

def propagate(self, transmissions: Sequence[DeviceOutput]) -> List[List[ChannelPropagation]]:
Expand Down Expand Up @@ -721,6 +770,7 @@ class SimulationRunner(object):
"""Runner remote thread deployed by Monte Carlo routines"""

__scenario: SimulationScenario # Scenario to be run
__trigger_realizations: Sequence[TriggerRealization]
__propagation: Sequence[Sequence[ChannelPropagation]] | None
__processed_inputs: Sequence[ProcessedSimulatedDeviceInput]

Expand All @@ -733,6 +783,7 @@ def __init__(self, scenario: SimulationScenario) -> None:
"""

self.__scenario = scenario
self.__trigger_realizations = None
self.__propagation = None
self.__processed_inputs = []

Expand All @@ -751,8 +802,8 @@ def generate_outputs(self) -> None:
Internally resolves to the scenario's generate outputs routine :meth:`SimulationScenario.generate_outputs`.
"""

# Resolve to the scenario output generation routine
_ = self.__scenario.generate_outputs()
self.__trigger_realizations = self.__scenario.realize_triggers()
_ = self.__scenario.generate_outputs(None, self.__trigger_realizations)

def propagate(self) -> None:
"""Propagate the signals generated by registered transmitters over the channel model.
Expand Down Expand Up @@ -789,9 +840,19 @@ def process_inputs(self) -> None:

propagation_matrix = self.__propagation

if self.__trigger_realizations is None:
raise RuntimeError(
"Process inputs simulation stage without prior call to generate outputs"
)

if len(self.__trigger_realizations) != self.__scenario.num_devices:
raise RuntimeError(
"Number of trigger realizations does not match the number of registered devices"
)

if propagation_matrix is None:
raise RuntimeError(
"Receive device simulation stage called without prior channel propagation"
"Process inputs simulation stage called without prior channel propagation"
)

if len(propagation_matrix) != self.__scenario.num_devices:
Expand All @@ -801,12 +862,15 @@ def process_inputs(self) -> None:
)

self.__processed_inputs: Sequence[ProcessedSimulatedDeviceInput] = []
for device, impinging_signals in zip(self.__scenario.devices, propagation_matrix):
for device, impinging_signals, trigger_realization in zip(
self.__scenario.devices, propagation_matrix, self.__trigger_realizations
):
self.__processed_inputs.append(
device.process_input(
impinging_signals=impinging_signals,
noise_level=self.__scenario.noise_level,
noise_model=self.__scenario.noise_model,
trigger_realization=trigger_realization,
)
)

Expand Down
38 changes: 37 additions & 1 deletion tests/integration_tests/test_trigger_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import numpy as np

from hermespy.core import Transformation
from hermespy.simulation import RandomTrigger, SimulationScenario, SingleCarrierIdealChannelEstimation
from hermespy.simulation import RandomTrigger, Simulation, SimulationScenario, SingleCarrierIdealChannelEstimation
from hermespy.channel import StreetCanyonLineOfSight
from hermespy.modem import BitErrorEvaluator, SimplexLink, RootRaisedCosineWaveform, SingleCarrierZeroForcingChannelEqualization
from unit_tests.utils import SimulationTestContext

__author__ = "Jan Adler"
__copyright__ = "Copyright 2023, Barkhausen Institut gGmbH"
Expand Down Expand Up @@ -64,3 +65,38 @@ def test_drop(self) -> None:
beta_ber = self.beta_error.evaluate().artifact().to_scalar()
self.assertGreaterEqual(0.01, alpha_ber)
self.assertGreaterEqual(0.01, beta_ber)

def test_pseudo_randomness(self) -> None:
"""Setting the scenario's random seed should yield reproducable trigger realizations"""

num_drops = 5

self.scenario.seed = 42
initial_realizations = []
for _ in range(num_drops):
drop = self.scenario.drop()
initial_realizations.append(drop.device_transmissions[0].trigger_realization)

self.scenario.seed = 42
replayed_realizations = []
for _ in range(num_drops):
drop = self.scenario.drop()
replayed_realizations.append(drop.device_transmissions[0].trigger_realization)

for initial_realization, replayed_realization in zip(initial_realizations, replayed_realizations):
self.assertEqual(initial_realization.num_offset_samples, replayed_realization.num_offset_samples)

def test_simulation(self) -> None:
"""Trigger groups should be correctly applied during simulation runtime"""

with SimulationTestContext():

simulation = Simulation(self.scenario)
simulation.num_drops = 10
simulation.add_evaluator(self.alpha_error)
simulation.add_evaluator(self.beta_error)

result = simulation.run()

self.assertGreaterEqual(0.01, result.evaluation_results[0].to_array().flat[0])
self.assertGreaterEqual(0.01, result.evaluation_results[1].to_array().flat[0])

0 comments on commit 68fa7b1

Please sign in to comment.