Note

This page was generated from a Jupyter Notebook. Download the notebook (.ipynb)

Advanced Features and Operations of FrequencySeries

Open In Colab

This notebook introduces the new methods and features of the FrequencySeries class extended in gwexpy. We will focus on handling complex spectra, calculus operations, filtering (smoothing), and integration with other libraries.

[1]:
import matplotlib.pyplot as plt
import numpy as np

from gwexpy.frequencyseries import FrequencySeries
from gwexpy.plot import Plot
from gwexpy.timeseries import TimeSeries

plt.rcParams["figure.figsize"] = (10, 6)

1. Data Preparation

First, we create a FrequencySeries from a TimeSeries using FFT. Here, we generate a test signal with specific frequency components.

[2]:
fs = 1024
t = np.arange(0, 4, 1 / fs)
exp = np.exp(-t / 1.5)
exp[: int(exp.size / 4)] = 0
data = (
    np.sin(2 * np.pi * 10.1 * t)
    + 5 * exp * np.sin(2 * np.pi * 100.1 * t)
    + np.random.normal(scale=0.3, size=len(t))
)
ts = TimeSeries(data, dt=1 / fs, unit="um", name="Test Signal")
print(ts)
ts.plot(title=ts.name)

# Perform FFT to obtain FrequencySeries (using transient mode with padding)
spec = ts.fft(mode="transient", pad_left=1.0, pad_right=1.0, nfft_mode="next_fast_len")
print(f"Type: {type(spec)}")
print(f"Length: {len(spec)}")
print(f"df: {spec.df}")
TimeSeries([-0.30926935,  0.07921011, -0.38691015, ...,
             1.5878545 ,  1.02586626,  1.05521698],
           unit: um,
           t0: 0.0 s,
           dt: 0.0009765625 s,
           name: Test Signal,
           channel: None)
Type: <class 'gwexpy.frequencyseries.frequencyseries.FrequencySeries'>
Length: 3073
df: 0.16666666666666666 Hz
../../../../_images/web_en_user_guide_tutorials_intro_frequencyseries_3_1.png

2. Visualization and Conversion of Complex Spectra

Phase and Amplitude

Using the phase(), degree(), and to_db() methods, you can convert complex spectra into intuitive units.

[3]:
# Convert amplitude to dB (ref=1.0, 20*log10)
spec_db = spec.to_db()

# Get phase (in degrees, unwrap=True for continuity)
spec_phase = spec.degree(unwrap=True)

plot = Plot(spec_db, spec_phase, separate=True, sharex=True, xscale="log")
ax = plot.axes
ax[0].set_ylabel("Magnitude [dB (m)]")
ax[0].grid(True, which="both")

ax[1].set_ylabel("Phase [deg]")
ax[1].set_xlabel("Frequency [Hz]")
ax[1].grid(True, which="both")
plot.figure.suptitle("Complex Spectrum Analysis")
plot.show()
../../../../_images/web_en_user_guide_tutorials_intro_frequencyseries_5_0.png

3. Calculus in Frequency Domain

The differentiate() and integrate() methods allow you to perform differentiation and integration in the frequency domain. You can specify the order using the order argument (default is 1). This feature makes it easy to convert between displacement, velocity, and acceleration (multiplication/division by \((2 \pi i f)^n\)).

[4]:
# Differentiate from displacement (m) to velocity (m/s) (order=1)
vel_spec = spec.differentiate()

# Differentiate from displacement (m) to acceleration (m/s^2) (order=2)
accel_spec = spec.differentiate(order=2)

# Integration is also possible: acceleration -> velocity
vel_from_accel = accel_spec.integrate()

plot = Plot(
    spec.abs(), vel_spec.abs(), accel_spec.abs(), xscale="log", yscale="log", alpha=0.8
)
ax = plot.gca()
ax.get_lines()[0].set_label("Displacement [m]")
ax.get_lines()[1].set_label("Velocity [m/s]")
ax.get_lines()[2].set_label("Acceleration [m/s^2]")
ax.legend()
ax.grid(True, which="both")
ax.set_title("Calculus in Frequency Domain")
ax.set_ylabel("Magnitude")
plot.show()
../../../../_images/web_en_user_guide_tutorials_intro_frequencyseries_7_0.png

4. Spectrum Smoothing and Peak Detection

Smoothing

The smooth() method enables smoothing of the spectrum using moving averages and other techniques.

[5]:
# Smooth in amplitude domain with 11 samples
spec_smooth = spec.smooth(width=11)

plot = Plot(spec.abs(), spec_smooth.abs(), xscale="log", yscale="log")
ax = plot.gca()
ax.get_lines()[0].set_label("Original")
ax.get_lines()[0].set_alpha(0.6)
ax.get_lines()[1].set_label("Smoothed (width=11)")
ax.get_lines()[1].set_color("red")
ax.legend()
ax.grid(True, which="both")
ax.set_title("Spectrum Smoothing")
plot.show()
../../../../_images/web_en_user_guide_tutorials_intro_frequencyseries_9_0.png

Peak Detection

The find_peaks() method wraps scipy.signal.find_peaks, making it easy to extract peaks above a specific threshold.

[6]:
# Find peaks with amplitude >= 0.2
peaks, props = spec.find_peaks(threshold=0.2)

plot = Plot(spec.abs())
ax = plot.gca()
ax.plot(
    peaks.abs(), color="red", marker=".", ms=15, lw=0, zorder=3, label="Detected Peaks"
)
ax.set_xlim(1, 150)
ax.set_xscale("log")
ax.set_yscale("log")
ax.set_title("Peak Detection")
ax.legend()
plot.show()
../../../../_images/web_en_user_guide_tutorials_intro_frequencyseries_11_0.png

5. Advanced Analysis Features

Group Delay

The group_delay() method calculates the group delay (delay of the signal envelope) from the frequency derivative of the phase.

[7]:
gd = spec.group_delay()

plot = Plot(gd)
ax = plot.gca()
ax.set_ylabel("Group Delay [s]")
ax.set_xlabel("Frequency [Hz]")
ax.set_xlim(0, 200)
ax.set_title("Group Delay Calculation")
plot.show()
/home/washimi/work/gwexpy/.venv-docs-exec/lib/python3.12/site-packages/gwpy/plot/axes.py:235: UserWarning: Attempt to set non-positive xlim on a log-scaled axis will be ignored.
  return super().set_xlim(left=left, right=right, **kwargs)
../../../../_images/web_en_user_guide_tutorials_intro_frequencyseries_13_1.png

Inverse FFT (ifft)

The ifft() method returns a TimeSeries. Even for results from FFT with mode="transient", it can inherit the information and control restoration to the original length (trim=True).

[8]:
# Convert back to TimeSeries with inverse FFT
# mode="auto" reads the transient information from the input FrequencySeries and processes it appropriately
inv_ts = spec.ifft(mode="auto")
red_ts = inv_ts - ts

plot = Plot(ts, inv_ts, red_ts)
ax = plot.gca()
ax.get_lines()[0].set_label("Original")
ax.get_lines()[1].set_label("IFFT Result")
ax.get_lines()[2].set_label("Residual")
ax.get_lines()[1].set_linestyle("--")
ax.get_lines()[1].set_alpha(0.8)
ax.get_lines()[2].set_color("black")
ax.legend()
ax.set_title("Time Domain Round-trip (FFT -> IFFT)")
plot.show()

red_ts.plot(title="Residual Time Series after IFFT");
../../../../_images/web_en_user_guide_tutorials_intro_frequencyseries_15_0.png
../../../../_images/web_en_user_guide_tutorials_intro_frequencyseries_15_1.png

6. Integration with Other Libraries

Interconversion with Pandas, xarray, and control libraries has been added.

[9]:
# Convert to Pandas Series
pd_series = spec.to_pandas()
print("Pandas index sample:", pd_series.index[:5])
display(pd_series)

# Convert to xarray DataArray
try:
    da = spec.to_xarray()
    print("xarray coord name:", list(da.coords))
    display(da)
except ImportError:
    print("xarray not installed, skipping DataArray conversion.")

# Convert to control.FRD (can be used for control system analysis)
try:
    from control import FRD

    _ = FRD
    frd_obj = spec.to_control_frd()
    print("Successfully converted to control.FRD")
    display(frd_obj)
except ImportError:
    print("python-control library not installed")
Pandas index sample: Index([0.0, 0.16666666666666666, 0.3333333333333333, 0.5, 0.6666666666666666], dtype='float64', name='frequency')
frequency
0.000000      0.008480+0.000000j
0.166667      0.003430+0.008560j
0.333333     -0.009082-0.002793j
0.500000     -0.008785-0.004524j
0.666667     -0.002683+0.002147j
                     ...
511.333333   -0.001844-0.007221j
511.500000   -0.004601+0.005607j
511.666667   -0.003659-0.003937j
511.833333    0.007865+0.003549j
512.000000   -0.002854+0.000000j
Name: Test Signal, Length: 3073, dtype: complex128
xarray coord name: ['frequency']
<xarray.DataArray 'Test Signal' (frequency: 3073)> Size: 49kB
array([ 0.00847973+0.j        ,  0.00342982+0.00856023j,
       -0.00908238-0.00279319j, ..., -0.00365943-0.00393728j,
        0.00786504+0.00354929j, -0.00285359+0.j        ],
      shape=(3073,))
Coordinates:
  * frequency  (frequency) float64 25kB 0.0 0.1667 0.3333 ... 511.7 511.8 512.0
Attributes:
    unit:     um
    channel:  None
    epoch:    0.0
Successfully converted to control.FRD
FrequencyResponseData(
array([[[ 0.00847973+0.j        ,  0.00342982+0.00856023j,
         -0.00908238-0.00279319j, ...,
         -0.00365943-0.00393728j,  0.00786504+0.00354929j,
         -0.00285359+0.j        ]]], shape=(1, 1, 3073)),
array([0.00000000e+00, 1.66666667e-01, 3.33333333e-01, ...,
       5.11666667e+02, 5.11833333e+02, 5.12000000e+02],
      shape=(3073,)),
outputs=1, inputs=1)

7. Integration with Python Control Library

Interconversion with the Frequency Response Data (FRD) object from the control library, which is standard in the field of control engineering, is possible. This allows transfer functions measured with GWExPy to be directly used for control system design and analysis.

[10]:
try:
    import control

    # Convert FrequencySeries -> control.FRD
    # Specifying frequency_unit="Hz" appropriately converts to rad/s internally
    frd_sys = spec.to_control_frd(frequency_unit="Hz")

    print("\n--- Converted to Control FRD ---")
    print(str(frd_sys)[:1000] + "\n... (truncated) ...")

    # Plot Bode diagram (control library functionality)
    control.bode(frd_sys)  # (executable if plotting environment is available)

    # Restore control.FRD -> FrequencySeries
    fs_restored = FrequencySeries.from_control_frd(frd_sys, frequency_unit="Hz")

    print("\n--- Restored FrequencySeries ---")
    print(fs_restored)

except ImportError:
    print("Python Control Systems Library is not installed.")

--- Converted to Control FRD ---
<FrequencyResponseData>: sys[1]
Inputs (1): ['u[0]']
Outputs (1): ['y[0]']

Freq [rad/s]  Response
------------  ---------------------
       0.000     0.00848        +0j
       0.167     0.00343  +0.00856j
       0.333   -0.009082 -0.002793j
       0.500   -0.008785 -0.004524j
       0.667   -0.002683 +0.002147j
       0.833    0.001165 +0.004118j
       1.000     0.01692 -0.006842j
       1.167  -0.0005442  +0.00236j
       1.333   -0.009248 +0.003667j
       1.500    0.001655-0.0006437j
       1.667    -0.01334  -0.00551j
       1.833    0.008506 +0.003161j
       2.000    0.003991+0.0007447j
       2.167     0.01086 +0.001774j
       2.333   -0.002041 -0.003237j
       2.500    -0.01549+0.0005027j
       2.667   -0.005399-0.0006456j
       2.833     0.00288  +0.00314j
       3.000     0.01733 -0.002428j
       3.167    0.005347 +0.001379j
       3.333    -0.01469 -0.002597j
       3.500    -0.00194 +0.002184j
       3.667   -0.007244 -0.001797j
       3.833    0.004792 +0.003576j

... (truncated) ...

--- Restored FrequencySeries ---
FrequencySeries([ 0.00847973+0.j        ,
                  0.00342982+0.00856023j,
                 -0.00908238-0.00279319j, ...,
                 -0.00365943-0.00393728j,
                  0.00786504+0.00354929j,
                 -0.00285359+0.j        ],
                unit: dimensionless,
                f0: 0.0 Hz,
                df: 0.02652582384864922 Hz,
                epoch: None,
                name: None,
                channel: None)
../../../../_images/web_en_user_guide_tutorials_intro_frequencyseries_19_1.png

8. Quadrature Sum

This feature calculates the sum of orthogonal components.

[11]:
# Generate noisy data
np.random.seed(42)
f = spec.frequencies.value
noise = np.abs(np.random.randn(f.size))
peak = 10.0 * np.exp(-((f - 200) ** 2) / 50.0)
data = noise + peak

raw = FrequencySeries(data, frequencies=f, unit="V", name="Raw Data")

# Smooth
smoothed = raw.smooth(width=10, method="amplitude")
smoothed.name = "Smoothed"

# Convert to dB
raw_db = raw.to_db()
smoothed_db = smoothed.to_db()
raw_db.name = "Raw (dB)"
smoothed_db.name = "Smoothed (dB)"

plot = raw_db.plot(label="Raw", title="Smoothing & dB")
ax = plot.gca()
ax.plot(smoothed_db, label="Smoothed", linewidth=2)
ax.legend()
plot.show()
plt.close()

# Quadrature Sum (Noise Budget example)
noise_a = FrequencySeries(np.ones_like(f), frequencies=f, unit="V", name="Noise A")
noise_b = FrequencySeries(np.ones_like(f) * 2, frequencies=f, unit="V", name="Noise B")

total = noise_a.quadrature_sum(noise_b)
print(f"Noise A: {noise_a.value[0]}, Noise B: {noise_b.value[0]}")
print(f"Total (Sqrt Sum): {total.value[0]}")
Plot(total, noise_a, noise_b, alpha=0.8)
plt.legend(["Total", "Noise A", "Noise B"])
plt.xscale("log")
plt.yscale("log")
plt.show()
../../../../_images/web_en_user_guide_tutorials_intro_frequencyseries_21_0.png
Noise A: 1.0, Noise B: 2.0
Total (Sqrt Sum): 2.23606797749979
../../../../_images/web_en_user_guide_tutorials_intro_frequencyseries_21_2.png