Produced some unit tests for the seizure detection algorithm

This commit is contained in:
Graham Jones
2019-06-23 16:59:24 +01:00
parent 238b3c504f
commit 69e3668649
3 changed files with 141 additions and 64 deletions

View File

@@ -20,6 +20,9 @@ android {
testOptions { testOptions {
unitTests.returnDefaultValues = true unitTests.returnDefaultValues = true
unitTests {
includeAndroidResources = true
}
} }
} }
@@ -42,6 +45,7 @@ dependencies {
implementation 'com.github.wendykierp:JTransforms:3.0' implementation 'com.github.wendykierp:JTransforms:3.0'
implementation 'com.google.android.gms:play-services-location:10.0.0' implementation 'com.google.android.gms:play-services-location:10.0.0'
//implementation 'com.github.RohitSurwase.UCE-Handler:uce_handler:1.3' //implementation 'com.github.RohitSurwase.UCE-Handler:uce_handler:1.3'
testImplementation 'org.robolectric:robolectric:4.3'
} }

View File

@@ -43,7 +43,7 @@ public class SdAnalyser {
private double mWarnTime; private double mWarnTime;
private double mAlarmTime; private double mAlarmTime;
private double mAlarmThresh; private double mAlarmThresh;
private double mAlarmRatioThresh; double mAlarmRatioThresh;
private double mFreqRes; private double mFreqRes;
private int mAlarmCount; private int mAlarmCount;
private double mFreqCutoff; private double mFreqCutoff;
@@ -79,13 +79,10 @@ public class SdAnalyser {
} }
/** /**
* doAnalysis() - analyse the data if the accelerometer data array mAccData * getSpecPower(rawData): Calculate the average bin power in the spectrum of rawData
* and populate the output data structure mSdData * between 0 and freqCutoff Hz, excluding the DC component
*/ */
void calculateSpectralPowers(double[] rawData) { double getSpecPower(double[] rawData) {
// Set the frequency bounds for the analysis in fft output bin numbers.
int nMin = freq2fftBin(mAlarmFreqMin);
int nMax = freq2fftBin(mAlarmFreqMax);
int nFreqCutoff = freq2fftBin(mFreqCutoff); int nFreqCutoff = freq2fftBin(mFreqCutoff);
DoubleFFT_1D fftDo = new DoubleFFT_1D(mNSamp); DoubleFFT_1D fftDo = new DoubleFFT_1D(mNSamp);
@@ -104,7 +101,24 @@ public class SdAnalyser {
fft[2 * i + 1] = 0.; fft[2 * i + 1] = 0.;
} }
} }
specPower = specPower / (mNSamp / 2); specPower = specPower / mNSamp / 2;
return(specPower);
}
/**
* getRoiPower(rawData): Calculate the average bin power in the spectrum of rawData
* between alarmFreqMin and alarmFreqMax Hz.
*/
double getRoiPower(double[] rawData) {
// Set the frequency bounds for the analysis in fft output bin numbers.
int nMin = freq2fftBin(mAlarmFreqMin);
int nMax = freq2fftBin(mAlarmFreqMax);
int nFreqCutoff = freq2fftBin(mFreqCutoff);
DoubleFFT_1D fftDo = new DoubleFFT_1D(mNSamp);
double[] fft = new double[mNSamp * 2];
System.arraycopy(rawData, 0, fft, 0, mNSamp);
fftDo.realForward(fft);
// Calculate the Region of Interest power and power ratio. // Calculate the Region of Interest power and power ratio.
double roiPower = 0; double roiPower = 0;
@@ -112,10 +126,49 @@ public class SdAnalyser {
roiPower = roiPower + getMagnitude(fft, i); roiPower = roiPower + getMagnitude(fft, i);
} }
roiPower = roiPower / (nMax - nMin); roiPower = roiPower / (nMax - nMin);
double roiRatio = 10 * roiPower / specPower; return(roiPower);
} }
/**
* getSpectrumRatio(rawData) - return the ratio of the ROI power to the whole spectrum power.
* @param rawData
* @return
*/
double getSpectrumRatio(double rawData[]) {
double specPower;
double roiPower;
double specRatio;
specPower = getSpecPower(rawData);
roiPower = getRoiPower(rawData);
if (specPower > mAlarmThresh) {
specRatio = 10.0 * roiPower / specPower;
} else {
specRatio = 0.0;
}
return(specRatio);
}
/**
* getAlarmState(rawData) - determines the alarm state associated with the snapshot of raw
* acceleration data rawData[]
* @return the alarm state (0=ok, 1 = alarm)
*/
int getAlarmState(double rawData[]) {
int alarmState;
double alarmRatio = getSpectrumRatio(rawData);
if (alarmRatio <= mAlarmRatioThresh) {
alarmState = 0;
} else {
alarmState = 1;
}
return(alarmState);
}
/** /**
* Calculate the magnitude of entry i in the fft array fft * Calculate the magnitude of entry i in the fft array fft
* *
@@ -129,67 +182,34 @@ public class SdAnalyser {
return mag; return mag;
} }
// Force the data stored in this datasource to update in line with the JSON string encoded data provided. double[] getAccelDataFromJSON(String jsonStr) {
// Used by webServer to update the GarminDatasource.
// Returns a message string that is passed back to the watch.
public String updateFromJSON(String jsonStr) {
String retVal = "undefined"; String retVal = "undefined";
Log.v(TAG,"updateFromJSON - "+jsonStr); double[] rawData = new double[mNSamp];
Log.v(TAG,"getAccelDataFromJSON - "+jsonStr);
try { try {
JSONObject mainObject = new JSONObject(jsonStr); JSONObject mainObject = null;
//JSONObject dataObject = mainObject.getJSONObject("dataObj"); mainObject = new JSONObject(jsonStr);
JSONObject dataObject = mainObject; JSONObject dataObject = mainObject;
String dataTypeStr = dataObject.getString("dataType"); String dataTypeStr = dataObject.getString("dataType");
Log.v(TAG,"updateFromJSON - dataType="+dataTypeStr); Log.v(TAG,"getAccelDataFromJSON - dataType="+dataTypeStr);
if (dataTypeStr.equals("raw")) { if (dataTypeStr.equals("raw")) {
Log.v(TAG,"updateFromJSON - processing raw data"); Log.v(TAG, "getAccelDataFromJSON - processing raw data");
try {
mSdData.mHR = dataObject.getDouble("HR");
} catch (JSONException e) {
// if we get 'null' HR (For example if the heart rate is not working)
mSdData.mHR = -1;
}
try {
mMute = dataObject.getInt("Mute");
} catch (JSONException e) {
// if we get 'null' HR (For example if the heart rate is not working)
mMute = 0;
}
JSONArray accelVals = dataObject.getJSONArray("data"); JSONArray accelVals = dataObject.getJSONArray("data");
Log.v(TAG, "Received " + accelVals.length() + " acceleration values"); Log.v(TAG, "Received " + accelVals.length() + " acceleration values");
int i; int i;
for (i = 0; i < accelVals.length(); i++) { for (i = 0; i < accelVals.length(); i++) {
mSdData.rawData[i] = accelVals.getInt(i); rawData[i] = accelVals.getInt(i);
} }
mSdData.mNsamp = accelVals.length();
//mNSamp = accelVals.length();
mWatchAppRunningCheck = true;
doAnalysis();
if (mSdData.haveSettings == false) {
retVal = "sendSettings";
} else {
retVal = "OK";
}
} else if (dataTypeStr.equals("settings")){
Log.v(TAG,"updateFromJSON - processing settings");
mSamplePeriod = (short)dataObject.getInt("analysisPeriod");
mSampleFreq = (short)dataObject.getInt("sampleFreq");
mSdData.batteryPc = (short)dataObject.getInt("battery");
Log.v(TAG,"updateFromJSON - mSamplePeriod="+mSamplePeriod+" mSampleFreq="+mSampleFreq);
mSdData.haveSettings = true;
mSdData.mSampleFreq = mSampleFreq;
mWatchAppRunningCheck = true;
retVal = "OK";
} else { } else {
Log.e(TAG,"updateFromJSON - unrecognised dataType "+dataTypeStr); Log.e(TAG,"getAccelDataFromJSON - unrecognised dataType "+dataTypeStr);
retVal = "ERROR"; retVal = "ERROR";
} }
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG,"updateFromJSON - Error Parsing JSON String - "+e.toString()); Log.e(TAG,"getAccelDataFromJSON - Error Parsing JSON String - "+e.toString());
e.printStackTrace(); e.printStackTrace();
retVal = "ERROR"; retVal = "ERROR";
} }
return(retVal); return(rawData);
} }
} }

View File

@@ -3,14 +3,17 @@ package uk.org.openseizuredetector;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@RunWith(RobolectricTestRunner.class)
public class SdAnalyserTest { public class SdAnalyserTest {
public SdAnalyser sda; public SdAnalyser sda;
public String alarmJSON = "{ dataType: 'raw', " + public String alarmJSON = "{ 'dataType': 'raw', " +
"data: [" + "'data': [" +
"1644, 1316, 1144, 1332, 1716, 1716, 1392, 1148, 1276, 1660, " + "1644, 1316, 1144, 1332, 1716, 1716, 1392, 1148, 1276, 1660, " +
"1716, 1496, 1196, 1232, 1572, 1684, 1552, 1236, 1228, 1528, " + "1716, 1496, 1196, 1232, 1572, 1684, 1552, 1236, 1228, 1528, " +
"1648, 1572, 1268, 1208, 1492, 1680, 1596, 1272, 1192, 1424, " + "1648, 1572, 1268, 1208, 1492, 1680, 1596, 1272, 1192, 1424, " +
@@ -24,13 +27,13 @@ public class SdAnalyserTest {
"1188, 1360, 1680, 1724, 1424, 1192, 1224, 1556, 1744, 1588, " + "1188, 1360, 1680, 1724, 1424, 1192, 1224, 1556, 1744, 1588, " +
"1260, 1220, 1472, 1692, 1608, 1328, 1192, 1412, 1668, 1656, " + "1260, 1220, 1472, 1692, 1608, 1328, 1192, 1412, 1668, 1656, " +
"1356, 1216, 1304, 1636, 1712], " + "1356, 1216, 1304, 1636, 1712], " +
"HR:54, " + "'HR':54, " +
"Mute:0 " + "'Mute':0 " +
"}"; "}";
private String okJSON = "{ " + private String okJSON = "{ " +
"dataType: 'raw', " + "\"dataType\": \"raw\", " +
"data: [" + "\"data\": [" +
"1140, 1188, 1144, 1172, 1228, 1212, 1236, 1236, 1256, 1320, " + "1140, 1188, 1144, 1172, 1228, 1212, 1236, 1236, 1256, 1320, " +
"1316, 1280, 1240, 1280, 1324, 1284, 1292, 1268, 1284, 1276, " + "1316, 1280, 1240, 1280, 1324, 1284, 1292, 1268, 1284, 1276, " +
"1296, 1324, 1308, 1288, 1304, 1276, 1304, 1304, 1276, 1296, " + "1296, 1324, 1308, 1288, 1304, 1276, 1304, 1304, 1276, 1296, " +
@@ -44,8 +47,8 @@ public class SdAnalyserTest {
"1268, 1292, 1292, 1304, 1288, 1284, 1280, 1276, 1288, 1280, " + "1268, 1292, 1292, 1304, 1288, 1284, 1280, 1276, 1288, 1280, " +
"1300, 1288, 1320, 1268, 1288, 1280, 1304, 1280, 1280, 1288, " + "1300, 1288, 1320, 1268, 1288, 1280, 1304, 1280, 1280, 1288, " +
"1292, 1308, 1268, 1292, 1280], " + "1292, 1308, 1268, 1292, 1280], " +
"HR:57, " + "\"HR\":57, " +
"Mute:0 " + "\"Mute\":0 " +
"}"; "}";
@Before @Before
@@ -65,7 +68,7 @@ public class SdAnalyserTest {
} }
@Test @Test
public void freq2fftBin() { public void testFreq2fftBin() {
int n; int n;
n = sda.freq2fftBin(0.0); n = sda.freq2fftBin(0.0);
assertEquals(0,n); assertEquals(0,n);
@@ -74,7 +77,7 @@ public class SdAnalyserTest {
} }
@Test @Test
public void getMagnitude() { public void testGetMagnitude() {
double[] fft = {1, 1, double[] fft = {1, 1,
2, 1, 2, 1,
2, 2}; 2, 2};
@@ -87,4 +90,54 @@ public class SdAnalyserTest {
assertEquals(8.0, m, m * 1e-4); assertEquals(8.0, m, m * 1e-4);
} }
@Test
public void testGetAccelDataFromJson() {
double[] retVal;
retVal = sda.getAccelDataFromJSON(okJSON);
assertNotNull(retVal);
assertEquals(125,retVal.length);
assertEquals(1140,retVal[0],0.001);
assertEquals(1280,retVal[124],0.001);
}
@Test
public void testGetSpectrumRatio() {
double[] okRawVals;
double[] alarmRawVals;
okRawVals = sda.getAccelDataFromJSON(okJSON);
alarmRawVals = sda.getAccelDataFromJSON(alarmJSON);
double okRatio;
double alarmRatio;
okRatio = sda.getSpectrumRatio(okRawVals);
alarmRatio = sda.getSpectrumRatio(alarmRawVals);
assertTrue("Check Spectrum Ratio for OK data "+okRatio+" is <="+sda.mAlarmRatioThresh,
okRatio <= sda.mAlarmRatioThresh);
assertTrue("Check Spectrum Ratio for ALARM data "+alarmRatio+" is >"+sda.mAlarmRatioThresh,
alarmRatio > sda.mAlarmRatioThresh);
}
@Test
public void testgetAlarmState() {
double[] okRawVals;
double[] alarmRawVals;
okRawVals = sda.getAccelDataFromJSON(okJSON);
alarmRawVals = sda.getAccelDataFromJSON(alarmJSON);
int okAlarmState;
int alarmAlarmState;
okAlarmState = sda.getAlarmState(okRawVals);
alarmAlarmState = sda.getAlarmState(alarmRawVals);
assertEquals("check OK start detected from raw data",0, okAlarmState);
assertEquals("check alarm state detected from raw data",1, alarmAlarmState);
}
} }