Skip to content

PHY and link simulation

Use this path for link-level work, waveform algorithms, and experiments on individual physical-layer blocks. No MAC owner is required.

One granular PUSCH chain

This checked example calls the OAI ULSCH transmitter, gNB PUSCH receiver, and ULSCH decoder directly:

"""Run one granular PUSCH transmit/receive/decode chain."""

from oai_python.bindings.nr.phy import pusch
from oai_python.bindings.nr.phy.ulsch import decoding, transmit


def main() -> None:
    payload = bytes((index * 29 + 3) & 0xFF for index in range(384))
    config = pusch.PuschConfig()

    with (
        transmit.UlschTransmitter() as transmitter,
        pusch.GnbPuschReceiver(timeout=10.0) as receiver,
    ):
        waveform = transmitter.transmit(payload, config)
        reception = receiver.receive(config, waveform.frequency_grid)

    decoded = decoding.nr_ulsch_decoding(
        reception.llrs,
        transport_block_bits=len(payload) * 8,
        base_graph=config.ldpc_base_graph,
        modulation_order=config.modulation_order,
        rb_size=config.rb_size,
        nr_of_symbols=config.symbol_count,
        dmrs_symbol_mask=config.dmrs_symbol_mask,
        start_symbol_index=config.start_symbol,
        dmrs_config_type=config.dmrs_config_type,
        num_dmrs_cdm_groups_no_data=config.dmrs_cdm_groups_without_data,
        unavailable_resources=reception.unavailable_resource_elements,
        layers=1,
        maximum_iterations=8,
        redundancy_version=config.redundancy_version,
        tbslbrm=config.tbslbrm,
        mcs_index=config.mcs_index,
        rnti=config.rnti,
        frame=config.frame,
        slot=config.slot,
    )

    assert waveform.matched_bits == reception.llr_count == 6_276
    assert decoded[1] is True and bytes(decoded[0]) == payload
    print(f"decoded PUSCH SDU: {len(payload)} bytes from {reception.llr_count} LLRs")


if __name__ == "__main__":
    main()

Expected output:

decoded PUSCH SDU: 384 bytes from 6276 LLRs

NrPhyLink composes the reviewed SISO downlink and uplink chains. It accepts payloads, advances slots, and returns copied results while retaining OAI state.

"""Run MAC-independent downlink and uplink through the complete PHY."""

from oai_python.bindings.nr.phy.link import NrPhyLink


def main() -> None:
    downlink = bytes(range(64))
    uplink = bytes((index * 29 + 3) & 0xFF for index in range(384))

    with NrPhyLink(history_limit=2) as simulation:
        downlink_slot = simulation.step(downlink_payload=downlink)
        uplink_slot = simulation.step(uplink_payload=uplink)
        simulation.run_slots(20)

    assert len(downlink_slot.transmissions) == len(uplink_slot.transmissions) == 1
    (received_downlink,) = downlink_slot.transmissions
    (received_uplink,) = uplink_slot.transmissions
    assert received_downlink.crc_ok
    assert received_uplink.crc_ok
    assert received_downlink.decoded_payload == downlink
    assert received_uplink.decoded_payload == uplink
    print(f"downlink PHY SDU: {received_downlink.decoded_payload!r}")
    print(f"uplink PHY SDU: {len(received_uplink.decoded_payload)} bytes")


if __name__ == "__main__":
    main()

Expected output:

downlink PHY SDU: b'\x00\x01\x02...'
uplink PHY SDU: 384 bytes

Insert any external channel

The wheel does not implement propagation models. It exposes a channel-agnostic hook over copied time-domain IQ samples:

tx = phy.transmit(...)
rx = my_channel(tx, context=context)
result = phy.receive(rx, ...)

At the owner level, pass downlink_channel= or uplink_channel= to NrPhyLink or NrPhyStack. A channel callable receives a contiguous int16 IQ array plus ChannelContext with the direction, frame, slot, frequency, sample rate, waveform kind, and antenna metadata already known by the binding. Simple NumPy functions can ignore the context; mobility or ray-tracing models can use it. See the Sionna RT example.

Replace a block

Call the transmitter and receiver as separate granular operations, run your Python function between them, and preserve the documented dtype, shape, contiguity, and ownership contract. The supported API lists the stable entry points; canonical stubs carry the exact signatures.