WIP basic architecture done. Need to fix broken LinearRegressionTests
This commit is contained in:
commit
95e549b8f3
110
pom.xml
Normal file
110
pom.xml
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<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/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.aselimov.app</groupId>
|
||||||
|
<artifactId>SteadyState-Detector</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<name>SteadyState-Detector</name>
|
||||||
|
<!-- FIXME change it to the project's website -->
|
||||||
|
<url>http://www.example.com</url>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
<exec.mainClass>com.aselimov.app.SteadyDetect</exec.mainClass>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit</groupId>
|
||||||
|
<artifactId>junit-bom</artifactId>
|
||||||
|
<version>5.11.0</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- Optionally: parameterized tests support -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-params</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- Jackson Core -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-core</artifactId>
|
||||||
|
<version>2.15.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Jackson Databind (includes jackson-annotations) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.15.2</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/args4j/args4j -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>args4j</groupId>
|
||||||
|
<artifactId>args4j</artifactId>
|
||||||
|
<version>2.37</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
|
||||||
|
<plugins>
|
||||||
|
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-clean-plugin</artifactId>
|
||||||
|
<version>3.4.0</version>
|
||||||
|
</plugin>
|
||||||
|
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-resources-plugin</artifactId>
|
||||||
|
<version>3.3.1</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.4.2</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-install-plugin</artifactId>
|
||||||
|
<version>3.1.2</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-deploy-plugin</artifactId>
|
||||||
|
<version>3.1.2</version>
|
||||||
|
</plugin>
|
||||||
|
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-site-plugin</artifactId>
|
||||||
|
<version>3.12.1</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-project-info-reports-plugin</artifactId>
|
||||||
|
<version>3.6.1</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
</build>
|
||||||
|
</project>
|
78
src/main/java/com/aselimov/app/SteadyDetect.java
Normal file
78
src/main/java/com/aselimov/app/SteadyDetect.java
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package com.aselimov.app;
|
||||||
|
|
||||||
|
import static org.kohsuke.args4j.ExampleMode.ALL;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.kohsuke.args4j.Argument;
|
||||||
|
import org.kohsuke.args4j.CmdLineException;
|
||||||
|
import org.kohsuke.args4j.CmdLineParser;
|
||||||
|
|
||||||
|
import com.aselimov.app.socket.SocketListener;
|
||||||
|
import com.aselimov.app.steady_state_analysis.LinearRegression;
|
||||||
|
import com.aselimov.app.steady_state_analysis.SteadyStateCalculator;
|
||||||
|
import com.aselimov.app.timedata.TimeData;
|
||||||
|
|
||||||
|
public class SteadyDetect {
|
||||||
|
/**
|
||||||
|
* Main function accepts 1 argument which is the port that should be bound.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
new SteadyDetect().doMain(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SocketListener socketListener;
|
||||||
|
private TimeData data = new TimeData();
|
||||||
|
|
||||||
|
private SteadyStateCalculator steadyStateCalc;
|
||||||
|
|
||||||
|
@Argument
|
||||||
|
private Integer port = null;
|
||||||
|
|
||||||
|
@Argument
|
||||||
|
private Double slopeTol = null;
|
||||||
|
|
||||||
|
@Argument
|
||||||
|
private Integer numNeededMatching = null;
|
||||||
|
|
||||||
|
@Argument
|
||||||
|
private Integer windowSize = null;
|
||||||
|
|
||||||
|
public void doMain(String[] args) throws IOException {
|
||||||
|
|
||||||
|
CmdLineParser parser = new CmdLineParser(this);
|
||||||
|
parser.setUsageWidth(80);
|
||||||
|
try {
|
||||||
|
parser.parseArgument(args);
|
||||||
|
|
||||||
|
if (port == null)
|
||||||
|
throw new CmdLineException(parser, "No argument is given");
|
||||||
|
|
||||||
|
} catch (CmdLineException e) {
|
||||||
|
// if there's a problem in the command line,
|
||||||
|
// you'll get this exception. this will report
|
||||||
|
// an error message.
|
||||||
|
System.err.println(e.getMessage());
|
||||||
|
System.err.println("java SteadyDetect [options...] port");
|
||||||
|
// print the list of available options
|
||||||
|
parser.printUsage(System.err);
|
||||||
|
System.err.println();
|
||||||
|
|
||||||
|
// print option sample. This is useful some time
|
||||||
|
System.err.println(" Example: java SteadyDetect" + parser.printExample(ALL));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("Binding SteadyDetect to port " + port);
|
||||||
|
socketListener = new SocketListener(port, TimeData.TimeDataPoint.class, data);
|
||||||
|
socketListener.start();
|
||||||
|
|
||||||
|
// Initialize the SteadyStateCalc to the LinearRegression calculator
|
||||||
|
steadyStateCalc = new LinearRegression(slopeTol, numNeededMatching, windowSize);
|
||||||
|
while (true) {
|
||||||
|
steadyStateCalc.printIfSteady(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
154
src/main/java/com/aselimov/app/socket/SocketListener.java
Normal file
154
src/main/java/com/aselimov/app/socket/SocketListener.java
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
package com.aselimov.app.socket;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonFactory;
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for data over a socket
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class SocketListener<T> {
|
||||||
|
|
||||||
|
private final int port;
|
||||||
|
private final Class<T> dataClass;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final Consumer<T> dataConsumer;
|
||||||
|
private ServerSocket serverSocket;
|
||||||
|
private boolean running = false;
|
||||||
|
private ExecutorService executorService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new StreamingSocketJsonListener.
|
||||||
|
*
|
||||||
|
* @param port The port to listen on
|
||||||
|
* @param targetClass The class to parse incoming JSON into
|
||||||
|
* @param dataConsumer Consumer that processes parsed data objects
|
||||||
|
*/
|
||||||
|
public SocketListener(int port, Class<T> dataClass, Consumer<T> dataConsumer) {
|
||||||
|
this.port = port;
|
||||||
|
this.dataClass = dataClass;
|
||||||
|
this.dataConsumer = dataConsumer;
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the SocketListener.
|
||||||
|
* This creates a threadpool to process the socket. For each piece of data
|
||||||
|
* received we immediately call the consumer
|
||||||
|
* function
|
||||||
|
*/
|
||||||
|
public void start() {
|
||||||
|
if (running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("Started listening on " + port);
|
||||||
|
running = true;
|
||||||
|
// Use a new Cached Thread Pool since we expect low numbers of clients
|
||||||
|
// connecting
|
||||||
|
executorService = Executors.newCachedThreadPool();
|
||||||
|
|
||||||
|
executorService.execute(() -> {
|
||||||
|
try {
|
||||||
|
serverSocket = new ServerSocket(port);
|
||||||
|
while (running) {
|
||||||
|
try {
|
||||||
|
Socket clientSocket = serverSocket.accept();
|
||||||
|
handleConnection(clientSocket);
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (running) {
|
||||||
|
System.err.println("Error accepting connections: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Error starting server because: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleConnection(Socket clientSocket) {
|
||||||
|
System.out.println("Opening new client connection");
|
||||||
|
executorService.execute(() -> {
|
||||||
|
try {
|
||||||
|
BufferedInputStream inputStream = new BufferedInputStream(clientSocket.getInputStream());
|
||||||
|
|
||||||
|
JsonFactory factory = objectMapper.getFactory();
|
||||||
|
JsonParser parser = factory.createParser(inputStream);
|
||||||
|
|
||||||
|
// Set the parser for streaming mode
|
||||||
|
parser.setCodec(objectMapper);
|
||||||
|
|
||||||
|
while (running && clientSocket.isConnected() && !clientSocket.isClosed()) {
|
||||||
|
try {
|
||||||
|
T parsedData = parser.readValueAs(dataClass);
|
||||||
|
|
||||||
|
if (parsedData != null) {
|
||||||
|
// NOTE: This is going to run pretty slow for multi-client architectures because
|
||||||
|
// each JSON object in the stream is going to lock the data structure to push a
|
||||||
|
// new data point. Probably here we should use per thread buffers and then add
|
||||||
|
// large groups of data points at once
|
||||||
|
dataConsumer.accept(parsedData);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
|
||||||
|
if (clientSocket.isClosed() || !clientSocket.isConnected()) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
System.err.println("Error parser JSON because " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Error reading from socket because " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
clientSocket.close();
|
||||||
|
System.out.println("Closed client connection");
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Error closing client socket because " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the listener and cleans up resources.
|
||||||
|
*/
|
||||||
|
public void stop() {
|
||||||
|
running = false;
|
||||||
|
|
||||||
|
System.out.println("Stopping SteadyDetect and freeing port " + port);
|
||||||
|
|
||||||
|
if (serverSocket != null && !serverSocket.isClosed()) {
|
||||||
|
try {
|
||||||
|
serverSocket.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Error closing server socket: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (executorService != null) {
|
||||||
|
executorService.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("Streaming socket listener stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void finalize() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
package com.aselimov.app.steady_state_analysis;
|
||||||
|
|
||||||
|
import com.aselimov.app.timedata.TimeData;
|
||||||
|
import com.aselimov.app.timedata.TimeData.TimeDataPoints;
|
||||||
|
import com.aselimov.app.utilities.Statistics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether a time series is steady state using continuous linear
|
||||||
|
* regression.
|
||||||
|
*
|
||||||
|
* This class performs a basic linear regression on chunks of the input signal.
|
||||||
|
* If a user specified number of chunks have a slope within a specified
|
||||||
|
* tolerance, then the signal is labeled as having reached steady state
|
||||||
|
*/
|
||||||
|
public class LinearRegression extends SteadyStateCalculator {
|
||||||
|
private double slopeTol;
|
||||||
|
private int numNeededMatching;
|
||||||
|
private int windowSize;
|
||||||
|
private int numMatching = 0;
|
||||||
|
private int lastChecked = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a LinearRegression object
|
||||||
|
*
|
||||||
|
* @param slopeTol Tolerance for the slope to be detected as 0
|
||||||
|
* @param numNeededMatching the number of consecutive matches before we detect
|
||||||
|
* the signal as steady state
|
||||||
|
* @param windowSize The size of the window to perform linear regression
|
||||||
|
* on
|
||||||
|
*/
|
||||||
|
public LinearRegression(double slopeTol, int numNeededMatching, int windowSize) {
|
||||||
|
this.slopeTol = slopeTol;
|
||||||
|
this.numNeededMatching = numNeededMatching;
|
||||||
|
this.windowSize = windowSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether a signal is steady or not.
|
||||||
|
* This performs linear regression on the final windowSize number of data
|
||||||
|
* points and determines whether the signal is steady state or not
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected SignalStatus isSteady(TimeData inData) {
|
||||||
|
|
||||||
|
// TODO: This is a race condition, in that the time between checking whether we
|
||||||
|
// need to check steady state can yield additional data points, meaning some
|
||||||
|
// points aren't getting checked. Probably need ot refactor this code so that
|
||||||
|
// calling the steady checker happens on the data addition
|
||||||
|
if ((inData.getNumPnts() - lastChecked) < windowSize) {
|
||||||
|
return SignalStatus.NOT_CHECKED;
|
||||||
|
}
|
||||||
|
TimeDataPoints data = inData.threadsafeClone();
|
||||||
|
lastChecked = data.times.size();
|
||||||
|
|
||||||
|
// Estimate the slope of the fitted line (the intercept doesn't matter)
|
||||||
|
int endIdx = data.times.size();
|
||||||
|
int startIdx = endIdx - windowSize;
|
||||||
|
double timeMean = Statistics.calcMean(data.times, startIdx, endIdx);
|
||||||
|
double valueMean = Statistics.calcMean(data.values, startIdx, endIdx);
|
||||||
|
|
||||||
|
double timeVariance = Statistics.calcVariance(data.times, timeMean, startIdx, endIdx);
|
||||||
|
|
||||||
|
double covariance = Statistics.calcCovariance(data.times, data.values, timeMean, valueMean, startIdx, endIdx);
|
||||||
|
|
||||||
|
double slope = covariance / timeVariance;
|
||||||
|
|
||||||
|
if (Math.abs(slope) < slopeTol) {
|
||||||
|
numMatching += 1;
|
||||||
|
} else {
|
||||||
|
numMatching = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numMatching >= numNeededMatching) {
|
||||||
|
return SignalStatus.STEADY;
|
||||||
|
} else {
|
||||||
|
return SignalStatus.NOT_STEADY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.aselimov.app.steady_state_analysis;
|
||||||
|
|
||||||
|
import com.aselimov.app.timedata.TimeData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A calculator that determines whether input time_series data is steady state.
|
||||||
|
*/
|
||||||
|
public abstract class SteadyStateCalculator {
|
||||||
|
public enum SignalStatus {
|
||||||
|
STEADY,
|
||||||
|
NOT_STEADY,
|
||||||
|
NOT_CHECKED
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printIfSteady(TimeData data) {
|
||||||
|
SignalStatus status = isSteady(data);
|
||||||
|
switch (status) {
|
||||||
|
case STEADY:
|
||||||
|
System.out.println("Signal is steady-state");
|
||||||
|
break;
|
||||||
|
case NOT_STEADY:
|
||||||
|
System.out.println("Signal is not steady-state");
|
||||||
|
break;
|
||||||
|
case NOT_CHECKED:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract SignalStatus isSteady(TimeData data);
|
||||||
|
}
|
94
src/main/java/com/aselimov/app/timedata/TimeData.java
Normal file
94
src/main/java/com/aselimov/app/timedata/TimeData.java
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package com.aselimov.app.timedata;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import com.aselimov.app.steady_state_analysis.SteadyStateCalculator;
|
||||||
|
|
||||||
|
public class TimeData implements Consumer<TimeData.TimeDataPoint> {
|
||||||
|
|
||||||
|
private TimeDataPoints data = new TimeDataPoints();
|
||||||
|
private ReentrantLock mutex = new ReentrantLock();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single time data point.
|
||||||
|
* Used for formatted parsing of incoming JSON data
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public record TimeDataPoint(double time, double value) {
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of data points
|
||||||
|
*/
|
||||||
|
public class TimeDataPoints {
|
||||||
|
public List<Double> times;
|
||||||
|
public List<Double> values;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty constructor
|
||||||
|
*/
|
||||||
|
public TimeDataPoints() {
|
||||||
|
times = new ArrayList();
|
||||||
|
values = new ArrayList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement copy constructors for TimeDataPoints
|
||||||
|
*/
|
||||||
|
public TimeDataPoints(TimeDataPoints data) {
|
||||||
|
this(data.times, data.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeDataPoints(List<Double> times, List<Double> values) {
|
||||||
|
this.times = new ArrayList(times);
|
||||||
|
this.values = new ArrayList(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(double time, double value) {
|
||||||
|
times.add(time);
|
||||||
|
values.add(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow TimeData to be a consumer of TimeDataPoint.
|
||||||
|
* Add the TimeDataPoint to the overall time_data
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void accept(TimeDataPoint arg0) {
|
||||||
|
mutex.lock();
|
||||||
|
try {
|
||||||
|
data.add(arg0.time(), arg0.value());
|
||||||
|
} finally {
|
||||||
|
mutex.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a clone of the TimeDataPoints.
|
||||||
|
* We get a clone for local operation while ensuring the memory access is
|
||||||
|
* threadsafe
|
||||||
|
*/
|
||||||
|
public TimeDataPoints threadsafeClone() {
|
||||||
|
mutex.lock();
|
||||||
|
try {
|
||||||
|
return new TimeDataPoints(data.times, data.values);
|
||||||
|
} finally {
|
||||||
|
mutex.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the number of data points in the class
|
||||||
|
*
|
||||||
|
* @return number of data points
|
||||||
|
*/
|
||||||
|
public int getNumPnts() {
|
||||||
|
return data.times.size();
|
||||||
|
}
|
||||||
|
}
|
60
src/main/java/com/aselimov/app/utilities/Statistics.java
Normal file
60
src/main/java/com/aselimov/app/utilities/Statistics.java
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package com.aselimov.app.utilities;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Statistics {
|
||||||
|
/**
|
||||||
|
* Calculate the mean of a set of data
|
||||||
|
*
|
||||||
|
* @param data input data
|
||||||
|
* @param startIdx start idx of data
|
||||||
|
* @param endIdx end idx of data
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static double calcMean(List<Double> data, int startIdx, int endIdx) {
|
||||||
|
double sum = 0.0;
|
||||||
|
for (int i = startIdx; i < endIdx; i++) {
|
||||||
|
sum += data.get(i);
|
||||||
|
}
|
||||||
|
return sum / (endIdx - startIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the variance of a set of data
|
||||||
|
*
|
||||||
|
* @param data input data
|
||||||
|
* @param mean mean value of data
|
||||||
|
* @param startIdx start_idx of data
|
||||||
|
* @param endIdx end_idx of data
|
||||||
|
* @return variance of data
|
||||||
|
*/
|
||||||
|
public static double calcVariance(List<Double> data, double mean, int startIdx, int endIdx) {
|
||||||
|
double variance = 0.0;
|
||||||
|
for (int i = startIdx; i < endIdx; i++) {
|
||||||
|
double xMinusMean = data.get(i) - mean;
|
||||||
|
variance += xMinusMean * xMinusMean;
|
||||||
|
}
|
||||||
|
return variance / (endIdx - startIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the covariance of two variables
|
||||||
|
*
|
||||||
|
* @param x List of data representing one variable
|
||||||
|
* @param y List of data representing other variable
|
||||||
|
* @param xMean mean of x data
|
||||||
|
* @param yMean mean of y data
|
||||||
|
* @param startIdx start index for both data
|
||||||
|
* @param endIdx end index for both data
|
||||||
|
* @return covariance of x and y
|
||||||
|
*/
|
||||||
|
public static double calcCovariance(List<Double> x, List<Double> y, double xMean, double yMean, int startIdx,
|
||||||
|
int endIdx) {
|
||||||
|
double covariance = 0;
|
||||||
|
for (int i = startIdx; i < endIdx; i++) {
|
||||||
|
covariance += (x.get(i) - xMean) * (y.get(i) - yMean);
|
||||||
|
}
|
||||||
|
return covariance / (endIdx - startIdx - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
19
src/test/java/com/aselimov/app/AppTest.java
Normal file
19
src/test/java/com/aselimov/app/AppTest.java
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package com.aselimov.app;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit test for simple App.
|
||||||
|
*/
|
||||||
|
public class AppTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rigorous Test :-)
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldAnswerWithTrue() {
|
||||||
|
assertTrue(true);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
package com.aselimov.app.steady_state_analysis;
|
||||||
|
|
||||||
|
import com.aselimov.app.timedata.TimeData;
|
||||||
|
import com.aselimov.app.timedata.TimeData.TimeDataPoints;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class LinearRegressionTest {
|
||||||
|
private LinearRegression linearRegression;
|
||||||
|
private TimeData timeData;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// Set up a LinearRegression instance with reasonable default values
|
||||||
|
linearRegression = new LinearRegression(0.01, 3, 5); // slope tolerance = 0.01, need 3 matches, window size = 5
|
||||||
|
timeData = new TimeData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNotEnoughData() {
|
||||||
|
// Test when there's not enough data points
|
||||||
|
timeData.accept(new TimeData.TimeDataPoint(1.0, 1.0));
|
||||||
|
timeData.accept(new TimeData.TimeDataPoint(2.0, 2.0));
|
||||||
|
|
||||||
|
assertEquals(SteadyStateCalculator.SignalStatus.NOT_CHECKED, linearRegression.isSteady(timeData));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSteadyStateDetection() {
|
||||||
|
// Test steady state detection with a flat line
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
timeData.accept(new TimeData.TimeDataPoint(i * 1.0, 1.0)); // Constant value of 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(SteadyStateCalculator.SignalStatus.STEADY, linearRegression.isSteady(timeData));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNonSteadyState() {
|
||||||
|
// Test non-steady state with a linearly increasing signal
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
timeData.accept(new TimeData.TimeDataPoint(i * 1.0, i * 1.0)); // Linear increase
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(SteadyStateCalculator.SignalStatus.NOT_STEADY, linearRegression.isSteady(timeData));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMultipleWindows() {
|
||||||
|
// Test multiple windows with a signal that becomes steady
|
||||||
|
// First add non-steady data
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
timeData.accept(new TimeData.TimeDataPoint(i * 1.0, i * 1.0)); // Linear increase
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add steady data
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
timeData.accept(new TimeData.TimeDataPoint(i * 1.0 + 5.0, 5.0)); // Constant value of 5.0
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(SteadyStateCalculator.SignalStatus.STEADY, linearRegression.isSteady(timeData));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSlopeTolerance() {
|
||||||
|
// Test with a signal that has a small slope within tolerance
|
||||||
|
linearRegression = new LinearRegression(0.1, 3, 5); // Increase slope tolerance
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
// Create a signal with a small slope (0.05)
|
||||||
|
timeData.accept(new TimeData.TimeDataPoint(i * 1.0, 1.0 + i * 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(SteadyStateCalculator.SignalStatus.STEADY, linearRegression.isSteady(timeData));
|
||||||
|
}
|
||||||
|
}
|
58
src/test/java/com/aselimov/app/utilities/StatisticsTest.java
Normal file
58
src/test/java/com/aselimov/app/utilities/StatisticsTest.java
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package com.aselimov.app.utilities;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit test the statistics class
|
||||||
|
*/
|
||||||
|
public class StatisticsTest {
|
||||||
|
List<Double> x;
|
||||||
|
List<Double> y;
|
||||||
|
double xMeanFull;
|
||||||
|
double xMeanHalf;
|
||||||
|
double yMeanFull;
|
||||||
|
double yMeanHalf;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
x = new ArrayList(Arrays.asList(1.30656191, -0.9354737, -1.10250799, 1.00903942, 1.37715207, -0.163614,
|
||||||
|
-1.17995963, 0.52334468, -0.24819095, 0.22252544));
|
||||||
|
y = new ArrayList(Arrays.asList(0.56089952, 1.9944321, -0.39985776, 0.60433577, 1.01884808,
|
||||||
|
0.39328237, 1.72282243, -1.3340576, -0.12969627, -0.1063279));
|
||||||
|
|
||||||
|
xMeanFull = Statistics.calcMean(x, 0, x.size());
|
||||||
|
xMeanHalf = Statistics.calcMean(x, 5, x.size());
|
||||||
|
yMeanFull = Statistics.calcMean(y, 0, y.size());
|
||||||
|
yMeanHalf = Statistics.calcMean(y, 5, y.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMean() {
|
||||||
|
assertEquals(0.08088772442417858, xMeanFull, 1e-8);
|
||||||
|
assertEquals(0.43246807465934944, yMeanFull, 1e-8);
|
||||||
|
assertEquals(-0.1691788946337382, xMeanHalf, 1e-8);
|
||||||
|
assertEquals(0.10920460800791403, yMeanHalf, 1e-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testVariance() {
|
||||||
|
assertEquals(0.8451099344901358, Statistics.calcVariance(x, xMeanFull, 0, x.size()), 1e-8);
|
||||||
|
assertEquals(0.33219455205301196, Statistics.calcVariance(x, xMeanHalf, 5, x.size()), 1e-8);
|
||||||
|
assertEquals(0.8915865222994588, Statistics.calcVariance(y, yMeanFull, 0, y.size()), 1e-8);
|
||||||
|
assertEquals(0.9741992662607654, Statistics.calcVariance(y, yMeanHalf, 5, y.size()), 1e-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCovariance() {
|
||||||
|
assertEquals(-0.20175547500418511, Statistics.calcCovariance(x, y, xMeanFull, yMeanFull, 0, x.size()), 1e-8);
|
||||||
|
assertEquals(-0.6736187544740592, Statistics.calcCovariance(x, y, xMeanHalf, yMeanHalf, 5, x.size()), 1e-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user