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
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,
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
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=Trueand the design is not orthogonal.
References
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
Constellationoffer: 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 – andreference.
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_alphabetreturns 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
FlatMIMOChanneldirectly.- 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 ofcomnumpy.mimo.detectorsinstead.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
Hwas 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