Frames#

Frame structure objects (decision D28).

A frame is described by a FrameStructure: an ordered list of typed fields (FrameField with a FieldRole), shared by the Framer and the Deframer exactly like a CarrierAllocation is shared by the carrier allocator and extractor. FieldRole types the time axis the way CarrierType types the frequency axis.

class FrameField(name: str, role: FieldRole, values: ndarray | None = None, *, length: int | None = None)[source]#

One typed field of a frame.

Parameters:
  • name (str) – Field name (“STF”, “LTF”, “SIG”, “PAYLOAD”…).

  • role (FieldRole) – What the field is for.

  • values (np.ndarray, optional) – Transmitted samples. None means unknown at transmit time – only allowed for the PAYLOAD field, whose length is then given by length.

  • length (int, optional) – Field length in samples; inferred from values when provided.

property size: int#

Field length in samples (guaranteed set after construction).

class FrameStructure(fields: tuple[FrameField, ...], *, standard: str = 'custom', reference: str = '')[source]#

Ordered list of typed frame fields (decision D28).

Invariants: exactly one PAYLOAD field, and every other field carries its transmitted values.

Examples

>>> from comnumpy.core.sequences import zadoff_chu
>>> frame = FrameStructure((
...     FrameField("SYNC", FieldRole.SYNC, zadoff_chu(1, 63)),
...     FrameField("PAYLOAD", FieldRole.PAYLOAD, length=1000),
... ), standard="example")
>>> frame.frame_length, frame.payload_length
(1063, 1000)
slice_of(name: str) → slice[source]#

Position of a field inside the frame.

class Framer(frame: FrameStructure, *, name: str = 'framer')[source]#

Insert the frame fields around the payload (decision D28).

Signal Model#

\[\mathbf{y} = [\, \mathbf{s}_1 \,|\, \cdots \,|\, \mathbf{x} \,|\, \cdots \,|\, \mathbf{s}_M \,]\]

where the \(\mathbf{s}_i\) are the known field values of the shared FrameStructure and \(\mathbf{x}\) is the payload.

The model is structural rather than analytical: no arithmetic is performed on the samples. Writing the frame as the ordered list of its \(M\) typed fields, of lengths \(L_1, \dots, L_M\), the output row of index \(t\) is the concatenation

\[\begin{split}y_t[n] = \begin{cases} s_i\left[n - \sum_{j<i} L_j\right] & \text{if field } i \text{ is known at TX} \\ x_t\left[n - \sum_{j<i} L_j\right] & \text{if field } i \text{ is the PAYLOAD} \end{cases}\end{split}\]

for \(0 \le n < N_{\mathrm{frame}} = \sum_i L_i\). Exactly one field carries the role PAYLOAD and receives the input; every other field (SYNC, TRAINING, HEADER, TAIL, PAD) carries values fixed at construction time. The output is complex, since the known fields generally are.

Axes: declared axis – Block layout (..., T, N_payload) with the frame index on T; fields are added on the content axis -1. The S/P block size is not a free parameter: it equals frame.payload_length and prepare() enforces it.

param frame:

Frame description shared with the receiver’s Deframer. Its payload_length fixes the accepted input size \(N_{\mathrm{payload}}\) and its frame_length the output size \(N_{\mathrm{frame}}\).

type frame:

FrameStructure

param name:

Name of the framer instance. Default is "framer".

type name:

str, optional, keyword-only

raises ShapeError:

If the last axis of the input does not equal frame.payload_length.

References

IEEE Std 802.11-2020, Clause 17 (Orthogonal frequency division multiplexing PHY) – example of a real typed frame structure: short training field, long training field, SIGNAL field, then DATA.

Examples

>>> frame = FrameStructure((
...     FrameField("SYNC", FieldRole.SYNC, np.array([1.0, -1.0])),
...     FrameField("PAYLOAD", FieldRole.PAYLOAD, length=3),
... ), standard="example")
>>> framer = Framer(frame)
>>> print(framer(np.array([[1.0, 2.0, 3.0]])))
[[ 1.+0.j -1.+0.j  1.+0.j  2.+0.j  3.+0.j]]
class Deframer(frame: FrameStructure, *, name: str = 'deframer')[source]#

Extract the payload (and expose every field) from received frames.

Signal Model#

The exact inverse of Framer, and equally structural: the received frame is cut back along the field boundaries of the shared FrameStructure. Field \(i\) occupies the slice \([\sum_{j<i} L_j, \; \sum_{j \le i} L_j)\) of the frame axis,

\[\hat{\mathbf{s}}_i = \mathbf{y}\!\left[\, \sum_{j<i} L_j : \, \sum_{j \le i} L_j \,\right], \qquad i = 1, \dots, M\]

and forward returns the single field whose role is PAYLOAD:

\[\hat{\mathbf{x}} = \hat{\mathbf{s}}_{i_\mathrm{payload}}\]

The other fields are not discarded: the received \(\hat{\mathbf{s}}_i\) are recorded and read back with get_field() – that is how a receiver gets the SYNC field for timing or the TRAINING field for channel estimation without adding a block to the chain (D42).

Axes: declared axis – Block layout (..., T, frame_length). After forward, the received samples of each field are available through get_field().

param frame:

Frame description, the very same object as the transmitter’s Framer. Its frame_length fixes the accepted input size \(N_{\mathrm{frame}}\).

type frame:

FrameStructure

param name:

Name of the deframer instance. Default is "deframer".

type name:

str, optional, keyword-only

raises ShapeError:

If the last axis of the input does not equal frame.frame_length.

References

IEEE Std 802.11-2020, Clause 17 (Orthogonal frequency division multiplexing PHY) – example of a real typed frame structure: short training field, long training field, SIGNAL field, then DATA.

Examples

>>> frame = FrameStructure((
...     FrameField("SYNC", FieldRole.SYNC, np.array([1.0, -1.0])),
...     FrameField("PAYLOAD", FieldRole.PAYLOAD, length=3),
... ), standard="example")
>>> deframer = Deframer(frame)
>>> print(deframer(np.array([[1.0, -1.0, 1.0, 2.0, 3.0]])))
[[1. 2. 3.]]
>>> print(deframer.get_field("SYNC"))
[[ 1. -1.]]
get_field(name: str) → ndarray[source]#

Received samples of a field, recorded by the last forward.