Showing raised cosine and FFT.

Raised cosine is displayed in an interactive JavaFX GUI. The beta
value can be dyanmically adjusted from 0 to 1 and an FFT displays
the spectral impact to the most recent adjustment.
A low-pass filter can replace the raised cosine FIR filter to show
the difference between the two.
This commit is contained in:
Eric Ratliff
2024-09-13 19:18:08 -05:00
commit dc4e379b95
9 changed files with 998 additions and 0 deletions

40
.gitattributes vendored Normal file
View File

@@ -0,0 +1,40 @@
# Java sources
*.java text diff=java
*.kt text diff=kotlin
*.groovy text diff=java
*.scala text diff=java
*.gradle text diff=java
*.gradle.kts text diff=kotlin
# These files are text and should be normalized (Convert crlf => lf)
*.css text diff=css
*.scss text diff=css
*.sass text
*.df text
*.htm text diff=html
*.html text diff=html
*.js text
*.mjs text
*.cjs text
*.jsp text
*.jspf text
*.jspx text
*.properties text
*.tld text
*.tag text
*.tagx text
*.xml text
# These files are binary and should be left untouched
# (binary is a macro for -text -diff)
*.class binary
*.dll binary
*.ear binary
*.jar binary
*.so binary
*.war binary
*.jks binary
# Common build-tool wrapper scripts ('.cmd' versions are handled by 'Common.gitattributes')
mvnw text eol=lf
gradlew text eol=lf

42
.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# Maven target directory
/target/
# Compiled class files
*.class
# Log files
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# Virtual machine crash logs
hs_err_pid*
# Maven specific files
dependency-reduced-pom.xml
release.properties
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
# Avoid Mac specific files
.DS_Store
# Avoid Linux/Unix specific files
*~
# Avoid Windows specific files
Thumbs.db

47
README.md Normal file
View File

@@ -0,0 +1,47 @@
# Speed Graph with Adjustable Raised Cosine Filter
This JavaFX application demonstrates the concept of a raised cosine filter applied to signal data, visualized in a dynamic graph. The user can control two key parameters:
## Key Features
### 1. Speed (Vertical Slider)
- Controlled via a vertical slider.
- This parameter determines the value of the signal (represented as speed) at each time step.
- The slider's range is from -100 to 100.
### 2. Beta (Roll-off Factor - Horizontal Slider)
- Controlled via a horizontal slider.
- Adjusts the roll-off factor (β) of the raised cosine filter.
- A higher beta value produces smoother transitions in the signal.
- The range is from 0 (sharp transitions) to 1 (smooth transitions).
## Graph Display
The graph displays two series:
- **Original Speed**: Represents the unfiltered signal, directly derived from the speed slider.
- **Filtered Speed (Raised Cosine)**: Represents the signal after it has been processed by a raised cosine filter, smoothing transitions based on the roll-off factor (beta).
### Dynamic Graph
- The graph updates in real-time, continuously adding new points.
- It provides a sliding window effect, always showing the latest data over a fixed time window.
### Pre-populated Graph
- The graph is filled with zeros at startup, ensuring smoother transitions immediately.
### Adjustable Raised Cosine Filter
- The filtered speed is processed using a raised cosine filter.
- The filter is adjustable via the beta slider to simulate smooth signal transitions.
### Signal Smoothing with FIR Filtering
- The raised cosine filter is implemented as a finite impulse response (FIR) filter.
- The coefficients (taps) of the filter are dynamically generated and applied to the input signal for smoothing.
### Live Beta Value Display
- The current beta value is displayed dynamically as the slider is adjusted.
- This helps the user visualize the filters effect in real-time.
## Technologies Used
- **JavaFX**: For the graphical user interface, including sliders and real-time graph.
- **JFreeChart**: For rendering the dynamic line chart visualizing signal data.
## Purpose
This application simulates how a raised cosine filter can smooth transitions in a signal, with user-controllable parameters. It's particularly relevant for digital communication systems, where pulse shaping is critical for minimizing bandwidth and reducing inter-symbol interference (ISI).

78
build_and_run.md Normal file
View File

@@ -0,0 +1,78 @@
# Build and Run Instructions for Speed Graph Application
This guide explains how to build and run the Speed Graph with Adjustable Raised Cosine Filter application on different platforms: **Arch Linux**, **Ubuntu**, and **Windows**.
## Prerequisites
Before proceeding, ensure you have the following installed:
- **Java Development Kit (JDK)** 17 or higher
- **Maven** (for handling dependencies and building the project)
- **Git** (for cloning the repository)
---
## Arch Linux
### 1. Install Dependencies
You can install the necessary dependencies using `pacman`:
```bash
sudo pacman -S jdk17-openjdk maven git
```
## 2. Clone the Repository and Build
```bash
git clone https://github.com/eric-waveletsolutions/speedgraph.git
cd speedgraph
mvn clean install
```
## 3. Run the Application
```bash
mvn javafx:run
```
## Ubuntu
### 1. Install Dependencies
You can install the required packages using apt:
```bash
sudo apt update
sudo apt install openjdk-17-jdk maven git
```
### 2. Clone the Repository and Build
```bash
git clone https://github.com/eric-waveletsolutions/speedgraph.git
cd speedgraph
mvn clean install
```
### 3. Run the Application
```bash
mvn javafx:run
```
## Windows
### 1. Install Dependencies
1. Install Java 17: Download and install the JDK from the Oracle website or using AdoptOpenJDK.
2. Install Maven: Download Maven from the Apache Maven website, extract it, and set it up in your system's PATH.
3. Install Git: Download and install Git from the Git website.
### 2. Clone the Repository and Build
Open the terminal (or Git Bash) and run:
```bash
git clone https://github.com/eric-waveletsolutions/speedgraph.git
cd speedgraph
mvn clean install
```
### 3. Run the Application
```bash
mvn javafx:run
```

72
pom.xml Normal file
View File

@@ -0,0 +1,72 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.waveletsolutions.robot</groupId>
<artifactId>SpeedGraph</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>SpeedGraph</name>
<url>http://maven.apache.org</url>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- JavaFX -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>17.0.1</version>
</dependency>
<!-- JFreeChart for graphing -->
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.3</version>
</dependency>
<!-- Apache Commons Math library for FFT calculations -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<executions>
<execution>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.waveletsolutions.robot.App</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,474 @@
package com.waveletsolutions.robot;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.animation.Animation;
import javafx.application.Application;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.LineChart;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.animation.PauseTransition;
import javafx.util.Duration;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.math3.complex.Complex;
import org.apache.commons.math3.transform.DftNormalization;
import org.apache.commons.math3.transform.FastFourierTransformer;
import org.apache.commons.math3.transform.TransformType;
/**
* The {@code App} class is a JavaFX application that demonstrates signal processing concepts
* such as filtering and Fast Fourier Transform (FFT) analysis.
* <p>
* This program allows users to control the speed of a motor (simulated via a slider),
* apply different types of filters (raised cosine and low-pass), and visualize the time-domain
* signal and its frequency-domain representation (FFT).
* </p>
* <p>
* Key features include:
* <ul>
* <li>A vertical slider to adjust the speed of the signal in real-time.</li>
* <li>A horizontal slider to control the beta value (roll-off factor) of the raised cosine filter.</li>
* <li>Radio buttons to switch between a raised cosine filter and a low-pass filter.</li>
* <li>An oscillation feature to automate speed changes in a square wave pattern.</li>
* <li>Real-time plots of both time-domain and frequency-domain (FFT) data.</li>
* </ul>
* </p>
* <p>
* This class is intended as an educational tool for students learning about signal processing
* in the context of controlling a robot motor.
* </p>
*
* <h2>Usage:</h2>
* <ol>
* <li>Run the application and adjust the speed using the vertical slider.</li>
* <li>Switch between different filters using the radio buttons.</li>
* <li>Observe the changes in the time-domain plot and the FFT (frequency-domain) plot.</li>
* <li>Press the "Start Oscillation" button to automatically oscillate the speed between 0 and 100.</li>
* </ol>
*
* <h2>Technical Concepts Covered:</h2>
* <ul>
* <li>Sampling rate: How often the signal is updated.</li>
* <li>Filter length: The number of recent samples used for filtering.</li>
* <li>Fast Fourier Transform (FFT): Converts a time-domain signal to the frequency domain.</li>
* <li>Raised cosine and low-pass filters: Techniques to shape and smooth the signal.</li>
* </ul>
*
* <p>
* This application is designed to be simple and interactive, making it a useful tool
* for beginners in both programming and signal processing.
* </p>
*
* @author Eric Ratliff
* @version 1.0.0
* @since 2024-09-09
*/
public class App extends Application {
// Series for plotting raw (unfiltered) and filtered data in the time domain
private XYChart.Series<Number, Number> series = new XYChart.Series<>();
private XYChart.Series<Number, Number> filteredSeries = new XYChart.Series<>();
// Series for plotting the FFT (frequency domain) of unfiltered and filtered signals
private XYChart.Series<Number, Number> fftSeriesUnfiltered = new XYChart.Series<>();
private XYChart.Series<Number, Number> fftSeriesFiltered = new XYChart.Series<>();
// Counter for tracking time (used for x-axis in time-domain plot)
private int time = 0;
// The sampling rate defines how many samples are collected per second (in Hz)
private static final double SAMPLING_RATE = 500.0; // 500 samples per second
// Window size is how many samples we display at once in the time-domain plot
private final int WINDOW_SIZE = 200; // Shows the last 200 samples
// Interval between each x-axis point in the time-domain plot
private final double X_INTERVAL = 0.05; // Spacing between time points (0.05 seconds)
// Filter length refers to how many recent samples are used to smooth or filter the signal
private final int FILTER_LENGTH = 8; // Smaller filter length = faster transitions, less smoothing
// FFT size refers to how many samples are used for frequency analysis (FFT)
private final int FFT_SIZE = 512; // Larger FFT size = better frequency resolution but more computation
// Beta controls the sharpness of the raised cosine filter (only used if the raised cosine filter is selected)
private double beta = 0.5; // Lower beta = sharper transition for the raised cosine filter
// Cutoff frequency for the low-pass filter (removes frequencies above this value)
private double cutoffFrequency = 32.0; // 32 Hz cutoff frequency for the low-pass filter
// A flag to determine which filter to use (true for raised cosine, false for low-pass)
private boolean useRaisedCosine = true;
// Store recent raw samples for time-domain plotting and FFT calculation
private List<Double> recentSamples = new ArrayList<>();
// Store recent filtered samples for FFT calculation
private List<Double> recentFilteredSamples = new ArrayList<>();
// Counter to control how often the graph updates (to avoid too many updates)
private int updateCount = 0;
// Threshold to limit the frequency of graph updates (update graph every 4 slider changes)
private final int UPDATE_THRESHOLD = 4; // Reduces lag by updating graph less frequently
/**
* Resets the FFT (Fast Fourier Transform) series for both the unfiltered and filtered data.
* <p>
* This method clears the data in the FFT graphs and sets the values back to zero.
* It is useful when the user stops interacting with the slider or when a filter change occurs.
* The FFT series for both the raw (unfiltered) and filtered signals are updated here.
*/
private void resetFFT() {
// Clear the existing FFT data for both unfiltered and filtered series
fftSeriesUnfiltered.getData().clear();
fftSeriesFiltered.getData().clear();
// Populate the FFT series with zeroes to reset the graph visually
for (int i = -FFT_SIZE / 2; i < FFT_SIZE / 2; i++) {
fftSeriesUnfiltered.getData().add(new XYChart.Data<>(i, 0)); // Reset to zero for unfiltered series
fftSeriesFiltered.getData().add(new XYChart.Data<>(i, 0)); // Reset to zero for filtered series
}
}
/**
* The entry point for the JavaFX application.
* <p>
* This method sets up the user interface for controlling motor speed and viewing
* the time-domain and frequency-domain (FFT) graphs. It includes sliders, filter options,
* and a button to oscillate the motor speed automatically.
*
* @param primaryStage The primary stage for this JavaFX application.
*/
@Override
public void start(Stage primaryStage) {
// Slider for controlling motor speed
Slider speedSlider = new Slider(-100, 100, 0);
speedSlider.setShowTickLabels(true);
speedSlider.setShowTickMarks(true);
speedSlider.setMajorTickUnit(50);
speedSlider.setMinorTickCount(5);
speedSlider.setOrientation(Orientation.VERTICAL);
Label speedLabel = new Label("Speed");
// Slider for adjusting beta (roll-off factor) for the raised cosine filter
Slider betaSlider = new Slider(0.0, 1.0, beta);
betaSlider.setShowTickLabels(true);
betaSlider.setShowTickMarks(true);
betaSlider.setMajorTickUnit(0.1);
betaSlider.setMinorTickCount(10);
Label betaLabel = new Label("Beta (Roll-off Factor)");
Label betaValueLabel = new Label("Beta Value: " + beta);
// Radio buttons for selecting between raised cosine and low-pass filters
RadioButton raisedCosineRadio = new RadioButton("Raised Cosine");
RadioButton lowPassRadio = new RadioButton("Low-Pass");
ToggleGroup filterGroup = new ToggleGroup();
raisedCosineRadio.setToggleGroup(filterGroup);
lowPassRadio.setToggleGroup(filterGroup);
raisedCosineRadio.setSelected(true);
// Update graphs and FFT when filter selection changes
filterGroup.selectedToggleProperty().addListener((obs, oldVal, newVal) -> {
if (newVal == raisedCosineRadio) {
useRaisedCosine = true;
} else {
useRaisedCosine = false;
}
updateGraph(speedSlider.getValue());
updateFFT();
});
// Pause transition to reset the FFT after slider movement stops
PauseTransition pause = new PauseTransition(Duration.seconds(0.5));
// Trigger FFT reset when slider stops moving
pause.setOnFinished(event -> resetFFT());
// Add listener to update the FFT when the slider changes and reset pause timer
speedSlider.valueProperty().addListener((observable, oldValue, newValue) -> {
updateCount++;
if (updateCount % UPDATE_THRESHOLD == 0) {
updateGraph(newValue.doubleValue());
updateFFT();
updateCount = 0; // Reset update counter
}
});
// Setup for time-domain graph (displays speed over time)
NumberAxis xAxis = new NumberAxis(0, WINDOW_SIZE * X_INTERVAL, 5);
NumberAxis yAxis = new NumberAxis(-100, 100, 10);
LineChart<Number, Number> lineChart = new LineChart<>(xAxis, yAxis);
lineChart.setCreateSymbols(false);
series.setName("Original Speed");
filteredSeries.setName("Filtered Speed");
lineChart.getData().add(series);
lineChart.getData().add(filteredSeries);
// Setup for frequency-domain (FFT) graph
NumberAxis freqAxis = new NumberAxis(-FFT_SIZE / 2, FFT_SIZE / 2 - 1, FFT_SIZE / 16);
NumberAxis magAxis = new NumberAxis(0, 5000, 1000); // Constant y-axis for FFT
LineChart<Number, Number> fftChart = new LineChart<>(freqAxis, magAxis);
fftChart.setTitle("Frequency Domain");
fftSeriesUnfiltered.setName("FFT (Unfiltered)");
fftSeriesFiltered.setName("FFT (Filtered)");
fftChart.getData().add(fftSeriesUnfiltered);
fftChart.getData().add(fftSeriesFiltered);
fftChart.setCreateSymbols(false);
// Pre-fill the time-domain chart with zeros
for (int i = 0; i < WINDOW_SIZE; i++) {
series.getData().add(new XYChart.Data<>(i * X_INTERVAL, 0));
filteredSeries.getData().add(new XYChart.Data<>(i * X_INTERVAL, 0));
recentSamples.add(0.0);
recentFilteredSamples.add(0.0); // Initialize filtered samples with zeros
}
time = WINDOW_SIZE;
// Timeline for continuously updating the time-domain graph
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(50), e -> {
double speed = speedSlider.getValue();
beta = betaSlider.getValue();
betaValueLabel.setText("Beta Value: " + String.format("%.2f", beta));
updateGraph(speed); // Update time-domain graph only
}));
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
// Button to toggle between manual control and automatic oscillation
Button oscillateButton = new Button("Start Oscillation");
// Timeline for oscillating motor speed (square wave)
Timeline oscillationTimeline = new Timeline(new KeyFrame(Duration.millis(50), e -> {
double frequency = 0.5; // 0.5 Hz = 2 seconds per cycle
double timeInMillis = System.currentTimeMillis();
double period = 1000 / frequency; // Period of the square wave in milliseconds
// Alternate between 0 and 100 (square wave logic)
double oscillatedSpeed = (timeInMillis % period) < (period / 2) ? 0 : 100;
speedSlider.setValue(oscillatedSpeed); // Update slider automatically
updateFFT(); // Update FFT based on oscillating speed
}));
oscillationTimeline.setCycleCount(Timeline.INDEFINITE);
// Toggle between oscillation and manual control when button is pressed
oscillateButton.setOnAction(event -> {
if (oscillationTimeline.getStatus() == Animation.Status.RUNNING) {
oscillationTimeline.stop(); // Stop oscillation
oscillateButton.setText("Start Oscillation");
speedSlider.setDisable(false); // Re-enable manual control
} else {
oscillationTimeline.play(); // Start oscillation
oscillateButton.setText("Stop Oscillation");
speedSlider.setDisable(true); // Disable manual control
}
});
// Layout for controls and graphs
VBox controlLayout = new VBox(10, raisedCosineRadio, lowPassRadio, betaLabel, betaSlider, betaValueLabel, oscillateButton);
controlLayout.setAlignment(Pos.TOP_LEFT);
VBox speedLayout = new VBox(10, speedLabel, speedSlider);
speedLayout.setAlignment(Pos.CENTER);
// Arrange the layout in the main window
BorderPane layout = new BorderPane();
layout.setLeft(speedLayout);
layout.setRight(controlLayout);
layout.setCenter(lineChart);
layout.setBottom(fftChart); // FFT graph at the bottom
// Setup and display the scene
Scene scene = new Scene(layout, 900, 600);
primaryStage.setScene(scene);
primaryStage.setTitle("Speed Graph with FFT");
primaryStage.show();
}
/**
* Updates the time-domain graph with new data and applies the selected filter.
* <p>
* This method adds the current speed value (from the slider or oscillation)
* to the graph and applies either the raised cosine or low-pass filter to
* the recent samples. It ensures that both the original and filtered graphs
* stay within the window size, removing old data as new points are added.
* </p>
*
* @param speed The current speed value to be plotted on the graph.
*/
private void updateGraph(double speed) {
// Add the raw speed data to the original series
series.getData().add(new XYChart.Data<>(time * X_INTERVAL, speed));
recentSamples.add(speed); // Keep track of recent samples
// Remove oldest sample if the list exceeds the filter length
if (recentSamples.size() > FILTER_LENGTH) {
recentSamples.remove(0);
}
// Apply either the raised cosine filter or the low-pass filter
double filteredValue;
if (useRaisedCosine) {
double[] raisedCosineCoeffs = FilterUtils.raisedCosineCoefficients(FILTER_LENGTH, beta);
filteredValue = FilterUtils.applyRaisedCosineFilter(raisedCosineCoeffs, recentSamples);
} else {
double[] lowPassCoeffs = FilterUtils.lowPassCoefficients(FILTER_LENGTH, cutoffFrequency);
filteredValue = FilterUtils.applyLowPassFilter(lowPassCoeffs, recentSamples);
}
// Add the filtered speed data to the filtered series
filteredSeries.getData().add(new XYChart.Data<>(time * X_INTERVAL, filteredValue));
recentFilteredSamples.add(filteredValue); // Track recent filtered samples
// Keep the filtered sample list within the filter length
if (recentFilteredSamples.size() > FILTER_LENGTH) {
recentFilteredSamples.remove(0);
}
// Ensure both graphs (original and filtered) stay within the window size
if (series.getData().size() > WINDOW_SIZE) {
series.getData().remove(0);
filteredSeries.getData().remove(0);
}
// Update the x-axis to scroll as new data is added
NumberAxis xAxis = (NumberAxis) series.getChart().getXAxis();
xAxis.setLowerBound((time - WINDOW_SIZE) * X_INTERVAL);
xAxis.setUpperBound(time * X_INTERVAL);
// Increment the time step for the next data point
time++;
}
/**
* Updates the frequency-domain (FFT) graph for both unfiltered and filtered data.
* <p>
* This method calculates the FFT (Fast Fourier Transform) of the most recent
* samples (both original and filtered), computes the magnitudes, shifts the
* frequencies to center zero, and updates the FFT graph for both data sets.
* </p>
* <p>
* The FFT graph shows how the frequency content of the signal changes over time.
* It is recalculated and updated whenever the time-domain data is changed or filtered.
* </p>
*/
private void updateFFT() {
// Calculate the FFT for the unfiltered and filtered data, which returns complex values
Complex[] unfilteredFFT = FFTUtils.calculateFFT(recentSamples, FFT_SIZE);
Complex[] filteredFFT = FFTUtils.calculateFFT(recentFilteredSamples, FFT_SIZE);
// Convert the complex FFT values to magnitudes (absolute values)
double[] unfilteredMagnitudes = calculateMagnitudes(unfilteredFFT);
double[] filteredMagnitudes = calculateMagnitudes(filteredFFT);
// Shift the FFT output to center the zero frequency (DC component) in the middle of the graph
double[] shiftedUnfilteredMagnitudes = shiftFFT(unfilteredMagnitudes);
double[] shiftedFilteredMagnitudes = shiftFFT(filteredMagnitudes);
// Filter out any invalid values such as NaN or Infinity from the magnitude arrays
shiftedUnfilteredMagnitudes = filterInvalidValues(shiftedUnfilteredMagnitudes);
shiftedFilteredMagnitudes = filterInvalidValues(shiftedFilteredMagnitudes);
// Clear and update the FFT graph for the unfiltered signal
fftSeriesUnfiltered.getData().clear();
for (int i = 0; i < shiftedUnfilteredMagnitudes.length; i++) {
fftSeriesUnfiltered.getData().add(new XYChart.Data<>(i - FFT_SIZE / 2, shiftedUnfilteredMagnitudes[i]));
}
// Clear and update the FFT graph for the filtered signal
fftSeriesFiltered.getData().clear();
for (int i = 0; i < shiftedFilteredMagnitudes.length; i++) {
fftSeriesFiltered.getData().add(new XYChart.Data<>(i - FFT_SIZE / 2, shiftedFilteredMagnitudes[i]));
}
}
/**
* Shifts the FFT result so that zero frequency (DC component) is in the center of the graph.
* <p>
* FFT results place the zero frequency at the start, followed by positive and negative frequencies.
* This method rearranges the result so that the negative frequencies appear on the left side
* and positive frequencies appear on the right side, with zero frequency centered.
* </p>
*
* @param fftData The FFT result (magnitude or complex) to be shifted
* @return A shifted array with the zero frequency centered
*/
private double[] shiftFFT(double[] fftData) {
int n = fftData.length;
double[] shifted = new double[n];
int halfSize = n / 2;
// Move negative frequencies to the beginning of the array
System.arraycopy(fftData, halfSize, shifted, 0, halfSize); // Negative frequencies
// Move positive frequencies to the end of the array
System.arraycopy(fftData, 0, shifted, halfSize, halfSize); // Positive frequencies
return shifted;
}
/**
* Calculates the magnitudes of complex FFT data.
* <p>
* FFT results are complex numbers, which represent both amplitude and phase.
* The magnitude (absolute value) of a complex number represents the strength of each frequency component.
* </p>
*
* @param fftData The complex FFT data
* @return An array of magnitudes representing the strength of each frequency component
*/
private double[] calculateMagnitudes(Complex[] fftData) {
double[] magnitudes = new double[fftData.length];
// Loop through FFT results and calculate the magnitude (absolute value) for each complex number
for (int i = 0; i < fftData.length; i++) {
magnitudes[i] = fftData[i].abs(); // Magnitude = absolute value of the complex number
}
return magnitudes;
}
/**
* Filters out invalid values from a data array.
* <p>
* Sometimes the FFT or other calculations can produce invalid results such as NaN (Not a Number)
* or Infinity. This method replaces such values with zero to prevent display or calculation issues.
* </p>
*
* @param data The array of data to be filtered
* @return A cleaned array with invalid values replaced by zero
*/
private double[] filterInvalidValues(double[] data) {
// Loop through the data array
for (int i = 0; i < data.length; i++) {
// Check if the value is NaN (Not a Number) or Infinite and replace it with zero
if (Double.isNaN(data[i]) || Double.isInfinite(data[i])) {
data[i] = 0; // Replace invalid values with 0
}
}
return data;
}
/**
* The main method to launch the JavaFX application.
* <p>
* This method is the entry point for launching the JavaFX GUI application.
* It sets up the window and calls the {@code start} method to initialize the interface.
* </p>
*
* @param args Command line arguments (not used)
*/
public static void main(String[] args) {
launch(args); // Launches the JavaFX application
}
}

View File

@@ -0,0 +1,70 @@
package com.waveletsolutions.robot;
import org.apache.commons.math3.complex.Complex;
import org.apache.commons.math3.transform.DftNormalization;
import org.apache.commons.math3.transform.FastFourierTransformer;
import org.apache.commons.math3.transform.TransformType;
import java.util.List;
/**
* The {@code FFTUtils} class provides utility methods for performing
* Fast Fourier Transform (FFT) operations on time-domain signals.
* <p>
* This class is used to convert a list of time-domain samples into their
* frequency-domain representation using the FFT. The primary method is
* designed to handle input of varying sizes by either padding or truncating
* the input data to fit the required FFT size.
* </p>
*
* <h2>Usage:</h2>
* <ul>
* <li>Call the {@code calculateFFT} method with a list of time-domain samples and the desired FFT size.</li>
* <li>The method returns an array of {@code Complex} numbers representing the frequency components of the signal.</li>
* </ul>
*
* <h2>Technical Concepts:</h2>
* <ul>
* <li>Fast Fourier Transform (FFT): Converts time-domain signals to frequency-domain signals.</li>
* <li>Padding: If the number of input samples is smaller than the required FFT size, the input is padded with zeros.</li>
* <li>Truncation: If the number of input samples exceeds the FFT size, the input is truncated.</li>
* </ul>
*
* @author Eric Ratliff
* @version 1.0.0
* @since 2024-09-09
*/
public class FFTUtils {
/**
* Calculates the Fast Fourier Transform (FFT) of a list of time-domain samples.
* <p>
* This method takes in a list of time-domain samples, applies padding or truncation
* to ensure the correct FFT size, and then computes the FFT. The result is returned
* as an array of {@code Complex} numbers representing the frequency-domain data.
* </p>
*
* @param samples the list of time-domain samples to be transformed
* @param fftSize the desired size of the FFT (the number of points in the frequency domain)
* @return an array of {@code Complex} numbers representing the frequency components of the input signal
*
* <h2>Details:</h2>
* <ul>
* <li>If the number of samples is less than {@code fftSize}, the input array is padded with zeros.</li>
* <li>If the number of samples exceeds {@code fftSize}, the input array is truncated.</li>
* <li>The FFT is computed using the Apache Commons Math {@code FastFourierTransformer} class.</li>
* </ul>
*/
public static Complex[] calculateFFT(List<Double> samples, int fftSize) {
FastFourierTransformer transformer = new FastFourierTransformer(DftNormalization.STANDARD);
double[] inputArray = samples.stream().mapToDouble(Double::doubleValue).toArray();
// Ensure that the input array has the right size
if (inputArray.length < fftSize) {
inputArray = java.util.Arrays.copyOf(inputArray, fftSize); // Pad with zeros if too small
} else if (inputArray.length > fftSize) {
inputArray = java.util.Arrays.copyOfRange(inputArray, 0, fftSize); // Truncate if too large
}
return transformer.transform(inputArray, TransformType.FORWARD);
}
}

View File

@@ -0,0 +1,137 @@
package com.waveletsolutions.robot;
import java.util.List;
/**
* The {@code FilterUtils} class provides utility methods for creating and applying
* digital filters, such as raised cosine filters and low-pass filters.
* <p>
* These filters are commonly used in signal processing to smooth or shape signals.
* The class offers methods to generate filter coefficients and apply them to a set
* of samples. The filters implemented here are designed for basic signal processing
* tasks.
* </p>
*
* <h2>Filters Implemented:</h2>
* <ul>
* <li>Raised Cosine Filter: Used for shaping signals with controlled bandwidth.</li>
* <li>Low-Pass Filter: Allows signals below a certain frequency to pass while attenuating higher frequencies.</li>
* </ul>
*
* <h2>Usage:</h2>
* <ul>
* <li>Use the {@code raisedCosineCoefficients} or {@code lowPassCoefficients} to generate filter coefficients.</li>
* <li>Apply the generated filter to a set of samples using {@code applyRaisedCosineFilter} or {@code applyLowPassFilter}.</li>
* </ul>
*
* @author Eric Ratliff
* @version 1.0.0
* @since 2024-09-09
*/
public class FilterUtils {
/**
* Generates raised cosine filter coefficients.
* <p>
* The raised cosine filter is used for pulse shaping in communication systems.
* It shapes the signal to control the bandwidth while reducing intersymbol interference.
* The filter is defined by a roll-off factor {@code beta}, which controls how much excess
* bandwidth is allowed.
* </p>
*
* @param length the number of filter coefficients (the length of the filter)
* @param beta the roll-off factor (ranges between 0 and 1)
* @return an array of raised cosine filter coefficients
*/
public static double[] raisedCosineCoefficients(int length, double beta) {
double[] coeffs = new double[length];
double sum = 0.0;
for (int i = 0; i < length; i++) {
double t = (double) i / (length - 1);
double cosPart = 0.5 * (1 + Math.cos(Math.PI * (2 * t - 1) * beta));
coeffs[i] = cosPart;
sum += cosPart;
}
// Normalize the coefficients to ensure proper filtering
for (int i = 0; i < length; i++) {
coeffs[i] /= sum;
}
return coeffs;
}
/**
* Applies the raised cosine filter to a list of samples.
* <p>
* This method multiplies the given raised cosine filter coefficients with
* the most recent samples to smooth the signal.
* </p>
*
* @param coeffs the raised cosine filter coefficients
* @param samples the time-domain samples to be filtered
* @return the filtered value of the signal at the current time step
*/
public static double applyRaisedCosineFilter(double[] coeffs, List<Double> samples) {
double result = 0;
for (int i = 0; i < coeffs.length; i++) {
result += coeffs[i] * samples.get(samples.size() - 1 - i);
}
return result;
}
/**
* Generates low-pass filter coefficients using a Hamming window.
* <p>
* This method creates a low-pass filter, which allows frequencies below a certain
* cutoff to pass through while attenuating higher frequencies. The filter is generated
* using a Hamming window for smooth transitions.
* </p>
*
* @param length the number of filter coefficients (the length of the filter)
* @param cutoffFrequency the cutoff frequency as a fraction of the sampling rate (e.g., 0.1 for 10%)
* @return an array of low-pass filter coefficients
*/
public static double[] lowPassCoefficients(int length, double cutoffFrequency) {
double[] coeffs = new double[length];
double sum = 0.0;
for (int i = 0; i < length; i++) {
double t = i - (length - 1) / 2.0;
if (t == 0.0) {
coeffs[i] = 2 * cutoffFrequency;
} else {
coeffs[i] = Math.sin(2 * Math.PI * cutoffFrequency * t) / (Math.PI * t);
}
coeffs[i] *= 0.54 - 0.46 * Math.cos(2 * Math.PI * i / (length - 1)); // Apply Hamming window
sum += coeffs[i];
}
// Normalize the coefficients to ensure proper filtering
for (int i = 0; i < length; i++) {
coeffs[i] /= sum;
}
return coeffs;
}
/**
* Applies a low-pass filter to a list of samples.
* <p>
* This method multiplies the given low-pass filter coefficients with
* the most recent samples to smooth the signal and reduce high-frequency noise.
* </p>
*
* @param coeffs the low-pass filter coefficients
* @param samples the time-domain samples to be filtered
* @return the filtered value of the signal at the current time step
*/
public static double applyLowPassFilter(double[] coeffs, List<Double> samples) {
double result = 0;
for (int i = 0; i < coeffs.length; i++) {
result += coeffs[i] * samples.get(samples.size() - 1 - i);
}
return result;
}
}

View File

@@ -0,0 +1,38 @@
package com.waveletsolutions.robot;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
/**
* Unit test for simple App.
*/
public class AppTest
extends TestCase
{
/**
* Create the test case
*
* @param testName name of the test case
*/
public AppTest( String testName )
{
super( testName );
}
/**
* @return the suite of tests being tested
*/
public static Test suite()
{
return new TestSuite( AppTest.class );
}
/**
* Rigourous Test :-)
*/
public void testApp()
{
assertTrue( true );
}
}