438 changes: 438 additions & 0 deletions src/plugins/dataobject/activitylevel/activitylevel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,438 @@
/***************************************************************************
* *
* copyright : (C) 2013 The Kst Team *
* kst@kde.org *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/


#include "activitylevel.h"
#include "objectstore.h"
#include "ui_activitylevel.h"

static const QString& VECTOR_IN = "Vector In";
static const QString& SCALAR_IN_SAMPLING = "Sampling";
static const QString& SCALAR_IN_WINDOWWIDTH = "Window Width";
static const QString& SCALAR_IN_THRESHOLD = "Threshold";
static const QString& VECTOR_OUT_ACTIVITY = "Activity";
static const QString& VECTOR_OUT_REVERSALS = "Nb Reversals";
static const QString& VECTOR_OUT_STDDEV = "Sliding Standard Deviation";
static const QString& VECTOR_OUT_DENOISED = "Denoised Input";

class ConfigWidgetActivityLevelPlugin : public Kst::DataObjectConfigWidget, public Ui_ActivityLevelConfig {
public:
ConfigWidgetActivityLevelPlugin(QSettings* cfg) : DataObjectConfigWidget(cfg), Ui_ActivityLevelConfig() {
setupUi(this);
}

~ConfigWidgetActivityLevelPlugin() {}

void setObjectStore(Kst::ObjectStore* store) {
_store = store;
_vector->setObjectStore(store);
_windowWidth->setObjectStore(store);
_windowWidth->setDefaultValue(3);
_samplingTime->setObjectStore(store);
_samplingTime->setDefaultValue(0.025);
_noiseThreshold->setObjectStore(store);
_noiseThreshold->setDefaultValue(0.2);
}

void setupSlots(QWidget* dialog) {
if (dialog) {
connect(_vector, SIGNAL(selectionChanged(QString)), dialog, SIGNAL(modified()));
connect(_samplingTime, SIGNAL(selectionChanged(QString)), dialog, SIGNAL(modified()));
connect(_windowWidth, SIGNAL(selectionChanged(QString)), dialog, SIGNAL(modified()));
connect(_noiseThreshold, SIGNAL(selectionChanged(QString)), dialog, SIGNAL(modified()));
}
}

Kst::VectorPtr selectedVector() { return _vector->selectedVector(); };
void setSelectedVector(Kst::VectorPtr vector) { return _vector->setSelectedVector(vector); };

Kst::ScalarPtr selectedSamplingTime() { return _samplingTime->selectedScalar(); };
void setSelectedSamplingTime(Kst::ScalarPtr scalar) { return _samplingTime->setSelectedScalar(scalar); };

Kst::ScalarPtr selectedWindowWidth() { return _windowWidth->selectedScalar(); };
void setSelectedWindowWidth(Kst::ScalarPtr scalar) { return _windowWidth->setSelectedScalar(scalar); };

Kst::ScalarPtr selectedNoiseThreshold() { return _noiseThreshold->selectedScalar(); };
void setSelectedNoiseThreshold(Kst::ScalarPtr scalar) { return _noiseThreshold->setSelectedScalar(scalar); };

virtual void setupFromObject(Kst::Object* dataObject) {
if (ActivityLevelSource* source = static_cast<ActivityLevelSource*>(dataObject)) {
setSelectedVector(source->vector());
setSelectedSamplingTime(source->samplingTime());
setSelectedWindowWidth(source->windowWidth());
setSelectedNoiseThreshold(source->noiseThreshold());
}
}

virtual bool configurePropertiesFromXml(Kst::ObjectStore *store, QXmlStreamAttributes& attrs) {
Q_UNUSED(store);
Q_UNUSED(attrs);

bool validTag = true;

// QStringRef av;
// av = attrs.value("value");
// if (!av.isNull()) {
// _configValue = QVariant(av.toString()).toBool();
// }

return validTag;
}

public slots:
virtual void save() {
if (_cfg) {
_cfg->beginGroup("Activity Level DataObject Plugin");
_cfg->setValue("Input Vector", _vector->selectedVector()->Name());
_cfg->setValue("Input Scalar Sampling Time", _samplingTime->selectedScalar()->Name());
_cfg->setValue("Input Scalar Window Width", _windowWidth->selectedScalar()->Name());
_cfg->setValue("Input Scalar Noise Threshold", _noiseThreshold->selectedScalar()->Name());
_cfg->endGroup();
}
}

virtual void load() {
if (_cfg && _store) {
_cfg->beginGroup("Activity Level DataObject Plugin");
QString vectorName = _cfg->value("Input Vector").toString();
Kst::Object* object = _store->retrieveObject(vectorName);
Kst::Vector* vector = static_cast<Kst::Vector*>(object);
if (vector) {
setSelectedVector(vector);
}
// Sampling Time
QString scalarName = _cfg->value("Input Scalar Sampling Time").toString();
object = _store->retrieveObject(scalarName);
Kst::Scalar* scalar = static_cast<Kst::Scalar*>(object);
if (scalar) {
setSelectedSamplingTime(scalar);
}
// Window Width
scalarName = _cfg->value("Input Scalar Window Width").toString();
object = _store->retrieveObject(scalarName);
scalar = static_cast<Kst::Scalar*>(object);
if (scalar) {
setSelectedWindowWidth(scalar);
}
// Noise Threshold
scalarName = _cfg->value("Input Scalar Noise Threshold").toString();
object = _store->retrieveObject(scalarName);
scalar = static_cast<Kst::Scalar*>(object);
if (scalar) {
setSelectedNoiseThreshold(scalar);
}
_cfg->endGroup();
}
}

private:
Kst::ObjectStore *_store;

};


ActivityLevelSource::ActivityLevelSource(Kst::ObjectStore *store)
: Kst::BasicPlugin(store) {
}


ActivityLevelSource::~ActivityLevelSource() {
}


QString ActivityLevelSource::_automaticDescriptiveName() const {
if (vector()) {
return QString(vector()->descriptiveName() + " Activity Level");
} else {
return QString("Actvity Level");
}
}


QString ActivityLevelSource::descriptionTip() const {
QString tip;
tip = i18n("Activity Level: %1\n Sampling Time: %2 (ms)\n Window width: %3 (s)\n Noise Threshold: %4 \n").
arg(Name()).arg(samplingTime()->value()).arg(windowWidth()->value()).arg(noiseThreshold()->value());
tip += i18n("\nInput: %1").arg(vector()->descriptionTip());
return tip;
}


void ActivityLevelSource::change(Kst::DataObjectConfigWidget *configWidget) {
if (ConfigWidgetActivityLevelPlugin* config = static_cast<ConfigWidgetActivityLevelPlugin*>(configWidget)) {
setInputVector(VECTOR_IN, config->selectedVector());
setInputScalar(SCALAR_IN_SAMPLING, config->selectedSamplingTime());
setInputScalar(SCALAR_IN_WINDOWWIDTH, config->selectedWindowWidth());
setInputScalar(SCALAR_IN_THRESHOLD, config->selectedNoiseThreshold());
}
}


void ActivityLevelSource::setupOutputs() {
setOutputVector(VECTOR_OUT_ACTIVITY, "");
setOutputVector(VECTOR_OUT_REVERSALS, "");
setOutputVector(VECTOR_OUT_STDDEV, "");
setOutputVector(VECTOR_OUT_DENOISED, "");
}


bool ActivityLevelSource::algorithm() {
Kst::VectorPtr inputVector = _inputVectors[VECTOR_IN];
Kst::ScalarPtr samplingTime = _inputScalars[SCALAR_IN_SAMPLING];
Kst::ScalarPtr windowWidth = _inputScalars[SCALAR_IN_WINDOWWIDTH];
Kst::ScalarPtr noiseThreshold = _inputScalars[SCALAR_IN_THRESHOLD];
Kst::VectorPtr outputVectorActivity = _outputVectors[VECTOR_OUT_ACTIVITY];
Kst::VectorPtr outputVectorReversals = _outputVectors[VECTOR_OUT_REVERSALS];
Kst::VectorPtr outputVectorStdDeviation = _outputVectors[VECTOR_OUT_STDDEV];
Kst::VectorPtr outputVectorDenoised = _outputVectors[VECTOR_OUT_DENOISED];

int i, length;
int iSamplesForWindow = (int) (windowWidth->value() / samplingTime->value());
double dStandardDeviation = 0.0, dTotal = 0.0, dVariance = 0.0, dSquaredTotal = 0.0;
int iTrendPrevious = 0, iTrend = 0;
double dNbReversals = 0.0; // Compute as a double since we output a vector of doubles anyway
length = inputVector->length();
/* The metric is computed over a couple of seconds, let us compute the corresponding number of samples */
if (iSamplesForWindow > length) {
_errorString = "Error: Input vector too short.";
return false;
}
if (iSamplesForWindow < 2) {
_errorString = "Error: the window must be broader.";
return false;
}

/* Array allocations */
outputVectorActivity->resize(length, true);
outputVectorReversals->resize(length, true);
outputVectorStdDeviation->resize(length, true);
outputVectorDenoised->resize(length, true);

// /* Requantize to avoid noise creating many unwanted sign changes */
// if (noiseThreshold->value() > 0.0) {
// for (i = 0; i < length; ++i) {
// outputVectorDenoised->value()[i] = (double) rint( inputVector->value()[i] / noiseThreshold->value() ) * noiseThreshold->value();
// }
// }
/* Recompute input data, taking direction changes only when they exceed a given threshold */
if (noiseThreshold->value() > 0.0) {
iTrendPrevious = (inputVector->value()[1]-inputVector->value()[0] > 0) ? 1 : -1;
outputVectorDenoised->value()[0] = inputVector->value()[0];
bool bFreeze = false;
for (i = 1; i < length; ++i) {
// Update current trend
if (inputVector->value()[i] == inputVector->value()[i-1]) {
iTrend = 0;
} else {
iTrend = ( (inputVector->value()[i]-inputVector->value()[i-1]) > 0) ? 1 : -1;
}
// Check what to do with the value
if ( iTrendPrevious * iTrend >= 0 && !bFreeze) {
outputVectorDenoised->value()[i] = inputVector->value()[i];
iTrendPrevious = iTrend;
} else { // Change of direction: check whether the delta is significant, otherwise freeze the value
if ( qAbs(inputVector->value()[i] - outputVectorDenoised->value()[i-1]) >= noiseThreshold->value() ) { // Delta is significant: keep value
outputVectorDenoised->value()[i] = inputVector->value()[i];
bFreeze = false;
iTrendPrevious = iTrend;
} else {
outputVectorDenoised->value()[i] = outputVectorDenoised->value()[i-1];
bFreeze = true;
}
}
}
}

/* Compute initial values for first windowWidth seconds */
dTotal = outputVectorDenoised->value()[0] + outputVectorDenoised->value()[1];
dSquaredTotal += outputVectorDenoised->value()[0] * outputVectorDenoised->value()[0] + outputVectorDenoised->value()[1] * outputVectorDenoised->value()[1];
outputVectorReversals->value()[1] = outputVectorReversals->value()[0] = 0.0;
outputVectorStdDeviation->value()[1] = outputVectorStdDeviation->value()[0] = 0.0;
outputVectorActivity->value()[1] = outputVectorActivity->value()[0] = 0.0;
for (i = 2; i < iSamplesForWindow; ++i) {
/* Update previous sign if needed */
if (outputVectorDenoised->value()[i-1] != outputVectorDenoised->value()[i-2]) {
iTrendPrevious = ( (outputVectorDenoised->value()[i-1] - outputVectorDenoised->value()[i-2]) > 0 ) ? 1 : -1;
}
/* Compute current sign */
if (outputVectorDenoised->value()[i] != outputVectorDenoised->value()[i-1]) {
iTrend = ( (outputVectorDenoised->value()[i] - outputVectorDenoised->value()[i-1]) > 0 ) ? 1 : -1;
} else {
iTrend = 0;
}
/* Check for reversal */
if ( iTrend * iTrendPrevious < 0 ) {
dNbReversals += 1.0;
}
dTotal += outputVectorDenoised->value()[i];
dSquaredTotal += outputVectorDenoised->value()[i] * outputVectorDenoised->value()[i];
/* Store zeros as long as we do not have enough values */
outputVectorReversals->value()[i] = 0.0;
outputVectorStdDeviation->value()[i] = 0.0;
outputVectorActivity->value()[i] = 0.0;
}
dVariance = 1.0 / ( (double)iSamplesForWindow - 1.0 );
dVariance *= dSquaredTotal - ( dTotal * dTotal / (double)iSamplesForWindow );
if( dVariance > 0.0 ) { // The computation method can have numerical artefacts leading to negative values here!
dStandardDeviation = sqrt( dVariance );
} else {
dVariance = 0.0;
dStandardDeviation = 0.0;
}
/* Now, we can actually store the first useful value (exactly the right number of samples processed) */
outputVectorReversals->value()[i] = dNbReversals;
outputVectorStdDeviation->value()[i] = dStandardDeviation;
outputVectorActivity->value()[i] = dNbReversals * dStandardDeviation;

/* Finally, update continuously for each new value for the rest of values */
double outgoingValue, outgoingValuePrev, outgoingValueNext, incomingValue, incomingValuePrev, incomingValueNext;
for (i = iSamplesForWindow; i < length; ++i) {
dTotal += outputVectorDenoised->value()[i] - outputVectorDenoised->value()[i-iSamplesForWindow];
dSquaredTotal += outputVectorDenoised->value()[i] * outputVectorDenoised->value()[i] - outputVectorDenoised->value()[i-iSamplesForWindow] * outputVectorDenoised->value()[i-iSamplesForWindow];
dVariance = 1.0 / ( (double)iSamplesForWindow - 1.0 );
dVariance *= dSquaredTotal - ( dTotal * dTotal / (double)iSamplesForWindow );
if( dVariance > 0.0 ) {
dStandardDeviation = sqrt( dVariance );
} else {
dVariance = 0.0;
dStandardDeviation = 0.0;
}
/* Update the number of reversals, by removing 1 if the outgoing data point was a reversal and adding 1 if the incoming point is one */
outgoingValue = outputVectorDenoised->value()[i-iSamplesForWindow];
outgoingValuePrev = outputVectorDenoised->value()[i-iSamplesForWindow-1];
outgoingValueNext = outputVectorDenoised->value()[i-iSamplesForWindow+1];
incomingValue = outputVectorDenoised->value()[i];
incomingValuePrev = outputVectorDenoised->value()[i-1];
if (i == length-1) { // Protect against accessing past the boundary of the vector
incomingValueNext = outputVectorDenoised->value()[i];
} else {
incomingValueNext = outputVectorDenoised->value()[i+1];
}
if ( (outgoingValue-outgoingValuePrev)*(outgoingValueNext-outgoingValue) < 0) {
dNbReversals = qMax(dNbReversals - 1.0, 0.0); // Avoid getting negative values, which can happen
}
if ( (incomingValue-incomingValuePrev)*(incomingValueNext-incomingValue) < 0) {
dNbReversals += 1.0;
}

/* Store values */
outputVectorReversals->value()[i] = dNbReversals;
outputVectorStdDeviation->value()[i] = dStandardDeviation;
outputVectorActivity->value()[i] = dNbReversals * dStandardDeviation;
}
return true;
}


Kst::VectorPtr ActivityLevelSource::vector() const {
return _inputVectors[VECTOR_IN];
}


Kst::ScalarPtr ActivityLevelSource::samplingTime() const {
return _inputScalars[SCALAR_IN_SAMPLING];
}


Kst::ScalarPtr ActivityLevelSource::windowWidth() const {
return _inputScalars[SCALAR_IN_WINDOWWIDTH];
}


Kst::ScalarPtr ActivityLevelSource::noiseThreshold() const {
return _inputScalars[SCALAR_IN_THRESHOLD];
}


QStringList ActivityLevelSource::inputVectorList() const {
return QStringList( VECTOR_IN );
}


QStringList ActivityLevelSource::inputScalarList() const {
QStringList scalars( SCALAR_IN_SAMPLING );
scalars += SCALAR_IN_WINDOWWIDTH;
scalars += SCALAR_IN_THRESHOLD;
return scalars;
}


QStringList ActivityLevelSource::inputStringList() const {
return QStringList( /*STRING_IN*/ );
}


QStringList ActivityLevelSource::outputVectorList() const {
QStringList vectors( VECTOR_OUT_ACTIVITY );
vectors += VECTOR_OUT_REVERSALS;
vectors += VECTOR_OUT_STDDEV;
vectors += VECTOR_OUT_DENOISED;
return vectors;
}


QStringList ActivityLevelSource::outputScalarList() const {
return QStringList( /*SCALAR_OUT*/ );
}


QStringList ActivityLevelSource::outputStringList() const {
return QStringList( /*STRING_OUT*/ );
}


void ActivityLevelSource::saveProperties(QXmlStreamWriter &s) {
Q_UNUSED(s);
// s.writeAttribute("value", _configValue);
}


QString ActivityLevelPlugin::pluginName() const { return "Activity Level"; }
QString ActivityLevelPlugin::pluginDescription() const { return "Computes the activity level of a signal as the product of standard deviation and number of reversals over a sliding window."; }


Kst::DataObject *ActivityLevelPlugin::create(Kst::ObjectStore *store, Kst::DataObjectConfigWidget *configWidget, bool setupInputsOutputs) const {

if (ConfigWidgetActivityLevelPlugin* config = static_cast<ConfigWidgetActivityLevelPlugin*>(configWidget)) {

ActivityLevelSource* object = store->createObject<ActivityLevelSource>();

if (setupInputsOutputs) {
object->setInputScalar(SCALAR_IN_SAMPLING, config->selectedSamplingTime());
object->setInputScalar(SCALAR_IN_WINDOWWIDTH, config->selectedWindowWidth());
object->setInputScalar(SCALAR_IN_THRESHOLD, config->selectedNoiseThreshold());
object->setupOutputs();
object->setInputVector(VECTOR_IN, config->selectedVector());
}

object->setPluginName(pluginName());

object->writeLock();
object->registerChange();
object->unlock();

return object;
}
return 0;
}


Kst::DataObjectConfigWidget *ActivityLevelPlugin::configWidget(QSettings *settingsObject) const {
ConfigWidgetActivityLevelPlugin *widget = new ConfigWidgetActivityLevelPlugin(settingsObject);
return widget;
}

#ifndef QT5
Q_EXPORT_PLUGIN2(kstplugin_ActivityLevelPlugin, ActivityLevelPlugin)
#endif

// vim: ts=2 sw=2 et
77 changes: 77 additions & 0 deletions src/plugins/dataobject/activitylevel/activitylevel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/***************************************************************************
* *
* copyright : (C) 2013 The Kst Team *
* kst@kde.org *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#ifndef ACTIVITYLEVELPLUGIN_H
#define ACTIVITYLEVELPLUGIN_H

#include <QFile>

#include <basicplugin.h>
#include <dataobjectplugin.h>

class ActivityLevelSource : public Kst::BasicPlugin {
Q_OBJECT

public:
virtual QString _automaticDescriptiveName() const;
virtual QString descriptionTip() const;

Kst::VectorPtr vector() const;
Kst::ScalarPtr samplingTime() const;
Kst::ScalarPtr windowWidth() const;
Kst::ScalarPtr noiseThreshold() const;

virtual void change(Kst::DataObjectConfigWidget *configWidget);

void setupOutputs();
virtual bool algorithm();

virtual QStringList inputVectorList() const;
virtual QStringList inputScalarList() const;
virtual QStringList inputStringList() const;
virtual QStringList outputVectorList() const;
virtual QStringList outputScalarList() const;
virtual QStringList outputStringList() const;

virtual void saveProperties(QXmlStreamWriter &s);

protected:
ActivityLevelSource(Kst::ObjectStore *store);
~ActivityLevelSource();

friend class Kst::ObjectStore;


};


class ActivityLevelPlugin : public QObject, public Kst::DataObjectPluginInterface {
Q_OBJECT
Q_INTERFACES(Kst::DataObjectPluginInterface)
Q_PLUGIN_METADATA(IID "com.kst.DataObjectPluginInterface/2.0")
public:
virtual ~ActivityLevelPlugin() {}

virtual QString pluginName() const;
virtual QString pluginDescription() const;

virtual DataObjectPluginInterface::PluginTypeID pluginType() const { return Generic; }

virtual bool hasConfigWidget() const { return true; }

virtual Kst::DataObject *create(Kst::ObjectStore *store, Kst::DataObjectConfigWidget *configWidget, bool setupInputsOutputs = true) const;

virtual Kst::DataObjectConfigWidget *configWidget(QSettings *settingsObject) const;
};

#endif
// vim: ts=2 sw=2 et
136 changes: 136 additions & 0 deletions src/plugins/dataobject/activitylevel/activitylevel.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ActivityLevelConfig</class>
<widget class="QWidget" name="ActivityLevelConfig">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>419</width>
<height>130</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>0</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Input Vector</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="Kst::VectorSelector" name="_vector" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Sampling Time (s)</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="Kst::ScalarSelector" name="_samplingTime" native="true"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_5">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Window Width (s)</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="Kst::ScalarSelector" name="_windowWidth" native="true"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_6">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Noise Threshold</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="Kst::ScalarSelector" name="_noiseThreshold" native="true"/>
</item>
</layout>
</item>
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
<customwidget>
<class>Kst::VectorSelector</class>
<extends>QWidget</extends>
<header>vectorselector.h</header>
</customwidget>
<customwidget>
<class>Kst::ScalarSelector</class>
<extends>QWidget</extends>
<header>scalarselector.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Service
ServiceTypes=Kst Data Object
X-KDE-ModuleType=Plugin
X-KDE-Library=activitylevel
X-Kst-Plugin-Author=Nicolas Brisset
Name=Activity Level Data Object Plugin
Comment=Computes an activity level (workload) based on signal reversals and amplitude of changes.