Clock recovery using Mueller and Muller adds noise affecting EVM or SNR (Two cases - GNU Radio & Python code) The implementation of the Mueller and Muller clock recovery that I am seeing adds a large amount of 'noise' or degradation to the Error Vector Magnitude (EVM) to the symbols on the constellation. This is done independently in GNU Radio and a manual script in Python - giving similar results. Is this normal?
I understand that I need to adjust the parameters in this algorithm but I cant get anywhere near an optimisation that is adequate, its too noisy.
In both implementations GNU radio and python , the only impairment to the original data signal is the performance of the RRC pulse shaping filters. You can see that's its an almost perfect received constellation with an EVM &lt; -60 dB using a filter taps of 300 and roll off factor of 0.35. See below image.

However, once I add the M&amp;M algorithm I have no luck getting anywhere close to this and it generates a lot of noise. See below image.

It somewhat makes it difficult to assess the true extent of impairments I may later add in the model, like AWGN. Because if I add AWGN to this then the EVM or SNR just gets worse when the M&amp;M is being used.
Here is the GNU radio flow.
EDIT Note that in my python code the Rx RRC filter is performed before the M&amp;M, but in GNU radio I do it before an RRC because having an RRC before the MM block doesn't work give any result in the flow below.

Below is the M&amp;M python algorithm, its from pySDR.com
samples = input_signal
interp = 16
samples_interpolated = signal.resample_poly(samples,interp,1)
mu = 2 #2 # initial estimate of phase of sample
out = np.zeros(len(samples) + 10, dtype=np.complex)
out_rail = np.zeros(len(samples) + 10, dtype=np.complex) # stores values, each iteration we need the previous 2 values plus current value
i_in = 0 # input samples index
i_out = 2 # output index (let first two outputs be 0)
while i_out &lt; len(samples) and i_in &lt; len(samples):
    out[i_out] = samples_interpolated[i_in*interp + int(mu*interp)] # grab what we think is the &quot;best&quot; sample
    out_rail[i_out] = int(np.real(out[i_out]) &gt; 0) + 1j*int(np.imag(out[i_out]) &gt; 0)
    z = (out_rail[i_out] - out_rail[i_out-2]) * np.conj(out[i_out-1])
    zz = (out[i_out] - out[i_out-2]) * np.conj(out_rail[i_out-1])
    mm_val = np.real(zz - z)
    mu += sps + 0.3*mm_val
    i_in += int(np.floor(mu)) # round down to nearest int since we are using it as an index
    mu = mu - np.floor(mu) # remove the integer part of mu
    i_out += 1 # increment output index
out = out[2:i_out-1] # remove the first two, and anything after i_out (that was never filled out)
Unlike the Gardner Loop, the M&amp;M synchronizer should be performed after the RRC filter in the receiver for best performance.  With cases of high RRC alpha, the M&amp;M won't work as expected without the complete Raised-Cosine filtering (RRC in transmitter followed by RRC in receiver) as the slope of the error term will reverse, with high self-noise, as I demonstrate in this plot below:

The blue line is the discriminator output showing average error versus offset, and the red line is the standard deviation of that error showing the self-noise. In the first case the slope of error output versus offset actually inverts in proximity of the lock point (so it would work, barely, if the feedback is inverted but with very high self-noise and low slope so very low timing SNR otherwise it would lock interestingly at 1/4 sample offset).
Specific to the OP's case, below is the same chart with a lower (and typical) RRC alpha where the discriminator curve is quite similar but the self-noise is much higher at the lock condition if the M&amp;M is used prior to the second RRC filter.

Both the Gardner Loop and M&amp;M will have self noise, but the self noise is typically averaged out in the recovery loop to be well below the receiver noise floor once the accumulated time sample location is determined for each new sample.
Why the difference between Gardner and M&amp;M?
It is interesting that the Gardner performs better using the samples prior to the second RRC filter while the M&amp;M synchronizer is better after. This is because the M&amp;M synchronizer operates on one sample per symbol using sample locations at the decision locations and thus depends on the slope of the waveform in vicinity of these locations to determine timing error. The Gardner operates using 2 samples per symbol and operates by approximating the slope at the zero crossings, which have less timing jitter before the second RRC filter as we see in the eye diagrams below (notice the wider range of zero-crossing transition times in the waveform after the second RRC filter):

Generally for timing detection our best estimates should be obtained from the zero crossings where the slope and therefore sensitivity to timing error would be highest. This is true on average and is one of the reasons the Gardner Loop has better SNR performance in low SNR conditions (and M&amp;M has better performance in high SNR conditions since it uses decisions in its computation, which eliminate noise if the decisions are correct). However, the competing requirement of having no inter-symbol interference at the data decision location results in increased zero-crossing jitter at the waveform locations of interest for timing detection. One solution to this if self-noise was an issue (it typically isn't as the timing loop bandwidth is so much smaller as to average it out) is the use of pre-filters which can equalize the ISI at the zero crossing locations for timing detection, on a separate data-path from the data which has zero ISI at the symbol detection locations as desired for best data-recovery performance.