Taking the RMS of a real cosine signal is ~√2:
>>> sample_rate = 44100
>>> mark = 1200
>>> wavelength = sample_rate / mark
>>> c = np.cos(np.arange(wavelength))
>>> s = np.sin(np.arange(wavelength))
>>> np.sqrt((c**2).mean())
0.7080413005146723
>>> np.sqrt((s**2).mean())
0.7061710251528956

But for a complex signal, I get a wildly different thing?
>>> x = c+s*1j
>>> np.sqrt((x**2).mean())
(0.1079690475801299+0.09493339120848676j)
>>> np.absolute(np.sqrt((x**2).mean()))
0.14376948216399732

Why?

Follow

@isomer

Isn't `x**2` literally the square, which will be a complex value that just rotates twice as quickly?

@robryk Well, yes. However, the magnitude should remain at 1?

>>> np.all(np.absolute(x) -np.absolute(x**2) < 1e-10)

I guess the mean of the complex numbers isn't particularly useful?

>>> np.absolute(sum(x) / len(x))
1.4102891750589582e-16

I guess the mean is just finding the origin, and the sqrt of the origin is also the origin.

So, next question: What should I be doing to compare signals?

@isomer

Yes, magnitude(mean) != mean(magnitude).

Mean of complex numbers is useful or not in a very contextual way :) (e.g. see why the Fourier basis is linearly independent).

It seems that numpy is slightly silly and your options for computing squared norm is either np.real(x)**2+np.imag(x)**2 or np.abs(x)**2.

@robryk I think I've figured out what's going on.

Euclidian distance in 1d is √x²
Euclidian distance in 2d is √(re² + im²).
(and so on)

So the "squared" in RMS is actually actually asking for Euclidian distance from the origin.

It's not asking for literally x², since as pointed out, that doesn't actually work.

@isomer

Why do you want RMS of a complex signal? What is the physical thing you are trying to model?

@robryk taking a I/Q signal from an SDR. Convolve with a quadrature cosine of 1200Hz. Convolve with a quadrature cosine of 2400Hz.

Compare the RMS of the two convolutions to see which FSK is active.

@isomer

Don't you mean "pointwise multiply" when you say convolve? (Then, the _resulting signal_ would average out to 0 if the sine was absent and to a complex number with absolute value prop. to sine's strength in the original signal and arg indicating the phase offset. Note that this is about the average of the signal and not the absolute value of the signal.)

@robryk

You're proposing:

>>> one_hz = 2*pi*np.arange(1000)
>>> signal = np.cos(one_hz/50) + np.sin(one_hz/50)*1j
>>> quadrature50 = np.cos(one_hz/50) + np.sin(one_hz/50)*1j
>>> quadrature100 = np.cos(one_hz/100) + np.sin(one_hz/100)*1j
>>> quadrature75 = np.cos(one_hz/75) + np.sin(one_hz/75)*1j
>>> np.absolute((signal * quadrature50)).mean()
1.0
>>> np.absolute((signal * quadrature75)).mean()
1.0
>>> np.absolute((signal * quadrature100)).mean()
1.0

?

@isomer

No, absolute value of the mean rather than mean of the absolute value.

@isomer

Ah, and you need to have these be counterrotating (i.e. flip the sign of time in quadratureXXX definitions).

The reason that works is that if you have a spinny thing that spins significantly more than once during your sampling period, the value of the spinny thing will average to zero. If the spinny thing makes much less than one revolution, the average will be close to something on the unit circle. So, for a signal that's a single sine wave this should work well to detect whether its frequency is close to the given frequency (up until the point when aliasing becomes a problem -- which is when the spinny thing spins at least ~once between two adjacent samples).

It also works for arbitrary signals because everything up to the point where you take the absolute value is linear, and all the uninteresting signals contribute ~0.

@robryk So if we have the signal and the defence spin in opposite directions at their various frequencies they will cancel out and hopefully settle at a specific stationary point. Averaging those points will give you a location.

The angle of that location is the phase difference between the two. The magnitude is the product of the magnitudes of the two inputs.

Yes?

@isomer

Yes. And that isn't affected by components of the signal of sufficiently different frequencies (with the "sufficient" distance being on the order of magnitude of 1/sampling window).

@robryk now ideally, the next thing is I want this to be able to work on real and complex input signals. (Eg if you're getting the output of a radio through a sound card microphone you get real samples. If you're using an SDR you get complex quadrature samples).

I'd like to be able to use the same algorithm for both, assuming that the reference signal is always complex. (Although it might be nice to have one algorithm that can work for all 4 combinations of real and complex)

@isomer

There's something simple that works that escapes me this moment. Just one comment: if you get it via a soundcard-like input you get real values, because you shifted it to baseband and IIUC lost the distinction between negative and positive frequencies. If it was shifted s.t. carrier wasn't DC you'd IIRC be able to retrieve the expected complex signal (because carrier-eps and carrier+eps are distinguishable frequencies).

@robryk of course yes. But I'm playing with the cards I am dealt here.

Most people have radios that don't have a useful output except for line out.

Id prefer direct access to the I/q, but that's not always available

@robryk @robryk Fortunately I'm writing this in rust, I was just using numpy as a quick shorthand to explain to everyone what was going on without pasting a massive pile of code.

Sign in to participate in the conversation
Qoto Mastodon

QOTO: Question Others to Teach Ourselves
An inclusive, Academic Freedom, instance
All cultures welcome.
Hate speech and harassment strictly forbidden.