0
$\begingroup$

I am trying to model the interaction of two signals and for various reasons, am trying to do it in the complex baseband domain.

Signal-A is a complex baseband signal (no frequency symmetry around $f=0$). Signal-B is also as above.

I need to shift signal-B upwards from $f=0$ by some amount = $f_B$. If I just multiply signal-B by $\exp(j2\pi f_B t)$, the resulting spectrum shows various images, which I assume I must resolve.

If I use an I/Q modulator type of method, I will be discarding negative frequencies, and will end up with a real signal for the upshifted signal-B only, while signal-A remains complex valued. I believe this would be incorrect.

If I convert signal-B to an analytic equivalent (by taking Hilbert transforms of the real & imaginary parts & adding with 90-deg phase shift), I will once again lose negative frequency content, although I wouldn't have images - this should also be incorrect.

What is the correct method to do this? Should I simply stick to multiplying by $\exp(j2\pi f_B t)$ and then bandpass filter the result around $f=f_B$? If so, I assume this would need to be a complex filter (so as not to retain images in the region around $f=-f_B$).

See example spectra, with 0,1,10,25MHz $f_B$ values. We are talking about the red curve. Ignore the blue curve. enter image description here

$\endgroup$
5
  • $\begingroup$ "If I just multiply signal-B by $\exp(j2\pi f_B t)$, the resulting spectrum shows various images..." $$ $$ Why? I see no reason why that multiplication does not simply slide your complex baseband signal to the right by the amount of $f_B$. $\endgroup$ Commented May 31, 2025 at 2:16
  • $\begingroup$ Robert, I think he is doing this in discrete-time in spite of his use of continuous-time variables. I could be mistaken. $\endgroup$ Commented May 31, 2025 at 14:55
  • 1
    $\begingroup$ @RandyYates yah, I thought that too. But then another variable that is necessary to know is the sample rate. Still, even in discrete time, multiplying $x[n]$ by $e^{j \omega_0 n}$ will shift the DTFT spectrum to the right (if $\omega_0 > 0$) by a displacement in the amount of $\omega_0$. If part of that shifted spectrum hits $\pi$ (a.k.a. "Nyquist"), the part that exceeds $\pi$ (shifted to the right of Nyquist) will fold over. $\endgroup$ Commented May 31, 2025 at 18:58
  • $\begingroup$ Yes, the data is a discrete time sequence - I am just more comfortable writing the continuous time expressions. The sample rate is very high and should be enough not to cause aliasing, which is why I am puzzled about the result. More comments below RandyYates's answer. $\endgroup$ Commented Jun 2, 2025 at 16:35
  • $\begingroup$ Well, the math is pretty clear. If your complex-valued signal is bandlimited to below $\frac{f_s}(2) - f_B$, that means any frequency components at or above $\frac{f_s}(2) - f_B$ have zero amplitude, then when you multiply that complex signal by $e^{j 2 \pi f_B t}$, then no non-zero frequency component will reach Nyquist (which is $\frac{f_s}(2)$) and there can be no aliasing. Sampling any non-zero signal (even a properly bandlimited signal) always results in images, but they are all above the Nyquist frequency. $\endgroup$ Commented Jun 2, 2025 at 17:26

3 Answers 3

2
$\begingroup$

Multiplication of your discrete time signal by a time varying complex exponential is the correct way to effect a circular shift of the digital spectrum (i.e. rotate the spectrum about the origin of the z-plane):

$$ B_s[n] = B[n] \cdot e^{j\pi \frac{f_B}{F_s/2} n }$$

I will speculate that the artifacts you are seeing are due to numerical problems.

With frequency rotators, numerical problems usually show up in one of two ways:

  1. When using the complex exponential directly, most numerical implementations of $\sin()$ and $\cos()$ will start to behave poorly when their arguments get too far from the interval $[-2\pi, 2\pi]$
  2. When using a phase increment complex number that is applied repeatedly, repeated multiplications can cause the resultant number to have a modulus that moves away from 1.

Here is some GNU Octave/Matlab code, that I wrote years ago, that generates a vector of values to effect frequency rotation that avoids these numerical problems. The output of vector $W$ of this function should be multiplied point by point with your signal samples vector.

function [W, ZN] = freq_rotator(F, FS, N, MODE, A)
% [W, ZN] = freq_rotator(F, FS, N, 'arith')
% [W, ZN] = freq_rotator(F, FS, N, 'arith', Z0)
% W = freq_rotator(F, FS, N, 'exp')
% W = freq_rotator(F, FS, N, 'exp', PHI_C)
%
% *** This is a looping implementation to assist in porting to C/C++/CUDA
% Generate a vector of complex values W, that when multiplied term by
% term with a time domain sequence, effects a cyclic DFT spectrum shift
% in the DFT frequency domain.
%
%     F  the freqeuncy shift desired, with values in the range [-FS/2, FS/2]
%        encompassing the complete output range of possible rotations.
%
%     FS the sample rate in samples/second.
%
%     N  the number of output values to generate, i.e. the length of W.
%
%     MODE is the algorithm to use 'arith' for arithmetic or 'exp' for
%        complex exponential.  The arithmetic mode is useful for streaming,
%        but is serial.  The exponential mode uses complex exponentials
%        to compute the terms.  This function does it serially, but it could
%        be parallelized easily.
%
%     Z0 the initial complex value to use as the start of the rotation sequence.
%        It will be normalized to unit modulus.  It allows successive calls to
%        this function to maintain continuous phase of the rotator.  If not
%        specified, it defaults to 1+0j.
%
%     ZN the complex value to use in a subsequent call to this function to
%        maintain continuous phase, if continuing the rotation sequence.
%
%     PHI_C is the desired phase angle of the center term in radians.  If not
%        specified, it defaults to 0 radians.

if (nargin < 4)
    MODE = 'arith';
end


W = zeros(1,N);
radian_phase_inc = pi/(FS/2).*F;

if (strcmp (MODE, 'arith'))
    % Arithmetic mode algorithm. Good for streaming, but strictly serial,
    % because it's recursive:
    %     z[n] = z_phase_inc * z[n-1]

    if (nargin < 5)
        z0 = 1+0j; % exp(1j*0) because why not.
    else
        z0 = A/abs(A); % normalize to ensure it is on the unit circle.
    end

    z_phase_inc = exp(1j*radian_phase_inc);

    % N.B. all values in W should be unit modulus, i.e. on the unit circle
    W(1) = z0;
    j = 0;
    for i = 2:N
        W(i) = W(i-1)*z_phase_inc; % mult. of unit vectors adds phase angles.

        % renormalize once in a while, so accumlated error doesn't move us too
        % far away from the unit circle.
        j = j+1;
        if j == 256
            W(i) = W(i)/abs(W(i));
            j = 0;
        end
    end

    ZN = W(N)*z_phase_inc; % mult. of complex unit vectors adds phase angles.
    ZN = ZN/abs(ZN); % normalize to ensure it is on the unit circle.
else
    % Exponential mode algorithm - can be parallelized, but calls sin() & cos().
    % Conceptually this algorithm does this (ignoring index shifts) and can
    % be parallelized on the index n:
    %
    %   z[n] = exp(j*n*theta)*exp(j*phi_c) = 
    %             (cos(n*theta) + j*sin(n*theta))*(cos(phi_c) + j * sin(phi_c))
    %
    % There is a symmetry around the center value, so only half the terms
    % actually need to be computed by calling cos() & sin().  The other half
    % of the terms are obtained by reflection and conjugation.

    if (nargin < 5)
        w_phase_shift_vec = 1+0j; % i.e. no additional phase shift.
    else
        % Ensure within [0,2*pi).
        % When porting to C/C++/CUDA, take care with how mod() handles
        % negative signs. See the Octave or MatLab documentation.
        w_phase_shift_vec = exp(1j*mod(A, 2*pi));
    end

    if (mod(N, 2) == 0)
        center = N/2+1;
        neg_count = N/2;
        pos_count = N/2-1;
    else
        center = (N-1)/2+1;
        neg_count = (N-1)/2;
        pos_count = (N-1)/2;
    end

    % Center value.
    W(center) = 1+0j;
    % Add user requested phase shift.
    if (nargin >= 5)
        W(center) = W(center) * w_phase_shift_vec;
    end

    for i = 1:neg_count
        % Compute phase angle for negative offset from center.
        % Ensure within [0,2*pi).
        % When porting to C/C++/CUDA, take care with how mod() handles
        % negative signs. See the Octave or MatLab documentation.
        phase_angle = mod(-i*radian_phase_inc, 2*pi);

        % Compute required complex exponential values from phase angle

        % Negative offset from center.
        W(center-i) = exp(1j*phase_angle);

        % Positive offset from center.
        if (i <= pos_count)
            % We're symmetrical around the center point, so just conjugate and
            % copy for the positive offset from center.
            W(center+i) = conj(W(center-i));
        end

        % Add user requested phase shift.
        if (nargin >= 5)
            % Negative offset from center.
            W(center-i) = W(center-i) * w_phase_shift_vec;
            % Positive offset from center.
            if (i <= pos_count)
                W(center+i) = W(center+i) * w_phase_shift_vec;
            end
        end
    end
    ZN = 0; % meaningless output
end
$\endgroup$
2
  • $\begingroup$ When using the complex exponential directly, most numerical implementations of sin() and cos() will start to behave poorly when their arguments get too far from the interval [−2π,2π]. - - - - - I don't think people should use the library functions for sin and cos anyway. They're slow. Table lookup or the incremental thing you do with the complex exponential (CORDIC) is the way to do this. $\endgroup$ Commented Jul 3, 2025 at 15:35
  • $\begingroup$ @robertbristow-johnson I agree. $\endgroup$ Commented Jul 3, 2025 at 16:39
1
$\begingroup$

The shortest shorthand notation for frequency shifting a real signal $x(t)$ by an amount $f_B$ is:

$$ y(t) = \Re e \Big\{ \big(x(t) + j \hat{x}(t) \big) e^{j 2 \pi f_B t} \Big\} $$

where

$$\begin{align} \hat{x}(t) & \triangleq \mathscr{H} \Big\{ x(t) \Big\} \\ \\ & = \int\limits_{-\infty}^{\infty} \frac{1}{\pi u} \ x(t-u) \ \mathrm{d}u \end{align}$$

And $\mathscr{H} \{\cdot\}$ is the Hilbert transform.

Now, to do this digitally (in discrete time) means you need to make a discrete-time Hilbert transformer. That means turning an infinite impulse response into a finite impulse response (suggest using the Kaiser window) and sampling it. This FIR (which is centered at $t=0$ and therefore acausal) must be delayed by a known amount to make it causal so what you get out of the Hilbert transformer is really:

$$ \hat{x}(t- T_D) = \int\limits_{-T_D}^{+T_D} \frac{1}{\pi u} w(u) \ x(t - T_D - u) \ \mathrm{d}u $$

$w(t)$ is the Kaiser window centered at $t=0$. Since the Hilbert output, $\hat{x}(t-T_D)$ is delayed by $T_D$, so also should $x(t)$ be delayed by the same amount for this to work. So you get instead:

$$ y(t) = \Re e \Big\{ \big(x(t-T_D) + j \hat{x}(t-T_D) \big) e^{j 2 \pi f_B t} \Big\} $$

In this answer, I go through what you need to do to make a discrete Hilbert transformer.

$\endgroup$
0
$\begingroup$

First of all I'm assuming you are only interested in a band-limited section of signal B around DC of bandwidth $B_i$.

If $|f_B| + B_i/2 < F_s/2$ then you can just mix and BP filter, otherwise you will have the situation where your signal of interest occupies sections of both the positive and negative digital bandwidths (i.e., it gets split across the image.

In that case you would first need to resample the input signal B to a higher sample rate $F_{s2}$ so that $|f_B| + B_i/2 < F_{s2}/2$.

Notes:

  1. $f_B$ can be either positive or negative.
  2. the BP filter will be complex
  3. the output will be complex
  4. I'm unsure of the relevence of signal A: you define it but its use in your question is not clear.
$\endgroup$
2
  • $\begingroup$ In this example, |fB| + Bi/2 < Fs/2. In the example I am using here, fB = various values between 0 and 25MHz. Bi = 5MHz. Fs > 120Msps. Please note that I am upsampling the signal data to reach the above quoted Fs and I don't see any artifacts in the upsampled spectrum if I set fB = 0. I am also using Matlab's pwelch method to plot spectra. Let me see if I can attach images for example spectra to show this. $\endgroup$ Commented Jun 2, 2025 at 16:39
  • $\begingroup$ I forgot to mention - signal A is irrelevant at this point. I have edited the original question to include spectrum images. Please ignore the blue curve. $\endgroup$ Commented Jun 2, 2025 at 16:56

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.