commit dc4e379b953231714588dd2a115765ea36feed65 Author: Eric Ratliff Date: Fri Sep 13 19:18:08 2024 -0500 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. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f62ade6 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..091cf80 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c541f4d --- /dev/null +++ b/README.md @@ -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). diff --git a/build_and_run.md b/build_and_run.md new file mode 100644 index 0000000..5926832 --- /dev/null +++ b/build_and_run.md @@ -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 +``` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0de0c66 --- /dev/null +++ b/pom.xml @@ -0,0 +1,72 @@ + + 4.0.0 + com.waveletsolutions.robot + SpeedGraph + jar + 1.0-SNAPSHOT + SpeedGraph + http://maven.apache.org + + + 17 + 17 + + + + + junit + junit + 3.8.1 + test + + + + org.openjfx + javafx-controls + 17.0.1 + + + + org.jfree + jfreechart + 1.5.3 + + + + org.apache.commons + commons-math3 + 3.6.1 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 17 + 17 + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + + + run + + + + + com.waveletsolutions.robot.App + + + + + diff --git a/src/main/java/com/waveletsolutions/robot/App.java b/src/main/java/com/waveletsolutions/robot/App.java new file mode 100644 index 0000000..9c0103e --- /dev/null +++ b/src/main/java/com/waveletsolutions/robot/App.java @@ -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. + *

+ * 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). + *

+ *

+ * Key features include: + *

+ *

+ *

+ * This class is intended as an educational tool for students learning about signal processing + * in the context of controlling a robot motor. + *

+ * + *

Usage:

+ *
    + *
  1. Run the application and adjust the speed using the vertical slider.
  2. + *
  3. Switch between different filters using the radio buttons.
  4. + *
  5. Observe the changes in the time-domain plot and the FFT (frequency-domain) plot.
  6. + *
  7. Press the "Start Oscillation" button to automatically oscillate the speed between 0 and 100.
  8. + *
+ * + *

Technical Concepts Covered:

+ * + * + *

+ * This application is designed to be simple and interactive, making it a useful tool + * for beginners in both programming and signal processing. + *

+ * + * @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 series = new XYChart.Series<>(); + private XYChart.Series filteredSeries = new XYChart.Series<>(); + + // Series for plotting the FFT (frequency domain) of unfiltered and filtered signals + private XYChart.Series fftSeriesUnfiltered = new XYChart.Series<>(); + private XYChart.Series 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 recentSamples = new ArrayList<>(); + + // Store recent filtered samples for FFT calculation + private List 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. + *

+ * 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. + *

+ * 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 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 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. + *

+ * 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. + *

+ * + * @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. + *

+ * 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. + *

+ *

+ * 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. + *

+ */ + 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. + *

+ * 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. + *

+ * + * @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. + *

+ * 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. + *

+ * + * @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. + *

+ * 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. + *

+ * + * @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. + *

+ * 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. + *

+ * + * @param args Command line arguments (not used) + */ + public static void main(String[] args) { + launch(args); // Launches the JavaFX application + } +} diff --git a/src/main/java/com/waveletsolutions/robot/FFTUtils.java b/src/main/java/com/waveletsolutions/robot/FFTUtils.java new file mode 100644 index 0000000..1014af1 --- /dev/null +++ b/src/main/java/com/waveletsolutions/robot/FFTUtils.java @@ -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. + *

+ * 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. + *

+ * + *

Usage:

+ *
    + *
  • Call the {@code calculateFFT} method with a list of time-domain samples and the desired FFT size.
  • + *
  • The method returns an array of {@code Complex} numbers representing the frequency components of the signal.
  • + *
+ * + *

Technical Concepts:

+ *
    + *
  • Fast Fourier Transform (FFT): Converts time-domain signals to frequency-domain signals.
  • + *
  • Padding: If the number of input samples is smaller than the required FFT size, the input is padded with zeros.
  • + *
  • Truncation: If the number of input samples exceeds the FFT size, the input is truncated.
  • + *
+ * + * @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. + *

+ * 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. + *

+ * + * @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 + * + *

Details:

+ *
    + *
  • If the number of samples is less than {@code fftSize}, the input array is padded with zeros.
  • + *
  • If the number of samples exceeds {@code fftSize}, the input array is truncated.
  • + *
  • The FFT is computed using the Apache Commons Math {@code FastFourierTransformer} class.
  • + *
+ */ + public static Complex[] calculateFFT(List 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); + } +} diff --git a/src/main/java/com/waveletsolutions/robot/FilterUtils.java b/src/main/java/com/waveletsolutions/robot/FilterUtils.java new file mode 100644 index 0000000..8380c7c --- /dev/null +++ b/src/main/java/com/waveletsolutions/robot/FilterUtils.java @@ -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. + *

+ * 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. + *

+ * + *

Filters Implemented:

+ *
    + *
  • Raised Cosine Filter: Used for shaping signals with controlled bandwidth.
  • + *
  • Low-Pass Filter: Allows signals below a certain frequency to pass while attenuating higher frequencies.
  • + *
+ * + *

Usage:

+ *
    + *
  • Use the {@code raisedCosineCoefficients} or {@code lowPassCoefficients} to generate filter coefficients.
  • + *
  • Apply the generated filter to a set of samples using {@code applyRaisedCosineFilter} or {@code applyLowPassFilter}.
  • + *
+ * + * @author Eric Ratliff + * @version 1.0.0 + * @since 2024-09-09 + */ +public class FilterUtils { + + /** + * Generates raised cosine filter coefficients. + *

+ * 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. + *

+ * + * @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. + *

+ * This method multiplies the given raised cosine filter coefficients with + * the most recent samples to smooth the signal. + *

+ * + * @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 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. + *

+ * 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. + *

+ * + * @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. + *

+ * This method multiplies the given low-pass filter coefficients with + * the most recent samples to smooth the signal and reduce high-frequency noise. + *

+ * + * @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 samples) { + double result = 0; + for (int i = 0; i < coeffs.length; i++) { + result += coeffs[i] * samples.get(samples.size() - 1 - i); + } + return result; + } +} diff --git a/src/test/java/com/waveletsolutions/robot/AppTest.java b/src/test/java/com/waveletsolutions/robot/AppTest.java new file mode 100644 index 0000000..5dba93c --- /dev/null +++ b/src/test/java/com/waveletsolutions/robot/AppTest.java @@ -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 ); + } +}