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:
40
.gitattributes
vendored
Normal file
40
.gitattributes
vendored
Normal 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
42
.gitignore
vendored
Normal 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
47
README.md
Normal 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 filter’s 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
78
build_and_run.md
Normal 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
72
pom.xml
Normal 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>
|
474
src/main/java/com/waveletsolutions/robot/App.java
Normal file
474
src/main/java/com/waveletsolutions/robot/App.java
Normal 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
|
||||
}
|
||||
}
|
70
src/main/java/com/waveletsolutions/robot/FFTUtils.java
Normal file
70
src/main/java/com/waveletsolutions/robot/FFTUtils.java
Normal 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);
|
||||
}
|
||||
}
|
137
src/main/java/com/waveletsolutions/robot/FilterUtils.java
Normal file
137
src/main/java/com/waveletsolutions/robot/FilterUtils.java
Normal 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;
|
||||
}
|
||||
}
|
38
src/test/java/com/waveletsolutions/robot/AppTest.java
Normal file
38
src/test/java/com/waveletsolutions/robot/AppTest.java
Normal 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 );
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user