Skip to content

Commit f96dcfe

Browse files
committed
[1323] downlinkHandling: expand unit tests and add debug toggle
1 parent c5d0495 commit f96dcfe

1 file changed

Lines changed: 206 additions & 9 deletions

File tree

src/simulation/communication/downlinkHandling/_UnitTest/test_downlinkHandling.py

Lines changed: 206 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- Formula parity against the Python-equivalent BER/PER/ARQ model.
2222
- Zero-throughput behavior when link quality inputs are invalid.
2323
- Retry-cap effects on storage drawdown and packet drop probability.
24+
- Removal-policy behavior (attempted removal vs delivered-only removal).
2425
- Storage-limited operation and remaining-data estimate behavior.
2526
- Automatic receiver-path selection from linkBudget antenna states/CNR values.
2627
@@ -45,6 +46,10 @@
4546

4647
DownlinkHandlingMsgPayload = importlib.import_module("Basilisk.architecture.messaging.DownlinkHandlingMsgPayload")
4748
LinkBudgetMsgPayload = importlib.import_module("Basilisk.architecture.messaging.LinkBudgetMsgPayload")
49+
DataStorageStatusMsgPayload = importlib.import_module("Basilisk.architecture.messaging.DataStorageStatusMsgPayload")
50+
51+
REMOVE_ATTEMPTED = 0
52+
REMOVE_DELIVERED_ONLY = 1
4853

4954
# Debug is false by default; can be enabled by environment variable or --debug when running this file directly.
5055
DEBUG_DOWNLINK_TEST = False
@@ -72,7 +77,7 @@ def q_function(x):
7277
return 0.5 * math.erfc(x / math.sqrt(2.0))
7378

7479

75-
def python_equivalent_from_link(cnr_linear, bandwidth_hz, bit_rate_bps, packet_bits, max_retx):
80+
def python_equivalent_from_link(cnr_linear, bandwidth_hz, bit_rate_bps, packet_bits, max_retx, removal_policy=REMOVE_ATTEMPTED):
7681
cnr_db = 10.0 * math.log10(cnr_linear)
7782
c_n0_dbhz = cnr_db + 10.0 * math.log10(bandwidth_hz)
7883
eb_n0_db = c_n0_dbhz - 10.0 * math.log10(bit_rate_bps)
@@ -90,9 +95,13 @@ def python_equivalent_from_link(cnr_linear, bandwidth_hz, bit_rate_bps, packet_b
9095
else:
9196
expected_attempts = packet_success / one_try_success
9297

93-
storage_removal_rate = bit_rate_bps / expected_attempts
94-
delivered_rate = storage_removal_rate * packet_success
95-
dropped_rate = storage_removal_rate - delivered_rate
98+
modeled_storage_removal_rate = bit_rate_bps / expected_attempts
99+
delivered_rate = modeled_storage_removal_rate * packet_success
100+
dropped_rate = modeled_storage_removal_rate - delivered_rate
101+
if removal_policy == REMOVE_DELIVERED_ONLY:
102+
storage_removal_rate = delivered_rate
103+
else:
104+
storage_removal_rate = modeled_storage_removal_rate
96105

97106
return {
98107
"c_n0_dbhz": c_n0_dbhz,
@@ -108,6 +117,21 @@ def python_equivalent_from_link(cnr_linear, bandwidth_hz, bit_rate_bps, packet_b
108117
}
109118

110119

120+
def make_storage_status_msg(storage_level_bits, partition_entries):
121+
payload = DataStorageStatusMsgPayload.DataStorageStatusMsgPayload()
122+
payload.storageLevel = storage_level_bits # [bit]
123+
124+
names = DataStorageStatusMsgPayload.StringVector()
125+
values = DataStorageStatusMsgPayload.DoubleVector()
126+
for name, bits in partition_entries:
127+
names.push_back(name)
128+
values.push_back(bits) # [bit]
129+
130+
payload.storedDataName = names
131+
payload.storedData = values
132+
return DataStorageStatusMsgPayload.DataStorageStatusMsg().write(payload)
133+
134+
111135
def run_downlink_case(
112136
cnr1=0.0,
113137
cnr2=0.5,
@@ -118,6 +142,7 @@ def run_downlink_case(
118142
packet_bits=256.0,
119143
max_retx=10,
120144
receiver_index=2,
145+
removal_policy=REMOVE_ATTEMPTED,
121146
initial_bits=1.0e9,
122147
task_dt_s=1.0,
123148
stop_time_s=3.0
@@ -127,6 +152,7 @@ def run_downlink_case(
127152
f"cnr1={cnr1}, cnr2={cnr2}, bandwidth_hz={bandwidth_hz}, "
128153
f"bit_rate_bps={bit_rate_bps}, packet_bits={packet_bits}, "
129154
f"max_retx={max_retx}, receiver_index={receiver_index}, "
155+
f"removal_policy={removal_policy}, "
130156
f"initial_bits={initial_bits}, task_dt_s={task_dt_s}, stop_time_s={stop_time_s}"
131157
)
132158
unit_task_name = "unitTask"
@@ -138,11 +164,12 @@ def run_downlink_case(
138164

139165
test_module = downlinkHandling.DownlinkHandling()
140166
test_module.ModelTag = "downlink"
141-
test_module.bitRateRequest = bit_rate_bps
142-
test_module.packetSizeBits = packet_bits
143-
test_module.maxRetransmissions = max_retx
144-
test_module.receiverAntenna = receiver_index
145-
test_module.requireFullPacket = True
167+
assert test_module.setBitRateRequest(bit_rate_bps)
168+
assert test_module.setPacketSizeBits(packet_bits)
169+
assert test_module.setMaxRetransmissions(max_retx)
170+
assert test_module.setReceiverAntenna(receiver_index)
171+
assert test_module.setRemovalPolicy(removal_policy)
172+
test_module.setRequireFullPacket(True)
146173
unit_test_sim.AddModelToTask(unit_task_name, test_module)
147174

148175
data_storage = simpleStorageUnit.SimpleStorageUnit()
@@ -183,6 +210,7 @@ def run_downlink_case(
183210
debug_print(
184211
"case results: "
185212
f"linkActive={downlink_log.linkActive[-1]}, "
213+
f"removalPolicy={downlink_log.removalPolicy[-1]}, "
186214
f"receiverIndex={downlink_log.receiverIndex[-1]}, "
187215
f"ber={downlink_log.ber[-1]:.16e}, per={downlink_log.per[-1]:.16e}, "
188216
f"storageRemovalRate={downlink_log.storageRemovalRate[-1]:.16e}, "
@@ -196,6 +224,7 @@ def run_downlink_case(
196224

197225

198226
def test_downlink_matches_python_equivalent():
227+
"""Verify C++ module outputs match the Python-equivalent BER/PER/ARQ model."""
199228
debug_print("test_downlink_matches_python_equivalent")
200229
cnr = 0.5
201230
bandwidth = 1.0e6
@@ -222,6 +251,7 @@ def test_downlink_matches_python_equivalent():
222251
debug_compare("nodeBaudRate", node_log.baudRate[-1], -expected["storage_removal_rate"])
223252

224253
assert downlink_log.linkActive[-1] == 1
254+
assert downlink_log.removalPolicy[-1] == REMOVE_ATTEMPTED
225255
assert downlink_log.ber[-1] == pytest.approx(expected["ber"], rel=1e-12, abs=1e-15)
226256
assert downlink_log.per[-1] == pytest.approx(expected["per"], rel=1e-12, abs=1e-15)
227257
assert downlink_log.expectedAttemptsPerPacket[-1] == pytest.approx(expected["expected_attempts"], rel=1e-12, abs=1e-15)
@@ -232,6 +262,7 @@ def test_downlink_matches_python_equivalent():
232262

233263

234264
def test_downlink_invalid_link_outputs_zero_flow():
265+
"""Verify invalid link inputs produce zero link-active flag and zero throughput."""
235266
debug_print("test_downlink_invalid_link_outputs_zero_flow")
236267
_, node_log, _, downlink_log = run_downlink_case(
237268
cnr2=0.0,
@@ -250,6 +281,7 @@ def test_downlink_invalid_link_outputs_zero_flow():
250281

251282

252283
def test_downlink_retry_limit_changes_storage_draw_not_goodput():
284+
"""Verify higher retry caps reduce storage draw while preserving expected delivered-rate level."""
253285
debug_print("test_downlink_retry_limit_changes_storage_draw_not_goodput")
254286
common = dict(
255287
cnr2=0.5,
@@ -276,7 +308,51 @@ def test_downlink_retry_limit_changes_storage_draw_not_goodput():
276308
assert downlink_log_m1.deliveredDataRate[-1] == pytest.approx(downlink_log_m8.deliveredDataRate[-1], rel=1e-12, abs=1e-9)
277309

278310

311+
def test_downlink_remove_delivered_only_retains_undelivered_bits():
312+
"""Verify delivered-only removal mode keeps dropped/undelivered bits onboard."""
313+
debug_print("test_downlink_remove_delivered_only_retains_undelivered_bits")
314+
common = dict(
315+
cnr2=0.5,
316+
bandwidth_hz=1.0e6,
317+
bit_rate_bps=1.0e5,
318+
packet_bits=256.0,
319+
max_retx=4,
320+
initial_bits=1.0e9,
321+
stop_time_s=3.0
322+
)
323+
324+
_, node_attempted, storage_attempted, downlink_attempted = run_downlink_case(
325+
removal_policy=REMOVE_ATTEMPTED, **common
326+
)
327+
_, node_delivered, storage_delivered, downlink_delivered = run_downlink_case(
328+
removal_policy=REMOVE_DELIVERED_ONLY, **common
329+
)
330+
331+
assert downlink_attempted.removalPolicy[-1] == REMOVE_ATTEMPTED
332+
assert downlink_delivered.removalPolicy[-1] == REMOVE_DELIVERED_ONLY
333+
334+
assert downlink_attempted.attemptedDataRate[-1] == pytest.approx(
335+
downlink_delivered.attemptedDataRate[-1], rel=1e-12, abs=1e-9
336+
)
337+
assert downlink_attempted.deliveredDataRate[-1] == pytest.approx(
338+
downlink_delivered.deliveredDataRate[-1], rel=1e-12, abs=1e-9
339+
)
340+
assert downlink_attempted.droppedDataRate[-1] == pytest.approx(
341+
downlink_delivered.droppedDataRate[-1], rel=1e-12, abs=1e-9
342+
)
343+
344+
assert downlink_attempted.storageRemovalRate[-1] > downlink_delivered.storageRemovalRate[-1]
345+
assert downlink_delivered.storageRemovalRate[-1] == pytest.approx(
346+
downlink_delivered.deliveredDataRate[-1], rel=1e-12, abs=1e-9
347+
)
348+
349+
assert node_attempted.baudRate[-1] == pytest.approx(-downlink_attempted.storageRemovalRate[-1], rel=1e-12, abs=1e-9)
350+
assert node_delivered.baudRate[-1] == pytest.approx(-downlink_delivered.storageRemovalRate[-1], rel=1e-12, abs=1e-9)
351+
assert storage_delivered.storageLevel[-1] > storage_attempted.storageLevel[-1]
352+
353+
279354
def test_downlink_storage_limited_case_caps_rate_and_drains_storage():
355+
"""Verify storage-limited operation caps removal rate and drains remaining data correctly."""
280356
debug_print("test_downlink_storage_limited_case_caps_rate_and_drains_storage")
281357
initial_bits = 300.0
282358
_, _, storage_log, downlink_log = run_downlink_case(
@@ -302,6 +378,7 @@ def test_downlink_storage_limited_case_caps_rate_and_drains_storage():
302378

303379

304380
def test_downlink_auto_receiver_selects_valid_rx_path():
381+
"""Verify auto receiver selection chooses a valid RX path with nonzero CNR."""
305382
debug_print("test_downlink_auto_receiver_selects_valid_rx_path")
306383
_, _, _, downlink_log = run_downlink_case(
307384
cnr1=0.8,
@@ -317,6 +394,126 @@ def test_downlink_auto_receiver_selects_valid_rx_path():
317394
assert downlink_log.receiverIndex[-1] == 1
318395

319396

397+
def test_downlink_setter_validation_rejects_invalid_inputs():
398+
"""Check that module setters reject invalid values and preserve prior valid configuration."""
399+
debug_print("test_downlink_setter_validation_rejects_invalid_inputs")
400+
test_module = downlinkHandling.DownlinkHandling()
401+
402+
assert test_module.setBitRateRequest(1.0e5) # [bit/s]
403+
assert test_module.setPacketSizeBits(1024.0) # [bit]
404+
assert test_module.setMaxRetransmissions(6) # [-]
405+
assert test_module.setReceiverAntenna(2) # [-]
406+
assert test_module.setRemovalPolicy(REMOVE_DELIVERED_ONLY)
407+
test_module.setRequireFullPacket(True)
408+
409+
with pytest.raises(Exception):
410+
test_module.setBitRateRequest(-1.0) # [bit/s]
411+
with pytest.raises(Exception):
412+
test_module.setBitRateRequest(float("nan")) # [bit/s]
413+
with pytest.raises(Exception):
414+
test_module.setPacketSizeBits(0.0) # [bit]
415+
with pytest.raises(Exception):
416+
test_module.setPacketSizeBits(float("inf")) # [bit]
417+
with pytest.raises(Exception):
418+
test_module.setMaxRetransmissions(0) # [-]
419+
with pytest.raises(Exception):
420+
test_module.setReceiverAntenna(4) # [-]
421+
with pytest.raises(Exception):
422+
test_module.setRemovalPolicy(2) # [-]
423+
424+
assert test_module.getBitRateRequest() == pytest.approx(1.0e5, abs=1e-12)
425+
assert test_module.getPacketSizeBits() == pytest.approx(1024.0, abs=1e-12)
426+
assert test_module.getMaxRetransmissions() == 6
427+
assert test_module.getReceiverAntenna() == 2
428+
assert test_module.getRemovalPolicy() == REMOVE_DELIVERED_ONLY
429+
assert test_module.getRequireFullPacket() is True
430+
431+
432+
def test_downlink_forced_receiver_invalid_path_disables_link():
433+
"""Verify that forcing an unavailable receiver path yields no selected receiver and zero throughput."""
434+
debug_print("test_downlink_forced_receiver_invalid_path_disables_link")
435+
_, node_log, _, downlink_log = run_downlink_case(
436+
cnr1=0.8,
437+
cnr2=0.0,
438+
ant_state1=1,
439+
ant_state2=2,
440+
receiver_index=2,
441+
initial_bits=1.0e8
442+
)
443+
444+
assert downlink_log.receiverIndex[-1] == 0
445+
assert downlink_log.linkActive[-1] == 0
446+
assert downlink_log.storageRemovalRate[-1] == pytest.approx(0.0, abs=1e-12)
447+
assert node_log.baudRate[-1] == pytest.approx(0.0, abs=1e-12)
448+
449+
450+
def test_downlink_duplicate_storage_message_is_rejected():
451+
"""Verify duplicate storage message registration is rejected by the module."""
452+
debug_print("test_downlink_duplicate_storage_message_is_rejected")
453+
454+
test_module = downlinkHandling.DownlinkHandling()
455+
storage_msg = make_storage_status_msg(100.0, [("DATA", 100.0)]) # [bit]
456+
457+
assert test_module.addStorageUnitToDownlink(storage_msg) is True
458+
assert test_module.addStorageUnitToDownlink(storage_msg) is False
459+
460+
461+
def test_downlink_selects_largest_partition_across_storage_messages():
462+
"""Verify storage selection uses the largest partition across all linked storage status inputs."""
463+
debug_print("test_downlink_selects_largest_partition_across_storage_messages")
464+
465+
unit_task_name = "unitTask"
466+
unit_process_name = "unitProcess"
467+
468+
unit_test_sim = SimulationBaseClass.SimBaseClass()
469+
test_proc = unit_test_sim.CreateNewProcess(unit_process_name)
470+
test_proc.addTask(unit_test_sim.CreateNewTask(unit_task_name, macros.sec2nano(1.0))) # [s]
471+
472+
test_module = downlinkHandling.DownlinkHandling()
473+
test_module.ModelTag = "downlink"
474+
assert test_module.setBitRateRequest(1.0e5) # [bit/s]
475+
assert test_module.setPacketSizeBits(256.0) # [bit]
476+
assert test_module.setMaxRetransmissions(4) # [-]
477+
assert test_module.setReceiverAntenna(2) # [-]
478+
test_module.setRequireFullPacket(True) # [-]
479+
unit_test_sim.AddModelToTask(unit_task_name, test_module)
480+
481+
storage_msg_a = make_storage_status_msg(
482+
900.0, # [bit]
483+
[("A_BIG", 400.0), ("A_SMALL", 100.0)] # [bit]
484+
)
485+
storage_msg_b = make_storage_status_msg(
486+
2000.0, # [bit]
487+
[("B_TOP", 1200.0), ("B_OTHER", 800.0)] # [bit]
488+
)
489+
assert test_module.addStorageUnitToDownlink(storage_msg_a)
490+
assert test_module.addStorageUnitToDownlink(storage_msg_b)
491+
492+
link_payload = LinkBudgetMsgPayload.LinkBudgetMsgPayload()
493+
link_payload.antennaName1 = "TX_Ant"
494+
link_payload.antennaName2 = "RX_Ant"
495+
link_payload.antennaState1 = 2 # [-] TX
496+
link_payload.antennaState2 = 1 # [-] RX
497+
link_payload.CNR1 = 0.0 # [-]
498+
link_payload.CNR2 = 20.0 # [-]
499+
link_payload.bandwidth = 1.0e6 # [Hz]
500+
link_payload.frequency = 2.2e9 # [Hz]
501+
link_msg = LinkBudgetMsgPayload.LinkBudgetMsg().write(link_payload)
502+
test_module.linkBudgetInMsg.subscribeTo(link_msg)
503+
504+
downlink_reader = DownlinkHandlingMsgPayload.DownlinkHandlingMsgReader()
505+
downlink_reader.subscribeTo(test_module.downlinkOutMsg)
506+
downlink_log = downlink_reader.recorder()
507+
unit_test_sim.AddModelToTask(unit_task_name, downlink_log)
508+
509+
unit_test_sim.InitializeSimulation()
510+
unit_test_sim.ConfigureStopTime(macros.sec2nano(1.0)) # [s]
511+
unit_test_sim.ExecuteSimulation()
512+
513+
assert downlink_log.availableDataBits[-1] == pytest.approx(1200.0, abs=1e-12)
514+
assert downlink_log.dataName[-1] == "B_TOP"
515+
516+
320517
if __name__ == "__main__":
321518
# Allow direct execution:
322519
# python test_downlinkHandling.py -k test_name -q -s --debug

0 commit comments

Comments
 (0)