Space-Time Coding#

Space-time block codes: spending antennas on reliability or on rate.

A MIMO channel offers two things that cannot both be taken in full: diversity, the number of independent fading coefficients an error must defeat, and multiplexing, the number of symbols carried per channel use. A space-time code is the choice of where to sit between them, and this module describes codes in the one form that makes that choice checkable.

Every code here is a linear dispersion code. A codeword occupying \(T\) channel uses on \(N_t\) antennas is

\[\mathbf{G}(\mathbf{s}) = \sum_{k=1}^{K} \left( \mathbf{A}_k s_k + \mathbf{B}_k s_k^{*} \right)\]

with fixed matrices \(\mathbf{A}_k, \mathbf{B}_k\) of size \(N_t \times T\). Alamouti, the orthogonal designs of Tarokh et al., spatial multiplexing and the Golden code are all of this form; the conjugate term is what lets an orthogonal design exist at all.

Why that form and not a table of matrices. Writing \(s_k = u_k + \mathrm{j} v_k\) makes the received block a real linear function of the \(2K\) real unknowns,

\[\begin{split}\begin{bmatrix} \Re\{\mathrm{vec}\,\mathbf{Y}\} \\ \Im\{\mathrm{vec}\,\mathbf{Y}\} \end{bmatrix} = \mathbf{M}(\mathbf{H}) \begin{bmatrix} \mathbf{u} \\ \mathbf{v} \end{bmatrix} + \text{noise}\end{split}\]

and \(\mathbf{M}(\mathbf{H})\) – the equivalent channel, SpaceTimeCode.equivalent_channel() – is where every property of the code becomes a statement about a matrix. A design is orthogonal exactly when

\[\mathbf{M}^{\mathsf{T}} \mathbf{M} = c \left\|\mathbf{H}\right\|_F^2 \, \mathbf{I}_{2K}\]

which is checked at construction for every code declaring itself orthogonal (D20), so a mistyped sign in a dispersion matrix cannot ship. That same identity is why an orthogonal code is decoded by a matched filter and not by a search: the maximum-likelihood estimate is \(\mathbf{M}^{\mathsf{T}} \mathbf{y} / (c\|\mathbf{H}\|_F^2)\), exactly. The constant \(c\) is a property of the design – 1 for Alamouti, 2 for the rate-1/2 designs that repeat each symbol over a conjugated half – and is measured with the identity rather than assumed.

Codes are taken from a registry, as constellations are taken from get_alphabet():

code = get_code("alamouti")
chain = Sequential([SymbolGenerator(4), SymbolMapper(alphabet),
                    SpaceTimeEncoder(code), FlatMIMOChannel(H), ...])

References

S. M. Alamouti, “A simple transmit diversity technique for wireless communications”, IEEE J. Sel. Areas Commun., vol. 16, no. 8, pp. 1451-1458, Oct. 1998; V. Tarokh, H. Jafarkhani, A. R. Calderbank, “Space-time block codes from orthogonal designs”, IEEE Trans. Inf. Theory, vol. 45, no. 5, pp. 1456-1467, Jul. 1999; B. Hassibi, B. M. Hochwald, “High-rate codes that are linear in space and time”, IEEE Trans. Inf. Theory, vol. 48, no. 7, pp. 1804-1824, Jul. 2002 (linear dispersion); J.-C. Belfiore, G. Rekaya, E. Viterbo, “The Golden code: a 2x2 full-rate space-time code with nonvanishing determinants”, IEEE Trans. Inf. Theory, vol. 51, no. 4, pp. 1432-1436, Apr. 2005; V. Tarokh, N. Seshadri, A. R. Calderbank, “Space-time codes for high data rate wireless communication: performance criterion and code construction”, IEEE Trans. Inf. Theory, vol. 44, no. 2, pp. 744-765, Mar. 1998 (the rank and determinant criteria).

Examples

>>> import numpy as np
>>> code = get_code("alamouti")
>>> code.n_tx, code.n_slots, code.n_symbols, code.rate
(2, 2, 2, 1.0)
>>> print(np.round(code.encode(np.array([1 + 0j, 2 + 1j])), 3))
[[ 1.+0.j -2.+1.j]
 [ 2.+1.j  1.+0.j]]
>>> code.is_orthogonal
True
class SpaceTimeCode(name: str, dispersion_direct: ndarray, dispersion_conjugate: ndarray, *, orthogonal: bool = False, reference: str = '')[source]#

A space-time block code, as its linear dispersion matrices.

Signal Model#

The codeword transmitted over \(T\) channel uses is

\[\mathbf{G}(\mathbf{s}) = \sum_{k=1}^{K} \left(\mathbf{A}_k s_k + \mathbf{B}_k s_k^{*}\right) \in \mathbb{C}^{N_t \times T}\]

and the receiver observes \(\mathbf{Y} = \mathbf{H}\mathbf{G}(\mathbf{s}) + \mathbf{V}\), with \(\mathbf{V}\) the noise (\(\mathbf{B}_k\) being taken by the conjugate dispersion matrices above). The rate is \(K/T\) symbols per channel use, the diversity order is the minimum rank of \(\mathbf{G}(\mathbf{s}) - \mathbf{G}(\mathbf{s}')\) over distinct symbol vectors (the rank criterion), and a code reaching \(N_t\) there is full diversity.

Axes: declared axis – codewords are (n_tx, n_slots), antennas on axis -2, as everywhere else in the library (D2).

param name:

Registry name.

type name:

str

param dispersion_direct:

The matrices \(\mathbf{A}_k\), stacked as (K, n_tx, n_slots).

type dispersion_direct:

np.ndarray

param dispersion_conjugate:

The matrices \(\mathbf{B}_k\), same shape. Zero for a code that does not conjugate, such as spatial multiplexing or the Golden code.

type dispersion_conjugate:

np.ndarray

param orthogonal:

Declares the design orthogonal. It is verified at construction, not trusted (D20).

type orthogonal:

bool, optional, keyword-only

param reference:

Where the code comes from.

type reference:

str, optional, keyword-only

raises ShapeError:

If the two dispersion stacks disagree in shape.

raises ValueError:

If orthogonal=True and the design is not orthogonal.

References

  1. Hassibi, B. M. Hochwald, IEEE Trans. Inf. Theory 48(7), 2002.

Examples

>>> zero = np.zeros((1, 1, 1))
>>> identity = np.ones((1, 1, 1))
>>> trivial = SpaceTimeCode("siso", identity, zero, orthogonal=True)
>>> trivial.rate
1.0
property n_symbols: int#

\(K\), symbols carried by one codeword.

property n_tx: int#

\(N_t\), transmit antennas.

property n_slots: int#

\(T\), channel uses spanned by one codeword.

property rate: float#

\(K/T\), symbols per channel use.

property is_orthogonal: bool#

Whether the design was declared – and verified – orthogonal.

info() → dict[str, Any][source]#

What the code is, as a dictionary ready to print.

The same service the channels and Constellation offer: the facts a page would otherwise restate in prose, read off the object that holds them.

Returns:

dict – name, n_tx, n_slots, n_symbols, rate, orthogonal, orthogonality_gain – the constant \(c\) of \(M^T M = c \|H\|_F^2 I\) that the matched filter divides by, zero for a non-orthogonal design – and reference.

Examples

>>> info = get_code("alamouti").info()
>>> info["n_tx"], info["rate"], info["orthogonal"]
(2, 1.0, True)
encode(symbols: ndarray) → ndarray[source]#

Build codewords from symbols.

Parameters:

symbols (np.ndarray) – (..., K) or (..., n * K): the last axis is read in blocks of \(K\).

Returns:

np.ndarray – (..., n_tx, n * n_slots), antennas on axis -2.

equivalent_channel(H: ndarray) → ndarray[source]#

The real matrix \(\mathbf{M}(\mathbf{H})\) of the module docstring.

Parameters:

H (np.ndarray) – Channel matrix, (n_rx, n_tx).

Returns:

np.ndarray – Real matrix of shape (2 * n_rx * n_slots, 2 * K), mapping \([\mathbf{u}; \mathbf{v}]\) onto the stacked real and imaginary parts of the received block.

minimum_rank(alphabet: ndarray) → int[source]#

Diversity order by the rank criterion, over one alphabet.

The minimum rank of \(\mathbf{G}(\mathbf{s}) - \mathbf{G}(\mathbf{s}')\) over distinct symbol vectors. It equals \(N_t\) for a full-diversity code and 1 for spatial multiplexing, and the error probability of the code decays as \(\mathrm{SNR}^{-r N_r}\) with \(r\) that rank.

Exhaustive over \(|\mathcal{A}|^{K}\) pairs of a difference vector, so it is meant for small alphabets and short codes.

get_code(name: str, **kwargs: object) → SpaceTimeCode[source]#

Return a catalog code by name, as get_alphabet returns a constellation.

Parameters:
  • name (str) – Entry name; see available_codes().

  • **kwargs – Forwarded to the entry factory.

Returns:

SpaceTimeCode – The code.

Raises:

KeyError – If no entry answers to that name.

Examples

>>> get_code("alamouti").n_tx
2
>>> get_code("spatial-multiplexing", n_tx=4).rate
4.0
register_code(name: str) → Callable[[Callable[[...], SpaceTimeCode]], Callable[[...], SpaceTimeCode]][source]#

Register a code factory; users can add their own.

Parameters:

name (str) – Name the factory answers to in get_code().

Examples

>>> @register_code("repetition-2")
... def _repetition():
...     direct = np.zeros((1, 2, 2), dtype=complex)
...     direct[0, 0, 0] = direct[0, 1, 1] = 1
...     return SpaceTimeCode("repetition-2", direct,
...                          np.zeros_like(direct))
>>> get_code("repetition-2").rate
0.5
available_codes() → list[str][source]#

Names get_code() answers to.

Examples

>>> "alamouti" in available_codes()
True
class SpaceTimeEncoder(code: SpaceTimeCode, *, name: str = 'space-time encoder')[source]#

Map a symbol stream onto antennas and time slots.

Signal Model#

Blocks of \(K\) symbols become codewords \(\mathbf{G}(\mathbf{s})\) of \(N_t \times T\), concatenated along time. A stream of \(N\) symbols therefore leaves as \((N_t, N T / K)\) samples: the code changes the number of channel uses, by the factor \(T/K = 1/\text{rate}\).

Axes: declared axis – output carries antennas on axis -2 (D2), so it feeds FlatMIMOChannel directly.

param code:

The code, usually from get_code().

type code:

SpaceTimeCode

param name:

Block name.

type name:

str, optional, keyword-only

raises ShapeError:

If the symbol count is not a multiple of \(K\).

References

S. M. Alamouti, IEEE JSAC 16(8), 1998; V. Tarokh et al., IEEE Trans. Inf. Theory 45(5), 1999.

Examples

>>> encoder = SpaceTimeEncoder(get_code("alamouti"))
>>> symbols = np.array([1 + 0j, 2 + 1j, -1 + 0j, 0 + 1j])
>>> encoder(symbols).shape                      # 4 symbols -> 2x4
(2, 4)
class SpaceTimeDecoder(code: SpaceTimeCode, *, H: ndarray | None = None, name: str = 'space-time decoder')[source]#

Recover the symbols of an orthogonal design, exactly.

Signal Model#

With the equivalent channel \(\mathbf{M}(\mathbf{H})\) of the module docstring, the received block reads \(\mathbf{y} = \mathbf{M}[\mathbf{u};\mathbf{v}] + \mathbf{b}\) with white real noise. When the design is orthogonal, \(\mathbf{M}^{\mathsf{T}}\mathbf{M} = c\|\mathbf{H}\|_F^2 \mathbf{I}\), so the least-squares solution is a matched filter

\[\begin{split}\begin{bmatrix}\widehat{\mathbf{u}} \\ \widehat{\mathbf{v}} \end{bmatrix} = \frac{\mathbf{M}^{\mathsf{T}} \mathbf{y}} {c \left\|\mathbf{H}\right\|_F^2}\end{split}\]

and it is the maximum-likelihood estimate, not an approximation of it: this is the whole point of orthogonal designs, and the reason they cost \(O(K)\) instead of \(O(|\mathcal{A}|^K)\). Each symbol comes out with an SNR multiplied by \(c\|\mathbf{H}\|_F^2\), i.e. with diversity \(N_t N_r\); the constant \(c\) belongs to the design and is measured when it is built, not assumed.

A non-orthogonal code has no such shortcut, and this block refuses it rather than returning a zero-forcing answer under a name that promises optimality; SpaceTimeCode.equivalent_channel() hands the problem to the detectors of comnumpy.mimo.detectors instead.

Axes: declared axis – input carries antennas on axis -2.

param code:

The code used at the transmitter; must be orthogonal.

type code:

SpaceTimeCode

param H:

Channel matrix (n_rx, n_tx), assumed known at the receiver.

type H:

np.ndarray, optional, keyword-only

param name:

Block name.

type name:

str, optional, keyword-only

raises ValueError:

If the code is not orthogonal, or if H was not set.

raises ShapeError:

If the number of received samples is not a multiple of the code’s n_slots.

References

V. Tarokh, H. Jafarkhani, A. R. Calderbank, IEEE Trans. Inf. Theory 45(5), 1999, Section III (linear decoding of orthogonal designs).

Examples

>>> code = get_code("alamouti")
>>> H = np.array([[1 + 0j, 0.5 - 0.2j]])
>>> symbols = np.array([1 + 1j, -1 + 0j])
>>> received = H @ code.encode(symbols)
>>> print(np.round(SpaceTimeDecoder(code, H=H)(received), 6) + 0.0)
[ 1.+1.j -1.+0.j]
coding_gain(code: SpaceTimeCode, alphabet: ndarray) → float[source]#

Determinant criterion: the coding gain of a full-diversity code.

Signal Model#

When the rank criterion is met with full rank \(N_t\), the pairwise error probability is governed by

\[\delta = \min_{\mathbf{s} \neq \mathbf{s}'} \det\left[ \left(\mathbf{G} - \mathbf{G}'\right) \left(\mathbf{G} - \mathbf{G}'\right)^{H} \right]\]

and a code with \(\delta\) bounded away from zero as the constellation grows is said to have a non-vanishing determinant – the property the Golden code was built for.

Axes: element-wise – takes a code and a 1-D alphabet.

param code:

The code to measure.

type code:

SpaceTimeCode

param alphabet:

Constellation the symbols are drawn from.

type alphabet:

np.ndarray

returns:

float – \(\delta\), zero if the code is not full diversity.

References

V. Tarokh, N. Seshadri, A. R. Calderbank, IEEE Trans. Inf. Theory 44(2), 1998 (determinant criterion); Belfiore, Rekaya, Viterbo, IEEE Trans. Inf. Theory 51(4), 2005.

Examples

>>> from comnumpy.core.utils import get_alphabet
>>> qpsk = get_alphabet("QAM", 4)
>>> print(round(coding_gain(get_code("alamouti"), qpsk), 6))
4.0