This section is dedicated to the implementation of the type of step detection algorithm discussed in the previous section. Our implementation for the algorithm will consist of four major components of android: first is android service, which will stay in the background, second is a set of two threads using the ScheduledExecutorService
, and third is the activity to show the pedometer application data. The last component is the SQLite database to store the steps' information. The following is the high-level class diagram of the application; we will discuss each class in detail in their own sections. Now, let's explore the first component in detail:
StepsTrackerService
service, which will remain in the background and provide a container for execution. Inside this service, we create the StepDetectorListener
and AccelerometerListener
classes and implement them with the SensorEventListener
interface so that they can receive the sensor events. In the onCreate()
method of the service, we initiate SensorManager
and the step detector sensor object after checking its availability. We register the StepDetectorListener
with SensorManager
as soon as the service is created. As discussed earlier, in order to make the algorithm battery and CPU efficient, we will only register the accelerometer listener and start processing the data when any step is detected using the step detector sensor.Hence, we only create the accelerometer Sensor
object in the onCreate()
method and wait for any step detection before creating the object of the AccelerometerListener
and registering it with the SensorManager
. We also create the object of the StepsTrackerDBHelper
class, which is our SQLite database utility for handling all the database operations:
public class StepsTrackerService extends Service{ private SensorManager mSensorManager; private Sensor mStepDetectorSensor; private Sensor mAccelerometerSensor; private AccelerometerListener mAccelerometerListener; private StepDetectorListener mStepDetectorListener; StepsTrackerDBHelper mStepsTrackerDBHelper; @Override public void onCreate() { super.onCreate(); mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE); if(mSensorManager.getDefaultSensor (Sensor.TYPE_STEP_DETECTOR) != null) { mStepDetectorSensor = mSensorManager.getDefaultSensor (Sensor.TYPE_STEP_DETECTOR); mStepDetectorListener = new StepDetectorListener(); mSensorManager.registerListener (mStepDetectorListener, mStepDetectorSensor, SensorManager.SENSOR_DELAY_FASTEST); } if(mSensorManager.getDefaultSensor (Sensor.TYPE_ACCELEROMETER) != null) { mAccelerometerSensor = mSensorManager.getDefaultSensor (Sensor.TYPE_ACCELEROMETER); } mStepsTrackerDBHelper = new StepsTrackerDBHelper(this); }
ScheduledExecutorService
. It is the system level Android thread executor service that provides the required number of threads from its available pool of threads for execution. We initiate the object of the ScheduledExecutorService
with only two threads, using the Executors.newScheduledThreadPool(2)
method, and we use it to schedule the execution of our two threads. The ScheduledFuture
class instance is used to store the reference of the scheduled thread obtained by scheduling it using the ScheduledExecutorService.schedule()
method.The ScheduledFuture
instance will also be used to cancel the execution of a scheduled thread using the ScheduledFuture.cancel()
method. For the step detection algorithm, we use two threads: first is the UnregisterAcceleromterTask
thread, which implements the runnable interface and is responsible for unregistering the accelerometer sensor, and second is ProcessDataTask
, which also implements the runnable interface and is used to process the accelerometer data periodically. We use the mScheduledUnregisterAccelerometerTask
instance of the ScheduledFuture
class to store the reference of scheduled execution of the UnregisterAcceleromterTask
thread, and similarly, we store the reference of the scheduled execution of the ProcessDataTask
thread in mScheduledProcessDataTask
, which is also the instance of the ScheduledFuture
class.
As the first part to the logic, we want to only register the accelerometer listener and start processing the accelerometer data when we detect any steps and this is achieved inside the onSensorChanged()
method of the StepDetectorListener
class, where we create the object of AccelerometerListener
and register it with SensorManager
. We only register AccelerometerListener
if it is has not been registered earlier, and we check this by using a Boolean variable called isAccelerometerRegistered
. When the AccelerometerListener
is registered, we make it true and when it unregisters inside the run()
method UnregisterAcceleromterTask
thread, we make it false.
Before registering, we also make sure that the mAccelerometerSensor
is not null, that is, the accelerometer sensor is present on the device. Now, the second part of the logic is to unregister the accelerometer listener and stop processing the accelerometer data when no steps have been detected for the last 20 seconds. This is achieved by scheduling the execution of a new instance of the UnregisterAcceleromterTask
thread after 20 seconds every time that a new step is detected, and cancelling the last scheduled execution of instance of the UnregisterAcceleromterTask
thread, if present. This keeps on postponing the un-registration of the accelerometer listener until no step is detected for the last 20 seconds.
We use the isScheduleUnregistered
Boolean variable to check whether there is any old scheduled execution instances of the UnregisterAcceleromterTask
thread pending for execution; if yes, then we cancel its execution using the cancel()
method of the mScheduledUnregisterAccelerometerTask
. As soon as the UnregisterAcceleromterTask
thread is scheduled for future execution, we make the isScheduleUnregistered
Boolean variable true, and after the successful execution of the UnregisterAcceleromterTask
thread, we make it false. Inside the run()
method of the UnregisterAcceleromterTask
thread, we also stop the processing of accelerometer data by stopping the execution of the ProcessDataTask
thread using the cancel()
method of mScheduledProcessDataTask
, which stores its scheduled execution reference:
ScheduledExecutorService mScheduledExecutorService = Executors.newScheduledThreadPool(2); private ScheduledFuture mScheduledUnregisterAccelerometerTask; private ScheduledFuture mScheduledProcessDataTask; private UnregisterAcceleromterTask mUnregisterAcceleromterTask; private ProcessDataTask mProcessDataTask; private boolean isScheduleUnregistered = false; private boolean isAccelerometerRegistered = false; class StepDetectorListener implements SensorEventListener{ @Override public void onSensorChanged(SensorEvent event) { if(!isAccelerometerRegistered && mAccelerometerSensor!=null) { mAccelerometerListener = new AccelerometerListener(); mSensorManager.registerListener (mAccelerometerListener, mAccelerometerSensor, SensorManager.SENSOR_DELAY_FASTEST); isAccelerometerRegistered = true; } if(isScheduleUnregistered) { mScheduledUnregisterAccelerometerTask .cancel(true); } mUnregisterAcceleromterTask = new UnregisterAcceleromterTask(); mScheduledUnregisterAccelerometerTask = mScheduledExecutorService.schedule (mUnregisterAcceleromterTask, 20000, TimeUnit.MILLISECONDS); isScheduleUnregistered = true; } } class UnregisterAcceleromterTask implements Runnable { @Override public void run() { isAccelerometerRegistered = false; mSensorManager.unregisterListener (mAccelerometerListener); isScheduleUnregistered = false; mScheduledProcessDataTask.cancel(false); } }
AccelerometerData
(POJO) class to hold the accelerometer data together. It has the float x, y, and z variables to hold the acceleration values on the x, y, and z axes. A double value holds the square root of the sum of the squares of the acceleration acting on all the three axes, and long time is used for storing the timestamp of the event. The Boolean variable isTruePeak
is initiated to true and is helpful in finding the peak values corresponding to each step. (More on this in the next section). We create four instances of ArrayList
on the AccelerometerData
objects to process the accelerometer data.Each of them will be discussed as we use them. In the AccelerometerListener
constructor, we schedule the periodic execution of the ProcessDataTask
thread to execute every 10 seconds with an initial delay of 10 seconds using the scheduleWithFixedDelay()
method of ScheduledExecutorService
. The ProcessDataTask
thread contains all the logic needed to process the raw accelerometer data and find the type of steps through it. Going back to AccelerometerListener
, it's only responsible for two tasks: the first is scheduling the period execution of the ProcessDataTask
thread, and the second is collecting the raw accelerometer data in the onSensorChanged()
method and storing it in mAccelerometerDataList
, which is the ArrayList
of the AccelerometerData
objects.
Now, let's discuss the ProcessDataTask
thread in detail, which executes every 10 seconds and processes the last 10 seconds of accelerometer data, which is stored in the mAccelerometerDataList
. Inside the run()
method of the ProcessDataTask
thread, the first task we do is copy all the elements of the mAccelerometerDataList
into a new ArrayList
of AccelerometerData
objects called mRawDataList
. After coping, we empty the mAccelerometerDataList
by calling its clear()
method. This is done to avoid the concurrent access of mAccelerometerDataList
from the OnSensorChanged()
method of AccelerometerListener
, which tries to add values to it, and from the ProcessDataTask
thread's run()
method, which reads values from it.
After this step, we calculate the square root of the sum of the squares of the acceleration on the x, y, and z axes and store it in the double value
variable using a for
loop, and we also update the SensorEvent
timestamp to the current epoch timestamp in milliseconds in the same loop. By default, the timestamp of any SensorEvent
is the time in nanoseconds from the system's boot time and not the epoch time (also called the Unix timestamp). A simple way to convert it to epoch time is to first divide the SensorEvent
timestamp by 1,000,000 (to convert from nanoseconds to milliseconds) and then add the offset value. The offset value is the time in milliseconds from the start of the epoch time until the phone boot up time, and this can be calculated by subtracting the SystemClock.elapsedtime()
from System.currentTimeMillis()
. The remaining steps are discussed in the next section:
class AccelerometerData { public double value; public float x; public float y; public float z; public long time; public boolean isTruePeak = true; } private long timeOffsetValue; ArrayList<AccelerometerData> mAccelerometerDataList = new ArrayList<AccelerometerData>(); ArrayList<AccelerometerData> mRawDataList = new ArrayList<AccelerometerData>(); ArrayList<AccelerometerData> mAboveThresholdValuesList = new ArrayList<AccelerometerData>(); ArrayList<AccelerometerData> mHighestPeakList = new ArrayList<AccelerometerData>(); class AccelerometerListener implements SensorEventListener{ public AccelerometerListener() { mProcessDataTask = new ProcessDataTask(); mScheduledProcessDataTask = mScheduledExecutorService.scheduleWithFixedDelay (mProcessDataTask, 10000, 10000, TimeUnit.MILLISECONDS); } @Override public void onSensorChanged(SensorEvent event) { AccelerometerData mAccelerometerData = new AccelerometerData(); mAccelerometerData.x = event.values[0]; mAccelerometerData.y = event.values[1]; mAccelerometerData.z = event.values[2]; mAccelerometerData.time = event.timestamp; mAccelerometerDataList.add(mAccelerometerData); } } class ProcessDataTask implements Runnable { @Override public void run() { //Copy accelerometer data from main sensor array in separate array for processing mRawDataList.addAll(mAccelerometerDataList); mAccelerometerDataList.clear(); //Calculating the magnitude (Square root of sum of squares of x, y, z) & converting time from nano seconds from boot time to epoc time timeOffsetValue = System.currentTimeMillis() - SystemClock.elapsedRealtime(); int dataSize = mRawDataList.size(); for (int i = 0; i < dataSize; i++) { mRawDataList.get(i).value = Math.sqrt(Math.pow(mRawDataList.get(i).x, 2) + Math.pow(mRawDataList.get(i).y, 2) + Math.pow(mRawDataList.get(i).z, 2)); mRawDataList.get(i).time = (mRawDataList.get(i).time/1000000L) + timeOffsetValue; } //Calculating the High Peaks findHighPeaks(); //Remove high peaks close to each other which are within range of 0.4 seconds removeClosePeaks(); //Find the type of step (Running, jogging, walking) & store in Database findStepTypeAndStoreInDB(); mRawDataList.clear(); mAboveThresholdValuesList.clear(); mHighestPeakList.clear(); }
In the algorithm, we also have to deal with the false positive values (marked with a yellow circle). These false positive values are registered very close to the highest peak values.
The first step in the algorithm is to find all the values that are above the walking threshold value and store them in mAboveThresholdValuesListArrayList
of the AccelerometerData
objects. We do this inside a for
loop using the if
condition, which is executed over the entire 10 seconds of accelerometer data stored in mRawDataList
. The second step in the algorithm is to find all the potential highest peak values from the preceding threshold values. This is achieved by only adding above the threshold values in the mAboveThresholdValuesList
until any value lower than the threshold value is received. As soon as any value lower than the threshold is found, we take the values collected thus far in the mAboveThresholdValuesList
and sort them to find the highest potential peak among the values collected until that point that are above the threshold.
We use a custom DataSorter
collection comparator class to sort the mAboveThresholdValuesList
values. Now, this highest potential peak found among the above threshold values could be either a false positive (marked with a yellow circle in the figure), or a true highest peak value (marked with a green circle) corresponding to one step. We save this highest potential peak value in a separate mHighestPeakListArrayList
. Before moving to the next group of above-threshold values, we clear all the data from the mAboveThresholdValuesListArrayList
. After executing the for
loop over the entire mRawDataList
, we will get all the highest potential peaks, which consist of both false positive peak values (marked with the yellow circle in the figure) and the true highest peak values (marked with the green circle) in the mHighestPeakList
.
Now, the third step of the algorithm is to filter out the false positive peak values from the true highest peak values. We know from the analysis of the data that these false positive values are pretty close to the true highest peak values, and also, their magnitude is less than the true highest peak's magnitude. We use the same logic in for
loop to filter out the false positive values. We assume, by default, that all the values collected in the mHighestPeakList
are true peaks. We do this using the Boolean variable, isTruePeak
, in the AccelerometerData
model class and initializing it to true. Inside for
loop, we check the time difference between the two consecutive values; if the time difference is less than 0.4 seconds, then we assume that they are pretty close to each other and one of them is a false positive, and after this, we further compare their magnitude of length of vector, and whichever is smaller, we mark that value as a false positive.
After executing the for
loop over the entire mHighestPeakList
, we are able to filter out all the false positive values that have a lower magnitude and are near the highest peak values by setting their isTruePeak
Boolean variable to false
. Now, the final step in the algorithm is to detect the type of steps and save it in the database. This is done using a for
loop over the mHighestPeakList
inside the findStepTypeAndStoreInDB()
method. Each type of step (running, jogging, and walking) is assigned a peak constant threshold value, which we have derived from the experimental data. For our algorithm, we are using the running peak value as 30, jogging peak value as 25, and walking peak value as 15. We execute a for
loop only on those values of the mHighestPeakList
that have the isTruePeak
Boolean value as true
. We categorize the elements of the mHighestPeakList
by comparing the magnitude (length of vector) with the peak values of running, jogging, and walking, and also, we use the mStepsTrackerDBHelper.createStepsEntry()
method to save the type and time of the step in the SQLite database:
public void findHighPeaks(){ //Calculating the High Peaks boolean isAboveMeanLastValueTrue = false; int dataSize = mRawDataList.size(); for (int i = 0; i < dataSize; i++) { if(mRawDataList.get(i).value > WALKINGPEAK) { mAboveThresholdValuesList.add (mRawDataList.get(i)); isAboveMeanLastValueTrue = false; } else { if(!isAboveMeanLastValueTrue && mAboveThresholdValuesList.size()>0) { Collections.sort(mAboveThresholdValuesList, new DataSorter()); mHighestPeakList.add(mAboveThresholdValuesList .get(mAboveThresholdValuesList.size()-1)); mAboveThresholdValuesList.clear(); } isAboveMeanLastValueTrue = true; } } } public void removeClosePeaks() { int dataSize = mHighestPeakList.size(); for (int i = 0; i < dataSize-1; i++) { if(mHighestPeakList.get(i).isTruePeak) { if(mHighestPeakList.get(i+1).time - mHighestPeakList.get(i).time < 400) { if(mHighestPeakList.get(i+1).value > mHighestPeakList.get(i).value) { mHighestPeakList.get(i).isTruePeak = false; } else { mHighestPeakList.get(i+1).isTruePeak = false; } } } } } public void findStepTypeAndStoreInDB() { int size = mHighestPeakList.size(); for (int i = 0; i < size; i++) { if(mHighestPeakList.get(i).isTruePeak) { if(mHighestPeakList.get(i).value > RUNNINGPEAK) { mStepsTrackerDBHelper.createStepsEntry (mHighestPeakList.get(i).time, RUNNING); } else { if(mHighestPeakList.get(i).value > JOGGINGPEAK) { mStepsTrackerDBHelper.createStepsEntry (mHighestPeakList.get(i).time, JOGGING); } else { mStepsTrackerDBHelper.createStepsEntry (mHighestPeakList.get(i).time, WALKING); } } } } } public class DataSorter implements Comparator<AccelerometerData>{ public int compare(AccelerometerData obj1, AccelerometerData obj2){ int returnVal = 0; if(obj1.value < obj2.value){ returnVal = -1; }else if(obj1.value > obj2.value){ returnVal = 1; } return returnVal; } }
StepsTrackerDBHelper
class to handle all the database operations and extend it from the Android SQLite built in the SQLiteOpenHelper
utility class, which provides access to the database. Inside the class, we create a database called StepsTrackerDatabase
, and it has only one table StepsTrackerSummary
, which consists of the following four columns (id
, steptype
, steptime
, and stepdate
):id
, is the unique integer identifier for each row of the table and is incremented automatically on the creation of every new row.steptype
, is used to store the type of step (running, jogging, or walking).steptime
, which is used to store the time in milliseconds.stepdate
, which is used to store the date in the mm/dd/yyyy
string format.This class has a createStepsEntry()
method that saves every step's information (the type, time, and date of every step) in a new row of the table. This method is called from StepTrackerService
every time a new step is processed. There is another method of this class called getStepsByDate()
, which is responsible for reading the total count of each type of step taken on a particular date, provided as the input parameter. This getStepsByDate()
is called from the CustomAlgoResultsActivity
, to display the pedometer data matrix. More on this in the next section:
public class StepsTrackerDBHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "StepsTrackerDatabase"; private static final String TABLE_STEPS_SUMMARY = "StepsTrackerSummary"; private static final String ID = "id"; private static final String STEP_TYPE = "steptype"; private static final String STEP_TIME = "steptime";//time is in milliseconds Epoch Time private static final String STEP_DATE = "stepdate";//Date format is mm/dd/yyyy private static final String CREATE_TABLE_STEPS_SUMMARY = "CREATE TABLE " + TABLE_STEPS_SUMMARY + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + STEP_DATE + " TEXT,"+ STEP_TIME + " INTEGER,"+ STEP_TYPE + " TEXT"+")"; public boolean createStepsEntry(long timeStamp, int stepType) { boolean createSuccessful = false; Calendar mCalendar = Calendar.getInstance(); String todayDate = String.valueOf(mCalendar.get(Calendar.MONTH)+1)+"/" + String.valueOf(mCalendar.get(Calendar.DAY_OF_MONTH))+"/" + String.valueOf(mCalendar.get(Calendar.YEAR)); try { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(STEP_TIME, timeStamp); values.put(STEP_DATE, todayDate); values.put(STEP_TYPE, stepType); long row = db.insert(TABLE_STEPS_SUMMARY, null, values); if(row!=-1) { createSuccessful = true; } db.close(); } catch (Exception e) { e.printStackTrace(); } return createSuccessful; } public int [] getStepsByDate(String date) { int stepType[] = new int[3]; String selectQuery = "SELECT " + STEP_TYPE + " FROM " + TABLE_STEPS_SUMMARY +" WHERE " + STEP_DATE +" = '"+ date + "'"; try { SQLiteDatabase db = this.getReadableDatabase(); Cursor c = db.rawQuery(selectQuery, null); if (c.moveToFirst()) { do { switch(c.getInt((c.getColumnIndex(STEP_TYPE)))) { case WALKING: ++stepType[0]; break; case JOGGING: ++stepType[1]; break; case RUNNING: ++stepType[2]; break; } } while (c.moveToNext()); } db.close(); } catch (Exception e) { e.printStackTrace(); } return stepType; } }
CustomAlgoResultsActivity
class. In the onCreate()
method of the activity, we initiate seven instances of TextView
to display the seven data matrix points (total steps, distance, duration, average speed, average step frequency, calorie burned, and type of steps). We also initiate the object of the StepsTrackerDBHelper
class, which is used to read the steps' data from the database. The calculateDataMatrix()
method, which is called from the onCreate()
method and is responsible for calculating the data matrix and assigning the values to respective TextView
on the user interface. Now, let's discuss how we can calculate each data point in the data matrix.getStepsByDate()
method of the StepsTrackerDBHelper
class to get the number of each type of step.getStepsByDate()
method of the StepsTrackerDBHelper
class.which returns the total number of each type of step in an integer array of capacity 3.public class CustomAlgoResultsActivity extends Activity{ private TextView mTotalStepsTextView; private TextView mTotalDistanceTextView; private TextView mTotalDurationTextView; private TextView mAverageSpeedTextView; private TextView mAveragFrequencyTextView; private TextView mTotalCalorieBurnedTextView; private TextView mPhysicalActivityTypeTextView; StepsTrackerDBHelper mStepsTrackerDBHelper; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.capability_layout); mTotalStepsTextView = (TextView)findViewById(R.id.total_steps); mTotalDistanceTextView = (TextView)findViewById(R.id.total_distance); mTotalDurationTextView = (TextView)findViewById(R.id.total_duration); mAverageSpeedTextView = (TextView)findViewById(R.id.average_speed); mAveragFrequencyTextView = (TextView)findViewById(R.id.average_frequency); mTotalCalorieBurnedTextView = (TextView)findViewById(R.id.calories_burned); mPhysicalActivityTypeTextView = (TextView)findViewById (R.id.physical_activitytype); mStepsTrackerDBHelper = new StepsTrackerDBHelper(this); Intent mStepsAnalysisIntent = new Intent(getApplicationContext(), StepsTrackerService.class); startService(mStepsAnalysisIntent); calculateDataMatrix(); } public void calculateDataMatrix() { Calendar mCalendar = Calendar.getInstance(); String todayDate = String.valueOf(mCalendar.get(Calendar.MONTH)) +"/" + String.valueOf(mCalendar.get (Calendar.DAY_OF_MONTH)+1) +"/"+ String.valueOf (mCalendar.get(Calendar.YEAR)); int stepType[] = mStepsTrackerDBHelper.getStepsByDate(todayDate); int walkingSteps = stepType[0]; int joggingSteps = stepType[1]; int runningSteps = stepType[2]; //Calculating total steps int totalStepTaken = walkingSteps + joggingSteps + runningSteps; mTotalStepsTextView.setText (String.valueOf(totalStepTaken)+ " Steps"); //Calculating total distance travelled float totalDistance = walkingSteps*0.5f + joggingSteps * 1.0f + runningSteps * 1.5f; mTotalDistanceTextView.setText (String.valueOf(totalDistance)+" meters"); //Calculating total duration float totalDuration = walkingSteps*1.0f + joggingSteps * 0.75f + runningSteps * 0.5f; float hours = totalDuration / 3600; float minutes = (totalDuration % 3600) / 60; float seconds = totalDuration % 60; mTotalDurationTextView.setText (String.format("%.0f",hours) + " hrs " + String.format("%.0f",minutes) + " mins " + String.format("%.0f",seconds)+ " secs"); //Calculating average speed if(totalDistance>0) { mAverageSpeedTextView.setText (String.format("%.2f", totalDistance/totalDuration)+" meter per seconds"); } else { mAverageSpeedTextView.setText ("0 meter per seconds"); } //Calculating average step frequency if(totalStepTaken>0) { mAveragFrequencyTextView.setText (String.format("%.0f",totalStepTaken/minutes)+" steps per minute"); } else { mAveragFrequencyTextView.setText ("0 steps per minute"); } //Calculating total calories burned float totalCaloriesBurned = walkingSteps * 0.05f + joggingSteps * 0.1f + runningSteps * 0.2f; mTotalCalorieBurnedTextView.setText (String.format("%.0f",totalCaloriesBurned)+" Calories"); //Calculating type of physical activity mPhysicalActivityTypeTextView.setText (String.valueOf(walkingSteps) + " Walking Steps " + " "+String.valueOf(joggingSteps) + " Jogging Steps " + " "+String.valueOf(runningSteps)+ " Running Steps"); }
The following is the screenshot of the pedometer app showing the pedometer data matrix (total steps, distance, duration, average speed, average step frequency, calories burned, and type of steps (walking, jogging, running)) active since the app was installed on the phone:
We did some really interesting developmental work for this pedometer application. First, we analyzed the accelerometer data to figure out the common pattern in the signatures of the walking, jogging, and running accelerometer sensor data. After that, we developed the type of step detection algorithm using this analysis. We also figured out how to use threads inside the service to process the accelerometer data in the background, and how to combine the step detector sensor with the accelerometer sensor to achieve battery optimization. We used our experimental data to derive the pedometer data matrix (total steps, distance, duration, average speed, average step frequency, calories burned, and type of steps) using the total number of each type of step detected by our algorithm.