Compensators#
- class BlindDualMIMOCompensator(L: int = 10, *, alphabet: ndarray, mu: float = 0.0001, oversampling: int = 1, mode: Literal['cma', 'rde', 'dd'] = 'cma', sub_block_length: int = 20, name: str = 'mimo filter')[source]#
Blind adaptive 2x2 MIMO equalizer (CMA, RDE or DD stochastic gradient).
Signal Model#
At each step \(n\), the two output polarizations are computed from the estimated equalizer matrix \(\mathbf{W}\) of size \(2 \times 2(2L+1)\) – written \(\mathbf{W}\), not \(\mathbf{H}\), because everywhere else in this package \(\mathbf{H}\) is the channel; the attribute is
H_for historical reasons, the trailing underscore marking it as estimated (D23):\[y_i[n] = \mathbf{w}_i^H \tilde{\mathbf{x}}[n], \qquad \tilde{\mathbf{x}}[n] = \begin{bmatrix} x_0[n] & \cdots & x_0[n-2L] & x_1[n] & \cdots & x_1[n-2L] \end{bmatrix}^T\]where \(\mathbf{w}_i^T\) is the \(i\)-th row of \(\mathbf{W}\) and \(x_0\), \(x_1\) are the two received polarizations. The equalizer is updated by stochastic gradient descent, \(\mathbf{W} \leftarrow \mathbf{W} + \mu \mathbf{g}[n]\), minimizing one of the following blind losses:
Constant Modulus Algorithm (CMA): \(\mathcal{L}_{CMA}(y[n]) = \left| R - |y[n]|^2 \right|^2\), where \(R = \mathbb{E}[|s|^4] / \mathbb{E}[|s|^2]\) is derived from the alphabet;
Radius Directed Equalization (RDE): \(\mathcal{L}_{RDE}(y[n]) = \left| \mathcal{P}_{rad}^2(|y[n]|) - |y[n]|^2 \right|^2\), where \(\mathcal{P}_{rad}\) projects onto the list of alphabet radii;
Decision Directed (DD): \(\mathcal{L}_{DD}(y[n]) = \left| \mathcal{P}_{\mathcal{M}}(y[n]) - y[n] \right|^2\), where \(\mathcal{P}_{\mathcal{M}}\) projects onto the alphabet.
The equalizer is adaptive (decision D22): every
forwardcall updates \(\mathbf{H}\) sample by sample, andpartial_fit(X)runs one adaptation pass overXfrom the current state.Staged adaptation#
CMA is the only one of the three that converges from a cold start. RDE and DD both need decisions that are already roughly right: started from the identity equalizer on 16-QAM, RDE stalls – 3.5 dB where the noise floor is 24. The usual recipe is to run CMA first and switch once it has opened the eye, which is what a
['cma', 'rde']schedule means in the literature. Two ways to do it here:between passes, with
partial_fit()andset_params()(or a plain assignment) to changemodeandmu;inside a single pass, by overriding
process_after_iteration(), which is called after every output sample with the most recent outputs and exists for exactly this (seeexamples/mimo/one_shot_CMA.py).
Measured on a static Jones rotation with a 24 dB noise floor, the three stages give 21.0, 23.9 and 23.98 dB: CMA opens the eye, RDE takes it to within a tenth of the floor, and DD closes what is left.
Axes: polarization pair – expects
(..., 2, N), produces(..., 2, N // oversampling)(one output sample peroversamplinginput samples: fractionally spaced equalizer). Leading axes are batch, and the butterfly is a per-pair algorithm: each pair gets its own equalizer state, adapted independently. After a batched passH_carries the batch axes in front,(..., 2, 2(2L+1)), so a furtherpartial_fitresumes each pair from its own state.Delay. The centre tap of the initial equalizer sits \(L\) input samples back, so output sample \(k\) is aligned with input sample \(k \cdot os - L\): the block has a group delay of \(L\) input samples, and comparing its output with the transmitted symbols without accounting for it reads as pure noise. The first \((2L+1)/os\) output samples are never written – the filter has no history yet – and are left at zero.
Cost, and why the loop is a loop. While it adapts, this block costs about 18 us per output sample: \(\mathbf{H}\) is updated from \(y[n]\) before \(y[n+1]\) is computed, so the samples cannot be processed together – that is what a stochastic gradient is, and no amount of vectorization removes it. The time goes into a dozen tiny numpy calls per iteration, each dominated by Python overhead rather than arithmetic, so the only real way down is a compiled kernel.
There is one exact exception, and it is the useful one. With \(\mu = 0\) the equalizer is frozen, every output is the same linear map, and the pass becomes a sequence of matrix products – measured 25 to 30 times faster on 100 000 output samples, and bit-for-bit the same output. That is the “fit, then apply” regime of decision D22: adapt on a preamble with
partial_fit(), setmu = 0, and run the payload through. The fast path is skipped whenprocess_after_iteration()is overridden, since that hook is defined per output sample.- param L:
Half-length of the filter (each row of \(\mathbf{H}\) holds \(2L+1\) taps per polarization). Default is 10.
- type L:
int, optional
- param alphabet:
Modulation alphabet, used to derive the CMA radius \(R\), the RDE radius list and the DD decisions.
- type alphabet:
np.ndarray, keyword-only
- param mu:
Step size \(\mu\) of the gradient update. Default is 1e-4.
- type mu:
float, optional, keyword-only
- param oversampling:
Oversampling factor of the input. When greater than one, the algorithm implements a fractionally spaced equalizer. Default is 1.
- type oversampling:
int, optional, keyword-only
- param mode:
Blind loss used for the update. Default is
"cma". See Staged adaptation above: only"cma"converges from a cold start.- type mode:
{“cma”, “rde”, “dd”}, optional, keyword-only
- param sub_block_length:
Number of recent output samples handed to
process_after_iteration(). Default is 20.- type sub_block_length:
int, optional, keyword-only
- param name:
Name of the processor instance. Default is
"mimo filter".- type name:
str, optional, keyword-only
- H_#
Estimated equalizer matrix of shape
(2, 2*(2*L+1))(decision D23: the trailing underscore distinguishes it from the configured channelHofFlatMIMOChannel). Initialized to the identity equalizer (center tap) and updated at each call.- Type:
np.ndarray
- raises ShapeError:
If the input does not carry a polarization pair
(..., 2, N), or ifH_carries the batch shape of a previous pass and the new input carries a different one – callinitialize_H()to restart from the identity equalizer.
References
D. N. Godard, “Self-recovering equalization and carrier tracking in two-dimensional data communication systems,” IEEE Transactions on Communications, vol. 28, no. 11, pp. 1867-1875, 1980.
M. S. Faruk, S. J. Savory, “Digital signal processing for coherent transceivers employing multilevel formats,” Journal of Lightwave Technology, vol. 35, no. 5, pp. 1125-1141, 2017.
Examples
>>> alphabet = np.array([1+1j, 1-1j, -1+1j, -1-1j]) / np.sqrt(2) >>> compensator = BlindDualMIMOCompensator(L=2, alphabet=alphabet) >>> rng = np.random.default_rng(0) >>> X = alphabet[rng.integers(0, 4, size=(2, 200))] >>> Y = compensator(X) >>> print(Y.shape, compensator.H_.shape) (2, 200) (2, 10)
- process_after_iteration(n: int, Y_sub: ndarray) None[source]#
Hook called after each output sample; does nothing by default.
Override it to schedule the stages of the adaptation (switch
modeonce the eye is open) or to correct a residual phase.- Parameters:
n (int) – Index of the output sample just produced.
Y_sub (np.ndarray) – The last
sub_block_lengthoutputs,(2, K), oldest first.