diff --git a/LICENSE b/LICENSE index baab717..17874ca 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Copyright (c) 2017 Kano Computing -Copyright (c) 2015 Fabian Canas -Author of the GRT library: Nick Gillian +Copyright (c) 2015 Fabian Canas (author of the node-native-boilerplate) +Copyright (c) 2012 Nicholas Gillian, Media Lab (MIT author of the GRT library) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/NativeExtension.cc b/NativeExtension.cc deleted file mode 100644 index 72a6b60..0000000 --- a/NativeExtension.cc +++ /dev/null @@ -1,14 +0,0 @@ -#include "NodeTimeSeriesClassificationData.h" -#include "NodeDTW.h" - -using v8::FunctionTemplate; - -// NativeExtension.cc represents the top level of the module. -// C++ constructs that are exposed to javascript are exported here - -NAN_MODULE_INIT(InitAll) { - NodeTimeSeriesClassificationData::Init(target); - NodeDTW::Init(target); -} - -NODE_MODULE(NativeExtension, InitAll) diff --git a/binding.gyp b/binding.gyp index a5645da..f894d2e 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,9 +1,9 @@ { "targets": [ { - "target_name": "NativeExtension", + "target_name": "GRT", "cflags_cc": ["-std=c++11", "-fexceptions", "-frtti"], - "sources": [ "NativeExtension.cc", "src/NodeTimeSeriesClassificationData.cc", "src/NodeDTW.cc" ], + "sources": ["src/GRT.cc", "src/NodeTimeSeriesClassificationData.cc", "src/NodeDTW.cc", "src/NodeHMM.cc", "src/NodeKMeansQuantizer.cc"], "include_dirs" : [ " #include -// Example with node ObjectWrap -// Based on https://nodejs.org/api/addons.html#addons_wrapping_c_objects but using NAN class NodeDTW : public Nan::ObjectWrap { public: static NAN_MODULE_INIT(Init); diff --git a/include/NodeHMM.h b/include/NodeHMM.h new file mode 100644 index 0000000..7d1c449 --- /dev/null +++ b/include/NodeHMM.h @@ -0,0 +1,31 @@ +#ifndef NODE_HMM_H +#define NODE_HMM_H + +#include +#include + +class NodeHMM : public Nan::ObjectWrap { + public: + static NAN_MODULE_INIT(Init); + + private: + explicit NodeHMM(); + ~NodeHMM(); + + static NAN_METHOD(New); + static NAN_METHOD(Train); + static NAN_METHOD(SetHMMType); + static NAN_METHOD(SetNumStates); + static NAN_METHOD(SetNumSymbols); + static NAN_METHOD(SetModelType); + static NAN_METHOD(SetMinChange); + static NAN_METHOD(SetMaxNumEpochs); + static NAN_METHOD(SetNumRandomTrainingIterations); + static NAN_METHOD(Predict); + static NAN_METHOD(GetPredictedClassLabel); + static NAN_METHOD(GetClassLikelihoods); + static Nan::Persistent constructor; + GRT::HMM *hmm; +}; + +#endif diff --git a/include/NodeKMeansQuantizer.h b/include/NodeKMeansQuantizer.h new file mode 100644 index 0000000..f2037e4 --- /dev/null +++ b/include/NodeKMeansQuantizer.h @@ -0,0 +1,26 @@ +#ifndef NODE_K_MEANS_QUANTIZER_H +#define NODE_K_MEANS_QUANTIZER_H + +#include +#include + +class NodeKMeansQuantizer : public Nan::ObjectWrap { + public: + static NAN_MODULE_INIT(Init); + + private: + explicit NodeKMeansQuantizer(); + explicit NodeKMeansQuantizer(const uint numClusters); + ~NodeKMeansQuantizer(); + + static NAN_METHOD(New); + static NAN_METHOD(Train); + static NAN_METHOD(Quantize); + static NAN_METHOD(GetFeatureVector); + static NAN_METHOD(Clear); + + static Nan::Persistent constructor; + GRT::KMeansQuantizer *kmeansquantizer; +}; + +#endif diff --git a/include/NodeTimeSeriesClassificationData.h b/include/NodeTimeSeriesClassificationData.h index f0e3411..a839185 100644 --- a/include/NodeTimeSeriesClassificationData.h +++ b/include/NodeTimeSeriesClassificationData.h @@ -4,8 +4,6 @@ #include #include -// Example with node ObjectWrap -// Based on https://nodejs.org/api/addons.html#addons_wrapping_c_objects but using NAN class NodeTimeSeriesClassificationData : public Nan::ObjectWrap { public: static NAN_MODULE_INIT(Init); @@ -13,6 +11,7 @@ class NodeTimeSeriesClassificationData : public Nan::ObjectWrap { private: explicit NodeTimeSeriesClassificationData(); + explicit NodeTimeSeriesClassificationData(const uint numDimensions); ~NodeTimeSeriesClassificationData(); static NAN_METHOD(New); diff --git a/index.js b/index.js index 6915795..79fd129 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,10 @@ -var NativeExtension = require('bindings')('NativeExtension'); -module.exports = NativeExtension; +var GRT = require('bindings')('GRT'); + +// HMM types to use for setHMMType method of HMM class +GRT.HMM_CONTINUOUS = 1; // default +GRT.HMM_DISCRETE = 0; + +// Model types to use for setModelType method of HMM class +GRT.HMM_LEFTRIGHT = 1; // default +GRT.HMM_ERGODIC = 0; +module.exports = GRT; diff --git a/package-lock.json b/package-lock.json index a2a119d..7988be5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,14 @@ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.0.tgz", "integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw==" }, + "bl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", + "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=", + "requires": { + "readable-stream": "2.3.3" + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -136,6 +144,11 @@ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -218,6 +231,11 @@ "type-detect": "4.0.8" } }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -228,6 +246,11 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, "diff": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", @@ -243,12 +266,25 @@ "jsbn": "0.1.1" } }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "1.4.0" + } + }, "escape-string-regexp": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", "dev": true }, + "expand-template": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-1.1.0.tgz", + "integrity": "sha512-kkjwkMqj0h4w/sb32ERCDxCQkREMCAgS39DscDnSwDsbxnwwM1BTZySdC3Bn1lhY7vL08n9GoO/fVTynjDgRyQ==" + }, "extend": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", @@ -328,6 +364,11 @@ "assert-plus": "1.0.0" } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + }, "glob": { "version": "3.2.11", "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", @@ -408,6 +449,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -563,6 +609,21 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" }, + "node-abi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.2.0.tgz", + "integrity": "sha512-FqVC0WNNL8fQWQK3GYTESfwZXZKDbSIiEEIvufq7HV6Lj0IDDZRVa4CU/KTA0JVlqY9eTDSuPiC8FS9UfGVuzA==", + "requires": { + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + } + } + }, "node-gyp": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz", @@ -606,6 +667,11 @@ } } }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" + }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -682,11 +748,49 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "prebuild-install": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.5.0.tgz", + "integrity": "sha512-3wlyZgmkeeyduOR8Ursu5gKr3yWAYObACa5aJOtt2farRRFV/+zXk/Y3wM6yQRMqmqHh+pHAwyKp5r82K699Rg==", + "requires": { + "detect-libc": "1.0.3", + "expand-template": "1.1.0", + "github-from-package": "0.0.0", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "node-abi": "2.2.0", + "noop-logger": "0.1.1", + "npmlog": "4.1.2", + "os-homedir": "1.0.2", + "pump": "1.0.3", + "rc": "1.2.5", + "simple-get": "1.4.3", + "tar-fs": "1.16.0", + "tunnel-agent": "0.6.0", + "xtend": "4.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, + "pump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", + "requires": { + "end-of-stream": "1.4.1", + "once": "1.4.0" + } + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -697,6 +801,24 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" }, + "rc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.5.tgz", + "integrity": "sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0=", + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, "readable-stream": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", @@ -797,6 +919,16 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "simple-get": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-1.4.3.tgz", + "integrity": "sha1-6XVe2kB+ltpAxeUVjJ6jezO+y+s=", + "requires": { + "once": "1.4.0", + "unzip-response": "1.0.2", + "xtend": "4.0.1" + } + }, "sntp": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", @@ -851,6 +983,11 @@ "ansi-regex": "2.1.1" } }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, "supports-color": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", @@ -867,6 +1004,28 @@ "inherits": "2.0.3" } }, + "tar-fs": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.0.tgz", + "integrity": "sha512-I9rb6v7mjWLtOfCau9eH5L7sLJyU2BnxtEZRQ5Mt+eRKmf1F0ohXmT/Jc3fr52kDvjJ/HV5MH3soQfPL5bQ0Yg==", + "requires": { + "chownr": "1.0.1", + "mkdirp": "0.5.1", + "pump": "1.0.3", + "tar-stream": "1.5.5" + } + }, + "tar-stream": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.5.tgz", + "integrity": "sha512-mQdgLPc/Vjfr3VWqWbfxW8yQNiJCbAZ+Gf6GDu1Cy0bdb33ofyiNGBtAY96jHFhDuivCwgW1H9DgTON+INiXgg==", + "requires": { + "bl": "1.2.1", + "end-of-stream": "1.4.1", + "readable-stream": "2.3.3", + "xtend": "4.0.1" + } + }, "to-iso-string": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz", @@ -900,6 +1059,11 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" }, + "unzip-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz", + "integrity": "sha1-uYTwh3/AqJwsdzzB73tbIytbBv4=" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -940,6 +1104,11 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" } } } diff --git a/src/GRT.cc b/src/GRT.cc new file mode 100644 index 0000000..9649fdd --- /dev/null +++ b/src/GRT.cc @@ -0,0 +1,15 @@ +#include "NodeTimeSeriesClassificationData.h" +#include "NodeDTW.h" +#include "NodeHMM.h" +#include "NodeKMeansQuantizer.h" + +using v8::FunctionTemplate; + +NAN_MODULE_INIT(InitAll) { + NodeTimeSeriesClassificationData::Init(target); + NodeDTW::Init(target); + NodeHMM::Init(target); + NodeKMeansQuantizer::Init(target); +} + +NODE_MODULE(GRT, InitAll) diff --git a/src/NodeDTW.cc b/src/NodeDTW.cc index 112476b..e3b98b1 100644 --- a/src/NodeDTW.cc +++ b/src/NodeDTW.cc @@ -101,4 +101,4 @@ NAN_METHOD(NodeDTW::New) { v8::Local cons = Nan::New(constructor); info.GetReturnValue().Set(cons->NewInstance()); } -} \ No newline at end of file +} diff --git a/src/NodeHMM.cc b/src/NodeHMM.cc new file mode 100644 index 0000000..133aedb --- /dev/null +++ b/src/NodeHMM.cc @@ -0,0 +1,263 @@ +#include "NodeHMM.h" +#include "NodeTimeSeriesClassificationData.h" +#include + +Nan::Persistent NodeHMM::constructor; + +NAN_MODULE_INIT(NodeHMM::Init) { + v8::Local tpl = Nan::New(New); + tpl->SetClassName(Nan::New("HMM").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + Nan::SetPrototypeMethod(tpl, "setHMMType", SetHMMType); + Nan::SetPrototypeMethod(tpl, "setNumStates", SetNumStates); + Nan::SetPrototypeMethod(tpl, "setNumSymbols", SetNumSymbols); + Nan::SetPrototypeMethod(tpl, "setModelType", SetModelType); + Nan::SetPrototypeMethod(tpl, "setMinChange", SetMinChange); + Nan::SetPrototypeMethod(tpl, "setMaxNumEpochs", SetMaxNumEpochs); + Nan::SetPrototypeMethod(tpl, "setNumRandomTrainingIterations", SetNumRandomTrainingIterations); + Nan::SetPrototypeMethod(tpl, "train", Train); + Nan::SetPrototypeMethod(tpl, "predict", Predict); + Nan::SetPrototypeMethod(tpl, "getPredictedClassLabel", GetPredictedClassLabel); + Nan::SetPrototypeMethod(tpl, "getClassLikelihoods", GetClassLikelihoods); + + constructor.Reset(Nan::GetFunction(tpl).ToLocalChecked()); + Nan::Set(target, Nan::New("HMM").ToLocalChecked(), Nan::GetFunction(tpl).ToLocalChecked()); +} + +NodeHMM::NodeHMM() { + hmm = new GRT::HMM(); +} + +NodeHMM::~NodeHMM() { +} + +NAN_METHOD(NodeHMM::Train) { + v8::Isolate* isolate = info.GetIsolate(); + + if (info.Length() < 1) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong number of arguments"))); + return; + } + + NodeHMM* obj = Nan::ObjectWrap::Unwrap(info.This()); + NodeTimeSeriesClassificationData* data = Nan::ObjectWrap::Unwrap(info[0]->ToObject()); + + bool returnValue = obj->hmm->train(*data->getTimeSeriesClassificationData()); + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeHMM::SetHMMType) { + v8::Isolate* isolate = info.GetIsolate(); + + if (info.Length() < 1) { + std::string argsLength = std::to_string(info.Length()); + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, ("Wrong number of arguments: expected 1, got " + argsLength).c_str()))); + return; + } + + if (!info[0]->IsInt32()) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong argument"))); + return; + } + + NodeHMM* obj = Nan::ObjectWrap::Unwrap(info.This()); + + int hmmType = ( int )( info[0]->Int32Value() ); + bool returnValue = obj->hmm->setHMMType(hmmType); + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeHMM::SetNumStates) { + v8::Isolate* isolate = info.GetIsolate(); + + if (info.Length() < 1) { + std::string argsLength = std::to_string(info.Length()); + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, ("Wrong number of arguments: expected 1, got " + argsLength).c_str()))); + return; + } + + if (!info[0]->IsInt32()) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong argument"))); + return; + } + + NodeHMM* obj = Nan::ObjectWrap::Unwrap(info.This()); + + int numStates = ( int )( info[0]->Int32Value() ); + bool returnValue = obj->hmm->setNumStates(numStates); + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeHMM::SetNumSymbols) { + v8::Isolate* isolate = info.GetIsolate(); + + if (info.Length() < 1) { + std::string argsLength = std::to_string(info.Length()); + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, ("Wrong number of arguments: expected 1, got " + argsLength).c_str()))); + return; + } + + if (!info[0]->IsInt32()) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong argument"))); + return; + } + + NodeHMM* obj = Nan::ObjectWrap::Unwrap(info.This()); + + int numSymbols = ( int )( info[0]->Int32Value() ); + bool returnValue = obj->hmm->setNumSymbols(numSymbols); + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeHMM::SetModelType) { + v8::Isolate* isolate = info.GetIsolate(); + + if (info.Length() < 1) { + std::string argsLength = std::to_string(info.Length()); + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, ("Wrong number of arguments: expected 1, got " + argsLength).c_str()))); + return; + } + + if (!info[0]->IsInt32()) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong argument"))); + return; + } + + NodeHMM* obj = Nan::ObjectWrap::Unwrap(info.This()); + + int modelType = ( int )( info[0]->Int32Value() ); + bool returnValue = obj->hmm->setModelType(modelType); + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeHMM::SetMinChange) { + v8::Isolate* isolate = info.GetIsolate(); + + if (info.Length() < 1) { + std::string argsLength = std::to_string(info.Length()); + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, ("Wrong number of arguments: expected 1, got " + argsLength).c_str()))); + return; + } + + if (!info[0]->IsNumber()) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong argument"))); + return; + } + + NodeHMM* obj = Nan::ObjectWrap::Unwrap(info.This()); + + int minChange = ( int )( info[0]->NumberValue() ); + bool returnValue = obj->hmm->setMinChange(minChange); + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeHMM::SetMaxNumEpochs) { + v8::Isolate* isolate = info.GetIsolate(); + + if (info.Length() < 1) { + std::string argsLength = std::to_string(info.Length()); + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, ("Wrong number of arguments: expected 1, got " + argsLength).c_str()))); + return; + } + + if (!info[0]->IsNumber()) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong argument"))); + return; + } + + NodeHMM* obj = Nan::ObjectWrap::Unwrap(info.This()); + + int maxNumEpochs = ( int )( info[0]->NumberValue() ); + bool returnValue = obj->hmm->setMaxNumEpochs(maxNumEpochs); + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeHMM::SetNumRandomTrainingIterations) { + v8::Isolate* isolate = info.GetIsolate(); + + if (info.Length() < 1) { + std::string argsLength = std::to_string(info.Length()); + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, ("Wrong number of arguments: expected 1, got " + argsLength).c_str()))); + return; + } + + if (!info[0]->IsNumber()) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong argument"))); + return; + } + + NodeHMM* obj = Nan::ObjectWrap::Unwrap(info.This()); + + int numRandomTrainingIterations = ( int )( info[0]->NumberValue() ); + bool returnValue = obj->hmm->setNumRandomTrainingIterations(numRandomTrainingIterations); + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeHMM::Predict) { + v8::Isolate* isolate = info.GetIsolate(); + + if (info.Length() < 1) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong number of arguments"))); + return; + } + + NodeHMM* obj = Nan::ObjectWrap::Unwrap(info.This()); + if (!info[0]->IsArray()) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong argument"))); + return; + } + GRT::MatrixFloat sample; + v8::Handle val; + v8::Handle itemVal; + v8::Handle item; + v8::Handle jsArray = v8::Handle::Cast(info[0]); + double number; + GRT::VectorFloat vector; + for (unsigned int i = 0; i < jsArray->Length(); i++) { + val = jsArray->Get(i); + if (!val->IsArray()) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong argument"))); + } + item = v8::Handle::Cast(val); + for (unsigned int j = 0; j < item->Length(); j++) { + itemVal = item->Get(j); + number = itemVal->NumberValue(); + vector.push_back(number); + } + sample.push_back(vector); + vector.clear(); + } + bool returnValue = obj->hmm->predict(sample); + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeHMM::GetPredictedClassLabel) { + NodeHMM* obj = Nan::ObjectWrap::Unwrap(info.This()); + int returnValue = obj->hmm->getPredictedClassLabel(); + std::cout << "Predicted class label: " << returnValue << std::endl; + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeHMM::GetClassLikelihoods) { + v8::Isolate* isolate = info.GetIsolate(); + NodeHMM* obj = Nan::ObjectWrap::Unwrap(info.This()); + GRT::VectorFloat vectorReturn = obj->hmm->getClassLikelihoods(); + v8::Handle returnValue = v8::Array::New(isolate, vectorReturn.getSize()); + for (unsigned int i = 0; i < vectorReturn.getSize(); i++) { + v8::Handle returnNum = v8::Number::New(isolate, vectorReturn[i]); + returnValue->Set(i, returnNum); + } + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeHMM::New) { + if (info.IsConstructCall()) { + NodeHMM *obj = new NodeHMM(); + obj->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); + } else { + v8::Local cons = Nan::New(constructor); + info.GetReturnValue().Set(cons->NewInstance()); + } +} diff --git a/src/NodeKMeansQuantizer.cc b/src/NodeKMeansQuantizer.cc new file mode 100644 index 0000000..e596047 --- /dev/null +++ b/src/NodeKMeansQuantizer.cc @@ -0,0 +1,110 @@ +#include "NodeKMeansQuantizer.h" +#include "NodeTimeSeriesClassificationData.h" +#include + +Nan::Persistent NodeKMeansQuantizer::constructor; + +NAN_MODULE_INIT(NodeKMeansQuantizer::Init) { + v8::Local tpl = Nan::New(New); + tpl->SetClassName(Nan::New("KMeansQuantizer").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + Nan::SetPrototypeMethod(tpl, "train", Train); + Nan::SetPrototypeMethod(tpl, "quantize", Quantize); + Nan::SetPrototypeMethod(tpl, "getFeatureVector", GetFeatureVector); + Nan::SetPrototypeMethod(tpl, "clear", Clear); + + constructor.Reset(Nan::GetFunction(tpl).ToLocalChecked()); + Nan::Set(target, Nan::New("KMeansQuantizer").ToLocalChecked(), Nan::GetFunction(tpl).ToLocalChecked()); +} + +NodeKMeansQuantizer::NodeKMeansQuantizer() { + kmeansquantizer = new GRT::KMeansQuantizer(); +} + +NodeKMeansQuantizer::NodeKMeansQuantizer(const uint numClusters) { + kmeansquantizer = new GRT::KMeansQuantizer(numClusters); +} + +NodeKMeansQuantizer::~NodeKMeansQuantizer() { +} + +NAN_METHOD(NodeKMeansQuantizer::Train) { + v8::Isolate* isolate = info.GetIsolate(); + + if (info.Length() < 1) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong number of arguments"))); + return; + } + + NodeKMeansQuantizer* obj = Nan::ObjectWrap::Unwrap(info.This()); + NodeTimeSeriesClassificationData* data = Nan::ObjectWrap::Unwrap(info[0]->ToObject()); + + bool returnValue = obj->kmeansquantizer->train(*data->getTimeSeriesClassificationData()); + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeKMeansQuantizer::Quantize) { + v8::Isolate* isolate = info.GetIsolate(); + + if (info.Length() < 1) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong number of arguments"))); + return; + } + + if (!info[0]->IsArray()) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong argument"))); + return; + } + + v8::Handle vertice = v8::Handle::Cast(info[0]); + v8::Handle verticeVal; + double number; + GRT::VectorFloat vector; + for (unsigned int i = 0; i < vertice->Length(); i++) { + verticeVal = vertice->Get(i); + number = verticeVal->NumberValue(); + vector.push_back(number); + } + + NodeKMeansQuantizer* obj = Nan::ObjectWrap::Unwrap(info.This()); + unsigned int rawReturn = obj->kmeansquantizer->quantize(vector); + v8::Handle returnValue = v8::Number::New(isolate, rawReturn); + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeKMeansQuantizer::GetFeatureVector) { + v8::Isolate* isolate = info.GetIsolate(); + NodeKMeansQuantizer* obj = Nan::ObjectWrap::Unwrap(info.This()); + GRT::VectorFloat vectorReturn = obj->kmeansquantizer->getFeatureVector(); + v8::Handle returnValue = v8::Array::New(isolate, vectorReturn.getSize()); + for (unsigned int i = 0; i < vectorReturn.getSize(); i++) { + v8::Handle returnNum = v8::Number::New(isolate, vectorReturn[i]); + returnValue->Set(i, returnNum); + } + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeKMeansQuantizer::Clear) { + v8::Isolate* isolate = info.GetIsolate(); + NodeKMeansQuantizer* obj = Nan::ObjectWrap::Unwrap(info.This()); + bool returnValue = obj->kmeansquantizer->clear(); + info.GetReturnValue().Set(returnValue); +} + +NAN_METHOD(NodeKMeansQuantizer::New) { + if (info.IsConstructCall()) { + NodeKMeansQuantizer *obj = NULL; + if (info.Length() == 1) { + const uint numClusters = ( uint )( info[0]->Uint32Value() ); + obj = new NodeKMeansQuantizer(numClusters); + } else { + obj = new NodeKMeansQuantizer(); + } + obj->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); + } else { + v8::Local cons = Nan::New(constructor); + info.GetReturnValue().Set(cons->NewInstance()); + } +} diff --git a/src/NodeTimeSeriesClassificationData.cc b/src/NodeTimeSeriesClassificationData.cc index 45222df..797771b 100644 --- a/src/NodeTimeSeriesClassificationData.cc +++ b/src/NodeTimeSeriesClassificationData.cc @@ -26,6 +26,10 @@ NodeTimeSeriesClassificationData::NodeTimeSeriesClassificationData() { tscd = new GRT::TimeSeriesClassificationData(); } +NodeTimeSeriesClassificationData::NodeTimeSeriesClassificationData(const uint numDimensions) { + tscd = new GRT::TimeSeriesClassificationData(numDimensions); +} + NodeTimeSeriesClassificationData::~NodeTimeSeriesClassificationData() { } @@ -57,14 +61,12 @@ NAN_METHOD(NodeTimeSeriesClassificationData::SetNumDimensions) { NAN_METHOD(NodeTimeSeriesClassificationData::GetNumDimensions) { NodeTimeSeriesClassificationData* obj = Nan::ObjectWrap::Unwrap(info.This()); - int returnValue = obj->tscd->getNumDimensions(); - std::cout << "Dimensions: " << returnValue << std::endl; info.GetReturnValue().Set(returnValue); } NAN_METHOD(NodeTimeSeriesClassificationData::SetDatasetName) { - v8::Isolate* isolate = info.GetIsolate(); + v8::Isolate* isolate = info.GetIsolate(); if (info.Length() < 1) { isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong number of arguments"))); @@ -85,7 +87,7 @@ NAN_METHOD(NodeTimeSeriesClassificationData::SetDatasetName) { } NAN_METHOD(NodeTimeSeriesClassificationData::SetInfoText) { - v8::Isolate* isolate = info.GetIsolate(); + v8::Isolate* isolate = info.GetIsolate(); if (info.Length() < 1) { isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(isolate, "Wrong number of arguments"))); @@ -213,12 +215,18 @@ NAN_METHOD(NodeTimeSeriesClassificationData::Clear) { } NAN_METHOD(NodeTimeSeriesClassificationData::New) { - if (info.IsConstructCall()) { - NodeTimeSeriesClassificationData *obj = new NodeTimeSeriesClassificationData(); - obj->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); - } else { - v8::Local cons = Nan::New(constructor); - info.GetReturnValue().Set(cons->NewInstance()); - } -} \ No newline at end of file + if (info.IsConstructCall()) { + NodeTimeSeriesClassificationData *obj = NULL; + if (info.Length() == 1 && info[0]->IsNumber()) { + const uint numDimensions = ( uint )( info[0]->Uint32Value() ); + obj = new NodeTimeSeriesClassificationData(numDimensions); + } else { + obj = new NodeTimeSeriesClassificationData(); + } + obj->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); + } else { + v8::Local cons = Nan::New(constructor); + info.GetReturnValue().Set(cons->NewInstance()); + } +} diff --git a/test/index.js b/test/index.js index b69b1fa..9e6c912 100644 --- a/test/index.js +++ b/test/index.js @@ -1,4 +1,4 @@ -const nativeExtension = require('../'); +const grt = require('../'); const assert = require('assert'); const { expect } = require('chai'); const fs = require('fs'); @@ -6,7 +6,7 @@ const fs = require('fs'); describe('TimeSeriesClassificationData', function() { let trainingData; before(() => { - trainingData = new nativeExtension.TimeSeriesClassificationData(); + trainingData = new grt.TimeSeriesClassificationData(); }); after(() => { @@ -132,8 +132,8 @@ describe('DTW', function() { let trainingData; let timeWarpingAlgorithm; before(() => { - timeWarpingAlgorithm = new nativeExtension.DTW(); - trainingData = new nativeExtension.TimeSeriesClassificationData(); + timeWarpingAlgorithm = new grt.DTW(); + trainingData = new grt.TimeSeriesClassificationData(); trainingData.setNumDimensions(2); trainingData.addSample(1, [[1, 2]]); }); @@ -178,3 +178,186 @@ describe('DTW', function() { assert.equal(timeWarpingAlgorithm.getMaximumLikelihood(), 1); }); }); + +describe('HMM', function() { + let trainingData; + let quantisedData; + let hmmAlgorithm; + let kMeansQuantizer; + before(() => { + hmmAlgorithm = new grt.HMM(); + trainingData = new grt.TimeSeriesClassificationData(); + trainingData.setNumDimensions(2); + quantisedData = new grt.TimeSeriesClassificationData(1); + quantisedData.setNumDimensions(1); + kMeansQuantizer = new grt.KMeansQuantizer(2); + trainingData.addSample(1, [[2, 4]]); + trainingData.addSample(1, [[4, 8]]); + trainingData.addSample(2, [[16, 8]]); + trainingData.addSample(2, [[8, 4]]); + }); + + // setHMMType + + it('setHMMType should throw an error with wrong number of arguments', function() { + expect(() => hmmAlgorithm.setHMMType()).to.throw('Wrong number of arguments'); + }); + + it('setHMMType should return true when called with either grt.HMM_DISCRETE (0) or grt.HMM_DISCRETE (1)', function() { + assert.equal(hmmAlgorithm.setHMMType(grt.HMM_DISCRETE), true); + }); + + // setNumStates + + it('setNumStates should throw an error with wrong number of arguments', function() { + expect(() => hmmAlgorithm.setNumStates()).to.throw('Wrong number of arguments'); + }); + + it('setNumStates should return true when called with one integer argument', function() { + assert.equal(hmmAlgorithm.setNumStates(4), true); + }); + + // setNumSymbols + + it('setNumSymbols should throw an error with wrong number of arguments', function() { + expect(() => hmmAlgorithm.setNumSymbols()).to.throw('Wrong number of arguments'); + }); + + it('setNumSymbols should return true when called with one integer argument', function() { + assert.equal(hmmAlgorithm.setNumSymbols(2), true); + }); + + // setModelType + + it('setModelType should throw an error with wrong number of arguments', function() { + expect(() => hmmAlgorithm.setModelType()).to.throw('Wrong number of arguments'); + }); + + it('setModelType should return true when called with either grt.HMM_ERGODIC (0) or grt.HMM_LEFTRIGHT (1)', function() { + assert.equal(hmmAlgorithm.setModelType(grt.HMM_LEFTRIGHT), true); + }); + + // setMinChange + + it('setMinChange should throw an error with wrong number of arguments', function() { + expect(() => hmmAlgorithm.setMinChange()).to.throw('Wrong number of arguments'); + }); + + it('setMinChange should return true when called with one float argument', function() { + assert.equal(hmmAlgorithm.setMinChange(0.00001), true); + }); + + // setMaxNumEpochs + + it('setMaxNumEpochs should throw an error with wrong number of arguments', function() { + expect(() => hmmAlgorithm.setMaxNumEpochs()).to.throw('Wrong number of arguments'); + }); + + it('setMaxNumEpochs should return true when called with one integer argument', function() { + assert.equal(hmmAlgorithm.setMaxNumEpochs(2), true); + }); + + // SetNumRandomTrainingIterations + + it('setNumRandomTrainingIterations should throw an error with wrong number of arguments', function() { + expect(() => hmmAlgorithm.setNumRandomTrainingIterations()).to.throw('Wrong number of arguments'); + }); + + it('setNumRandomTrainingIterations should return true when called with one integer argument', function() { + assert.equal(hmmAlgorithm.setNumRandomTrainingIterations(20), true); + }); + + // train + + it('train should throw an error with wrong number of arguments', function() { + expect(() => hmmAlgorithm.train()).to.throw('Wrong number of arguments'); + }); + + it('train should return true when called with a populated training data argument', function() { + kMeansQuantizer.train(trainingData); + let vertices = [[2, 4], [4, 8], [8, 16], [16, 8], [8, 4], [10, 5]]; + quantisedData.addSample(1, vertices.map((vertice) => { + kMeansQuantizer.quantize(vertice); + return kMeansQuantizer.getFeatureVector(vertice); + })); + assert.equal(hmmAlgorithm.train(quantisedData), true); + }); + + // predict + + it('predict should throw an error with wrong number of arguments', function() { + expect(() => hmmAlgorithm.predict()).to.throw('Wrong number of arguments'); + }); + + it('predict should throw an error if argument is not an array', function() { + expect(() => hmmAlgorithm.predict('foo', [])).to.throw('Wrong argument'); + }); + + it('predict should throw an error if argument is not a two-dimensional array', function() { + expect(() => hmmAlgorithm.predict([1])).to.throw('Wrong argument'); + }); + + it('predict should return true when calling the method with one array argument with the correct dimension', function() { + assert.equal(hmmAlgorithm.predict([[1]]), true); + }); + + // getPredictedClassLabel + + it('getPredictedClassLabel should return the correct label', function() { + assert.equal(hmmAlgorithm.getPredictedClassLabel(), 1); + }); + + // getClassLikelihoods + it('getClassLikelihoods should return 1 if sample and trainingData is a perfect match', function() { + assert.equal(hmmAlgorithm.getClassLikelihoods(), 1); + }); +}); + +describe('KMeansQuantizer', function() { + let trainingData; + let quantisedData; + let kMeansQuantizer; + before(() => { + trainingData = new grt.TimeSeriesClassificationData(); + trainingData.setNumDimensions(2); + quantisedData = new grt.TimeSeriesClassificationData(1); + quantisedData.setNumDimensions(1); + kMeansQuantizer = new grt.KMeansQuantizer(2); + trainingData.addSample(1, [[2, 4]]); + trainingData.addSample(1, [[4, 8]]); + trainingData.addSample(2, [[16, 8]]); + trainingData.addSample(2, [[8, 4]]); + }); + + // train + + it('train should throw an error with wrong number of arguments', function() { + expect(() => kMeansQuantizer.train()).to.throw('Wrong number of arguments'); + }); + + it('train should return true when calling the method with a populated training data argument', function() { + assert.equal(kMeansQuantizer.train(trainingData), true); + }); + + // quantize + + it('quantize should throw an error with wrong number of arguments', function() { + expect(() => kMeansQuantizer.quantize()).to.throw('Wrong number of arguments'); + }); + + it('quantize should return the quantized value when called with an array argument', function() { + assert.equal(kMeansQuantizer.quantize([1,2]), 1); + }); + + // getFeatureVector + + it('train should return true when calling the method with a populated training data argument', function() { + let vertices = [[2, 4], [4, 8], [8, 16], [16, 8], [8, 4], [10, 5]]; + quantisedData.addSample(1, vertices.map((vertice) => { + kMeansQuantizer.quantize(vertice); + return kMeansQuantizer.getFeatureVector(vertice); + })); + assert.equal(true, true); + }); + +});