Compare commits
45 Commits
bxinterval
...
replace-py
Author | SHA1 | Date | |
---|---|---|---|
10d58f6a47 | |||
e2464efc12 | |||
1beae5024e | |||
c7c65b6542 | |||
f41ff0a6e8 | |||
389c1d189f | |||
487298986e | |||
d4cd045c48 | |||
3816645313 | |||
83b937c720 | |||
b3e6e8976f | |||
c890ea93cb | |||
84c68c6913 | |||
6f1e6fe232 | |||
b0d76312d1 | |||
19c846c71c | |||
f355c73209 | |||
173014ba19 | |||
24d4752bc3 | |||
a85b273e2e | |||
7f73b4b304 | |||
f3eb6d1b79 | |||
9082cc9f44 | |||
bf64a40472 | |||
32dbeebc09 | |||
66ddc79b15 | |||
7a8bd0bf41 | |||
ee552de740 | |||
6d1fb61573 | |||
f094529e66 | |||
5fecec2a4c | |||
85bb46f45c | |||
17c329fd6d | |||
437e1b425a | |||
c0f87db3c1 | |||
a9c5c19e30 | |||
f39567b2bc | |||
99ec0f4946 | |||
f5c60f68dc | |||
bdef0986d6 | |||
c396c4dac8 | |||
0b443f510b | |||
66fa6f3824 | |||
875fbe969f | |||
e35e85886e |
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
db/
|
||||||
|
tests/*testdb/
|
||||||
|
.coverage
|
||||||
|
*.pyc
|
@@ -1,2 +1,4 @@
|
|||||||
sudo apt-get install python-nose python-coverage
|
sudo apt-get install python-nose python-coverage
|
||||||
sudo apt-get install python-tables cython python-cherrypy3
|
sudo apt-get install python-tables python-cherrypy3
|
||||||
|
sudo apt-get install cython # 0.17.1-1 or newer
|
||||||
|
|
||||||
|
4
TODO
4
TODO
@@ -1,5 +1 @@
|
|||||||
- Merge adjacent intervals on insert (maybe with client help?)
|
- Merge adjacent intervals on insert (maybe with client help?)
|
||||||
|
|
||||||
- Better testing:
|
|
||||||
- see about getting coverage on layout.pyx
|
|
||||||
- layout.pyx performance tests, before and after generalization
|
|
||||||
|
41
design.md
41
design.md
@@ -103,13 +103,13 @@ Speed
|
|||||||
|
|
||||||
- First approach was quadratic. Adding four hours of data:
|
- First approach was quadratic. Adding four hours of data:
|
||||||
|
|
||||||
$ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-110000 /bpnilm/1/raw
|
$ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-110000 /bpnilm/1/raw
|
||||||
real 24m31.093s
|
real 24m31.093s
|
||||||
$ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-120001 /bpnilm/1/raw
|
$ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-120001 /bpnilm/1/raw
|
||||||
real 43m44.528s
|
real 43m44.528s
|
||||||
$ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-130002 /bpnilm/1/raw
|
$ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-130002 /bpnilm/1/raw
|
||||||
real 93m29.713s
|
real 93m29.713s
|
||||||
$ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-140003 /bpnilm/1/raw
|
$ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-140003 /bpnilm/1/raw
|
||||||
real 166m53.007s
|
real 166m53.007s
|
||||||
|
|
||||||
- Disabling pytables indexing didn't help:
|
- Disabling pytables indexing didn't help:
|
||||||
@@ -122,19 +122,19 @@ Speed
|
|||||||
- Server RAM usage is constant.
|
- Server RAM usage is constant.
|
||||||
|
|
||||||
- Speed problems were due to IntervalSet speed, of parsing intervals
|
- Speed problems were due to IntervalSet speed, of parsing intervals
|
||||||
from the database and adding the new one each time.
|
from the database and adding the new one each time.
|
||||||
|
|
||||||
- First optimization is to cache result of `nilmdb:_get_intervals`,
|
- First optimization is to cache result of `nilmdb:_get_intervals`,
|
||||||
which gives the best speedup.
|
which gives the best speedup.
|
||||||
|
|
||||||
- Also switched to internally using bxInterval from bx-python package.
|
- Also switched to internally using bxInterval from bx-python package.
|
||||||
Speed of `tests/test_interval:TestIntervalSpeed` is pretty decent
|
Speed of `tests/test_interval:TestIntervalSpeed` is pretty decent
|
||||||
and seems to be growing logarithmically now. About 85μs per insertion
|
and seems to be growing logarithmically now. About 85μs per insertion
|
||||||
for inserting 131k entries.
|
for inserting 131k entries.
|
||||||
|
|
||||||
- Storing the interval data in SQL might be better, with a scheme like:
|
- Storing the interval data in SQL might be better, with a scheme like:
|
||||||
http://www.logarithmic.net/pfh/blog/01235197474
|
http://www.logarithmic.net/pfh/blog/01235197474
|
||||||
|
|
||||||
- Next slowdown target is nilmdb.layout.Parser.parse().
|
- Next slowdown target is nilmdb.layout.Parser.parse().
|
||||||
- Rewrote parsers using cython and sscanf
|
- Rewrote parsers using cython and sscanf
|
||||||
- Stats (rev 10831), with _add_interval disabled
|
- Stats (rev 10831), with _add_interval disabled
|
||||||
@@ -142,7 +142,14 @@ Speed
|
|||||||
layout.pyx.parse:63 13913 sec, 5.1g calls
|
layout.pyx.parse:63 13913 sec, 5.1g calls
|
||||||
numpy:records.py.fromrecords:569 7410 sec, 262k calls
|
numpy:records.py.fromrecords:569 7410 sec, 262k calls
|
||||||
- Probably OK for now.
|
- Probably OK for now.
|
||||||
|
|
||||||
|
- After all updates, now takes about 8.5 minutes to insert an hour of
|
||||||
|
data, constant after adding 171 hours (4.9 billion data points)
|
||||||
|
|
||||||
|
- Data set size: 98 gigs = 20 bytes per data point.
|
||||||
|
6 uint16 data + 1 uint32 timestamp = 16 bytes per point
|
||||||
|
So compression must be off -- will retry with compression forced on.
|
||||||
|
|
||||||
IntervalSet speed
|
IntervalSet speed
|
||||||
-----------------
|
-----------------
|
||||||
- Initial implementation was pretty slow, even with binary search in
|
- Initial implementation was pretty slow, even with binary search in
|
||||||
@@ -161,6 +168,18 @@ IntervalSet speed
|
|||||||
- Might be algorithmic improvements to be made in Interval.py,
|
- Might be algorithmic improvements to be made in Interval.py,
|
||||||
like in `__and__`
|
like in `__and__`
|
||||||
|
|
||||||
|
- Replaced again with rbtree. Seems decent. Numbers are time per
|
||||||
|
insert for 2**17 insertions, followed by total wall time and RAM
|
||||||
|
usage for running "make test" with `test_rbtree` and `test_interval`
|
||||||
|
with range(5,20):
|
||||||
|
- old values with bxinterval:
|
||||||
|
20.2 μS, total 20 s, 177 MB RAM
|
||||||
|
- rbtree, plain python:
|
||||||
|
97 μS, total 105 s, 846 MB RAM
|
||||||
|
- rbtree converted to cython:
|
||||||
|
26 μS, total 29 s, 320 MB RAM
|
||||||
|
- rbtree and interval converted to cython:
|
||||||
|
8.4 μS, total 12 s, 134 MB RAM
|
||||||
|
|
||||||
Layouts
|
Layouts
|
||||||
-------
|
-------
|
||||||
@@ -170,12 +189,12 @@ just collections and counts of a single type. We'll still use strings
|
|||||||
to describe them, with format:
|
to describe them, with format:
|
||||||
|
|
||||||
type_count
|
type_count
|
||||||
|
|
||||||
where type is "uint16", "float32", or "float64", and count is an integer.
|
where type is "uint16", "float32", or "float64", and count is an integer.
|
||||||
|
|
||||||
nilmdb.layout.named() will parse these strings into the appropriate
|
nilmdb.layout.named() will parse these strings into the appropriate
|
||||||
handlers. For compatibility:
|
handlers. For compatibility:
|
||||||
|
|
||||||
"RawData" == "uint16_6"
|
"RawData" == "uint16_6"
|
||||||
"RawNotchedData" == "uint16_9"
|
"RawNotchedData" == "uint16_9"
|
||||||
"PrepData" == "float32_8"
|
"PrepData" == "float32_8"
|
||||||
|
@@ -1,605 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// The RedBlackEntry class is an Abstract Base Class. This means that no
|
|
||||||
// instance of the RedBlackEntry class can exist. Only classes which
|
|
||||||
// inherit from the RedBlackEntry class can exist. Furthermore any class
|
|
||||||
// which inherits from the RedBlackEntry class must define the member
|
|
||||||
// function GetKey(). The Print() member function does not have to
|
|
||||||
// be defined because a default definition exists.
|
|
||||||
//
|
|
||||||
// The GetKey() function should return an integer key for that entry.
|
|
||||||
// The key for an entry should never change otherwise bad things might occur.
|
|
||||||
|
|
||||||
class RedBlackEntry {
|
|
||||||
public:
|
|
||||||
RedBlackEntry();
|
|
||||||
virtual ~RedBlackEntry();
|
|
||||||
virtual int GetKey() const = 0;
|
|
||||||
virtual void Print() const;
|
|
||||||
};
|
|
||||||
|
|
||||||
class RedBlackTreeNode {
|
|
||||||
friend class RedBlackTree;
|
|
||||||
public:
|
|
||||||
void Print(RedBlackTreeNode*,
|
|
||||||
RedBlackTreeNode*) const;
|
|
||||||
RedBlackTreeNode();
|
|
||||||
RedBlackTreeNode(RedBlackEntry *);
|
|
||||||
RedBlackEntry * GetEntry() const;
|
|
||||||
~RedBlackTreeNode();
|
|
||||||
protected:
|
|
||||||
RedBlackEntry * storedEntry;
|
|
||||||
int key;
|
|
||||||
int red; /* if red=0 then the node is black */
|
|
||||||
RedBlackTreeNode * left;
|
|
||||||
RedBlackTreeNode * right;
|
|
||||||
RedBlackTreeNode * parent;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RedBlackTree {
|
|
||||||
public:
|
|
||||||
RedBlackTree();
|
|
||||||
~RedBlackTree();
|
|
||||||
void Print() const;
|
|
||||||
RedBlackEntry * DeleteNode(RedBlackTreeNode *);
|
|
||||||
RedBlackTreeNode * Insert(RedBlackEntry *);
|
|
||||||
RedBlackTreeNode * GetPredecessorOf(RedBlackTreeNode *) const;
|
|
||||||
RedBlackTreeNode * GetSuccessorOf(RedBlackTreeNode *) const;
|
|
||||||
RedBlackTreeNode * Search(int key);
|
|
||||||
TemplateStack<RedBlackTreeNode *> * Enumerate(int low, int high) ;
|
|
||||||
void CheckAssumptions() const;
|
|
||||||
protected:
|
|
||||||
/* A sentinel is used for root and for nil. These sentinels are */
|
|
||||||
/* created when RedBlackTreeCreate is caled. root->left should always */
|
|
||||||
/* point to the node which is the root of the tree. nil points to a */
|
|
||||||
/* node which should always be black but has aribtrary children and */
|
|
||||||
/* parent and no key or info. The point of using these sentinels is so */
|
|
||||||
/* that the root and nil nodes do not require special cases in the code */
|
|
||||||
RedBlackTreeNode * root;
|
|
||||||
RedBlackTreeNode * nil;
|
|
||||||
void LeftRotate(RedBlackTreeNode *);
|
|
||||||
void RightRotate(RedBlackTreeNode *);
|
|
||||||
void TreeInsertHelp(RedBlackTreeNode *);
|
|
||||||
void TreePrintHelper(RedBlackTreeNode *) const;
|
|
||||||
void FixUpMaxHigh(RedBlackTreeNode *);
|
|
||||||
void DeleteFixUp(RedBlackTreeNode *);
|
|
||||||
};
|
|
||||||
|
|
||||||
const int MIN_INT=-MAX_INT;
|
|
||||||
|
|
||||||
RedBlackTreeNode::RedBlackTreeNode(){
|
|
||||||
};
|
|
||||||
|
|
||||||
RedBlackTreeNode::RedBlackTreeNode(RedBlackEntry * newEntry)
|
|
||||||
: storedEntry (newEntry) , key(newEntry->GetKey()) {
|
|
||||||
};
|
|
||||||
|
|
||||||
RedBlackTreeNode::~RedBlackTreeNode(){
|
|
||||||
};
|
|
||||||
|
|
||||||
RedBlackEntry * RedBlackTreeNode::GetEntry() const {return storedEntry;}
|
|
||||||
|
|
||||||
RedBlackEntry::RedBlackEntry(){
|
|
||||||
};
|
|
||||||
RedBlackEntry::~RedBlackEntry(){
|
|
||||||
};
|
|
||||||
void RedBlackEntry::Print() const {
|
|
||||||
cout << "No Print Method defined. Using Default: " << GetKey() << endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
RedBlackTree::RedBlackTree()
|
|
||||||
{
|
|
||||||
nil = new RedBlackTreeNode;
|
|
||||||
nil->left = nil->right = nil->parent = nil;
|
|
||||||
nil->red = 0;
|
|
||||||
nil->key = MIN_INT;
|
|
||||||
nil->storedEntry = NULL;
|
|
||||||
|
|
||||||
root = new RedBlackTreeNode;
|
|
||||||
root->parent = root->left = root->right = nil;
|
|
||||||
root->key = MAX_INT;
|
|
||||||
root->red=0;
|
|
||||||
root->storedEntry = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/***********************************************************************/
|
|
||||||
/* FUNCTION: LeftRotate */
|
|
||||||
/**/
|
|
||||||
/* INPUTS: the node to rotate on */
|
|
||||||
/**/
|
|
||||||
/* OUTPUT: None */
|
|
||||||
/**/
|
|
||||||
/* Modifies Input: this, x */
|
|
||||||
/**/
|
|
||||||
/* EFFECTS: Rotates as described in _Introduction_To_Algorithms by */
|
|
||||||
/* Cormen, Leiserson, Rivest (Chapter 14). Basically this */
|
|
||||||
/* makes the parent of x be to the left of x, x the parent of */
|
|
||||||
/* its parent before the rotation and fixes other pointers */
|
|
||||||
/* accordingly. */
|
|
||||||
/***********************************************************************/
|
|
||||||
|
|
||||||
void RedBlackTree::LeftRotate(RedBlackTreeNode* x) {
|
|
||||||
RedBlackTreeNode* y;
|
|
||||||
|
|
||||||
/* I originally wrote this function to use the sentinel for */
|
|
||||||
/* nil to avoid checking for nil. However this introduces a */
|
|
||||||
/* very subtle bug because sometimes this function modifies */
|
|
||||||
/* the parent pointer of nil. This can be a problem if a */
|
|
||||||
/* function which calls LeftRotate also uses the nil sentinel */
|
|
||||||
/* and expects the nil sentinel's parent pointer to be unchanged */
|
|
||||||
/* after calling this function. For example, when DeleteFixUP */
|
|
||||||
/* calls LeftRotate it expects the parent pointer of nil to be */
|
|
||||||
/* unchanged. */
|
|
||||||
|
|
||||||
y=x->right;
|
|
||||||
x->right=y->left;
|
|
||||||
|
|
||||||
if (y->left != nil) y->left->parent=x; /* used to use sentinel here */
|
|
||||||
/* and do an unconditional assignment instead of testing for nil */
|
|
||||||
|
|
||||||
y->parent=x->parent;
|
|
||||||
|
|
||||||
/* instead of checking if x->parent is the root as in the book, we */
|
|
||||||
/* count on the root sentinel to implicitly take care of this case */
|
|
||||||
if( x == x->parent->left) {
|
|
||||||
x->parent->left=y;
|
|
||||||
} else {
|
|
||||||
x->parent->right=y;
|
|
||||||
}
|
|
||||||
y->left=x;
|
|
||||||
x->parent=y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/***********************************************************************/
|
|
||||||
/* FUNCTION: RighttRotate */
|
|
||||||
/**/
|
|
||||||
/* INPUTS: node to rotate on */
|
|
||||||
/**/
|
|
||||||
/* OUTPUT: None */
|
|
||||||
/**/
|
|
||||||
/* Modifies Input?: this, y */
|
|
||||||
/**/
|
|
||||||
/* EFFECTS: Rotates as described in _Introduction_To_Algorithms by */
|
|
||||||
/* Cormen, Leiserson, Rivest (Chapter 14). Basically this */
|
|
||||||
/* makes the parent of x be to the left of x, x the parent of */
|
|
||||||
/* its parent before the rotation and fixes other pointers */
|
|
||||||
/* accordingly. */
|
|
||||||
/***********************************************************************/
|
|
||||||
|
|
||||||
void RedBlackTree::RightRotate(RedBlackTreeNode* y) {
|
|
||||||
RedBlackTreeNode* x;
|
|
||||||
|
|
||||||
/* I originally wrote this function to use the sentinel for */
|
|
||||||
/* nil to avoid checking for nil. However this introduces a */
|
|
||||||
/* very subtle bug because sometimes this function modifies */
|
|
||||||
/* the parent pointer of nil. This can be a problem if a */
|
|
||||||
/* function which calls LeftRotate also uses the nil sentinel */
|
|
||||||
/* and expects the nil sentinel's parent pointer to be unchanged */
|
|
||||||
/* after calling this function. For example, when DeleteFixUP */
|
|
||||||
/* calls LeftRotate it expects the parent pointer of nil to be */
|
|
||||||
/* unchanged. */
|
|
||||||
|
|
||||||
x=y->left;
|
|
||||||
y->left=x->right;
|
|
||||||
|
|
||||||
if (nil != x->right) x->right->parent=y; /*used to use sentinel here */
|
|
||||||
/* and do an unconditional assignment instead of testing for nil */
|
|
||||||
|
|
||||||
/* instead of checking if x->parent is the root as in the book, we */
|
|
||||||
/* count on the root sentinel to implicitly take care of this case */
|
|
||||||
x->parent=y->parent;
|
|
||||||
if( y == y->parent->left) {
|
|
||||||
y->parent->left=x;
|
|
||||||
} else {
|
|
||||||
y->parent->right=x;
|
|
||||||
}
|
|
||||||
x->right=y;
|
|
||||||
y->parent=x;
|
|
||||||
}
|
|
||||||
|
|
||||||
/***********************************************************************/
|
|
||||||
/* FUNCTION: TreeInsertHelp */
|
|
||||||
/**/
|
|
||||||
/* INPUTS: z is the node to insert */
|
|
||||||
/**/
|
|
||||||
/* OUTPUT: none */
|
|
||||||
/**/
|
|
||||||
/* Modifies Input: this, z */
|
|
||||||
/**/
|
|
||||||
/* EFFECTS: Inserts z into the tree as if it were a regular binary tree */
|
|
||||||
/* using the algorithm described in _Introduction_To_Algorithms_ */
|
|
||||||
/* by Cormen et al. This funciton is only intended to be called */
|
|
||||||
/* by the Insert function and not by the user */
|
|
||||||
/***********************************************************************/
|
|
||||||
|
|
||||||
void RedBlackTree::TreeInsertHelp(RedBlackTreeNode* z) {
|
|
||||||
/* This function should only be called by RedBlackTree::Insert */
|
|
||||||
RedBlackTreeNode* x;
|
|
||||||
RedBlackTreeNode* y;
|
|
||||||
|
|
||||||
z->left=z->right=nil;
|
|
||||||
y=root;
|
|
||||||
x=root->left;
|
|
||||||
while( x != nil) {
|
|
||||||
y=x;
|
|
||||||
if ( x->key > z->key) {
|
|
||||||
x=x->left;
|
|
||||||
} else { /* x->key <= z->key */
|
|
||||||
x=x->right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
z->parent=y;
|
|
||||||
if ( (y == root) ||
|
|
||||||
(y->key > z->key) ) {
|
|
||||||
y->left=z;
|
|
||||||
} else {
|
|
||||||
y->right=z;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Before calling InsertNode the node x should have its key set */
|
|
||||||
|
|
||||||
/***********************************************************************/
|
|
||||||
/* FUNCTION: InsertNode */
|
|
||||||
/**/
|
|
||||||
/* INPUTS: newEntry is the entry to insert*/
|
|
||||||
/**/
|
|
||||||
/* OUTPUT: This function returns a pointer to the newly inserted node */
|
|
||||||
/* which is guarunteed to be valid until this node is deleted. */
|
|
||||||
/* What this means is if another data structure stores this */
|
|
||||||
/* pointer then the tree does not need to be searched when this */
|
|
||||||
/* is to be deleted. */
|
|
||||||
/**/
|
|
||||||
/* Modifies Input: tree */
|
|
||||||
/**/
|
|
||||||
/* EFFECTS: Creates a node node which contains the appropriate key and */
|
|
||||||
/* info pointers and inserts it into the tree. */
|
|
||||||
/***********************************************************************/
|
|
||||||
/* jim */
|
|
||||||
RedBlackTreeNode * RedBlackTree::Insert(RedBlackEntry * newEntry)
|
|
||||||
{
|
|
||||||
RedBlackTreeNode * y;
|
|
||||||
RedBlackTreeNode * x;
|
|
||||||
RedBlackTreeNode * newNode;
|
|
||||||
|
|
||||||
x = new RedBlackTreeNode(newEntry);
|
|
||||||
TreeInsertHelp(x);
|
|
||||||
newNode = x;
|
|
||||||
x->red=1;
|
|
||||||
while(x->parent->red) { /* use sentinel instead of checking for root */
|
|
||||||
if (x->parent == x->parent->parent->left) {
|
|
||||||
y=x->parent->parent->right;
|
|
||||||
if (y->red) {
|
|
||||||
x->parent->red=0;
|
|
||||||
y->red=0;
|
|
||||||
x->parent->parent->red=1;
|
|
||||||
x=x->parent->parent;
|
|
||||||
} else {
|
|
||||||
if (x == x->parent->right) {
|
|
||||||
x=x->parent;
|
|
||||||
LeftRotate(x);
|
|
||||||
}
|
|
||||||
x->parent->red=0;
|
|
||||||
x->parent->parent->red=1;
|
|
||||||
RightRotate(x->parent->parent);
|
|
||||||
}
|
|
||||||
} else { /* case for x->parent == x->parent->parent->right */
|
|
||||||
/* this part is just like the section above with */
|
|
||||||
/* left and right interchanged */
|
|
||||||
y=x->parent->parent->left;
|
|
||||||
if (y->red) {
|
|
||||||
x->parent->red=0;
|
|
||||||
y->red=0;
|
|
||||||
x->parent->parent->red=1;
|
|
||||||
x=x->parent->parent;
|
|
||||||
} else {
|
|
||||||
if (x == x->parent->left) {
|
|
||||||
x=x->parent;
|
|
||||||
RightRotate(x);
|
|
||||||
}
|
|
||||||
x->parent->red=0;
|
|
||||||
x->parent->parent->red=1;
|
|
||||||
LeftRotate(x->parent->parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
root->left->red=0;
|
|
||||||
return(newNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***********************************************************************/
|
|
||||||
/* FUNCTION: GetSuccessorOf */
|
|
||||||
/**/
|
|
||||||
/* INPUTS: x is the node we want the succesor of */
|
|
||||||
/**/
|
|
||||||
/* OUTPUT: This function returns the successor of x or NULL if no */
|
|
||||||
/* successor exists. */
|
|
||||||
/**/
|
|
||||||
/* Modifies Input: none */
|
|
||||||
/**/
|
|
||||||
/* Note: uses the algorithm in _Introduction_To_Algorithms_ */
|
|
||||||
/***********************************************************************/
|
|
||||||
|
|
||||||
RedBlackTreeNode * RedBlackTree::GetSuccessorOf(RedBlackTreeNode * x) const
|
|
||||||
{
|
|
||||||
RedBlackTreeNode* y;
|
|
||||||
|
|
||||||
if (nil != (y = x->right)) { /* assignment to y is intentional */
|
|
||||||
while(y->left != nil) { /* returns the minium of the right subtree of x */
|
|
||||||
y=y->left;
|
|
||||||
}
|
|
||||||
return(y);
|
|
||||||
} else {
|
|
||||||
y=x->parent;
|
|
||||||
while(x == y->right) { /* sentinel used instead of checking for nil */
|
|
||||||
x=y;
|
|
||||||
y=y->parent;
|
|
||||||
}
|
|
||||||
if (y == root) return(nil);
|
|
||||||
return(y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/***********************************************************************/
|
|
||||||
/* FUNCTION: GetPredecessorOf */
|
|
||||||
/**/
|
|
||||||
/* INPUTS: x is the node to get predecessor of */
|
|
||||||
/**/
|
|
||||||
/* OUTPUT: This function returns the predecessor of x or NULL if no */
|
|
||||||
/* predecessor exists. */
|
|
||||||
/**/
|
|
||||||
/* Modifies Input: none */
|
|
||||||
/**/
|
|
||||||
/* Note: uses the algorithm in _Introduction_To_Algorithms_ */
|
|
||||||
/***********************************************************************/
|
|
||||||
|
|
||||||
RedBlackTreeNode * RedBlackTree::GetPredecessorOf(RedBlackTreeNode * x) const {
|
|
||||||
RedBlackTreeNode* y;
|
|
||||||
|
|
||||||
if (nil != (y = x->left)) { /* assignment to y is intentional */
|
|
||||||
while(y->right != nil) { /* returns the maximum of the left subtree of x */
|
|
||||||
y=y->right;
|
|
||||||
}
|
|
||||||
return(y);
|
|
||||||
} else {
|
|
||||||
y=x->parent;
|
|
||||||
while(x == y->left) {
|
|
||||||
if (y == root) return(nil);
|
|
||||||
x=y;
|
|
||||||
y=y->parent;
|
|
||||||
}
|
|
||||||
return(y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/***********************************************************************/
|
|
||||||
/* FUNCTION: Print */
|
|
||||||
/**/
|
|
||||||
/* INPUTS: none */
|
|
||||||
/**/
|
|
||||||
/* OUTPUT: none */
|
|
||||||
/**/
|
|
||||||
/* EFFECTS: This function recursively prints the nodes of the tree */
|
|
||||||
/* inorder. */
|
|
||||||
/**/
|
|
||||||
/* Modifies Input: none */
|
|
||||||
/**/
|
|
||||||
/* Note: This function should only be called from ITTreePrint */
|
|
||||||
/***********************************************************************/
|
|
||||||
|
|
||||||
void RedBlackTreeNode::Print(RedBlackTreeNode * nil,
|
|
||||||
RedBlackTreeNode * root) const {
|
|
||||||
storedEntry->Print();
|
|
||||||
printf(", key=%i ",key);
|
|
||||||
printf(" l->key=");
|
|
||||||
if( left == nil) printf("NULL"); else printf("%i",left->key);
|
|
||||||
printf(" r->key=");
|
|
||||||
if( right == nil) printf("NULL"); else printf("%i",right->key);
|
|
||||||
printf(" p->key=");
|
|
||||||
if( parent == root) printf("NULL"); else printf("%i",parent->key);
|
|
||||||
printf(" red=%i\n",red);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RedBlackTree::TreePrintHelper( RedBlackTreeNode* x) const {
|
|
||||||
|
|
||||||
if (x != nil) {
|
|
||||||
TreePrintHelper(x->left);
|
|
||||||
x->Print(nil,root);
|
|
||||||
TreePrintHelper(x->right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/***********************************************************************/
|
|
||||||
/* FUNCTION: Print */
|
|
||||||
/**/
|
|
||||||
/* INPUTS: none */
|
|
||||||
/**/
|
|
||||||
/* OUTPUT: none */
|
|
||||||
/**/
|
|
||||||
/* EFFECT: This function recursively prints the nodes of the tree */
|
|
||||||
/* inorder. */
|
|
||||||
/**/
|
|
||||||
/* Modifies Input: none */
|
|
||||||
/**/
|
|
||||||
/***********************************************************************/
|
|
||||||
|
|
||||||
void RedBlackTree::Print() const {
|
|
||||||
TreePrintHelper(root->left);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***********************************************************************/
|
|
||||||
/* FUNCTION: DeleteFixUp */
|
|
||||||
/**/
|
|
||||||
/* INPUTS: x is the child of the spliced */
|
|
||||||
/* out node in DeleteNode. */
|
|
||||||
/**/
|
|
||||||
/* OUTPUT: none */
|
|
||||||
/**/
|
|
||||||
/* EFFECT: Performs rotations and changes colors to restore red-black */
|
|
||||||
/* properties after a node is deleted */
|
|
||||||
/**/
|
|
||||||
/* Modifies Input: this, x */
|
|
||||||
/**/
|
|
||||||
/* The algorithm from this function is from _Introduction_To_Algorithms_ */
|
|
||||||
/***********************************************************************/
|
|
||||||
|
|
||||||
void RedBlackTree::DeleteFixUp(RedBlackTreeNode* x) {
|
|
||||||
RedBlackTreeNode * w;
|
|
||||||
RedBlackTreeNode * rootLeft = root->left;
|
|
||||||
|
|
||||||
while( (!x->red) && (rootLeft != x)) {
|
|
||||||
if (x == x->parent->left) {
|
|
||||||
|
|
||||||
//
|
|
||||||
w=x->parent->right;
|
|
||||||
if (w->red) {
|
|
||||||
w->red=0;
|
|
||||||
x->parent->red=1;
|
|
||||||
LeftRotate(x->parent);
|
|
||||||
w=x->parent->right;
|
|
||||||
}
|
|
||||||
if ( (!w->right->red) && (!w->left->red) ) {
|
|
||||||
w->red=1;
|
|
||||||
x=x->parent;
|
|
||||||
} else {
|
|
||||||
if (!w->right->red) {
|
|
||||||
w->left->red=0;
|
|
||||||
w->red=1;
|
|
||||||
RightRotate(w);
|
|
||||||
w=x->parent->right;
|
|
||||||
}
|
|
||||||
w->red=x->parent->red;
|
|
||||||
x->parent->red=0;
|
|
||||||
w->right->red=0;
|
|
||||||
LeftRotate(x->parent);
|
|
||||||
x=rootLeft; /* this is to exit while loop */
|
|
||||||
}
|
|
||||||
//
|
|
||||||
|
|
||||||
} else { /* the code below is has left and right switched from above */
|
|
||||||
w=x->parent->left;
|
|
||||||
if (w->red) {
|
|
||||||
w->red=0;
|
|
||||||
x->parent->red=1;
|
|
||||||
RightRotate(x->parent);
|
|
||||||
w=x->parent->left;
|
|
||||||
}
|
|
||||||
if ( (!w->right->red) && (!w->left->red) ) {
|
|
||||||
w->red=1;
|
|
||||||
x=x->parent;
|
|
||||||
} else {
|
|
||||||
if (!w->left->red) {
|
|
||||||
w->right->red=0;
|
|
||||||
w->red=1;
|
|
||||||
LeftRotate(w);
|
|
||||||
w=x->parent->left;
|
|
||||||
}
|
|
||||||
w->red=x->parent->red;
|
|
||||||
x->parent->red=0;
|
|
||||||
w->left->red=0;
|
|
||||||
RightRotate(x->parent);
|
|
||||||
x=rootLeft; /* this is to exit while loop */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
x->red=0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/***********************************************************************/
|
|
||||||
/* FUNCTION: DeleteNode */
|
|
||||||
/**/
|
|
||||||
/* INPUTS: tree is the tree to delete node z from */
|
|
||||||
/**/
|
|
||||||
/* OUTPUT: returns the RedBlackEntry stored at deleted node */
|
|
||||||
/**/
|
|
||||||
/* EFFECT: Deletes z from tree and but don't call destructor */
|
|
||||||
/**/
|
|
||||||
/* Modifies Input: z */
|
|
||||||
/**/
|
|
||||||
/* The algorithm from this function is from _Introduction_To_Algorithms_ */
|
|
||||||
/***********************************************************************/
|
|
||||||
|
|
||||||
RedBlackEntry * RedBlackTree::DeleteNode(RedBlackTreeNode * z){
|
|
||||||
RedBlackTreeNode* y;
|
|
||||||
RedBlackTreeNode* x;
|
|
||||||
RedBlackEntry * returnValue = z->storedEntry;
|
|
||||||
|
|
||||||
y= ((z->left == nil) || (z->right == nil)) ? z : GetSuccessorOf(z);
|
|
||||||
x= (y->left == nil) ? y->right : y->left;
|
|
||||||
if (root == (x->parent = y->parent)) { /* assignment of y->p to x->p is intentional */
|
|
||||||
root->left=x;
|
|
||||||
} else {
|
|
||||||
if (y == y->parent->left) {
|
|
||||||
y->parent->left=x;
|
|
||||||
} else {
|
|
||||||
y->parent->right=x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (y != z) { /* y should not be nil in this case */
|
|
||||||
|
|
||||||
/* y is the node to splice out and x is its child */
|
|
||||||
|
|
||||||
y->left=z->left;
|
|
||||||
y->right=z->right;
|
|
||||||
y->parent=z->parent;
|
|
||||||
z->left->parent=z->right->parent=y;
|
|
||||||
if (z == z->parent->left) {
|
|
||||||
z->parent->left=y;
|
|
||||||
} else {
|
|
||||||
z->parent->right=y;
|
|
||||||
}
|
|
||||||
if (!(y->red)) {
|
|
||||||
y->red = z->red;
|
|
||||||
DeleteFixUp(x);
|
|
||||||
} else
|
|
||||||
y->red = z->red;
|
|
||||||
delete z;
|
|
||||||
} else {
|
|
||||||
if (!(y->red)) DeleteFixUp(x);
|
|
||||||
delete y;
|
|
||||||
}
|
|
||||||
return returnValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/***********************************************************************/
|
|
||||||
/* FUNCTION: Enumerate */
|
|
||||||
/**/
|
|
||||||
/* INPUTS: tree is the tree to look for keys between [low,high] */
|
|
||||||
/**/
|
|
||||||
/* OUTPUT: stack containing pointers to the nodes between [low,high] */
|
|
||||||
/**/
|
|
||||||
/* Modifies Input: none */
|
|
||||||
/**/
|
|
||||||
/* EFFECT: Returns a stack containing pointers to nodes containing */
|
|
||||||
/* keys which in [low,high]/ */
|
|
||||||
/**/
|
|
||||||
/***********************************************************************/
|
|
||||||
|
|
||||||
TemplateStack<RedBlackTreeNode *> * RedBlackTree::Enumerate(int low,
|
|
||||||
int high) {
|
|
||||||
TemplateStack<RedBlackTreeNode *> * enumResultStack =
|
|
||||||
new TemplateStack<RedBlackTreeNode *>(4);
|
|
||||||
|
|
||||||
RedBlackTreeNode* x=root->left;
|
|
||||||
RedBlackTreeNode* lastBest=NULL;
|
|
||||||
|
|
||||||
while(nil != x) {
|
|
||||||
if ( x->key > high ) {
|
|
||||||
x=x->left;
|
|
||||||
} else {
|
|
||||||
lastBest=x;
|
|
||||||
x=x->right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while ( (lastBest) && (low <= lastBest->key) ) {
|
|
||||||
enumResultStack->Push(lastBest);
|
|
||||||
lastBest=GetPredecessorOf(lastBest);
|
|
||||||
}
|
|
||||||
return(enumResultStack);
|
|
||||||
}
|
|
@@ -3,14 +3,10 @@
|
|||||||
from .nilmdb import NilmDB
|
from .nilmdb import NilmDB
|
||||||
from .server import Server
|
from .server import Server
|
||||||
from .client import Client
|
from .client import Client
|
||||||
from .timer import Timer
|
|
||||||
|
|
||||||
import cmdline
|
|
||||||
|
|
||||||
import pyximport; pyximport.install()
|
import pyximport; pyximport.install()
|
||||||
import layout
|
import layout
|
||||||
|
|
||||||
import serializer
|
|
||||||
import timestamper
|
|
||||||
import interval
|
import interval
|
||||||
import du
|
|
||||||
|
import cmdline
|
||||||
|
|
||||||
|
297
nilmdb/bulkdata.py
Normal file
297
nilmdb/bulkdata.py
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
# Fixed record size bulk data storage
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
import nilmdb
|
||||||
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import cPickle as pickle
|
||||||
|
import struct
|
||||||
|
import fnmatch
|
||||||
|
import mmap
|
||||||
|
|
||||||
|
# Up to 256 open file descriptors at any given time
|
||||||
|
table_cache_size = 16
|
||||||
|
fd_cache_size = 16
|
||||||
|
|
||||||
|
@nilmdb.utils.must_close()
|
||||||
|
class BulkData(object):
|
||||||
|
def __init__(self, basepath):
|
||||||
|
self.basepath = basepath
|
||||||
|
self.root = os.path.join(self.basepath, "data")
|
||||||
|
|
||||||
|
# Make root path
|
||||||
|
if not os.path.isdir(self.root):
|
||||||
|
os.mkdir(self.root)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.getnode.cache_remove_all()
|
||||||
|
|
||||||
|
def create(self, path, layout_name):
|
||||||
|
"""
|
||||||
|
path: path to the data (e.g. '/newton/prep').
|
||||||
|
Paths must contain at least two elements, e.g.:
|
||||||
|
/newton/prep
|
||||||
|
/newton/raw
|
||||||
|
/newton/upstairs/prep
|
||||||
|
/newton/upstairs/raw
|
||||||
|
|
||||||
|
layout_name: string for nilmdb.layout.get_named(), e.g. 'float32_8'
|
||||||
|
"""
|
||||||
|
if path[0] != '/':
|
||||||
|
raise ValueError("paths must start with /")
|
||||||
|
[ group, node ] = path.rsplit("/", 1)
|
||||||
|
if group == '':
|
||||||
|
raise ValueError("invalid path")
|
||||||
|
|
||||||
|
# Get layout, and build format string for struct module
|
||||||
|
try:
|
||||||
|
layout = nilmdb.layout.get_named(layout_name)
|
||||||
|
struct_fmt = '<d' # Little endian, double timestamp
|
||||||
|
struct_mapping = {
|
||||||
|
"int8": 'b',
|
||||||
|
"uint8": 'B',
|
||||||
|
"int16": 'h',
|
||||||
|
"uint16": 'H',
|
||||||
|
"int32": 'i',
|
||||||
|
"uint32": 'I',
|
||||||
|
"int64": 'q',
|
||||||
|
"uint64": 'Q',
|
||||||
|
"float32": 'f',
|
||||||
|
"float64": 'd',
|
||||||
|
}
|
||||||
|
for n in range(layout.count):
|
||||||
|
struct_fmt += struct_mapping[layout.datatype]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError("no such layout, or bad data types")
|
||||||
|
|
||||||
|
# Create the table. Note that we make a distinction here
|
||||||
|
# between NilmDB paths (always Unix style, split apart
|
||||||
|
# manually) and OS paths (built up with os.path.join)
|
||||||
|
try:
|
||||||
|
# Make directories leading up to this one
|
||||||
|
elements = path.lstrip('/').split('/')
|
||||||
|
for i in range(len(elements)):
|
||||||
|
ospath = os.path.join(self.root, *elements[0:i])
|
||||||
|
if Table.exists(ospath):
|
||||||
|
raise ValueError("path is subdir of existing node")
|
||||||
|
if not os.path.isdir(ospath):
|
||||||
|
os.mkdir(ospath)
|
||||||
|
|
||||||
|
# Make the final dir
|
||||||
|
ospath = os.path.join(self.root, *elements)
|
||||||
|
if os.path.isdir(ospath):
|
||||||
|
raise ValueError("subdirs of this path already exist")
|
||||||
|
os.mkdir(ospath)
|
||||||
|
|
||||||
|
# Write format string to file
|
||||||
|
Table.create(ospath, struct_fmt)
|
||||||
|
except OSError as e:
|
||||||
|
raise ValueError("error creating table at that path: " + e.strerror)
|
||||||
|
|
||||||
|
# Open and cache it
|
||||||
|
self.getnode(path)
|
||||||
|
|
||||||
|
# Success
|
||||||
|
return
|
||||||
|
|
||||||
|
def destroy(self, path):
|
||||||
|
"""Fully remove all data at a particular path. No way to undo
|
||||||
|
it! The group/path structure is removed, too."""
|
||||||
|
|
||||||
|
# Get OS path
|
||||||
|
elements = path.lstrip('/').split('/')
|
||||||
|
ospath = os.path.join(self.root, *elements)
|
||||||
|
|
||||||
|
# Remove Table object from cache
|
||||||
|
self.getnode.cache_remove(self, ospath)
|
||||||
|
|
||||||
|
# Remove the contents of the target directory
|
||||||
|
if not os.path.isfile(os.path.join(ospath, "format")):
|
||||||
|
raise ValueError("nothing at that path")
|
||||||
|
for file in os.listdir(ospath):
|
||||||
|
os.remove(os.path.join(ospath, file))
|
||||||
|
|
||||||
|
# Remove empty parent directories
|
||||||
|
for i in reversed(range(len(elements))):
|
||||||
|
ospath = os.path.join(self.root, *elements[0:i+1])
|
||||||
|
try:
|
||||||
|
os.rmdir(ospath)
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Cache open tables
|
||||||
|
@nilmdb.utils.lru_cache(size = table_cache_size,
|
||||||
|
onremove = lambda x: x.close())
|
||||||
|
def getnode(self, path):
|
||||||
|
"""Return a Table object corresponding to the given database
|
||||||
|
path, which must exist."""
|
||||||
|
elements = path.lstrip('/').split('/')
|
||||||
|
ospath = os.path.join(self.root, *elements)
|
||||||
|
return Table(ospath)
|
||||||
|
|
||||||
|
@nilmdb.utils.must_close()
|
||||||
|
class Table(object):
|
||||||
|
"""Tools to help access a single table (data at a specific OS path)"""
|
||||||
|
|
||||||
|
# Class methods, to help keep format details in this class.
|
||||||
|
@classmethod
|
||||||
|
def exists(cls, root):
|
||||||
|
"""Return True if a table appears to exist at this OS path"""
|
||||||
|
return os.path.isfile(os.path.join(root, "format"))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, root, struct_fmt):
|
||||||
|
"""Initialize a table at the given OS path.
|
||||||
|
'struct_fmt' is a Struct module format description"""
|
||||||
|
format = { "rows_per_file": 4 * 1024 * 1024,
|
||||||
|
"struct_fmt": struct_fmt }
|
||||||
|
with open(os.path.join(root, "format"), "wb") as f:
|
||||||
|
pickle.dump(format, f, 2)
|
||||||
|
|
||||||
|
# Normal methods
|
||||||
|
def __init__(self, root):
|
||||||
|
"""'root' is the full OS path to the directory of this table"""
|
||||||
|
self.root = root
|
||||||
|
|
||||||
|
# Load the format and build packer
|
||||||
|
with open(self._fullpath("format"), "rb") as f:
|
||||||
|
format = pickle.load(f)
|
||||||
|
self.rows_per_file = format["rows_per_file"]
|
||||||
|
self.packer = struct.Struct(format["struct_fmt"])
|
||||||
|
self.file_size = self.packer.size * self.rows_per_file
|
||||||
|
|
||||||
|
# Find nrows by locating the lexicographically last filename
|
||||||
|
# and using its size.
|
||||||
|
pattern = '[0-9a-f]' * 8
|
||||||
|
allfiles = fnmatch.filter(os.listdir(self.root), pattern)
|
||||||
|
if allfiles:
|
||||||
|
filename = max(allfiles)
|
||||||
|
offset = os.path.getsize(self._fullpath(filename))
|
||||||
|
self.nrows = self._row_from_fnoffset(filename, offset)
|
||||||
|
else:
|
||||||
|
self.nrows = 0
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.mmap_open.cache_remove_all()
|
||||||
|
|
||||||
|
# Internal helpers
|
||||||
|
def _fullpath(self, filename):
|
||||||
|
return os.path.join(self.root, filename)
|
||||||
|
|
||||||
|
def _fnoffset_from_row(self, row):
|
||||||
|
"""Return a (filename, offset, count) tuple:
|
||||||
|
|
||||||
|
filename: the filename that contains the specified row
|
||||||
|
offset: byte offset of the specified row within the file
|
||||||
|
count: number of rows (starting at offste) that fit in the file
|
||||||
|
"""
|
||||||
|
filenum = row // self.rows_per_file
|
||||||
|
filename = sprintf("%08x", filenum)
|
||||||
|
offset = (row % self.rows_per_file) * self.packer.size
|
||||||
|
count = self.rows_per_file - (row % self.rows_per_file)
|
||||||
|
return (filename, offset, count)
|
||||||
|
|
||||||
|
def _row_from_fnoffset(self, filename, offset):
|
||||||
|
"""Return the row number that corresponds to the given
|
||||||
|
filename and byte-offset within that file."""
|
||||||
|
filenum = int(filename, 16)
|
||||||
|
if (offset % self.packer.size) != 0:
|
||||||
|
raise ValueError("file offset is not a multiple of data size")
|
||||||
|
row = (filenum * self.rows_per_file) + (offset // self.packer.size)
|
||||||
|
return row
|
||||||
|
|
||||||
|
# Cache open files
|
||||||
|
@nilmdb.utils.lru_cache(size = fd_cache_size,
|
||||||
|
onremove = lambda x: x.close())
|
||||||
|
def mmap_open(self, file, newsize = None):
|
||||||
|
"""Open and map a given filename (relative to self.root).
|
||||||
|
Will be automatically closed when evicted from the cache.
|
||||||
|
|
||||||
|
If 'newsize' is provided, the file is truncated to the given
|
||||||
|
size before the mapping is returned. (Note that the LRU cache
|
||||||
|
on this function means the truncate will only happen if the
|
||||||
|
object isn't already cached; mmap.resize should be used too)"""
|
||||||
|
f = open(os.path.join(self.root, file), "a+", 0)
|
||||||
|
if newsize is not None:
|
||||||
|
# mmap can't map a zero-length file, so this allows the
|
||||||
|
# caller to set the filesize between file creation and
|
||||||
|
# mmap.
|
||||||
|
f.truncate(newsize)
|
||||||
|
mm = mmap.mmap(f.fileno(), 0)
|
||||||
|
return mm
|
||||||
|
|
||||||
|
def append(self, data):
|
||||||
|
"""Append the data and flush it to disk.
|
||||||
|
data is a nested Python list [[row],[row],[...]]"""
|
||||||
|
remaining = len(data)
|
||||||
|
dataiter = iter(data)
|
||||||
|
while remaining:
|
||||||
|
# See how many rows we can fit into the current file, and open it
|
||||||
|
(filename, offset, count) = self._fnoffset_from_row(self.nrows)
|
||||||
|
if count > remaining:
|
||||||
|
count = remaining
|
||||||
|
newsize = offset + count * self.packer.size
|
||||||
|
mm = self.mmap_open(filename, newsize)
|
||||||
|
mm.seek(offset)
|
||||||
|
|
||||||
|
# Extend the file to the target length. We specified
|
||||||
|
# newsize when opening, but that may have been ignored if
|
||||||
|
# the mmap_open returned a cached object.
|
||||||
|
mm.resize(newsize)
|
||||||
|
|
||||||
|
# Write the data
|
||||||
|
for i in xrange(count):
|
||||||
|
row = dataiter.next()
|
||||||
|
mm.write(self.packer.pack(*row))
|
||||||
|
remaining -= count
|
||||||
|
self.nrows += count
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
"""Extract data and return it. Supports simple indexing
|
||||||
|
(table[n]) and range slices (table[n:m]). Returns a nested
|
||||||
|
Python list [[row],[row],[...]]"""
|
||||||
|
|
||||||
|
# Handle simple slices
|
||||||
|
if isinstance(key, slice):
|
||||||
|
# Fall back to brute force if the slice isn't simple
|
||||||
|
if ((key.step is not None and key.step != 1) or
|
||||||
|
key.start is None or
|
||||||
|
key.stop is None or
|
||||||
|
key.start >= key.stop or
|
||||||
|
key.start < 0 or
|
||||||
|
key.stop > self.nrows):
|
||||||
|
return [ self[x] for x in xrange(*key.indices(self.nrows)) ]
|
||||||
|
|
||||||
|
ret = []
|
||||||
|
row = key.start
|
||||||
|
remaining = key.stop - key.start
|
||||||
|
while remaining:
|
||||||
|
(filename, offset, count) = self._fnoffset_from_row(row)
|
||||||
|
if count > remaining:
|
||||||
|
count = remaining
|
||||||
|
mm = self.mmap_open(filename)
|
||||||
|
for i in xrange(count):
|
||||||
|
ret.append(list(self.packer.unpack_from(mm, offset)))
|
||||||
|
offset += self.packer.size
|
||||||
|
remaining -= count
|
||||||
|
row += count
|
||||||
|
return ret
|
||||||
|
|
||||||
|
# Handle single points
|
||||||
|
if key < 0 or key >= self.nrows:
|
||||||
|
raise IndexError("Index out of range")
|
||||||
|
(filename, offset, count) = self._fnoffset_from_row(key)
|
||||||
|
mm = self.mmap_open(filename)
|
||||||
|
# unpack_from ignores the mmap object's current seek position
|
||||||
|
return self.packer.unpack_from(mm, offset)
|
||||||
|
|
||||||
|
class TimestampOnlyTable(object):
|
||||||
|
"""Helper that lets us pass a Tables object into bisect, by
|
||||||
|
returning only the timestamp when a particular row is requested."""
|
||||||
|
def __init__(self, table):
|
||||||
|
self.table = table
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self.table[index][0]
|
@@ -1,495 +0,0 @@
|
|||||||
# cython: profile=False
|
|
||||||
# This is from bx-python 554:07aca5a9f6fc (BSD licensed), modified to
|
|
||||||
# store interval ranges as doubles rather than 32-bit integers.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Data structure for performing intersect queries on a set of intervals which
|
|
||||||
preserves all information about the intervals (unlike bitset projection methods).
|
|
||||||
|
|
||||||
:Authors: James Taylor (james@jamestaylor.org),
|
|
||||||
Ian Schenk (ian.schenck@gmail.com),
|
|
||||||
Brent Pedersen (bpederse@gmail.com)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Historical note:
|
|
||||||
# This module original contained an implementation based on sorted endpoints
|
|
||||||
# and a binary search, using an idea from Scott Schwartz and Piotr Berman.
|
|
||||||
# Later an interval tree implementation was implemented by Ian for Galaxy's
|
|
||||||
# join tool (see `bx.intervals.operations.quicksect.py`). This was then
|
|
||||||
# converted to Cython by Brent, who also added support for
|
|
||||||
# upstream/downstream/neighbor queries. This was modified by James to
|
|
||||||
# handle half-open intervals strictly, to maintain sort order, and to
|
|
||||||
# implement the same interface as the original Intersecter.
|
|
||||||
|
|
||||||
#cython: cdivision=True
|
|
||||||
|
|
||||||
import operator
|
|
||||||
|
|
||||||
cdef extern from "stdlib.h":
|
|
||||||
int ceil(float f)
|
|
||||||
float log(float f)
|
|
||||||
int RAND_MAX
|
|
||||||
int rand()
|
|
||||||
int strlen(char *)
|
|
||||||
int iabs(int)
|
|
||||||
|
|
||||||
cdef inline double dmax2(double a, double b):
|
|
||||||
if b > a: return b
|
|
||||||
return a
|
|
||||||
|
|
||||||
cdef inline double dmax3(double a, double b, double c):
|
|
||||||
if b > a:
|
|
||||||
if c > b:
|
|
||||||
return c
|
|
||||||
return b
|
|
||||||
if a > c:
|
|
||||||
return a
|
|
||||||
return c
|
|
||||||
|
|
||||||
cdef inline double dmin3(double a, double b, double c):
|
|
||||||
if b < a:
|
|
||||||
if c < b:
|
|
||||||
return c
|
|
||||||
return b
|
|
||||||
if a < c:
|
|
||||||
return a
|
|
||||||
return c
|
|
||||||
|
|
||||||
cdef inline double dmin2(double a, double b):
|
|
||||||
if b < a: return b
|
|
||||||
return a
|
|
||||||
|
|
||||||
cdef float nlog = -1.0 / log(0.5)
|
|
||||||
|
|
||||||
cdef class IntervalNode:
|
|
||||||
"""
|
|
||||||
A single node of an `IntervalTree`.
|
|
||||||
|
|
||||||
NOTE: Unless you really know what you are doing, you probably should us
|
|
||||||
`IntervalTree` rather than using this directly.
|
|
||||||
"""
|
|
||||||
cdef float priority
|
|
||||||
cdef public object interval
|
|
||||||
cdef public double start, end
|
|
||||||
cdef double minend, maxend, minstart
|
|
||||||
cdef IntervalNode cleft, cright, croot
|
|
||||||
|
|
||||||
property left_node:
|
|
||||||
def __get__(self):
|
|
||||||
return self.cleft if self.cleft is not EmptyNode else None
|
|
||||||
property right_node:
|
|
||||||
def __get__(self):
|
|
||||||
return self.cright if self.cright is not EmptyNode else None
|
|
||||||
property root_node:
|
|
||||||
def __get__(self):
|
|
||||||
return self.croot if self.croot is not EmptyNode else None
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "IntervalNode(%g, %g)" % (self.start, self.end)
|
|
||||||
|
|
||||||
def __cinit__(IntervalNode self, double start, double end, object interval):
|
|
||||||
# Python lacks the binomial distribution, so we convert a
|
|
||||||
# uniform into a binomial because it naturally scales with
|
|
||||||
# tree size. Also, python's uniform is perfect since the
|
|
||||||
# upper limit is not inclusive, which gives us undefined here.
|
|
||||||
self.priority = ceil(nlog * log(-1.0/(1.0 * rand()/RAND_MAX - 1)))
|
|
||||||
self.start = start
|
|
||||||
self.end = end
|
|
||||||
self.interval = interval
|
|
||||||
self.maxend = end
|
|
||||||
self.minstart = start
|
|
||||||
self.minend = end
|
|
||||||
self.cleft = EmptyNode
|
|
||||||
self.cright = EmptyNode
|
|
||||||
self.croot = EmptyNode
|
|
||||||
|
|
||||||
cpdef IntervalNode insert(IntervalNode self, double start, double end, object interval):
|
|
||||||
"""
|
|
||||||
Insert a new IntervalNode into the tree of which this node is
|
|
||||||
currently the root. The return value is the new root of the tree (which
|
|
||||||
may or may not be this node!)
|
|
||||||
"""
|
|
||||||
cdef IntervalNode croot = self
|
|
||||||
# If starts are the same, decide which to add interval to based on
|
|
||||||
# end, thus maintaining sortedness relative to start/end
|
|
||||||
cdef double decision_endpoint = start
|
|
||||||
if start == self.start:
|
|
||||||
decision_endpoint = end
|
|
||||||
|
|
||||||
if decision_endpoint > self.start:
|
|
||||||
# insert to cright tree
|
|
||||||
if self.cright is not EmptyNode:
|
|
||||||
self.cright = self.cright.insert( start, end, interval )
|
|
||||||
else:
|
|
||||||
self.cright = IntervalNode( start, end, interval )
|
|
||||||
# rebalance tree
|
|
||||||
if self.priority < self.cright.priority:
|
|
||||||
croot = self.rotate_left()
|
|
||||||
else:
|
|
||||||
# insert to cleft tree
|
|
||||||
if self.cleft is not EmptyNode:
|
|
||||||
self.cleft = self.cleft.insert( start, end, interval)
|
|
||||||
else:
|
|
||||||
self.cleft = IntervalNode( start, end, interval)
|
|
||||||
# rebalance tree
|
|
||||||
if self.priority < self.cleft.priority:
|
|
||||||
croot = self.rotate_right()
|
|
||||||
|
|
||||||
croot.set_ends()
|
|
||||||
self.cleft.croot = croot
|
|
||||||
self.cright.croot = croot
|
|
||||||
return croot
|
|
||||||
|
|
||||||
cdef IntervalNode rotate_right(IntervalNode self):
|
|
||||||
cdef IntervalNode croot = self.cleft
|
|
||||||
self.cleft = self.cleft.cright
|
|
||||||
croot.cright = self
|
|
||||||
self.set_ends()
|
|
||||||
return croot
|
|
||||||
|
|
||||||
cdef IntervalNode rotate_left(IntervalNode self):
|
|
||||||
cdef IntervalNode croot = self.cright
|
|
||||||
self.cright = self.cright.cleft
|
|
||||||
croot.cleft = self
|
|
||||||
self.set_ends()
|
|
||||||
return croot
|
|
||||||
|
|
||||||
cdef inline void set_ends(IntervalNode self):
|
|
||||||
if self.cright is not EmptyNode and self.cleft is not EmptyNode:
|
|
||||||
self.maxend = dmax3(self.end, self.cright.maxend, self.cleft.maxend)
|
|
||||||
self.minend = dmin3(self.end, self.cright.minend, self.cleft.minend)
|
|
||||||
self.minstart = dmin3(self.start, self.cright.minstart, self.cleft.minstart)
|
|
||||||
elif self.cright is not EmptyNode:
|
|
||||||
self.maxend = dmax2(self.end, self.cright.maxend)
|
|
||||||
self.minend = dmin2(self.end, self.cright.minend)
|
|
||||||
self.minstart = dmin2(self.start, self.cright.minstart)
|
|
||||||
elif self.cleft is not EmptyNode:
|
|
||||||
self.maxend = dmax2(self.end, self.cleft.maxend)
|
|
||||||
self.minend = dmin2(self.end, self.cleft.minend)
|
|
||||||
self.minstart = dmin2(self.start, self.cleft.minstart)
|
|
||||||
|
|
||||||
|
|
||||||
def intersect( self, double start, double end, sort=True ):
|
|
||||||
"""
|
|
||||||
given a start and a end, return a list of features
|
|
||||||
falling within that range
|
|
||||||
"""
|
|
||||||
cdef list results = []
|
|
||||||
self._intersect( start, end, results )
|
|
||||||
if sort:
|
|
||||||
results = sorted(results)
|
|
||||||
return results
|
|
||||||
|
|
||||||
find = intersect
|
|
||||||
|
|
||||||
cdef void _intersect( IntervalNode self, double start, double end, list results):
|
|
||||||
# Left subtree
|
|
||||||
if self.cleft is not EmptyNode and self.cleft.maxend > start:
|
|
||||||
self.cleft._intersect( start, end, results )
|
|
||||||
# This interval
|
|
||||||
if ( self.end > start ) and ( self.start < end ):
|
|
||||||
results.append( self.interval )
|
|
||||||
# Right subtree
|
|
||||||
if self.cright is not EmptyNode and self.start < end:
|
|
||||||
self.cright._intersect( start, end, results )
|
|
||||||
|
|
||||||
|
|
||||||
cdef void _seek_left(IntervalNode self, double position, list results, int n, double max_dist):
|
|
||||||
# we know we can bail in these 2 cases.
|
|
||||||
if self.maxend + max_dist < position:
|
|
||||||
return
|
|
||||||
if self.minstart > position:
|
|
||||||
return
|
|
||||||
|
|
||||||
# the ordering of these 3 blocks makes it so the results are
|
|
||||||
# ordered nearest to farest from the query position
|
|
||||||
if self.cright is not EmptyNode:
|
|
||||||
self.cright._seek_left(position, results, n, max_dist)
|
|
||||||
|
|
||||||
if -1 < position - self.end < max_dist:
|
|
||||||
results.append(self.interval)
|
|
||||||
|
|
||||||
# TODO: can these conditionals be more stringent?
|
|
||||||
if self.cleft is not EmptyNode:
|
|
||||||
self.cleft._seek_left(position, results, n, max_dist)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
cdef void _seek_right(IntervalNode self, double position, list results, int n, double max_dist):
|
|
||||||
# we know we can bail in these 2 cases.
|
|
||||||
if self.maxend < position: return
|
|
||||||
if self.minstart - max_dist > position: return
|
|
||||||
|
|
||||||
#print "SEEK_RIGHT:",self, self.cleft, self.maxend, self.minstart, position
|
|
||||||
|
|
||||||
# the ordering of these 3 blocks makes it so the results are
|
|
||||||
# ordered nearest to farest from the query position
|
|
||||||
if self.cleft is not EmptyNode:
|
|
||||||
self.cleft._seek_right(position, results, n, max_dist)
|
|
||||||
|
|
||||||
if -1 < self.start - position < max_dist:
|
|
||||||
results.append(self.interval)
|
|
||||||
|
|
||||||
if self.cright is not EmptyNode:
|
|
||||||
self.cright._seek_right(position, results, n, max_dist)
|
|
||||||
|
|
||||||
|
|
||||||
cpdef left(self, position, int n=1, double max_dist=2500):
|
|
||||||
"""
|
|
||||||
find n features with a start > than `position`
|
|
||||||
f: a Interval object (or anything with an `end` attribute)
|
|
||||||
n: the number of features to return
|
|
||||||
max_dist: the maximum distance to look before giving up.
|
|
||||||
"""
|
|
||||||
cdef list results = []
|
|
||||||
# use start - 1 becuase .left() assumes strictly left-of
|
|
||||||
self._seek_left( position - 1, results, n, max_dist )
|
|
||||||
if len(results) == n: return results
|
|
||||||
r = results
|
|
||||||
r.sort(key=operator.attrgetter('end'), reverse=True)
|
|
||||||
return r[:n]
|
|
||||||
|
|
||||||
cpdef right(self, position, int n=1, double max_dist=2500):
|
|
||||||
"""
|
|
||||||
find n features with a end < than position
|
|
||||||
f: a Interval object (or anything with a `start` attribute)
|
|
||||||
n: the number of features to return
|
|
||||||
max_dist: the maximum distance to look before giving up.
|
|
||||||
"""
|
|
||||||
cdef list results = []
|
|
||||||
# use end + 1 becuase .right() assumes strictly right-of
|
|
||||||
self._seek_right(position + 1, results, n, max_dist)
|
|
||||||
if len(results) == n: return results
|
|
||||||
r = results
|
|
||||||
r.sort(key=operator.attrgetter('start'))
|
|
||||||
return r[:n]
|
|
||||||
|
|
||||||
def traverse(self):
|
|
||||||
if self.cleft is not EmptyNode:
|
|
||||||
for node in self.cleft.traverse():
|
|
||||||
yield node
|
|
||||||
yield self.interval
|
|
||||||
if self.cright is not EmptyNode:
|
|
||||||
for node in self.cright.traverse():
|
|
||||||
yield node
|
|
||||||
|
|
||||||
cdef IntervalNode EmptyNode = IntervalNode( 0, 0, Interval(0, 0))
|
|
||||||
|
|
||||||
## ---- Wrappers that retain the old interface -------------------------------
|
|
||||||
|
|
||||||
cdef class Interval:
|
|
||||||
"""
|
|
||||||
Basic feature, with required integer start and end properties.
|
|
||||||
Also accepts optional strand as +1 or -1 (used for up/downstream queries),
|
|
||||||
a name, and any arbitrary data is sent in on the info keyword argument
|
|
||||||
|
|
||||||
>>> from bx.intervals.intersection import Interval
|
|
||||||
|
|
||||||
>>> f1 = Interval(23, 36)
|
|
||||||
>>> f2 = Interval(34, 48, value={'chr':12, 'anno':'transposon'})
|
|
||||||
>>> f2
|
|
||||||
Interval(34, 48, value={'anno': 'transposon', 'chr': 12})
|
|
||||||
|
|
||||||
"""
|
|
||||||
cdef public double start, end
|
|
||||||
cdef public object value, chrom, strand
|
|
||||||
|
|
||||||
def __init__(self, double start, double end, object value=None, object chrom=None, object strand=None ):
|
|
||||||
assert start <= end, "start must be less than end"
|
|
||||||
self.start = start
|
|
||||||
self.end = end
|
|
||||||
self.value = value
|
|
||||||
self.chrom = chrom
|
|
||||||
self.strand = strand
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
fstr = "Interval(%g, %g" % (self.start, self.end)
|
|
||||||
if not self.value is None:
|
|
||||||
fstr += ", value=" + str(self.value)
|
|
||||||
fstr += ")"
|
|
||||||
return fstr
|
|
||||||
|
|
||||||
def __richcmp__(self, other, op):
|
|
||||||
if op == 0:
|
|
||||||
# <
|
|
||||||
return self.start < other.start or self.end < other.end
|
|
||||||
elif op == 1:
|
|
||||||
# <=
|
|
||||||
return self == other or self < other
|
|
||||||
elif op == 2:
|
|
||||||
# ==
|
|
||||||
return self.start == other.start and self.end == other.end
|
|
||||||
elif op == 3:
|
|
||||||
# !=
|
|
||||||
return self.start != other.start or self.end != other.end
|
|
||||||
elif op == 4:
|
|
||||||
# >
|
|
||||||
return self.start > other.start or self.end > other.end
|
|
||||||
elif op == 5:
|
|
||||||
# >=
|
|
||||||
return self == other or self > other
|
|
||||||
|
|
||||||
cdef class IntervalTree:
|
|
||||||
"""
|
|
||||||
Data structure for performing window intersect queries on a set of
|
|
||||||
of possibly overlapping 1d intervals.
|
|
||||||
|
|
||||||
Usage
|
|
||||||
=====
|
|
||||||
|
|
||||||
Create an empty IntervalTree
|
|
||||||
|
|
||||||
>>> from bx.intervals.intersection import Interval, IntervalTree
|
|
||||||
>>> intersecter = IntervalTree()
|
|
||||||
|
|
||||||
An interval is a start and end position and a value (possibly None).
|
|
||||||
You can add any object as an interval:
|
|
||||||
|
|
||||||
>>> intersecter.insert( 0, 10, "food" )
|
|
||||||
>>> intersecter.insert( 3, 7, dict(foo='bar') )
|
|
||||||
|
|
||||||
>>> intersecter.find( 2, 5 )
|
|
||||||
['food', {'foo': 'bar'}]
|
|
||||||
|
|
||||||
If the object has start and end attributes (like the Interval class) there
|
|
||||||
is are some shortcuts:
|
|
||||||
|
|
||||||
>>> intersecter = IntervalTree()
|
|
||||||
>>> intersecter.insert_interval( Interval( 0, 10 ) )
|
|
||||||
>>> intersecter.insert_interval( Interval( 3, 7 ) )
|
|
||||||
>>> intersecter.insert_interval( Interval( 3, 40 ) )
|
|
||||||
>>> intersecter.insert_interval( Interval( 13, 50 ) )
|
|
||||||
|
|
||||||
>>> intersecter.find( 30, 50 )
|
|
||||||
[Interval(3, 40), Interval(13, 50)]
|
|
||||||
>>> intersecter.find( 100, 200 )
|
|
||||||
[]
|
|
||||||
|
|
||||||
Before/after for intervals
|
|
||||||
|
|
||||||
>>> intersecter.before_interval( Interval( 10, 20 ) )
|
|
||||||
[Interval(3, 7)]
|
|
||||||
>>> intersecter.before_interval( Interval( 5, 20 ) )
|
|
||||||
[]
|
|
||||||
|
|
||||||
Upstream/downstream
|
|
||||||
|
|
||||||
>>> intersecter.upstream_of_interval(Interval(11, 12))
|
|
||||||
[Interval(0, 10)]
|
|
||||||
>>> intersecter.upstream_of_interval(Interval(11, 12, strand="-"))
|
|
||||||
[Interval(13, 50)]
|
|
||||||
|
|
||||||
>>> intersecter.upstream_of_interval(Interval(1, 2, strand="-"), num_intervals=3)
|
|
||||||
[Interval(3, 7), Interval(3, 40), Interval(13, 50)]
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
cdef IntervalNode root
|
|
||||||
|
|
||||||
def __cinit__( self ):
|
|
||||||
root = None
|
|
||||||
|
|
||||||
# ---- Position based interfaces -----------------------------------------
|
|
||||||
|
|
||||||
def insert( self, double start, double end, object value=None ):
|
|
||||||
"""
|
|
||||||
Insert the interval [start,end) associated with value `value`.
|
|
||||||
"""
|
|
||||||
if self.root is None:
|
|
||||||
self.root = IntervalNode( start, end, value )
|
|
||||||
else:
|
|
||||||
self.root = self.root.insert( start, end, value )
|
|
||||||
|
|
||||||
add = insert
|
|
||||||
|
|
||||||
|
|
||||||
def find( self, start, end ):
|
|
||||||
"""
|
|
||||||
Return a sorted list of all intervals overlapping [start,end).
|
|
||||||
"""
|
|
||||||
if self.root is None:
|
|
||||||
return []
|
|
||||||
return self.root.find( start, end )
|
|
||||||
|
|
||||||
def before( self, position, num_intervals=1, max_dist=2500 ):
|
|
||||||
"""
|
|
||||||
Find `num_intervals` intervals that lie before `position` and are no
|
|
||||||
further than `max_dist` positions away
|
|
||||||
"""
|
|
||||||
if self.root is None:
|
|
||||||
return []
|
|
||||||
return self.root.left( position, num_intervals, max_dist )
|
|
||||||
|
|
||||||
def after( self, position, num_intervals=1, max_dist=2500 ):
|
|
||||||
"""
|
|
||||||
Find `num_intervals` intervals that lie after `position` and are no
|
|
||||||
further than `max_dist` positions away
|
|
||||||
"""
|
|
||||||
if self.root is None:
|
|
||||||
return []
|
|
||||||
return self.root.right( position, num_intervals, max_dist )
|
|
||||||
|
|
||||||
# ---- Interval-like object based interfaces -----------------------------
|
|
||||||
|
|
||||||
def insert_interval( self, interval ):
|
|
||||||
"""
|
|
||||||
Insert an "interval" like object (one with at least start and end
|
|
||||||
attributes)
|
|
||||||
"""
|
|
||||||
self.insert( interval.start, interval.end, interval )
|
|
||||||
|
|
||||||
add_interval = insert_interval
|
|
||||||
|
|
||||||
def before_interval( self, interval, num_intervals=1, max_dist=2500 ):
|
|
||||||
"""
|
|
||||||
Find `num_intervals` intervals that lie completely before `interval`
|
|
||||||
and are no further than `max_dist` positions away
|
|
||||||
"""
|
|
||||||
if self.root is None:
|
|
||||||
return []
|
|
||||||
return self.root.left( interval.start, num_intervals, max_dist )
|
|
||||||
|
|
||||||
def after_interval( self, interval, num_intervals=1, max_dist=2500 ):
|
|
||||||
"""
|
|
||||||
Find `num_intervals` intervals that lie completely after `interval` and
|
|
||||||
are no further than `max_dist` positions away
|
|
||||||
"""
|
|
||||||
if self.root is None:
|
|
||||||
return []
|
|
||||||
return self.root.right( interval.end, num_intervals, max_dist )
|
|
||||||
|
|
||||||
def upstream_of_interval( self, interval, num_intervals=1, max_dist=2500 ):
|
|
||||||
"""
|
|
||||||
Find `num_intervals` intervals that lie completely upstream of
|
|
||||||
`interval` and are no further than `max_dist` positions away
|
|
||||||
"""
|
|
||||||
if self.root is None:
|
|
||||||
return []
|
|
||||||
if interval.strand == -1 or interval.strand == "-":
|
|
||||||
return self.root.right( interval.end, num_intervals, max_dist )
|
|
||||||
else:
|
|
||||||
return self.root.left( interval.start, num_intervals, max_dist )
|
|
||||||
|
|
||||||
def downstream_of_interval( self, interval, num_intervals=1, max_dist=2500 ):
|
|
||||||
"""
|
|
||||||
Find `num_intervals` intervals that lie completely downstream of
|
|
||||||
`interval` and are no further than `max_dist` positions away
|
|
||||||
"""
|
|
||||||
if self.root is None:
|
|
||||||
return []
|
|
||||||
if interval.strand == -1 or interval.strand == "-":
|
|
||||||
return self.root.left( interval.start, num_intervals, max_dist )
|
|
||||||
else:
|
|
||||||
return self.root.right( interval.end, num_intervals, max_dist )
|
|
||||||
|
|
||||||
def traverse(self):
|
|
||||||
"""
|
|
||||||
iterator that traverses the tree
|
|
||||||
"""
|
|
||||||
if self.root is None:
|
|
||||||
return iter([])
|
|
||||||
return self.root.traverse()
|
|
||||||
|
|
||||||
# For backward compatibility
|
|
||||||
Intersecter = IntervalTree
|
|
@@ -1,13 +1,16 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""Class for performing HTTP client requests via libcurl"""
|
"""Class for performing HTTP client requests via libcurl"""
|
||||||
|
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
|
import itertools
|
||||||
|
|
||||||
import nilmdb.httpclient
|
import nilmdb.httpclient
|
||||||
|
|
||||||
@@ -16,6 +19,10 @@ from nilmdb.httpclient import ClientError, ServerError, Error
|
|||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
|
|
||||||
|
def float_to_string(f):
|
||||||
|
# Use repr to maintain full precision in the string output.
|
||||||
|
return repr(float(f))
|
||||||
|
|
||||||
class Client(object):
|
class Client(object):
|
||||||
"""Main client interface to the Nilm database."""
|
"""Main client interface to the Nilm database."""
|
||||||
|
|
||||||
@@ -84,33 +91,82 @@ class Client(object):
|
|||||||
"layout" : layout }
|
"layout" : layout }
|
||||||
return self.http.get("stream/create", params)
|
return self.http.get("stream/create", params)
|
||||||
|
|
||||||
def stream_insert(self, path, data):
|
def stream_destroy(self, path):
|
||||||
|
"""Delete stream and its contents"""
|
||||||
|
params = { "path": path }
|
||||||
|
return self.http.get("stream/destroy", params)
|
||||||
|
|
||||||
|
def stream_insert(self, path, data, start = None, end = None):
|
||||||
"""Insert data into a stream. data should be a file-like object
|
"""Insert data into a stream. data should be a file-like object
|
||||||
that provides ASCII data that matches the database layout for path."""
|
that provides ASCII data that matches the database layout for path.
|
||||||
|
|
||||||
|
start and end are the starting and ending timestamp of this
|
||||||
|
stream; all timestamps t in the data must satisfy 'start <= t
|
||||||
|
< end'. If left unspecified, 'start' is the timestamp of the
|
||||||
|
first line of data, and 'end' is the timestamp on the last line
|
||||||
|
of data, plus a small delta of 1μs.
|
||||||
|
"""
|
||||||
params = { "path": path }
|
params = { "path": path }
|
||||||
|
|
||||||
# See design.md for a discussion of how much data to send.
|
# See design.md for a discussion of how much data to send.
|
||||||
# These are soft limits -- actual data might be rounded up.
|
# These are soft limits -- actual data might be rounded up.
|
||||||
max_data = 1048576
|
max_data = 1048576
|
||||||
max_time = 30
|
max_time = 30
|
||||||
|
end_epsilon = 1e-6
|
||||||
|
|
||||||
|
def pairwise(iterable):
|
||||||
|
"s -> (s0,s1), (s1,s2), ..., (sn,None)"
|
||||||
|
a, b = itertools.tee(iterable)
|
||||||
|
next(b, None)
|
||||||
|
return itertools.izip_longest(a, b)
|
||||||
|
|
||||||
|
def extract_timestamp(line):
|
||||||
|
return float(line.split()[0])
|
||||||
|
|
||||||
def sendit():
|
def sendit():
|
||||||
result = self.http.put("stream/insert", send_data, params)
|
# If we have more data after this, use the timestamp of
|
||||||
params["old_timestamp"] = result[1]
|
# the next line as the end. Otherwise, use the given
|
||||||
return result
|
# overall end time, or add end_epsilon to the last data
|
||||||
|
# point.
|
||||||
|
if nextline:
|
||||||
|
block_end = extract_timestamp(nextline)
|
||||||
|
if end and block_end > end:
|
||||||
|
# This is unexpected, but we'll defer to the server
|
||||||
|
# to return an error in this case.
|
||||||
|
block_end = end
|
||||||
|
elif end:
|
||||||
|
block_end = end
|
||||||
|
else:
|
||||||
|
block_end = extract_timestamp(line) + end_epsilon
|
||||||
|
|
||||||
|
# Send it
|
||||||
|
params["start"] = float_to_string(block_start)
|
||||||
|
params["end"] = float_to_string(block_end)
|
||||||
|
return self.http.put("stream/insert", block_data, params)
|
||||||
|
|
||||||
|
clock_start = time.time()
|
||||||
|
block_data = ""
|
||||||
|
block_start = start
|
||||||
result = None
|
result = None
|
||||||
start = time.time()
|
for (line, nextline) in pairwise(data):
|
||||||
send_data = ""
|
# If we don't have a starting time, extract it from the first line
|
||||||
for line in data:
|
if block_start is None:
|
||||||
elapsed = time.time() - start
|
block_start = extract_timestamp(line)
|
||||||
send_data += line
|
|
||||||
|
|
||||||
if (len(send_data) > max_data) or (elapsed > max_time):
|
clock_elapsed = time.time() - clock_start
|
||||||
|
block_data += line
|
||||||
|
|
||||||
|
# If we have enough data, or enough time has elapsed,
|
||||||
|
# send this block to the server, and empty things out
|
||||||
|
# for the next block.
|
||||||
|
if (len(block_data) > max_data) or (clock_elapsed > max_time):
|
||||||
result = sendit()
|
result = sendit()
|
||||||
send_data = ""
|
block_start = None
|
||||||
start = time.time()
|
block_data = ""
|
||||||
if len(send_data):
|
clock_start = time.time()
|
||||||
|
|
||||||
|
# One last block?
|
||||||
|
if len(block_data):
|
||||||
result = sendit()
|
result = sendit()
|
||||||
|
|
||||||
# Return the most recent JSON result we got back, or None if
|
# Return the most recent JSON result we got back, or None if
|
||||||
@@ -125,9 +181,9 @@ class Client(object):
|
|||||||
"path": path
|
"path": path
|
||||||
}
|
}
|
||||||
if start is not None:
|
if start is not None:
|
||||||
params["start"] = repr(start) # use repr to keep precision
|
params["start"] = float_to_string(start)
|
||||||
if end is not None:
|
if end is not None:
|
||||||
params["end"] = repr(end)
|
params["end"] = float_to_string(end)
|
||||||
return self.http.get_gen("stream/intervals", params, retjson = True)
|
return self.http.get_gen("stream/intervals", params, retjson = True)
|
||||||
|
|
||||||
def stream_extract(self, path, start = None, end = None, count = False):
|
def stream_extract(self, path, start = None, end = None, count = False):
|
||||||
@@ -143,9 +199,9 @@ class Client(object):
|
|||||||
"path": path,
|
"path": path,
|
||||||
}
|
}
|
||||||
if start is not None:
|
if start is not None:
|
||||||
params["start"] = repr(start) # use repr to keep precision
|
params["start"] = float_to_string(start)
|
||||||
if end is not None:
|
if end is not None:
|
||||||
params["end"] = repr(end)
|
params["end"] = float_to_string(end)
|
||||||
if count:
|
if count:
|
||||||
params["count"] = 1
|
params["count"] = 1
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""Command line client functionality"""
|
"""Command line client functionality"""
|
||||||
|
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
import nilmdb.client
|
import nilmdb.client
|
||||||
|
|
||||||
import datetime_tz
|
import datetime_tz
|
||||||
@@ -15,7 +15,8 @@ version = "0.1"
|
|||||||
|
|
||||||
# Valid subcommands. Defined in separate files just to break
|
# Valid subcommands. Defined in separate files just to break
|
||||||
# things up -- they're still called with Cmdline as self.
|
# things up -- they're still called with Cmdline as self.
|
||||||
subcommands = [ "info", "create", "list", "metadata", "insert", "extract" ]
|
subcommands = [ "info", "create", "list", "metadata", "insert", "extract",
|
||||||
|
"destroy" ]
|
||||||
|
|
||||||
# Import the subcommand modules. Equivalent way of doing this would be
|
# Import the subcommand modules. Equivalent way of doing this would be
|
||||||
# from . import info as cmd_info
|
# from . import info as cmd_info
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
import nilmdb.client
|
import nilmdb.client
|
||||||
|
|
||||||
from argparse import ArgumentDefaultsHelpFormatter as def_form
|
from argparse import ArgumentDefaultsHelpFormatter as def_form
|
||||||
|
25
nilmdb/cmdline/destroy.py
Normal file
25
nilmdb/cmdline/destroy.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from __future__ import absolute_import
|
||||||
|
from nilmdb.utils.printf import *
|
||||||
|
import nilmdb.client
|
||||||
|
|
||||||
|
from argparse import ArgumentDefaultsHelpFormatter as def_form
|
||||||
|
|
||||||
|
def setup(self, sub):
|
||||||
|
cmd = sub.add_parser("destroy", help="Delete a stream and all data",
|
||||||
|
formatter_class = def_form,
|
||||||
|
description="""
|
||||||
|
Destroy the stream at the specified path. All
|
||||||
|
data and metadata related to the stream is
|
||||||
|
permanently deleted.
|
||||||
|
""")
|
||||||
|
cmd.set_defaults(handler = cmd_destroy)
|
||||||
|
group = cmd.add_argument_group("Required arguments")
|
||||||
|
group.add_argument("path",
|
||||||
|
help="Path of the stream to delete, e.g. /foo/bar")
|
||||||
|
|
||||||
|
def cmd_destroy(self):
|
||||||
|
"""Destroy stream"""
|
||||||
|
try:
|
||||||
|
self.client.stream_destroy(self.args.path)
|
||||||
|
except nilmdb.client.ClientError as e:
|
||||||
|
self.die("Error deleting stream: %s", str(e))
|
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
import nilmdb.client
|
import nilmdb.client
|
||||||
import nilmdb.layout
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
def setup(self, sub):
|
def setup(self, sub):
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
from argparse import ArgumentDefaultsHelpFormatter as def_form
|
from argparse import ArgumentDefaultsHelpFormatter as def_form
|
||||||
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
import nilmdb.client
|
import nilmdb.client
|
||||||
import nilmdb.layout
|
|
||||||
import nilmdb.timestamper
|
import nilmdb.timestamper
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
import nilmdb.client
|
import nilmdb.client
|
||||||
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
import nilmdb.client
|
import nilmdb.client
|
||||||
|
|
||||||
def setup(self, sub):
|
def setup(self, sub):
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
"""HTTP client library"""
|
"""HTTP client library"""
|
||||||
|
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
|
import nilmdb.utils
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
@@ -13,8 +14,6 @@ import urllib
|
|||||||
import pycurl
|
import pycurl
|
||||||
import cStringIO
|
import cStringIO
|
||||||
|
|
||||||
import nilmdb.iteratorizer
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
"""Base exception for both ClientError and ServerError responses"""
|
"""Base exception for both ClientError and ServerError responses"""
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
@@ -85,6 +84,10 @@ class HTTPClient(object):
|
|||||||
raise ClientError(**args)
|
raise ClientError(**args)
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
if code >= 500 and code <= 599:
|
if code >= 500 and code <= 599:
|
||||||
|
if args["message"] is None:
|
||||||
|
args["message"] = ("(no message; try disabling " +
|
||||||
|
"response.stream option in " +
|
||||||
|
"nilmdb.server for better debugging)")
|
||||||
raise ServerError(**args)
|
raise ServerError(**args)
|
||||||
else:
|
else:
|
||||||
raise Error(**args)
|
raise Error(**args)
|
||||||
@@ -109,7 +112,7 @@ class HTTPClient(object):
|
|||||||
self.curl.setopt(pycurl.WRITEFUNCTION, callback)
|
self.curl.setopt(pycurl.WRITEFUNCTION, callback)
|
||||||
self.curl.perform()
|
self.curl.perform()
|
||||||
try:
|
try:
|
||||||
for i in nilmdb.iteratorizer.Iteratorizer(func):
|
for i in nilmdb.utils.Iteratorizer(func):
|
||||||
if self._status == 200:
|
if self._status == 200:
|
||||||
# If we had a 200 response, yield the data to the caller.
|
# If we had a 200 response, yield the data to the caller.
|
||||||
yield i
|
yield i
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
"""Interval and IntervalSet
|
"""Interval, IntervalSet
|
||||||
|
|
||||||
Represents an interval of time, and a set of such intervals.
|
Represents an interval of time, and a set of such intervals.
|
||||||
|
|
||||||
Intervals are closed, ie. they include timestamps [start, end]
|
Intervals are half-open, ie. they include data points with timestamps
|
||||||
|
[start, end)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# First implementation kept a sorted list of intervals and used
|
# First implementation kept a sorted list of intervals and used
|
||||||
@@ -18,16 +19,20 @@ Intervals are closed, ie. they include timestamps [start, end]
|
|||||||
# Fourth version is an optimized rb-tree that stores interval starts
|
# Fourth version is an optimized rb-tree that stores interval starts
|
||||||
# and ends directly in the tree, like bxinterval did.
|
# and ends directly in the tree, like bxinterval did.
|
||||||
|
|
||||||
import rbtree
|
cimport rbtree
|
||||||
|
cdef extern from "stdint.h":
|
||||||
|
ctypedef unsigned long long uint64_t
|
||||||
|
|
||||||
class IntervalError(Exception):
|
class IntervalError(Exception):
|
||||||
"""Error due to interval overlap, etc"""
|
"""Error due to interval overlap, etc"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class Interval(object):
|
cdef class Interval:
|
||||||
"""Represents an interval of time."""
|
"""Represents an interval of time."""
|
||||||
|
|
||||||
def __init__(self, start, end):
|
cdef public double start, end
|
||||||
|
|
||||||
|
def __init__(self, double start, double end):
|
||||||
"""
|
"""
|
||||||
'start' and 'end' are arbitrary floats that represent time
|
'start' and 'end' are arbitrary floats that represent time
|
||||||
"""
|
"""
|
||||||
@@ -41,9 +46,9 @@ class Interval(object):
|
|||||||
return self.__class__.__name__ + "(" + s + ")"
|
return self.__class__.__name__ + "(" + s + ")"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "[" + str(self.start) + " -> " + str(self.end) + "]"
|
return "[" + repr(self.start) + " -> " + repr(self.end) + ")"
|
||||||
|
|
||||||
def __cmp__(self, other):
|
def __cmp__(self, Interval other):
|
||||||
"""Compare two intervals. If non-equal, order by start then end"""
|
"""Compare two intervals. If non-equal, order by start then end"""
|
||||||
if not isinstance(other, Interval):
|
if not isinstance(other, Interval):
|
||||||
raise TypeError("bad type")
|
raise TypeError("bad type")
|
||||||
@@ -57,20 +62,20 @@ class Interval(object):
|
|||||||
return -1
|
return -1
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def intersects(self, other):
|
cpdef intersects(self, Interval other):
|
||||||
"""Return True if two Interval objects intersect"""
|
"""Return True if two Interval objects intersect"""
|
||||||
if (self.end <= other.start or self.start >= other.end):
|
if (self.end <= other.start or self.start >= other.end):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def subset(self, start, end):
|
cpdef subset(self, double start, double end):
|
||||||
"""Return a new Interval that is a subset of this one"""
|
"""Return a new Interval that is a subset of this one"""
|
||||||
# A subclass that tracks additional data might override this.
|
# A subclass that tracks additional data might override this.
|
||||||
if start < self.start or end > self.end:
|
if start < self.start or end > self.end:
|
||||||
raise IntervalError("not a subset")
|
raise IntervalError("not a subset")
|
||||||
return Interval(start, end)
|
return Interval(start, end)
|
||||||
|
|
||||||
class DBInterval(Interval):
|
cdef class DBInterval(Interval):
|
||||||
"""
|
"""
|
||||||
Like Interval, but also tracks corresponding start/end times and
|
Like Interval, but also tracks corresponding start/end times and
|
||||||
positions within the database. These are not currently modified
|
positions within the database. These are not currently modified
|
||||||
@@ -85,6 +90,9 @@ class DBInterval(Interval):
|
|||||||
db_end = 200, db_endpos = 20000
|
db_end = 200, db_endpos = 20000
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
cpdef public double db_start, db_end
|
||||||
|
cpdef public uint64_t db_startpos, db_endpos
|
||||||
|
|
||||||
def __init__(self, start, end,
|
def __init__(self, start, end,
|
||||||
db_start, db_end,
|
db_start, db_end,
|
||||||
db_startpos, db_endpos):
|
db_startpos, db_endpos):
|
||||||
@@ -109,7 +117,7 @@ class DBInterval(Interval):
|
|||||||
s += ", " + repr(self.db_startpos) + ", " + repr(self.db_endpos)
|
s += ", " + repr(self.db_startpos) + ", " + repr(self.db_endpos)
|
||||||
return self.__class__.__name__ + "(" + s + ")"
|
return self.__class__.__name__ + "(" + s + ")"
|
||||||
|
|
||||||
def subset(self, start, end):
|
cpdef subset(self, double start, double end):
|
||||||
"""
|
"""
|
||||||
Return a new DBInterval that is a subset of this one
|
Return a new DBInterval that is a subset of this one
|
||||||
"""
|
"""
|
||||||
@@ -119,11 +127,13 @@ class DBInterval(Interval):
|
|||||||
self.db_start, self.db_end,
|
self.db_start, self.db_end,
|
||||||
self.db_startpos, self.db_endpos)
|
self.db_startpos, self.db_endpos)
|
||||||
|
|
||||||
class IntervalSet(object):
|
cdef class IntervalSet:
|
||||||
"""
|
"""
|
||||||
A non-intersecting set of intervals.
|
A non-intersecting set of intervals.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
cdef public rbtree.RBTree tree
|
||||||
|
|
||||||
def __init__(self, source=None):
|
def __init__(self, source=None):
|
||||||
"""
|
"""
|
||||||
'source' is an Interval or IntervalSet to add.
|
'source' is an Interval or IntervalSet to add.
|
||||||
@@ -148,7 +158,7 @@ class IntervalSet(object):
|
|||||||
descs = [ str(x) for x in self ]
|
descs = [ str(x) for x in self ]
|
||||||
return "[" + ", ".join(descs) + "]"
|
return "[" + ", ".join(descs) + "]"
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __match__(self, other):
|
||||||
# This isn't particularly efficient, but it shouldn't get used in the
|
# This isn't particularly efficient, but it shouldn't get used in the
|
||||||
# general case.
|
# general case.
|
||||||
"""Test equality of two IntervalSets.
|
"""Test equality of two IntervalSets.
|
||||||
@@ -167,8 +177,8 @@ class IntervalSet(object):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
this = [ x for x in self ]
|
this = list(self)
|
||||||
that = [ x for x in other ]
|
that = list(other)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
@@ -199,10 +209,20 @@ class IntervalSet(object):
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __ne__(self, other):
|
# Use __richcmp__ instead of __eq__, __ne__ for Cython.
|
||||||
return not self.__eq__(other)
|
def __richcmp__(self, other, int op):
|
||||||
|
if op == 2: # ==
|
||||||
|
return self.__match__(other)
|
||||||
|
elif op == 3: # !=
|
||||||
|
return not self.__match__(other)
|
||||||
|
return False
|
||||||
|
#def __eq__(self, other):
|
||||||
|
# return self.__match__(other)
|
||||||
|
#
|
||||||
|
#def __ne__(self, other):
|
||||||
|
# return not self.__match__(other)
|
||||||
|
|
||||||
def __iadd__(self, other):
|
def __iadd__(self, object other not None):
|
||||||
"""Inplace add -- modifies self
|
"""Inplace add -- modifies self
|
||||||
|
|
||||||
This throws an exception if the regions being added intersect."""
|
This throws an exception if the regions being added intersect."""
|
||||||
@@ -210,13 +230,19 @@ class IntervalSet(object):
|
|||||||
if self.intersects(other):
|
if self.intersects(other):
|
||||||
raise IntervalError("Tried to add overlapping interval "
|
raise IntervalError("Tried to add overlapping interval "
|
||||||
"to this set")
|
"to this set")
|
||||||
self.tree.insert(rbtree.RBNode(other))
|
self.tree.insert(rbtree.RBNode(other.start, other.end, other))
|
||||||
else:
|
else:
|
||||||
for x in other:
|
for x in other:
|
||||||
self.__iadd__(x)
|
self.__iadd__(x)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __isub__(self, other):
|
def iadd_nocheck(self, Interval other not None):
|
||||||
|
"""Inplace add -- modifies self.
|
||||||
|
'Optimized' version that doesn't check for intersection and
|
||||||
|
only inserts the new interval into the tree."""
|
||||||
|
self.tree.insert(rbtree.RBNode(other.start, other.end, other))
|
||||||
|
|
||||||
|
def __isub__(self, Interval other not None):
|
||||||
"""Inplace subtract -- modifies self
|
"""Inplace subtract -- modifies self
|
||||||
|
|
||||||
Removes an interval from the set. Must exist exactly
|
Removes an interval from the set. Must exist exactly
|
||||||
@@ -227,13 +253,13 @@ class IntervalSet(object):
|
|||||||
self.tree.delete(i)
|
self.tree.delete(i)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __add__(self, other):
|
def __add__(self, other not None):
|
||||||
"""Add -- returns a new object"""
|
"""Add -- returns a new object"""
|
||||||
new = IntervalSet(self)
|
new = IntervalSet(self)
|
||||||
new += IntervalSet(other)
|
new += IntervalSet(other)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def __and__(self, other):
|
def __and__(self, other not None):
|
||||||
"""
|
"""
|
||||||
Compute a new IntervalSet from the intersection of two others
|
Compute a new IntervalSet from the intersection of two others
|
||||||
|
|
||||||
@@ -244,15 +270,15 @@ class IntervalSet(object):
|
|||||||
|
|
||||||
if not isinstance(other, IntervalSet):
|
if not isinstance(other, IntervalSet):
|
||||||
for i in self.intersection(other):
|
for i in self.intersection(other):
|
||||||
out.tree.insert(rbtree.RBNode(i))
|
out.tree.insert(rbtree.RBNode(i.start, i.end, i))
|
||||||
else:
|
else:
|
||||||
for x in other:
|
for x in other:
|
||||||
for i in self.intersection(x):
|
for i in self.intersection(x):
|
||||||
out.tree.insert(rbtree.RBNode(i))
|
out.tree.insert(rbtree.RBNode(i.start, i.end, i))
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def intersection(self, interval):
|
def intersection(self, Interval interval not None):
|
||||||
"""
|
"""
|
||||||
Compute a sequence of intervals that correspond to the
|
Compute a sequence of intervals that correspond to the
|
||||||
intersection between `self` and the provided interval.
|
intersection between `self` and the provided interval.
|
||||||
@@ -269,23 +295,24 @@ class IntervalSet(object):
|
|||||||
if i:
|
if i:
|
||||||
if i.start >= interval.start and i.end <= interval.end:
|
if i.start >= interval.start and i.end <= interval.end:
|
||||||
yield i
|
yield i
|
||||||
elif i.start > interval.end:
|
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
subset = i.subset(max(i.start, interval.start),
|
subset = i.subset(max(i.start, interval.start),
|
||||||
min(i.end, interval.end))
|
min(i.end, interval.end))
|
||||||
yield subset
|
yield subset
|
||||||
|
|
||||||
def intersects(self, other):
|
cpdef intersects(self, Interval other):
|
||||||
### PROBABLY WRONG
|
|
||||||
"""Return True if this IntervalSet intersects another interval"""
|
"""Return True if this IntervalSet intersects another interval"""
|
||||||
node = self.tree.find_left(other.start, other.end)
|
for n in self.tree.intersect(other.start, other.end):
|
||||||
if node is None:
|
if n.obj.intersects(other):
|
||||||
return False
|
return True
|
||||||
for n in self.tree.inorder(node):
|
|
||||||
if n.obj:
|
|
||||||
if n.obj.intersects(other):
|
|
||||||
return True
|
|
||||||
if n.obj > other:
|
|
||||||
break
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def find_end(self, double t):
|
||||||
|
"""
|
||||||
|
Return an Interval from this tree that ends at time t, or
|
||||||
|
None if it doesn't exist.
|
||||||
|
"""
|
||||||
|
n = self.tree.find_left_end(t)
|
||||||
|
if n and n.obj.end == t:
|
||||||
|
return n.obj
|
||||||
|
return None
|
1
nilmdb/interval.pyxdep
Normal file
1
nilmdb/interval.pyxdep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rbtree.pxd
|
@@ -1,6 +1,5 @@
|
|||||||
# cython: profile=False
|
# cython: profile=False
|
||||||
|
|
||||||
import tables
|
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import inspect
|
import inspect
|
||||||
@@ -122,15 +121,6 @@ class Layout:
|
|||||||
s += " %d" % d[i+1]
|
s += " %d" % d[i+1]
|
||||||
return s + "\n"
|
return s + "\n"
|
||||||
|
|
||||||
# PyTables description
|
|
||||||
def description(self):
|
|
||||||
"""Return the PyTables description of this layout"""
|
|
||||||
desc = {}
|
|
||||||
desc['timestamp'] = tables.Col.from_type('float64', pos=0)
|
|
||||||
for n in range(self.count):
|
|
||||||
desc['c' + str(n+1)] = tables.Col.from_type(self.datatype, pos=n+1)
|
|
||||||
return tables.Description(desc)
|
|
||||||
|
|
||||||
# Get a layout by name
|
# Get a layout by name
|
||||||
def get_named(typestring):
|
def get_named(typestring):
|
||||||
try:
|
try:
|
||||||
|
224
nilmdb/nilmdb.py
224
nilmdb/nilmdb.py
@@ -4,17 +4,16 @@
|
|||||||
|
|
||||||
Object that represents a NILM database file.
|
Object that represents a NILM database file.
|
||||||
|
|
||||||
Manages both the SQL database and the PyTables storage backend.
|
Manages both the SQL database and the table storage backend.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Need absolute_import so that "import nilmdb" won't pull in nilmdb.py,
|
# Need absolute_import so that "import nilmdb" won't pull in nilmdb.py,
|
||||||
# but will pull the nilmdb module instead.
|
# but will pull the nilmdb module instead.
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
import nilmdb
|
import nilmdb
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import tables
|
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -25,6 +24,8 @@ import pyximport
|
|||||||
pyximport.install()
|
pyximport.install()
|
||||||
from nilmdb.interval import Interval, DBInterval, IntervalSet, IntervalError
|
from nilmdb.interval import Interval, DBInterval, IntervalSet, IntervalError
|
||||||
|
|
||||||
|
from . import bulkdata
|
||||||
|
|
||||||
# Note about performance and transactions:
|
# Note about performance and transactions:
|
||||||
#
|
#
|
||||||
# Committing a transaction in the default sync mode (PRAGMA synchronous=FULL)
|
# Committing a transaction in the default sync mode (PRAGMA synchronous=FULL)
|
||||||
@@ -87,19 +88,13 @@ class StreamError(NilmDBError):
|
|||||||
class OverlapError(NilmDBError):
|
class OverlapError(NilmDBError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Helper that lets us pass a Pytables table into bisect
|
@nilmdb.utils.must_close()
|
||||||
class BisectableTable(object):
|
|
||||||
def __init__(self, table):
|
|
||||||
self.table = table
|
|
||||||
def __getitem__(self, index):
|
|
||||||
return self.table[index][0]
|
|
||||||
|
|
||||||
class NilmDB(object):
|
class NilmDB(object):
|
||||||
verbose = 0
|
verbose = 0
|
||||||
|
|
||||||
def __init__(self, basepath, sync=True, max_results=None):
|
def __init__(self, basepath, sync=True, max_results=None):
|
||||||
# set up path
|
# set up path
|
||||||
self.basepath = os.path.abspath(basepath.rstrip('/'))
|
self.basepath = os.path.abspath(basepath)
|
||||||
|
|
||||||
# Create the database path if it doesn't exist
|
# Create the database path if it doesn't exist
|
||||||
try:
|
try:
|
||||||
@@ -108,16 +103,16 @@ class NilmDB(object):
|
|||||||
if e.errno != errno.EEXIST:
|
if e.errno != errno.EEXIST:
|
||||||
raise IOError("can't create tree " + self.basepath)
|
raise IOError("can't create tree " + self.basepath)
|
||||||
|
|
||||||
# Our HD5 file goes inside it
|
# Our data goes inside it
|
||||||
h5filename = os.path.abspath(self.basepath + "/data.h5")
|
self.data = bulkdata.BulkData(self.basepath)
|
||||||
self.h5file = tables.openFile(h5filename, "a", "NILM Database")
|
|
||||||
|
|
||||||
# SQLite database too
|
# SQLite database too
|
||||||
sqlfilename = os.path.abspath(self.basepath + "/data.sql")
|
sqlfilename = os.path.join(self.basepath, "data.sql")
|
||||||
# We use check_same_thread = False, assuming that the rest
|
# We use check_same_thread = False, assuming that the rest
|
||||||
# of the code (e.g. Server) will be smart and not access this
|
# of the code (e.g. Server) will be smart and not access this
|
||||||
# database from multiple threads simultaneously. That requirement
|
# database from multiple threads simultaneously. Otherwise
|
||||||
# may be relaxed later.
|
# false positives will occur when the database is only opened
|
||||||
|
# in one thread, and only accessed in another.
|
||||||
self.con = sqlite3.connect(sqlfilename, check_same_thread = False)
|
self.con = sqlite3.connect(sqlfilename, check_same_thread = False)
|
||||||
self._sql_schema_update()
|
self._sql_schema_update()
|
||||||
|
|
||||||
@@ -134,17 +129,6 @@ class NilmDB(object):
|
|||||||
else:
|
else:
|
||||||
self.max_results = 16384
|
self.max_results = 16384
|
||||||
|
|
||||||
self.opened = True
|
|
||||||
|
|
||||||
# Cached intervals
|
|
||||||
self._cached_iset = {}
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
if "opened" in self.__dict__: # pragma: no cover
|
|
||||||
fprintf(sys.stderr,
|
|
||||||
"error: NilmDB.close() wasn't called, path %s",
|
|
||||||
self.basepath)
|
|
||||||
|
|
||||||
def get_basepath(self):
|
def get_basepath(self):
|
||||||
return self.basepath
|
return self.basepath
|
||||||
|
|
||||||
@@ -152,8 +136,7 @@ class NilmDB(object):
|
|||||||
if self.con:
|
if self.con:
|
||||||
self.con.commit()
|
self.con.commit()
|
||||||
self.con.close()
|
self.con.close()
|
||||||
self.h5file.close()
|
self.data.close()
|
||||||
del self.opened
|
|
||||||
|
|
||||||
def _sql_schema_update(self):
|
def _sql_schema_update(self):
|
||||||
cur = self.con.cursor()
|
cur = self.con.cursor()
|
||||||
@@ -170,58 +153,78 @@ class NilmDB(object):
|
|||||||
with self.con:
|
with self.con:
|
||||||
cur.execute("PRAGMA user_version = {v:d}".format(v=version))
|
cur.execute("PRAGMA user_version = {v:d}".format(v=version))
|
||||||
|
|
||||||
|
@nilmdb.utils.lru_cache(size = 16)
|
||||||
def _get_intervals(self, stream_id):
|
def _get_intervals(self, stream_id):
|
||||||
"""
|
"""
|
||||||
Return a mutable IntervalSet corresponding to the given stream ID.
|
Return a mutable IntervalSet corresponding to the given stream ID.
|
||||||
"""
|
"""
|
||||||
# Load from database if not cached
|
iset = IntervalSet()
|
||||||
if stream_id not in self._cached_iset:
|
result = self.con.execute("SELECT start_time, end_time, "
|
||||||
iset = IntervalSet()
|
"start_pos, end_pos "
|
||||||
result = self.con.execute("SELECT start_time, end_time, "
|
"FROM ranges "
|
||||||
"start_pos, end_pos "
|
"WHERE stream_id=?", (stream_id,))
|
||||||
"FROM ranges "
|
try:
|
||||||
"WHERE stream_id=?", (stream_id,))
|
for (start_time, end_time, start_pos, end_pos) in result:
|
||||||
try:
|
iset += DBInterval(start_time, end_time,
|
||||||
for (start_time, end_time, start_pos, end_pos) in result:
|
start_time, end_time,
|
||||||
iset += DBInterval(start_time, end_time,
|
start_pos, end_pos)
|
||||||
start_time, end_time,
|
except IntervalError as e: # pragma: no cover
|
||||||
start_pos, end_pos)
|
raise NilmDBError("unexpected overlap in ranges table!")
|
||||||
except IntervalError as e: # pragma: no cover
|
|
||||||
raise NilmDBError("unexpected overlap in ranges table!")
|
|
||||||
self._cached_iset[stream_id] = iset
|
|
||||||
# Return cached value
|
|
||||||
return self._cached_iset[stream_id]
|
|
||||||
|
|
||||||
# TODO: Split add_interval into two pieces, one to add
|
return iset
|
||||||
# and one to flush to disk?
|
|
||||||
# Need to think about this. Basic problem is that we can't
|
|
||||||
# mess with intervals once they're in the IntervalSet,
|
|
||||||
# without mucking with bxinterval internals.
|
|
||||||
|
|
||||||
# Maybe add a separate optimization step?
|
|
||||||
# Join intervals that have a fairly small gap between them
|
|
||||||
|
|
||||||
def _add_interval(self, stream_id, interval, start_pos, end_pos):
|
def _add_interval(self, stream_id, interval, start_pos, end_pos):
|
||||||
"""
|
"""
|
||||||
Add interval to the internal interval cache, and to the database.
|
Add interval to the internal interval cache, and to the database.
|
||||||
Note: arguments must be ints (not numpy.int64, etc)
|
Note: arguments must be ints (not numpy.int64, etc)
|
||||||
"""
|
"""
|
||||||
# Ensure this stream's intervals are cached, and add the new
|
# Load this stream's intervals
|
||||||
# interval to that cache.
|
|
||||||
iset = self._get_intervals(stream_id)
|
iset = self._get_intervals(stream_id)
|
||||||
try:
|
|
||||||
iset += DBInterval(interval.start, interval.end,
|
# Check for overlap
|
||||||
interval.start, interval.end,
|
if iset.intersects(interval): # pragma: no cover (gets caught earlier)
|
||||||
start_pos, end_pos)
|
|
||||||
except IntervalError as e: # pragma: no cover
|
|
||||||
raise NilmDBError("new interval overlaps existing data")
|
raise NilmDBError("new interval overlaps existing data")
|
||||||
|
|
||||||
|
# Check for adjacency. If there's a stream in the database
|
||||||
|
# that ends exactly when this one starts, and the database
|
||||||
|
# rows match up, we can make one interval that covers the
|
||||||
|
# time range [adjacent.start -> interval.end)
|
||||||
|
# and database rows [ adjacent.start_pos -> end_pos ].
|
||||||
|
# Only do this if the resulting interval isn't too large.
|
||||||
|
max_merged_rows = 30000000 # a bit more than 1 hour at 8 KHz
|
||||||
|
adjacent = iset.find_end(interval.start)
|
||||||
|
if (adjacent is not None and
|
||||||
|
start_pos == adjacent.db_endpos and
|
||||||
|
(end_pos - adjacent.db_startpos) < max_merged_rows):
|
||||||
|
# First delete the old one, both from our iset and the
|
||||||
|
# database
|
||||||
|
iset -= adjacent
|
||||||
|
self.con.execute("DELETE FROM ranges WHERE "
|
||||||
|
"stream_id=? AND start_time=? AND "
|
||||||
|
"end_time=? AND start_pos=? AND "
|
||||||
|
"end_pos=?", (stream_id,
|
||||||
|
adjacent.db_start,
|
||||||
|
adjacent.db_end,
|
||||||
|
adjacent.db_startpos,
|
||||||
|
adjacent.db_endpos))
|
||||||
|
|
||||||
|
# Now update our interval so the fallthrough add is
|
||||||
|
# correct.
|
||||||
|
interval.start = adjacent.start
|
||||||
|
start_pos = adjacent.db_startpos
|
||||||
|
|
||||||
|
# Add the new interval to the iset
|
||||||
|
iset.iadd_nocheck(DBInterval(interval.start, interval.end,
|
||||||
|
interval.start, interval.end,
|
||||||
|
start_pos, end_pos))
|
||||||
|
|
||||||
# Insert into the database
|
# Insert into the database
|
||||||
self.con.execute("INSERT INTO ranges "
|
self.con.execute("INSERT INTO ranges "
|
||||||
"(stream_id,start_time,end_time,start_pos,end_pos) "
|
"(stream_id,start_time,end_time,start_pos,end_pos) "
|
||||||
"VALUES (?,?,?,?,?)",
|
"VALUES (?,?,?,?,?)",
|
||||||
(stream_id, interval.start, interval.end,
|
(stream_id, interval.start, interval.end,
|
||||||
int(start_pos), int(end_pos)))
|
int(start_pos), int(end_pos)))
|
||||||
|
|
||||||
self.con.commit()
|
self.con.commit()
|
||||||
|
|
||||||
def stream_list(self, path = None, layout = None):
|
def stream_list(self, path = None, layout = None):
|
||||||
@@ -285,38 +288,11 @@ class NilmDB(object):
|
|||||||
|
|
||||||
layout_name: string for nilmdb.layout.get_named(), e.g. 'float32_8'
|
layout_name: string for nilmdb.layout.get_named(), e.g. 'float32_8'
|
||||||
"""
|
"""
|
||||||
if path[0] != '/':
|
# Create the bulk storage. Raises ValueError on error, which we
|
||||||
raise ValueError("paths must start with /")
|
# pass along.
|
||||||
[ group, node ] = path.rsplit("/", 1)
|
self.data.create(path, layout_name)
|
||||||
if group == '':
|
|
||||||
raise ValueError("invalid path")
|
|
||||||
|
|
||||||
# Make the group structure, one element at a time
|
# Insert into SQL database once the bulk storage is happy
|
||||||
group_path = group.lstrip('/').split("/")
|
|
||||||
for i in range(len(group_path)):
|
|
||||||
parent = "/" + "/".join(group_path[0:i])
|
|
||||||
child = group_path[i]
|
|
||||||
try:
|
|
||||||
self.h5file.createGroup(parent, child)
|
|
||||||
except tables.NodeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Get description
|
|
||||||
try:
|
|
||||||
desc = nilmdb.layout.get_named(layout_name).description()
|
|
||||||
except KeyError:
|
|
||||||
raise ValueError("no such layout")
|
|
||||||
|
|
||||||
# Estimated table size (for PyTables optimization purposes): assume
|
|
||||||
# 3 months worth of data at 8 KHz. It's OK if this is wrong.
|
|
||||||
exp_rows = 8000 * 60*60*24*30*3
|
|
||||||
|
|
||||||
# Create the table
|
|
||||||
table = self.h5file.createTable(group, node,
|
|
||||||
description = desc,
|
|
||||||
expectedrows = exp_rows)
|
|
||||||
|
|
||||||
# Insert into SQL database once the PyTables is happy
|
|
||||||
with self.con as con:
|
with self.con as con:
|
||||||
con.execute("INSERT INTO streams (path, layout) VALUES (?,?)",
|
con.execute("INSERT INTO streams (path, layout) VALUES (?,?)",
|
||||||
(path, layout_name))
|
(path, layout_name))
|
||||||
@@ -337,8 +313,7 @@ class NilmDB(object):
|
|||||||
"""
|
"""
|
||||||
stream_id = self._stream_id(path)
|
stream_id = self._stream_id(path)
|
||||||
with self.con as con:
|
with self.con as con:
|
||||||
con.execute("DELETE FROM metadata "
|
con.execute("DELETE FROM metadata WHERE stream_id=?", (stream_id,))
|
||||||
"WHERE stream_id=?", (stream_id,))
|
|
||||||
for key in data:
|
for key in data:
|
||||||
if data[key] != '':
|
if data[key] != '':
|
||||||
con.execute("INSERT INTO metadata VALUES (?, ?, ?)",
|
con.execute("INSERT INTO metadata VALUES (?, ?, ?)",
|
||||||
@@ -361,44 +336,47 @@ class NilmDB(object):
|
|||||||
data.update(newdata)
|
data.update(newdata)
|
||||||
self.stream_set_metadata(path, data)
|
self.stream_set_metadata(path, data)
|
||||||
|
|
||||||
def stream_insert(self, path, parser, old_timestamp = None):
|
def stream_destroy(self, path):
|
||||||
|
"""Fully remove a table and all of its data from the database.
|
||||||
|
No way to undo it! Metadata is removed."""
|
||||||
|
stream_id = self._stream_id(path)
|
||||||
|
|
||||||
|
# Delete the cached interval data
|
||||||
|
self._get_intervals.cache_remove(self, stream_id)
|
||||||
|
|
||||||
|
# Delete the data
|
||||||
|
self.data.destroy(path)
|
||||||
|
|
||||||
|
# Delete metadata, stream, intervals
|
||||||
|
with self.con as con:
|
||||||
|
con.execute("DELETE FROM metadata WHERE stream_id=?", (stream_id,))
|
||||||
|
con.execute("DELETE FROM ranges WHERE stream_id=?", (stream_id,))
|
||||||
|
con.execute("DELETE FROM streams WHERE id=?", (stream_id,))
|
||||||
|
|
||||||
|
def stream_insert(self, path, start, end, data):
|
||||||
"""Insert new data into the database.
|
"""Insert new data into the database.
|
||||||
path: Path at which to add the data
|
path: Path at which to add the data
|
||||||
parser: nilmdb.layout.Parser instance full of data to insert
|
start: Starting timestamp
|
||||||
|
end: Ending timestamp
|
||||||
|
data: Rows of data, to be passed to PyTable's table.append
|
||||||
|
method. E.g. nilmdb.layout.Parser.data
|
||||||
"""
|
"""
|
||||||
if (not parser.min_timestamp or not parser.max_timestamp or
|
|
||||||
not len(parser.data)):
|
|
||||||
raise StreamError("no data provided")
|
|
||||||
|
|
||||||
# If we were provided with an old timestamp, the expectation
|
|
||||||
# is that the client has a contiguous block of time it is sending,
|
|
||||||
# but it's doing it over multiple calls to stream_insert.
|
|
||||||
# old_timestamp is the max_timestamp of the previous insert.
|
|
||||||
# To make things continuous, use that as our starting timestamp
|
|
||||||
# instead of what the parser found.
|
|
||||||
if old_timestamp:
|
|
||||||
min_timestamp = old_timestamp
|
|
||||||
else:
|
|
||||||
min_timestamp = parser.min_timestamp
|
|
||||||
|
|
||||||
# First check for basic overlap using timestamp info given.
|
# First check for basic overlap using timestamp info given.
|
||||||
stream_id = self._stream_id(path)
|
stream_id = self._stream_id(path)
|
||||||
iset = self._get_intervals(stream_id)
|
iset = self._get_intervals(stream_id)
|
||||||
interval = Interval(min_timestamp, parser.max_timestamp)
|
interval = Interval(start, end)
|
||||||
if iset.intersects(interval):
|
if iset.intersects(interval):
|
||||||
raise OverlapError("new data overlaps existing data: "
|
raise OverlapError("new data overlaps existing data at range: "
|
||||||
+ str(iset & interval))
|
+ str(iset & interval))
|
||||||
|
|
||||||
# Insert the data into pytables
|
# Insert the data
|
||||||
table = self.h5file.getNode(path)
|
table = self.data.getnode(path)
|
||||||
row_start = table.nrows
|
row_start = table.nrows
|
||||||
table.append(parser.data)
|
table.append(data)
|
||||||
row_end = table.nrows
|
row_end = table.nrows
|
||||||
table.flush()
|
|
||||||
|
|
||||||
# Insert the record into the sql database.
|
# Insert the record into the sql database.
|
||||||
# Casts are to convert from numpy.int64.
|
self._add_interval(stream_id, interval, row_start, row_end)
|
||||||
self._add_interval(stream_id, interval, int(row_start), int(row_end))
|
|
||||||
|
|
||||||
# And that's all
|
# And that's all
|
||||||
return "ok"
|
return "ok"
|
||||||
@@ -413,7 +391,7 @@ class NilmDB(object):
|
|||||||
# Optimization for the common case where an interval wasn't truncated
|
# Optimization for the common case where an interval wasn't truncated
|
||||||
if interval.start == interval.db_start:
|
if interval.start == interval.db_start:
|
||||||
return interval.db_startpos
|
return interval.db_startpos
|
||||||
return bisect.bisect_left(BisectableTable(table),
|
return bisect.bisect_left(bulkdata.TimestampOnlyTable(table),
|
||||||
interval.start,
|
interval.start,
|
||||||
interval.db_startpos,
|
interval.db_startpos,
|
||||||
interval.db_endpos)
|
interval.db_endpos)
|
||||||
@@ -432,7 +410,7 @@ class NilmDB(object):
|
|||||||
# want to include the given timestamp in the results. This is
|
# want to include the given timestamp in the results. This is
|
||||||
# so a queries like 1:00 -> 2:00 and 2:00 -> 3:00 return
|
# so a queries like 1:00 -> 2:00 and 2:00 -> 3:00 return
|
||||||
# non-overlapping data.
|
# non-overlapping data.
|
||||||
return bisect.bisect_left(BisectableTable(table),
|
return bisect.bisect_left(bulkdata.TimestampOnlyTable(table),
|
||||||
interval.end,
|
interval.end,
|
||||||
interval.db_startpos,
|
interval.db_startpos,
|
||||||
interval.db_endpos)
|
interval.db_endpos)
|
||||||
@@ -456,7 +434,7 @@ class NilmDB(object):
|
|||||||
than actually fetching the data. It is not limited by
|
than actually fetching the data. It is not limited by
|
||||||
max_results.
|
max_results.
|
||||||
"""
|
"""
|
||||||
table = self.h5file.getNode(path)
|
table = self.data.getnode(path)
|
||||||
stream_id = self._stream_id(path)
|
stream_id = self._stream_id(path)
|
||||||
intervals = self._get_intervals(stream_id)
|
intervals = self._get_intervals(stream_id)
|
||||||
requested = Interval(start or 0, end or 1e12)
|
requested = Interval(start or 0, end or 1e12)
|
||||||
|
23
nilmdb/rbtree.pxd
Normal file
23
nilmdb/rbtree.pxd
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
cdef class RBNode:
|
||||||
|
cdef public object obj
|
||||||
|
cdef public double start, end
|
||||||
|
cdef public int red
|
||||||
|
cdef public RBNode left, right, parent
|
||||||
|
|
||||||
|
cdef class RBTree:
|
||||||
|
cdef public RBNode nil, root
|
||||||
|
|
||||||
|
cpdef getroot(RBTree self)
|
||||||
|
cdef void __rotate_left(RBTree self, RBNode x)
|
||||||
|
cdef void __rotate_right(RBTree self, RBNode y)
|
||||||
|
cdef RBNode __successor(RBTree self, RBNode x)
|
||||||
|
cpdef RBNode successor(RBTree self, RBNode x)
|
||||||
|
cdef RBNode __predecessor(RBTree self, RBNode x)
|
||||||
|
cpdef RBNode predecessor(RBTree self, RBNode x)
|
||||||
|
cpdef insert(RBTree self, RBNode z)
|
||||||
|
cdef void __insert_fixup(RBTree self, RBNode x)
|
||||||
|
cpdef delete(RBTree self, RBNode z)
|
||||||
|
cdef inline void __delete_fixup(RBTree self, RBNode x)
|
||||||
|
cpdef RBNode find(RBTree self, double start, double end)
|
||||||
|
cpdef RBNode find_left_end(RBTree self, double t)
|
||||||
|
cpdef RBNode find_right_start(RBTree self, double t)
|
@@ -1,20 +1,27 @@
|
|||||||
"""Red-black tree, where keys are stored as start/end timestamps."""
|
# cython: profile=False
|
||||||
|
# cython: cdivision=True
|
||||||
|
|
||||||
|
"""
|
||||||
|
Jim Paris <jim@jtan.com>
|
||||||
|
|
||||||
|
Red-black tree, where keys are stored as start/end timestamps.
|
||||||
|
This is a basic interval tree that holds half-open intervals:
|
||||||
|
[start, end)
|
||||||
|
Intervals must not overlap. Fixing that would involve making this
|
||||||
|
into an augmented interval tree as described in CLRS 14.3.
|
||||||
|
|
||||||
|
Code that assumes non-overlapping intervals is marked with the
|
||||||
|
string 'non-overlapping'.
|
||||||
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
cimport rbtree
|
||||||
|
|
||||||
class RBNode(object):
|
cdef class RBNode:
|
||||||
"""One node of the Red/Black tree. obj points to any object,
|
"""One node of the Red/Black tree, containing a key (start, end)
|
||||||
'start' and 'end' are timestamps that represent the key."""
|
and value (obj)"""
|
||||||
def __init__(self, obj = None, start = None, end = None):
|
def __init__(self, double start, double end, object obj = None):
|
||||||
"""If given an object but no start/end times, get the
|
|
||||||
start/end times from the object.
|
|
||||||
|
|
||||||
If given start/end times, obj can be anything, including None."""
|
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
if start is None:
|
|
||||||
start = obj.start
|
|
||||||
if end is None:
|
|
||||||
end = obj.end
|
|
||||||
self.start = start
|
self.start = start
|
||||||
self.end = end
|
self.end = end
|
||||||
self.red = False
|
self.red = False
|
||||||
@@ -26,21 +33,23 @@ class RBNode(object):
|
|||||||
color = "R"
|
color = "R"
|
||||||
else:
|
else:
|
||||||
color = "B"
|
color = "B"
|
||||||
return ("[node "
|
if self.start == sys.float_info.min:
|
||||||
|
return "[node nil]"
|
||||||
|
return ("[node ("
|
||||||
|
+ str(self.obj) + ") "
|
||||||
+ str(self.start) + " -> " + str(self.end) + " "
|
+ str(self.start) + " -> " + str(self.end) + " "
|
||||||
+ color + "]")
|
+ color + "]")
|
||||||
|
|
||||||
class RBTree(object):
|
cdef class RBTree:
|
||||||
"""Red/Black tree"""
|
"""Red/Black tree"""
|
||||||
|
|
||||||
# Init
|
# Init
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.nil = RBNode(start = sys.float_info.min,
|
self.nil = RBNode(start = sys.float_info.min,
|
||||||
end = sys.float_info.min)
|
end = sys.float_info.min)
|
||||||
self.nil.left = self.nil
|
self.nil.left = self.nil
|
||||||
self.nil.right = self.nil
|
self.nil.right = self.nil
|
||||||
self.nil.parent = self.nil
|
self.nil.parent = self.nil
|
||||||
self.nil.nil = True
|
|
||||||
|
|
||||||
self.root = RBNode(start = sys.float_info.max,
|
self.root = RBNode(start = sys.float_info.max,
|
||||||
end = sys.float_info.max)
|
end = sys.float_info.max)
|
||||||
@@ -48,9 +57,21 @@ class RBTree(object):
|
|||||||
self.root.right = self.nil
|
self.root.right = self.nil
|
||||||
self.root.parent = self.nil
|
self.root.parent = self.nil
|
||||||
|
|
||||||
|
# We have a dummy root node to simplify operations, so from an
|
||||||
|
# external point of view, its left child is the real root.
|
||||||
|
cpdef getroot(self):
|
||||||
|
return self.root.left
|
||||||
|
|
||||||
# Rotations and basic operations
|
# Rotations and basic operations
|
||||||
def __rotate_left(self, x):
|
cdef void __rotate_left(self, RBNode x):
|
||||||
y = x.right
|
"""Rotate left:
|
||||||
|
# x y
|
||||||
|
# / \ --> / \
|
||||||
|
# z y x w
|
||||||
|
# / \ / \
|
||||||
|
# v w z v
|
||||||
|
"""
|
||||||
|
cdef RBNode y = x.right
|
||||||
x.right = y.left
|
x.right = y.left
|
||||||
if y.left is not self.nil:
|
if y.left is not self.nil:
|
||||||
y.left.parent = x
|
y.left.parent = x
|
||||||
@@ -62,8 +83,15 @@ class RBTree(object):
|
|||||||
y.left = x
|
y.left = x
|
||||||
x.parent = y
|
x.parent = y
|
||||||
|
|
||||||
def __rotate_right(self, y):
|
cdef void __rotate_right(self, RBNode y):
|
||||||
x = y.left
|
"""Rotate right:
|
||||||
|
# y x
|
||||||
|
# / \ --> / \
|
||||||
|
# x w z y
|
||||||
|
# / \ / \
|
||||||
|
# z v v w
|
||||||
|
"""
|
||||||
|
cdef RBNode x = y.left
|
||||||
y.left = x.right
|
y.left = x.right
|
||||||
if x.right is not self.nil:
|
if x.right is not self.nil:
|
||||||
x.right.parent = y
|
x.right.parent = y
|
||||||
@@ -75,9 +103,9 @@ class RBTree(object):
|
|||||||
x.right = y
|
x.right = y
|
||||||
y.parent = x
|
y.parent = x
|
||||||
|
|
||||||
def __successor(self, x):
|
cdef RBNode __successor(self, RBNode x):
|
||||||
"""Returns the successor of RBNode x"""
|
"""Returns the successor of RBNode x"""
|
||||||
y = x.right
|
cdef RBNode y = x.right
|
||||||
if y is not self.nil:
|
if y is not self.nil:
|
||||||
while y.left is not self.nil:
|
while y.left is not self.nil:
|
||||||
y = y.left
|
y = y.left
|
||||||
@@ -89,10 +117,14 @@ class RBTree(object):
|
|||||||
if y is self.root:
|
if y is self.root:
|
||||||
return self.nil
|
return self.nil
|
||||||
return y
|
return y
|
||||||
|
cpdef RBNode successor(self, RBNode x):
|
||||||
|
"""Returns the successor of RBNode x, or None"""
|
||||||
|
cdef RBNode y = self.__successor(x)
|
||||||
|
return y if y is not self.nil else None
|
||||||
|
|
||||||
def _predecessor(self, x):
|
cdef RBNode __predecessor(self, RBNode x):
|
||||||
"""Returns the predecessor of RBNode x"""
|
"""Returns the predecessor of RBNode x"""
|
||||||
y = x.left
|
cdef RBNode y = x.left
|
||||||
if y is not self.nil:
|
if y is not self.nil:
|
||||||
while y.right is not self.nil:
|
while y.right is not self.nil:
|
||||||
y = y.right
|
y = y.right
|
||||||
@@ -105,14 +137,18 @@ class RBTree(object):
|
|||||||
x = y
|
x = y
|
||||||
y = y.parent
|
y = y.parent
|
||||||
return y
|
return y
|
||||||
|
cpdef RBNode predecessor(self, RBNode x):
|
||||||
|
"""Returns the predecessor of RBNode x, or None"""
|
||||||
|
cdef RBNode y = self.__predecessor(x)
|
||||||
|
return y if y is not self.nil else None
|
||||||
|
|
||||||
# Insertion
|
# Insertion
|
||||||
def insert(self, z):
|
cpdef insert(self, RBNode z):
|
||||||
"""Insert RBNode z into RBTree and rebalance as necessary"""
|
"""Insert RBNode z into RBTree and rebalance as necessary"""
|
||||||
z.left = self.nil
|
z.left = self.nil
|
||||||
z.right = self.nil
|
z.right = self.nil
|
||||||
y = self.root
|
cdef RBNode y = self.root
|
||||||
x = self.root.left
|
cdef RBNode x = self.root.left
|
||||||
while x is not self.nil:
|
while x is not self.nil:
|
||||||
y = x
|
y = x
|
||||||
if (x.start > z.start or (x.start == z.start and x.end > z.end)):
|
if (x.start > z.start or (x.start == z.start and x.end > z.end)):
|
||||||
@@ -128,7 +164,7 @@ class RBTree(object):
|
|||||||
# relabel/rebalance
|
# relabel/rebalance
|
||||||
self.__insert_fixup(z)
|
self.__insert_fixup(z)
|
||||||
|
|
||||||
def __insert_fixup(self, x):
|
cdef void __insert_fixup(self, RBNode x):
|
||||||
"""Rebalance/fix RBTree after a simple insertion of RBNode x"""
|
"""Rebalance/fix RBTree after a simple insertion of RBNode x"""
|
||||||
x.red = True
|
x.red = True
|
||||||
while x.parent.red:
|
while x.parent.red:
|
||||||
@@ -163,10 +199,11 @@ class RBTree(object):
|
|||||||
self.root.left.red = False
|
self.root.left.red = False
|
||||||
|
|
||||||
# Deletion
|
# Deletion
|
||||||
def delete(self, z):
|
cpdef delete(self, RBNode z):
|
||||||
if z.left is None or z.right is None:
|
if z.left is None or z.right is None:
|
||||||
raise AttributeError("you can only delete a node object "
|
raise AttributeError("you can only delete a node object "
|
||||||
+ "from the tree; use find() to get one")
|
+ "from the tree; use find() to get one")
|
||||||
|
cdef RBNode x, y
|
||||||
if z.left is self.nil or z.right is self.nil:
|
if z.left is self.nil or z.right is self.nil:
|
||||||
y = z
|
y = z
|
||||||
else:
|
else:
|
||||||
@@ -203,10 +240,10 @@ class RBTree(object):
|
|||||||
if not y.red:
|
if not y.red:
|
||||||
self.__delete_fixup(x)
|
self.__delete_fixup(x)
|
||||||
|
|
||||||
def __delete_fixup(self, x):
|
cdef void __delete_fixup(self, RBNode x):
|
||||||
"""Rebalance/fix RBTree after a deletion. RBNode x is the
|
"""Rebalance/fix RBTree after a deletion. RBNode x is the
|
||||||
child of the spliced out node."""
|
child of the spliced out node."""
|
||||||
rootLeft = self.root.left
|
cdef RBNode rootLeft = self.root.left
|
||||||
while not x.red and x is not rootLeft:
|
while not x.red and x is not rootLeft:
|
||||||
if x is x.parent.left:
|
if x is x.parent.left:
|
||||||
w = x.parent.right
|
w = x.parent.right
|
||||||
@@ -252,141 +289,89 @@ class RBTree(object):
|
|||||||
x = rootLeft # exit loop
|
x = rootLeft # exit loop
|
||||||
x.red = False
|
x.red = False
|
||||||
|
|
||||||
# Rendering
|
|
||||||
def __render_dot_node(self, node, max_depth = 20):
|
|
||||||
from printf import sprintf
|
|
||||||
"""Render a single node and its children into a dot graph fragment"""
|
|
||||||
if max_depth == 0:
|
|
||||||
return ""
|
|
||||||
if node is self.nil:
|
|
||||||
return ""
|
|
||||||
def c(red):
|
|
||||||
if red:
|
|
||||||
return 'color="#ff0000", style=filled, fillcolor="#ffc0c0"'
|
|
||||||
else:
|
|
||||||
return 'color="#000000", style=filled, fillcolor="#c0c0c0"'
|
|
||||||
s = sprintf("%d [label=\"%g\\n%g\", %s];\n",
|
|
||||||
id(node),
|
|
||||||
node.start, node.end,
|
|
||||||
c(node.red))
|
|
||||||
|
|
||||||
if node.left is self.nil:
|
|
||||||
s += sprintf("L%d [label=\"-\", %s];\n", id(node), c(False))
|
|
||||||
s += sprintf("%d -> L%d [label=L];\n", id(node), id(node))
|
|
||||||
else:
|
|
||||||
s += sprintf("%d -> %d [label=L];\n", id(node), id(node.left))
|
|
||||||
if node.right is self.nil:
|
|
||||||
s += sprintf("R%d [label=\"-\", %s];\n", id(node), c(False))
|
|
||||||
s += sprintf("%d -> R%d [label=R];\n", id(node), id(node))
|
|
||||||
else:
|
|
||||||
s += sprintf("%d -> %d [label=R];\n", id(node), id(node.right))
|
|
||||||
s += self.__render_dot_node(node.left, max_depth-1)
|
|
||||||
s += self.__render_dot_node(node.right, max_depth-1)
|
|
||||||
return s
|
|
||||||
|
|
||||||
def render_dot(self, title = "RBTree"):
|
|
||||||
"""Render the entire RBTree as a dot graph"""
|
|
||||||
return ("digraph rbtree {\n"
|
|
||||||
+ self.__render_dot_node(self.root.left)
|
|
||||||
+ "}\n");
|
|
||||||
|
|
||||||
def render_dot_live(self, title = "RBTree"):
|
|
||||||
"""Render the entire RBTree as a dot graph, live GTK view"""
|
|
||||||
import gtk
|
|
||||||
import gtk.gdk
|
|
||||||
sys.path.append("/usr/share/xdot")
|
|
||||||
import xdot
|
|
||||||
xdot.Pen.highlighted = lambda pen: pen
|
|
||||||
s = ("digraph rbtree {\n"
|
|
||||||
+ self.__render_dot_node(self.root)
|
|
||||||
+ "}\n");
|
|
||||||
window = xdot.DotWindow()
|
|
||||||
window.set_dotcode(s)
|
|
||||||
window.set_title(title + " - any key to close")
|
|
||||||
window.connect('destroy', gtk.main_quit)
|
|
||||||
def quit(widget, event):
|
|
||||||
if not event.is_modifier:
|
|
||||||
window.destroy()
|
|
||||||
gtk.main_quit()
|
|
||||||
window.widget.connect('key-press-event', quit)
|
|
||||||
gtk.main()
|
|
||||||
|
|
||||||
# Walking, searching
|
# Walking, searching
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return self.inorder(self.root.left)
|
return self.inorder()
|
||||||
|
|
||||||
def inorder(self, x = None):
|
def inorder(self, RBNode x = None):
|
||||||
"""Generator that performs an inorder walk for the tree
|
"""Generator that performs an inorder walk for the tree
|
||||||
starting at RBNode x"""
|
rooted at RBNode x"""
|
||||||
if x is None:
|
if x is None:
|
||||||
x = self.root.left
|
x = self.getroot()
|
||||||
while x.left is not self.nil:
|
while x.left is not self.nil:
|
||||||
x = x.left
|
x = x.left
|
||||||
while x is not self.nil:
|
while x is not self.nil:
|
||||||
yield x
|
yield x
|
||||||
x = self.__successor(x)
|
x = self.__successor(x)
|
||||||
|
|
||||||
def __find_all(self, start, end, x):
|
cpdef RBNode find(self, double start, double end):
|
||||||
"""Find node with the specified (start,end) key.
|
"""Return the node with exactly the given start and end."""
|
||||||
Also returns the largest node less than or equal to key,
|
cdef RBNode x = self.getroot()
|
||||||
and the smallest node greater or equal to than key."""
|
|
||||||
if x is None:
|
|
||||||
x = self.root.left
|
|
||||||
largest = self.nil
|
|
||||||
smallest = self.nil
|
|
||||||
while x is not self.nil:
|
while x is not self.nil:
|
||||||
if start < x.start:
|
if start < x.start:
|
||||||
smallest = x
|
x = x.left
|
||||||
x = x.left # start <
|
|
||||||
elif start == x.start:
|
elif start == x.start:
|
||||||
if end < x.end:
|
if end == x.end:
|
||||||
smallest = x
|
break # found it
|
||||||
x = x.left # start =, end <
|
elif end < x.end:
|
||||||
elif end == x.end: # found it
|
x = x.left
|
||||||
smallest = x
|
|
||||||
largest = x
|
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
largest = x
|
x = x.right
|
||||||
x = x.right # start =, end >
|
|
||||||
else:
|
else:
|
||||||
largest = x
|
x = x.right
|
||||||
x = x.right # start >
|
return x if x is not self.nil else None
|
||||||
return (x, smallest, largest)
|
|
||||||
|
|
||||||
def find(self, start, end, x = None):
|
cpdef RBNode find_left_end(self, double t):
|
||||||
"""Find node with the key == (start,end), or None"""
|
"""Find the leftmode node with end >= t. With non-overlapping
|
||||||
y = self.__find_all(start, end, x)[1]
|
intervals, this is the first node that might overlap time t.
|
||||||
return y if y is not self.nil else None
|
|
||||||
|
|
||||||
def find_right(self, start, end, x = None):
|
Note that this relies on non-overlapping intervals, since
|
||||||
"""Find node with the smallest key >= (start,end), or None"""
|
it assumes that we can use the endpoints to traverse the
|
||||||
y = self.__find_all(start, end, x)[1]
|
tree even though it was created using the start points."""
|
||||||
return y if y is not self.nil else None
|
cdef RBNode x = self.getroot()
|
||||||
|
while x is not self.nil:
|
||||||
|
if t < x.end:
|
||||||
|
if x.left is self.nil:
|
||||||
|
break
|
||||||
|
x = x.left
|
||||||
|
elif t == x.end:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if x.right is self.nil:
|
||||||
|
x = self.__successor(x)
|
||||||
|
break
|
||||||
|
x = x.right
|
||||||
|
return x if x is not self.nil else None
|
||||||
|
|
||||||
def find_left(self, start, end, x = None):
|
cpdef RBNode find_right_start(self, double t):
|
||||||
"""Find node with the largest key <= (start,end), or None"""
|
"""Find the rightmode node with start <= t. With non-overlapping
|
||||||
y = self.__find_all(start, end, x)[2]
|
intervals, this is the last node that might overlap time t."""
|
||||||
return y if y is not self.nil else None
|
cdef RBNode x = self.getroot()
|
||||||
|
while x is not self.nil:
|
||||||
|
if t < x.start:
|
||||||
|
if x.left is self.nil:
|
||||||
|
x = self.__predecessor(x)
|
||||||
|
break
|
||||||
|
x = x.left
|
||||||
|
elif t == x.start:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if x.right is self.nil:
|
||||||
|
break
|
||||||
|
x = x.right
|
||||||
|
return x if x is not self.nil else None
|
||||||
|
|
||||||
# Intersections
|
# Intersections
|
||||||
def intersect(self, start, end):
|
def intersect(self, double start, double end):
|
||||||
"""Generator that returns nodes that overlap the given
|
"""Generator that returns nodes that overlap the given
|
||||||
(start,end) range, for the tree rooted at RBNode x.
|
(start,end) range. Assumes non-overlapping intervals."""
|
||||||
|
# Start with the leftmode node that ends after start
|
||||||
NOTE: this assumes non-overlapping intervals."""
|
cdef RBNode n = self.find_left_end(start)
|
||||||
# Start with the leftmost node before the starting point
|
while n is not None:
|
||||||
n = self.find_left(start, start)
|
if n.start >= end:
|
||||||
# If we didn't find one, look for the leftmode node before the
|
# this node starts after the requested end; we're done
|
||||||
# ending point instead.
|
break
|
||||||
if n is None:
|
if start < n.end:
|
||||||
n = self.find_left(end, end)
|
# this node overlaps our requested area
|
||||||
# If we still didn't find it, there are no intervals that intersect.
|
yield n
|
||||||
if n is None:
|
n = self.successor(n)
|
||||||
return none
|
|
||||||
|
|
||||||
# Now yield this node and all successors until their endpoints
|
|
||||||
|
|
||||||
if False:
|
|
||||||
yield
|
|
||||||
return
|
|
1
nilmdb/rbtree.pyxdep
Normal file
1
nilmdb/rbtree.pyxdep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rbtree.pxd
|
@@ -3,10 +3,9 @@
|
|||||||
# Need absolute_import so that "import nilmdb" won't pull in nilmdb.py,
|
# Need absolute_import so that "import nilmdb" won't pull in nilmdb.py,
|
||||||
# but will pull the nilmdb module instead.
|
# but will pull the nilmdb module instead.
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
from nilmdb.utils.printf import *
|
||||||
import nilmdb
|
import nilmdb
|
||||||
|
|
||||||
from nilmdb.printf import *
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -26,6 +25,33 @@ class NilmApp(object):
|
|||||||
|
|
||||||
version = "1.1"
|
version = "1.1"
|
||||||
|
|
||||||
|
# Decorators
|
||||||
|
def chunked_response(func):
|
||||||
|
"""Decorator to enable chunked responses"""
|
||||||
|
# Set this to False to get better tracebacks from some requests
|
||||||
|
# (/stream/extract, /stream/intervals).
|
||||||
|
func._cp_config = { 'response.stream': True }
|
||||||
|
return func
|
||||||
|
|
||||||
|
def workaround_cp_bug_1200(func): # pragma: no cover (just a workaround)
|
||||||
|
"""Decorator to work around CherryPy bug #1200 in a response
|
||||||
|
generator"""
|
||||||
|
# Even if chunked responses are disabled, you may still miss miss
|
||||||
|
# LookupError, or UnicodeError exceptions due to CherryPy bug
|
||||||
|
# #1200. This throws them as generic Exceptions insteads.
|
||||||
|
import functools
|
||||||
|
import traceback
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
for val in func(*args, **kwargs):
|
||||||
|
yield val
|
||||||
|
except (LookupError, UnicodeError) as e:
|
||||||
|
raise Exception("bug workaround; real exception is:\n" +
|
||||||
|
traceback.format_exc())
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
# CherryPy apps
|
||||||
class Root(NilmApp):
|
class Root(NilmApp):
|
||||||
"""Root application for NILM database"""
|
"""Root application for NILM database"""
|
||||||
|
|
||||||
@@ -59,7 +85,7 @@ class Root(NilmApp):
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
def dbsize(self):
|
def dbsize(self):
|
||||||
return nilmdb.du.du(self.db.get_basepath())
|
return nilmdb.utils.du(self.db.get_basepath())
|
||||||
|
|
||||||
class Stream(NilmApp):
|
class Stream(NilmApp):
|
||||||
"""Stream-specific operations"""
|
"""Stream-specific operations"""
|
||||||
@@ -88,6 +114,17 @@ class Stream(NilmApp):
|
|||||||
message = sprintf("%s: %s", type(e).__name__, e.message)
|
message = sprintf("%s: %s", type(e).__name__, e.message)
|
||||||
raise cherrypy.HTTPError("400 Bad Request", message)
|
raise cherrypy.HTTPError("400 Bad Request", message)
|
||||||
|
|
||||||
|
# /stream/destroy?path=/newton/prep
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
def destroy(self, path):
|
||||||
|
"""Delete a stream and its associated data."""
|
||||||
|
try:
|
||||||
|
return self.db.stream_destroy(path)
|
||||||
|
except Exception as e:
|
||||||
|
message = sprintf("%s: %s", type(e).__name__, e.message)
|
||||||
|
raise cherrypy.HTTPError("400 Bad Request", message)
|
||||||
|
|
||||||
# /stream/get_metadata?path=/newton/prep
|
# /stream/get_metadata?path=/newton/prep
|
||||||
# /stream/get_metadata?path=/newton/prep&key=foo&key=bar
|
# /stream/get_metadata?path=/newton/prep&key=foo&key=bar
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@@ -145,19 +182,11 @@ class Stream(NilmApp):
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
#@cherrypy.tools.disable_prb()
|
#@cherrypy.tools.disable_prb()
|
||||||
def insert(self, path, old_timestamp = None):
|
def insert(self, path, start, end):
|
||||||
"""
|
"""
|
||||||
Insert new data into the database. Provide textual data
|
Insert new data into the database. Provide textual data
|
||||||
(matching the path's layout) as a HTTP PUT.
|
(matching the path's layout) as a HTTP PUT.
|
||||||
|
|
||||||
old_timestamp is used when making multiple, split-up insertions
|
|
||||||
for a larger contiguous block of data. The first insert
|
|
||||||
will return the maximum timestamp that it saw, and the second
|
|
||||||
insert should provide this timestamp as an argument. This is
|
|
||||||
used to extend the previous database interval rather than
|
|
||||||
start a new one.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Important that we always read the input before throwing any
|
# Important that we always read the input before throwing any
|
||||||
# errors, to keep lengths happy for persistent connections.
|
# errors, to keep lengths happy for persistent connections.
|
||||||
# However, CherryPy 3.2.2 has a bug where this fails for GET
|
# However, CherryPy 3.2.2 has a bug where this fails for GET
|
||||||
@@ -182,22 +211,36 @@ class Stream(NilmApp):
|
|||||||
"Error parsing input data: " +
|
"Error parsing input data: " +
|
||||||
e.message)
|
e.message)
|
||||||
|
|
||||||
|
if (not parser.min_timestamp or not parser.max_timestamp or
|
||||||
|
not len(parser.data)):
|
||||||
|
raise cherrypy.HTTPError("400 Bad Request",
|
||||||
|
"no data provided")
|
||||||
|
|
||||||
|
# Check limits
|
||||||
|
start = float(start)
|
||||||
|
end = float(end)
|
||||||
|
if parser.min_timestamp < start:
|
||||||
|
raise cherrypy.HTTPError("400 Bad Request", "Data timestamp " +
|
||||||
|
repr(parser.min_timestamp) +
|
||||||
|
" < start time " + repr(start))
|
||||||
|
if parser.max_timestamp >= end:
|
||||||
|
raise cherrypy.HTTPError("400 Bad Request", "Data timestamp " +
|
||||||
|
repr(parser.max_timestamp) +
|
||||||
|
" >= end time " + repr(end))
|
||||||
|
|
||||||
# Now do the nilmdb insert, passing it the parser full of data.
|
# Now do the nilmdb insert, passing it the parser full of data.
|
||||||
try:
|
try:
|
||||||
if old_timestamp:
|
result = self.db.stream_insert(path, start, end, parser.data)
|
||||||
old_timestamp = float(old_timestamp)
|
|
||||||
result = self.db.stream_insert(path, parser, old_timestamp)
|
|
||||||
except nilmdb.nilmdb.NilmDBError as e:
|
except nilmdb.nilmdb.NilmDBError as e:
|
||||||
raise cherrypy.HTTPError("400 Bad Request", e.message)
|
raise cherrypy.HTTPError("400 Bad Request", e.message)
|
||||||
|
|
||||||
# Return the maximum timestamp that we saw. The client will
|
# Done
|
||||||
# return this back to us as the old_timestamp parameter, if
|
return "ok"
|
||||||
# it has more data to send.
|
|
||||||
return ("ok", parser.max_timestamp)
|
|
||||||
|
|
||||||
# /stream/intervals?path=/newton/prep
|
# /stream/intervals?path=/newton/prep
|
||||||
# /stream/intervals?path=/newton/prep&start=1234567890.0&end=1234567899.0
|
# /stream/intervals?path=/newton/prep&start=1234567890.0&end=1234567899.0
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@chunked_response
|
||||||
def intervals(self, path, start = None, end = None):
|
def intervals(self, path, start = None, end = None):
|
||||||
"""
|
"""
|
||||||
Get intervals from backend database. Streams the resulting
|
Get intervals from backend database. Streams the resulting
|
||||||
@@ -219,9 +262,9 @@ class Stream(NilmApp):
|
|||||||
if len(streams) != 1:
|
if len(streams) != 1:
|
||||||
raise cherrypy.HTTPError("404 Not Found", "No such stream")
|
raise cherrypy.HTTPError("404 Not Found", "No such stream")
|
||||||
|
|
||||||
|
@workaround_cp_bug_1200
|
||||||
def content(start, end):
|
def content(start, end):
|
||||||
# Note: disable response.stream below to get better debug info
|
# Note: disable chunked responses to see tracebacks from here.
|
||||||
# from tracebacks in this subfunction.
|
|
||||||
while True:
|
while True:
|
||||||
(intervals, restart) = self.db.stream_intervals(path,start,end)
|
(intervals, restart) = self.db.stream_intervals(path,start,end)
|
||||||
response = ''.join([ json.dumps(i) + "\n" for i in intervals ])
|
response = ''.join([ json.dumps(i) + "\n" for i in intervals ])
|
||||||
@@ -230,10 +273,10 @@ class Stream(NilmApp):
|
|||||||
break
|
break
|
||||||
start = restart
|
start = restart
|
||||||
return content(start, end)
|
return content(start, end)
|
||||||
intervals._cp_config = { 'response.stream': True } # chunked HTTP response
|
|
||||||
|
|
||||||
# /stream/extract?path=/newton/prep&start=1234567890.0&end=1234567899.0
|
# /stream/extract?path=/newton/prep&start=1234567890.0&end=1234567899.0
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@chunked_response
|
||||||
def extract(self, path, start = None, end = None, count = False):
|
def extract(self, path, start = None, end = None, count = False):
|
||||||
"""
|
"""
|
||||||
Extract data from backend database. Streams the resulting
|
Extract data from backend database. Streams the resulting
|
||||||
@@ -263,9 +306,9 @@ class Stream(NilmApp):
|
|||||||
# Get formatter
|
# Get formatter
|
||||||
formatter = nilmdb.layout.Formatter(layout)
|
formatter = nilmdb.layout.Formatter(layout)
|
||||||
|
|
||||||
|
@workaround_cp_bug_1200
|
||||||
def content(start, end, count):
|
def content(start, end, count):
|
||||||
# Note: disable response.stream below to get better debug info
|
# Note: disable chunked responses to see tracebacks from here.
|
||||||
# from tracebacks in this subfunction.
|
|
||||||
if count:
|
if count:
|
||||||
matched = self.db.stream_extract(path, start, end, count)
|
matched = self.db.stream_extract(path, start, end, count)
|
||||||
yield sprintf("%d\n", matched)
|
yield sprintf("%d\n", matched)
|
||||||
@@ -281,8 +324,6 @@ class Stream(NilmApp):
|
|||||||
return
|
return
|
||||||
start = restart
|
start = restart
|
||||||
return content(start, end, count)
|
return content(start, end, count)
|
||||||
extract._cp_config = { 'response.stream': True } # chunked HTTP response
|
|
||||||
|
|
||||||
|
|
||||||
class Exiter(object):
|
class Exiter(object):
|
||||||
"""App that exits the server, for testing"""
|
"""App that exits the server, for testing"""
|
||||||
@@ -307,7 +348,7 @@ class Server(object):
|
|||||||
# Need to wrap DB object in a serializer because we'll call
|
# Need to wrap DB object in a serializer because we'll call
|
||||||
# into it from separate threads.
|
# into it from separate threads.
|
||||||
self.embedded = embedded
|
self.embedded = embedded
|
||||||
self.db = nilmdb.serializer.WrapObject(db)
|
self.db = nilmdb.utils.Serializer(db)
|
||||||
cherrypy.config.update({
|
cherrypy.config.update({
|
||||||
'server.socket_host': host,
|
'server.socket_host': host,
|
||||||
'server.socket_port': port,
|
'server.socket_port': port,
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""File-like objects that add timestamps to the input lines"""
|
"""File-like objects that add timestamps to the input lines"""
|
||||||
|
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
8
nilmdb/utils/__init__.py
Normal file
8
nilmdb/utils/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"""NilmDB utilities"""
|
||||||
|
|
||||||
|
from .timer import Timer
|
||||||
|
from .iteratorizer import Iteratorizer
|
||||||
|
from .serializer import Serializer
|
||||||
|
from .lrucache import lru_cache
|
||||||
|
from .diskusage import du
|
||||||
|
from .mustclose import must_close
|
66
nilmdb/utils/lrucache.py
Normal file
66
nilmdb/utils/lrucache.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Memoize a function's return value with a least-recently-used cache
|
||||||
|
# Based on:
|
||||||
|
# http://code.activestate.com/recipes/498245-lru-and-lfu-cache-decorators/
|
||||||
|
# with added 'destructor' functionality.
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import functools
|
||||||
|
|
||||||
|
def lru_cache(size = 10, onremove = None):
|
||||||
|
"""Least-recently-used cache decorator.
|
||||||
|
|
||||||
|
@lru_cache(size = 10, onevict = None)
|
||||||
|
def f(...):
|
||||||
|
pass
|
||||||
|
|
||||||
|
Given a function and arguments, memoize its return value.
|
||||||
|
Up to 'size' elements are cached.
|
||||||
|
|
||||||
|
When evicting a value from the cache, call the function
|
||||||
|
'onremove' with the value that's being evicted.
|
||||||
|
|
||||||
|
Call f.cache_remove(...) to evict the cache entry with the given
|
||||||
|
arguments. Call f.cache_remove_all() to evict all entries.
|
||||||
|
f.cache_hits and f.cache_misses give statistics.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
cache = collections.OrderedDict() # order: least- to most-recent
|
||||||
|
|
||||||
|
def evict(value):
|
||||||
|
if onremove:
|
||||||
|
onremove(value)
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
key = args + tuple(sorted(kwargs.items()))
|
||||||
|
try:
|
||||||
|
value = cache.pop(key)
|
||||||
|
wrapper.cache_hits += 1
|
||||||
|
except KeyError:
|
||||||
|
value = func(*args, **kwargs)
|
||||||
|
wrapper.cache_misses += 1
|
||||||
|
if len(cache) >= size:
|
||||||
|
evict(cache.popitem(0)[1]) # evict LRU cache entry
|
||||||
|
cache[key] = value # (re-)insert this key at end
|
||||||
|
return value
|
||||||
|
|
||||||
|
def cache_remove(*args, **kwargs):
|
||||||
|
"""Remove the described key from this cache, if present.
|
||||||
|
Note that if the original wrapped function was implicitly
|
||||||
|
passed 'self', you need to pass it as an argument here too."""
|
||||||
|
key = args + tuple(sorted(kwargs.items()))
|
||||||
|
if key in cache:
|
||||||
|
evict(cache.pop(key))
|
||||||
|
|
||||||
|
def cache_remove_all():
|
||||||
|
for key in cache:
|
||||||
|
evict(cache.pop(key))
|
||||||
|
|
||||||
|
wrapper.cache_hits = 0
|
||||||
|
wrapper.cache_misses = 0
|
||||||
|
wrapper.cache_remove = cache_remove
|
||||||
|
wrapper.cache_remove_all = cache_remove_all
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
42
nilmdb/utils/mustclose.py
Normal file
42
nilmdb/utils/mustclose.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Class decorator that warns on stderr at deletion time if the class's
|
||||||
|
# close() member wasn't called.
|
||||||
|
|
||||||
|
from nilmdb.utils.printf import *
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def must_close(errorfile = sys.stderr):
|
||||||
|
def decorator(cls):
|
||||||
|
def dummy(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
if "__init__" not in cls.__dict__:
|
||||||
|
cls.__init__ = dummy
|
||||||
|
if "__del__" not in cls.__dict__:
|
||||||
|
cls.__del__ = dummy
|
||||||
|
if "close" not in cls.__dict__:
|
||||||
|
cls.close = dummy
|
||||||
|
|
||||||
|
orig_init = cls.__init__
|
||||||
|
orig_del = cls.__del__
|
||||||
|
orig_close = cls.close
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
ret = orig_init(self, *args, **kwargs)
|
||||||
|
self.__dict__["_must_close"] = True
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if "_must_close" in self.__dict__:
|
||||||
|
fprintf(errorfile, "error: %s.close() wasn't called!\n",
|
||||||
|
self.__class__.__name__)
|
||||||
|
return orig_del(self)
|
||||||
|
|
||||||
|
def close(self, *args, **kwargs):
|
||||||
|
del self._must_close
|
||||||
|
return orig_close(self)
|
||||||
|
|
||||||
|
cls.__init__ = __init__
|
||||||
|
cls.__del__ = __del__
|
||||||
|
cls.close = close
|
||||||
|
|
||||||
|
return cls
|
||||||
|
return decorator
|
@@ -67,3 +67,6 @@ class WrapObject(object):
|
|||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.__wrap_call_queue.put((None, None, None, None))
|
self.__wrap_call_queue.put((None, None, None, None))
|
||||||
self.__wrap_serializer.join()
|
self.__wrap_serializer.join()
|
||||||
|
|
||||||
|
# Just an alias
|
||||||
|
Serializer = WrapObject
|
@@ -3,14 +3,17 @@
|
|||||||
import nilmdb
|
import nilmdb
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Run the NILM server')
|
formatter = argparse.ArgumentDefaultsHelpFormatter
|
||||||
|
parser = argparse.ArgumentParser(description='Run the NILM server',
|
||||||
|
formatter_class = formatter)
|
||||||
parser.add_argument('-p', '--port', help='Port number', type=int, default=12380)
|
parser.add_argument('-p', '--port', help='Port number', type=int, default=12380)
|
||||||
|
parser.add_argument('-d', '--database', help='Database directory', default="db")
|
||||||
parser.add_argument('-y', '--yappi', help='Run with yappi profiler',
|
parser.add_argument('-y', '--yappi', help='Run with yappi profiler',
|
||||||
action='store_true')
|
action='store_true')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Start web app on a custom port
|
# Start web app on a custom port
|
||||||
db = nilmdb.NilmDB("db")
|
db = nilmdb.NilmDB(args.database)
|
||||||
server = nilmdb.Server(db, host = "127.0.0.1",
|
server = nilmdb.Server(db, host = "127.0.0.1",
|
||||||
port = args.port,
|
port = args.port,
|
||||||
embedded = False)
|
embedded = False)
|
||||||
|
@@ -10,10 +10,14 @@ cover-erase=
|
|||||||
##cover-branches= # need nose 1.1.3 for this
|
##cover-branches= # need nose 1.1.3 for this
|
||||||
stop=
|
stop=
|
||||||
verbosity=2
|
verbosity=2
|
||||||
|
#tests=tests/test_mustclose.py
|
||||||
|
#tests=tests/test_lrucache.py
|
||||||
#tests=tests/test_cmdline.py
|
#tests=tests/test_cmdline.py
|
||||||
#tests=tests/test_layout.py
|
#tests=tests/test_layout.py
|
||||||
#tests=tests/test_rbtree.py
|
#tests=tests/test_rbtree.py
|
||||||
tests=tests/test_interval.py
|
#tests=tests/test_interval.py
|
||||||
|
#tests=tests/test_rbtree.py,tests/test_interval.py
|
||||||
|
#tests=tests/test_interval.py
|
||||||
#tests=tests/test_client.py
|
#tests=tests/test_client.py
|
||||||
#tests=tests/test_timestamper.py
|
#tests=tests/test_timestamper.py
|
||||||
#tests=tests/test_serializer.py
|
#tests=tests/test_serializer.py
|
||||||
|
90
tests/renderdot.py
Normal file
90
tests/renderdot.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
class Renderer(object):
|
||||||
|
|
||||||
|
def __init__(self, getleft, getright,
|
||||||
|
getred, getstart, getend, nil):
|
||||||
|
self.getleft = getleft
|
||||||
|
self.getright = getright
|
||||||
|
self.getred = getred
|
||||||
|
self.getstart = getstart
|
||||||
|
self.getend = getend
|
||||||
|
self.nil = nil
|
||||||
|
|
||||||
|
# Rendering
|
||||||
|
def __render_dot_node(self, node, max_depth = 20):
|
||||||
|
from nilmdb.utils.printf import sprintf
|
||||||
|
"""Render a single node and its children into a dot graph fragment"""
|
||||||
|
if max_depth == 0:
|
||||||
|
return ""
|
||||||
|
if node is self.nil:
|
||||||
|
return ""
|
||||||
|
def c(red):
|
||||||
|
if red:
|
||||||
|
return 'color="#ff0000", style=filled, fillcolor="#ffc0c0"'
|
||||||
|
else:
|
||||||
|
return 'color="#000000", style=filled, fillcolor="#c0c0c0"'
|
||||||
|
s = sprintf("%d [label=\"%g\\n%g\", %s];\n",
|
||||||
|
id(node),
|
||||||
|
self.getstart(node), self.getend(node),
|
||||||
|
c(self.getred(node)))
|
||||||
|
|
||||||
|
if self.getleft(node) is self.nil:
|
||||||
|
s += sprintf("L%d [label=\"-\", %s];\n", id(node), c(False))
|
||||||
|
s += sprintf("%d -> L%d [label=L];\n", id(node), id(node))
|
||||||
|
else:
|
||||||
|
s += sprintf("%d -> %d [label=L];\n",
|
||||||
|
id(node),id(self.getleft(node)))
|
||||||
|
if self.getright(node) is self.nil:
|
||||||
|
s += sprintf("R%d [label=\"-\", %s];\n", id(node), c(False))
|
||||||
|
s += sprintf("%d -> R%d [label=R];\n", id(node), id(node))
|
||||||
|
else:
|
||||||
|
s += sprintf("%d -> %d [label=R];\n",
|
||||||
|
id(node), id(self.getright(node)))
|
||||||
|
s += self.__render_dot_node(self.getleft(node), max_depth-1)
|
||||||
|
s += self.__render_dot_node(self.getright(node), max_depth-1)
|
||||||
|
return s
|
||||||
|
|
||||||
|
def render_dot(self, rootnode, title = "Tree"):
|
||||||
|
"""Render the entire tree as a dot graph"""
|
||||||
|
return ("digraph rbtree {\n"
|
||||||
|
+ self.__render_dot_node(rootnode)
|
||||||
|
+ "}\n");
|
||||||
|
|
||||||
|
def render_dot_live(self, rootnode, title = "Tree"):
|
||||||
|
"""Render the entiretree as a dot graph, live GTK view"""
|
||||||
|
import gtk
|
||||||
|
import gtk.gdk
|
||||||
|
sys.path.append("/usr/share/xdot")
|
||||||
|
import xdot
|
||||||
|
xdot.Pen.highlighted = lambda pen: pen
|
||||||
|
s = ("digraph rbtree {\n"
|
||||||
|
+ self.__render_dot_node(rootnode)
|
||||||
|
+ "}\n");
|
||||||
|
window = xdot.DotWindow()
|
||||||
|
window.set_dotcode(s)
|
||||||
|
window.set_title(title + " - any key to close")
|
||||||
|
window.connect('destroy', gtk.main_quit)
|
||||||
|
def quit(widget, event):
|
||||||
|
if not event.is_modifier:
|
||||||
|
window.destroy()
|
||||||
|
gtk.main_quit()
|
||||||
|
window.widget.connect('key-press-event', quit)
|
||||||
|
gtk.main()
|
||||||
|
|
||||||
|
class RBTreeRenderer(Renderer):
|
||||||
|
def __init__(self, tree):
|
||||||
|
Renderer.__init__(self,
|
||||||
|
lambda node: node.left,
|
||||||
|
lambda node: node.right,
|
||||||
|
lambda node: node.red,
|
||||||
|
lambda node: node.start,
|
||||||
|
lambda node: node.end,
|
||||||
|
tree.nil)
|
||||||
|
self.tree = tree
|
||||||
|
|
||||||
|
def render(self, title = "RBTree", live = True):
|
||||||
|
if live:
|
||||||
|
return Renderer.render_dot_live(self, self.tree.getroot(), title)
|
||||||
|
else:
|
||||||
|
return Renderer.render_dot(self, self.tree.getroot(), title)
|
@@ -1,5 +1,5 @@
|
|||||||
import nilmdb
|
import nilmdb
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
from nilmdb.client import ClientError, ServerError
|
from nilmdb.client import ClientError, ServerError
|
||||||
|
|
||||||
import datetime_tz
|
import datetime_tz
|
||||||
@@ -131,6 +131,7 @@ class TestClient(object):
|
|||||||
|
|
||||||
testfile = "tests/data/prep-20120323T1000"
|
testfile = "tests/data/prep-20120323T1000"
|
||||||
start = datetime_tz.datetime_tz.smartparse("20120323T1000")
|
start = datetime_tz.datetime_tz.smartparse("20120323T1000")
|
||||||
|
start = start.totimestamp()
|
||||||
rate = 120
|
rate = 120
|
||||||
|
|
||||||
# First try a nonexistent path
|
# First try a nonexistent path
|
||||||
@@ -155,14 +156,41 @@ class TestClient(object):
|
|||||||
|
|
||||||
# Try forcing a server request with empty data
|
# Try forcing a server request with empty data
|
||||||
with assert_raises(ClientError) as e:
|
with assert_raises(ClientError) as e:
|
||||||
client.http.put("stream/insert", "", { "path": "/newton/prep" })
|
client.http.put("stream/insert", "", { "path": "/newton/prep",
|
||||||
|
"start": 0, "end": 0 })
|
||||||
in_("400 Bad Request", str(e.exception))
|
in_("400 Bad Request", str(e.exception))
|
||||||
in_("no data provided", str(e.exception))
|
in_("no data provided", str(e.exception))
|
||||||
|
|
||||||
|
# Specify start/end (starts too late)
|
||||||
|
data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
|
||||||
|
with assert_raises(ClientError) as e:
|
||||||
|
result = client.stream_insert("/newton/prep", data,
|
||||||
|
start + 5, start + 120)
|
||||||
|
in_("400 Bad Request", str(e.exception))
|
||||||
|
in_("Data timestamp 1332511200.0 < start time 1332511205.0",
|
||||||
|
str(e.exception))
|
||||||
|
|
||||||
|
# Specify start/end (ends too early)
|
||||||
|
data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
|
||||||
|
with assert_raises(ClientError) as e:
|
||||||
|
result = client.stream_insert("/newton/prep", data,
|
||||||
|
start, start + 1)
|
||||||
|
in_("400 Bad Request", str(e.exception))
|
||||||
|
# Client chunks the input, so the exact timestamp here might change
|
||||||
|
# if the chunk positions change.
|
||||||
|
in_("Data timestamp 1332511271.016667 >= end time 1332511201.0",
|
||||||
|
str(e.exception))
|
||||||
|
|
||||||
# Now do the real load
|
# Now do the real load
|
||||||
data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
|
data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
|
||||||
result = client.stream_insert("/newton/prep", data)
|
result = client.stream_insert("/newton/prep", data,
|
||||||
eq_(result[0], "ok")
|
start, start + 119.999777)
|
||||||
|
eq_(result, "ok")
|
||||||
|
|
||||||
|
# Verify the intervals. Should be just one, even if the data
|
||||||
|
# was inserted in chunks, due to nilmdb interval concatenation.
|
||||||
|
intervals = list(client.stream_intervals("/newton/prep"))
|
||||||
|
eq_(intervals, [[start, start + 119.999777]])
|
||||||
|
|
||||||
# Try some overlapping data -- just insert it again
|
# Try some overlapping data -- just insert it again
|
||||||
data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
|
data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
|
||||||
@@ -215,7 +243,8 @@ class TestClient(object):
|
|||||||
# Check PUT with generator out
|
# Check PUT with generator out
|
||||||
with assert_raises(ClientError) as e:
|
with assert_raises(ClientError) as e:
|
||||||
client.http.put_gen("stream/insert", "",
|
client.http.put_gen("stream/insert", "",
|
||||||
{ "path": "/newton/prep" }).next()
|
{ "path": "/newton/prep",
|
||||||
|
"start": 0, "end": 0 }).next()
|
||||||
in_("400 Bad Request", str(e.exception))
|
in_("400 Bad Request", str(e.exception))
|
||||||
in_("no data provided", str(e.exception))
|
in_("no data provided", str(e.exception))
|
||||||
|
|
||||||
@@ -238,7 +267,7 @@ class TestClient(object):
|
|||||||
# still disable chunked responses for debugging.
|
# still disable chunked responses for debugging.
|
||||||
x = client.http.get("stream/intervals", { "path": "/newton/prep" },
|
x = client.http.get("stream/intervals", { "path": "/newton/prep" },
|
||||||
retjson=False)
|
retjson=False)
|
||||||
eq_(x.count('\n'), 2)
|
lines_(x, 1)
|
||||||
if "transfer-encoding: chunked" not in client.http._headers.lower():
|
if "transfer-encoding: chunked" not in client.http._headers.lower():
|
||||||
warnings.warn("Non-chunked HTTP response for /stream/intervals")
|
warnings.warn("Non-chunked HTTP response for /stream/intervals")
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import nilmdb
|
import nilmdb
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
import nilmdb.cmdline
|
import nilmdb.cmdline
|
||||||
|
|
||||||
from nose.tools import *
|
from nose.tools import *
|
||||||
@@ -51,6 +51,7 @@ class TestCmdline(object):
|
|||||||
"""Run a cmdline client with the specified argument string,
|
"""Run a cmdline client with the specified argument string,
|
||||||
passing the given input. Returns a tuple with the output and
|
passing the given input. Returns a tuple with the output and
|
||||||
exit code"""
|
exit code"""
|
||||||
|
#print "TZ=UTC ./nilmtool.py " + arg_string
|
||||||
class stdio_wrapper:
|
class stdio_wrapper:
|
||||||
def __init__(self, stdin, stdout, stderr):
|
def __init__(self, stdin, stdout, stderr):
|
||||||
self.io = (stdin, stdout, stderr)
|
self.io = (stdin, stdout, stderr)
|
||||||
@@ -192,11 +193,22 @@ class TestCmdline(object):
|
|||||||
self.contain("no such layout")
|
self.contain("no such layout")
|
||||||
|
|
||||||
# Create a few streams
|
# Create a few streams
|
||||||
|
self.ok("create /newton/zzz/rawnotch RawNotchedData")
|
||||||
self.ok("create /newton/prep PrepData")
|
self.ok("create /newton/prep PrepData")
|
||||||
self.ok("create /newton/raw RawData")
|
self.ok("create /newton/raw RawData")
|
||||||
self.ok("create /newton/zzz/rawnotch RawNotchedData")
|
|
||||||
|
|
||||||
# Verify we got those 3 streams
|
# Should not be able to create a stream with another stream as
|
||||||
|
# its parent
|
||||||
|
self.fail("create /newton/prep/blah PrepData")
|
||||||
|
self.contain("path is subdir of existing node")
|
||||||
|
|
||||||
|
# Should not be able to create a stream at a location that
|
||||||
|
# has other nodes as children
|
||||||
|
self.fail("create /newton/zzz PrepData")
|
||||||
|
self.contain("subdirs of this path already exist")
|
||||||
|
|
||||||
|
# Verify we got those 3 streams and they're returned in
|
||||||
|
# alphabetical order.
|
||||||
self.ok("list")
|
self.ok("list")
|
||||||
self.match("/newton/prep PrepData\n"
|
self.match("/newton/prep PrepData\n"
|
||||||
"/newton/raw RawData\n"
|
"/newton/raw RawData\n"
|
||||||
@@ -362,36 +374,36 @@ class TestCmdline(object):
|
|||||||
def test_cmdline_07_detail(self):
|
def test_cmdline_07_detail(self):
|
||||||
# Just count the number of lines, it's probably fine
|
# Just count the number of lines, it's probably fine
|
||||||
self.ok("list --detail")
|
self.ok("list --detail")
|
||||||
eq_(self.captured.count('\n'), 11)
|
lines_(self.captured, 8)
|
||||||
|
|
||||||
self.ok("list --detail --path *prep")
|
self.ok("list --detail --path *prep")
|
||||||
eq_(self.captured.count('\n'), 7)
|
lines_(self.captured, 4)
|
||||||
|
|
||||||
self.ok("list --detail --path *prep --start='23 Mar 2012 10:02'")
|
self.ok("list --detail --path *prep --start='23 Mar 2012 10:02'")
|
||||||
eq_(self.captured.count('\n'), 5)
|
lines_(self.captured, 3)
|
||||||
|
|
||||||
self.ok("list --detail --path *prep --start='23 Mar 2012 10:05'")
|
self.ok("list --detail --path *prep --start='23 Mar 2012 10:05'")
|
||||||
eq_(self.captured.count('\n'), 3)
|
lines_(self.captured, 2)
|
||||||
|
|
||||||
self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15'")
|
self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15'")
|
||||||
eq_(self.captured.count('\n'), 2)
|
lines_(self.captured, 2)
|
||||||
self.contain("10:05:15.000")
|
self.contain("10:05:15.000")
|
||||||
|
|
||||||
self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15.50'")
|
self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15.50'")
|
||||||
eq_(self.captured.count('\n'), 2)
|
lines_(self.captured, 2)
|
||||||
self.contain("10:05:15.500")
|
self.contain("10:05:15.500")
|
||||||
|
|
||||||
self.ok("list --detail --path *prep --start='23 Mar 2012 19:05:15.50'")
|
self.ok("list --detail --path *prep --start='23 Mar 2012 19:05:15.50'")
|
||||||
eq_(self.captured.count('\n'), 2)
|
lines_(self.captured, 2)
|
||||||
self.contain("no intervals")
|
self.contain("no intervals")
|
||||||
|
|
||||||
self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15.50'"
|
self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15.50'"
|
||||||
+ " --end='23 Mar 2012 10:05:15.50'")
|
+ " --end='23 Mar 2012 10:05:15.50'")
|
||||||
eq_(self.captured.count('\n'), 2)
|
lines_(self.captured, 2)
|
||||||
self.contain("10:05:15.500")
|
self.contain("10:05:15.500")
|
||||||
|
|
||||||
self.ok("list --detail")
|
self.ok("list --detail")
|
||||||
eq_(self.captured.count('\n'), 11)
|
lines_(self.captured, 8)
|
||||||
|
|
||||||
def test_cmdline_08_extract(self):
|
def test_cmdline_08_extract(self):
|
||||||
# nonexistent stream
|
# nonexistent stream
|
||||||
@@ -444,7 +456,7 @@ class TestCmdline(object):
|
|||||||
|
|
||||||
# all data put in by tests
|
# all data put in by tests
|
||||||
self.ok("extract -a /newton/prep --start 2000-01-01 --end 2020-01-01")
|
self.ok("extract -a /newton/prep --start 2000-01-01 --end 2020-01-01")
|
||||||
eq_(self.captured.count('\n'), 43204)
|
lines_(self.captured, 43204)
|
||||||
self.ok("extract -c /newton/prep --start 2000-01-01 --end 2020-01-01")
|
self.ok("extract -c /newton/prep --start 2000-01-01 --end 2020-01-01")
|
||||||
self.match("43200\n")
|
self.match("43200\n")
|
||||||
|
|
||||||
@@ -453,6 +465,57 @@ class TestCmdline(object):
|
|||||||
server_stop()
|
server_stop()
|
||||||
server_start(max_results = 2)
|
server_start(max_results = 2)
|
||||||
self.ok("list --detail")
|
self.ok("list --detail")
|
||||||
eq_(self.captured.count('\n'), 11)
|
lines_(self.captured, 8)
|
||||||
server_stop()
|
server_stop()
|
||||||
server_start()
|
server_start()
|
||||||
|
|
||||||
|
def test_cmdline_10_destroy(self):
|
||||||
|
# Delete records
|
||||||
|
self.ok("destroy --help")
|
||||||
|
|
||||||
|
self.fail("destroy")
|
||||||
|
self.contain("too few arguments")
|
||||||
|
|
||||||
|
self.fail("destroy /no/such/stream")
|
||||||
|
self.contain("No stream at path")
|
||||||
|
|
||||||
|
self.fail("destroy asdfasdf")
|
||||||
|
self.contain("No stream at path")
|
||||||
|
|
||||||
|
# From previous tests, we have:
|
||||||
|
self.ok("list")
|
||||||
|
self.match("/newton/prep PrepData\n"
|
||||||
|
"/newton/raw RawData\n"
|
||||||
|
"/newton/zzz/rawnotch RawNotchedData\n")
|
||||||
|
|
||||||
|
# Notice how they're not empty
|
||||||
|
self.ok("list --detail")
|
||||||
|
lines_(self.captured, 8)
|
||||||
|
|
||||||
|
# Delete some
|
||||||
|
self.ok("destroy /newton/prep")
|
||||||
|
self.ok("list")
|
||||||
|
self.match("/newton/raw RawData\n"
|
||||||
|
"/newton/zzz/rawnotch RawNotchedData\n")
|
||||||
|
|
||||||
|
self.ok("destroy /newton/zzz/rawnotch")
|
||||||
|
self.ok("list")
|
||||||
|
self.match("/newton/raw RawData\n")
|
||||||
|
|
||||||
|
self.ok("destroy /newton/raw")
|
||||||
|
self.ok("create /newton/raw RawData")
|
||||||
|
self.ok("destroy /newton/raw")
|
||||||
|
self.ok("list")
|
||||||
|
self.match("")
|
||||||
|
|
||||||
|
# Re-create a previously deleted location, and some new ones
|
||||||
|
rebuild = [ "/newton/prep", "/newton/zzz",
|
||||||
|
"/newton/raw", "/newton/asdf/qwer" ]
|
||||||
|
for path in rebuild:
|
||||||
|
# Create the path
|
||||||
|
self.ok("create " + path + " PrepData")
|
||||||
|
self.ok("list")
|
||||||
|
self.contain(path)
|
||||||
|
# Make sure it was created empty
|
||||||
|
self.ok("list --detail --path " + path)
|
||||||
|
self.contain("(no intervals)")
|
||||||
|
@@ -20,6 +20,12 @@ def ne_(a, b):
|
|||||||
if not a != b:
|
if not a != b:
|
||||||
raise AssertionError("unexpected %s == %s" % (myrepr(a), myrepr(b)))
|
raise AssertionError("unexpected %s == %s" % (myrepr(a), myrepr(b)))
|
||||||
|
|
||||||
|
def lines_(a, n):
|
||||||
|
l = a.count('\n')
|
||||||
|
if not l == n:
|
||||||
|
raise AssertionError("wanted %d lines, got %d in output: '%s'"
|
||||||
|
% (n, l, a))
|
||||||
|
|
||||||
def recursive_unlink(path):
|
def recursive_unlink(path):
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(path)
|
shutil.rmtree(path)
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import nilmdb
|
import nilmdb
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
import datetime_tz
|
import datetime_tz
|
||||||
|
|
||||||
from nose.tools import *
|
from nose.tools import *
|
||||||
@@ -13,12 +13,19 @@ from nilmdb.interval import Interval, DBInterval, IntervalSet, IntervalError
|
|||||||
from test_helpers import *
|
from test_helpers import *
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
# set to False to skip live renders
|
||||||
|
do_live_renders = False
|
||||||
|
def render(iset, description = "", live = True):
|
||||||
|
import renderdot
|
||||||
|
r = renderdot.RBTreeRenderer(iset.tree)
|
||||||
|
return r.render(description, live and do_live_renders)
|
||||||
|
|
||||||
def makeset(string):
|
def makeset(string):
|
||||||
"""Build an IntervalSet from a string, for testing purposes
|
"""Build an IntervalSet from a string, for testing purposes
|
||||||
|
|
||||||
Each character is 1 second
|
Each character is 1 second
|
||||||
[ = interval start
|
[ = interval start
|
||||||
| = interval end + adjacent start
|
| = interval end + next start
|
||||||
] = interval end
|
] = interval end
|
||||||
. = zero-width interval (identical start and end)
|
. = zero-width interval (identical start and end)
|
||||||
anything else is ignored
|
anything else is ignored
|
||||||
@@ -31,7 +38,7 @@ def makeset(string):
|
|||||||
elif (c == "|"):
|
elif (c == "|"):
|
||||||
iset += Interval(start, day)
|
iset += Interval(start, day)
|
||||||
start = day
|
start = day
|
||||||
elif (c == "]"):
|
elif (c == ")"):
|
||||||
iset += Interval(start, day)
|
iset += Interval(start, day)
|
||||||
del start
|
del start
|
||||||
elif (c == "."):
|
elif (c == "."):
|
||||||
@@ -71,24 +78,24 @@ class TestInterval:
|
|||||||
assert(Interval(d1, d3) < Interval(d2, d3))
|
assert(Interval(d1, d3) < Interval(d2, d3))
|
||||||
assert(Interval(d2, d2) > Interval(d1, d3))
|
assert(Interval(d2, d2) > Interval(d1, d3))
|
||||||
assert(Interval(d3, d3) == Interval(d3, d3))
|
assert(Interval(d3, d3) == Interval(d3, d3))
|
||||||
with assert_raises(TypeError): # was AttributeError, that's wrong
|
#with assert_raises(TypeError): # was AttributeError, that's wrong
|
||||||
x = (i == 123)
|
# x = (i == 123)
|
||||||
|
|
||||||
# subset
|
# subset
|
||||||
assert(Interval(d1, d3).subset(d1, d2) == Interval(d1, d2))
|
eq_(Interval(d1, d3).subset(d1, d2), Interval(d1, d2))
|
||||||
with assert_raises(IntervalError):
|
with assert_raises(IntervalError):
|
||||||
x = Interval(d2, d3).subset(d1, d2)
|
x = Interval(d2, d3).subset(d1, d2)
|
||||||
|
|
||||||
# big integers and floats
|
# big integers and floats
|
||||||
x = Interval(5000111222, 6000111222)
|
x = Interval(5000111222, 6000111222)
|
||||||
eq_(str(x), "[5000111222.0 -> 6000111222.0]")
|
eq_(str(x), "[5000111222.0 -> 6000111222.0)")
|
||||||
x = Interval(123.45, 234.56)
|
x = Interval(123.45, 234.56)
|
||||||
eq_(str(x), "[123.45 -> 234.56]")
|
eq_(str(x), "[123.45 -> 234.56)")
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
i = Interval(d1, d2)
|
i = Interval(d1, d2)
|
||||||
eq_(repr(i), repr(eval(repr(i))))
|
eq_(repr(i), repr(eval(repr(i))))
|
||||||
eq_(str(i), "[1332561600.0 -> 1332648000.0]")
|
eq_(str(i), "[1332561600.0 -> 1332648000.0)")
|
||||||
|
|
||||||
def test_interval_intersect(self):
|
def test_interval_intersect(self):
|
||||||
# Test Interval intersections
|
# Test Interval intersections
|
||||||
@@ -109,7 +116,7 @@ class TestInterval:
|
|||||||
except IntervalError:
|
except IntervalError:
|
||||||
assert(i not in should_intersect[True] and
|
assert(i not in should_intersect[True] and
|
||||||
i not in should_intersect[False])
|
i not in should_intersect[False])
|
||||||
with assert_raises(AttributeError):
|
with assert_raises(TypeError):
|
||||||
x = i1.intersects(1234)
|
x = i1.intersects(1234)
|
||||||
|
|
||||||
def test_intervalset_construct(self):
|
def test_intervalset_construct(self):
|
||||||
@@ -130,6 +137,15 @@ class TestInterval:
|
|||||||
x = iseta != 3
|
x = iseta != 3
|
||||||
ne_(IntervalSet(a), IntervalSet(b))
|
ne_(IntervalSet(a), IntervalSet(b))
|
||||||
|
|
||||||
|
# Note that assignment makes a new reference (not a copy)
|
||||||
|
isetd = IntervalSet(isetb)
|
||||||
|
isete = isetd
|
||||||
|
eq_(isetd, isetb)
|
||||||
|
eq_(isetd, isete)
|
||||||
|
isetd -= a
|
||||||
|
ne_(isetd, isetb)
|
||||||
|
eq_(isetd, isete)
|
||||||
|
|
||||||
# test iterator
|
# test iterator
|
||||||
for interval in iseta:
|
for interval in iseta:
|
||||||
pass
|
pass
|
||||||
@@ -151,11 +167,18 @@ class TestInterval:
|
|||||||
iset = IntervalSet(a)
|
iset = IntervalSet(a)
|
||||||
iset += IntervalSet(b)
|
iset += IntervalSet(b)
|
||||||
eq_(iset, IntervalSet([a, b]))
|
eq_(iset, IntervalSet([a, b]))
|
||||||
|
|
||||||
iset = IntervalSet(a)
|
iset = IntervalSet(a)
|
||||||
iset += b
|
iset += b
|
||||||
eq_(iset, IntervalSet([a, b]))
|
eq_(iset, IntervalSet([a, b]))
|
||||||
|
|
||||||
|
iset = IntervalSet(a)
|
||||||
|
iset.iadd_nocheck(b)
|
||||||
|
eq_(iset, IntervalSet([a, b]))
|
||||||
|
|
||||||
iset = IntervalSet(a) + IntervalSet(b)
|
iset = IntervalSet(a) + IntervalSet(b)
|
||||||
eq_(iset, IntervalSet([a, b]))
|
eq_(iset, IntervalSet([a, b]))
|
||||||
|
|
||||||
iset = IntervalSet(b) + a
|
iset = IntervalSet(b) + a
|
||||||
eq_(iset, IntervalSet([a, b]))
|
eq_(iset, IntervalSet([a, b]))
|
||||||
|
|
||||||
@@ -168,61 +191,79 @@ class TestInterval:
|
|||||||
|
|
||||||
# misc
|
# misc
|
||||||
eq_(repr(iset), repr(eval(repr(iset))))
|
eq_(repr(iset), repr(eval(repr(iset))))
|
||||||
eq_(str(iset), "[[100.0 -> 200.0], [200.0 -> 300.0]]")
|
eq_(str(iset), "[[100.0 -> 200.0), [200.0 -> 300.0)]")
|
||||||
|
|
||||||
def test_intervalset_geniset(self):
|
def test_intervalset_geniset(self):
|
||||||
# Test basic iset construction
|
# Test basic iset construction
|
||||||
assert(makeset(" [----] ") ==
|
eq_(makeset(" [----) "),
|
||||||
makeset(" [-|--] "))
|
makeset(" [-|--) "))
|
||||||
|
|
||||||
assert(makeset("[] [--] ") +
|
eq_(makeset("[) [--) ") +
|
||||||
makeset(" [] [--]") ==
|
makeset(" [) [--)"),
|
||||||
makeset("[|] [-----]"))
|
makeset("[|) [-----)"))
|
||||||
|
|
||||||
assert(makeset(" [-------]") ==
|
eq_(makeset(" [-------)"),
|
||||||
makeset(" [-|-----|"))
|
makeset(" [-|-----|"))
|
||||||
|
|
||||||
|
|
||||||
def test_intervalset_intersect(self):
|
def test_intervalset_intersect(self):
|
||||||
# Test intersection (&)
|
# Test intersection (&)
|
||||||
with assert_raises(TypeError): # was AttributeError
|
with assert_raises(TypeError): # was AttributeError
|
||||||
x = makeset("[--]") & 1234
|
x = makeset("[--)") & 1234
|
||||||
|
|
||||||
assert(makeset("[---------]") &
|
# Intersection with interval
|
||||||
makeset(" [---] ") ==
|
eq_(makeset("[---|---)[)") &
|
||||||
makeset(" [---] "))
|
list(makeset(" [------) "))[0],
|
||||||
|
makeset(" [-----) "))
|
||||||
|
|
||||||
assert(makeset(" [---] ") &
|
# Intersection with sets
|
||||||
makeset("[---------]") ==
|
eq_(makeset("[---------)") &
|
||||||
makeset(" [---] "))
|
makeset(" [---) "),
|
||||||
|
makeset(" [---) "))
|
||||||
|
|
||||||
assert(makeset(" [-----]") &
|
eq_(makeset(" [---) ") &
|
||||||
makeset(" [-----] ") ==
|
makeset("[---------)"),
|
||||||
makeset(" [--] "))
|
makeset(" [---) "))
|
||||||
|
|
||||||
assert(makeset(" [--] [--]") &
|
eq_(makeset(" [-----)") &
|
||||||
makeset(" [------] ") ==
|
makeset(" [-----) "),
|
||||||
makeset(" [-] [-] "))
|
makeset(" [--) "))
|
||||||
|
|
||||||
assert(makeset(" [---]") &
|
eq_(makeset(" [--) [--)") &
|
||||||
makeset(" [--] ") ==
|
makeset(" [------) "),
|
||||||
makeset(" "))
|
makeset(" [-) [-) "))
|
||||||
|
|
||||||
assert(makeset(" [---]") &
|
eq_(makeset(" [---)") &
|
||||||
makeset(" [----] ") ==
|
makeset(" [--) "),
|
||||||
makeset(" . "))
|
makeset(" "))
|
||||||
|
|
||||||
assert(makeset(" [-|---]") &
|
eq_(makeset(" [-|---)") &
|
||||||
makeset(" [-----|-] ") ==
|
makeset(" [-----|-) "),
|
||||||
makeset(" [----] "))
|
makeset(" [----) "))
|
||||||
|
|
||||||
assert(makeset(" [-|-] ") &
|
eq_(makeset(" [-|-) ") &
|
||||||
makeset(" [-|--|--] ") ==
|
makeset(" [-|--|--) "),
|
||||||
makeset(" [---] "))
|
makeset(" [---) "))
|
||||||
|
|
||||||
assert(makeset(" [----][--]") &
|
# Border cases -- will give different results if intervals are
|
||||||
makeset("[-] [--] []") ==
|
# half open or fully closed. Right now, they are half open,
|
||||||
makeset(" [] [-]. []"))
|
# although that's a little messy since the database intervals
|
||||||
|
# often contain a data point at the endpoint.
|
||||||
|
half_open = True
|
||||||
|
if half_open:
|
||||||
|
eq_(makeset(" [---)") &
|
||||||
|
makeset(" [----) "),
|
||||||
|
makeset(" "))
|
||||||
|
eq_(makeset(" [----)[--)") &
|
||||||
|
makeset("[-) [--) [)"),
|
||||||
|
makeset(" [) [-) [)"))
|
||||||
|
else:
|
||||||
|
eq_(makeset(" [---)") &
|
||||||
|
makeset(" [----) "),
|
||||||
|
makeset(" . "))
|
||||||
|
eq_(makeset(" [----)[--)") &
|
||||||
|
makeset("[-) [--) [)"),
|
||||||
|
makeset(" [) [-). [)"))
|
||||||
|
|
||||||
class TestIntervalDB:
|
class TestIntervalDB:
|
||||||
def test_dbinterval(self):
|
def test_dbinterval(self):
|
||||||
@@ -273,12 +314,13 @@ class TestIntervalTree:
|
|||||||
import random
|
import random
|
||||||
random.seed(1234)
|
random.seed(1234)
|
||||||
|
|
||||||
# make a set of 500 intervals
|
# make a set of 100 intervals
|
||||||
iset = IntervalSet()
|
iset = IntervalSet()
|
||||||
j = 500
|
j = 100
|
||||||
for i in random.sample(xrange(j),j):
|
for i in random.sample(xrange(j),j):
|
||||||
interval = Interval(i, i+1)
|
interval = Interval(i, i+1)
|
||||||
iset += interval
|
iset += interval
|
||||||
|
render(iset, "Random Insertion")
|
||||||
|
|
||||||
# remove about half of them
|
# remove about half of them
|
||||||
for i in random.sample(xrange(j),j):
|
for i in random.sample(xrange(j),j):
|
||||||
@@ -288,10 +330,15 @@ class TestIntervalTree:
|
|||||||
# try removing an interval that doesn't exist
|
# try removing an interval that doesn't exist
|
||||||
with assert_raises(IntervalError):
|
with assert_raises(IntervalError):
|
||||||
iset -= Interval(1234,5678)
|
iset -= Interval(1234,5678)
|
||||||
|
render(iset, "Random Insertion, deletion")
|
||||||
|
|
||||||
# show the graph
|
# make a set of 100 intervals, inserted in order
|
||||||
if False:
|
iset = IntervalSet()
|
||||||
iset.tree.render_dot_live()
|
j = 100
|
||||||
|
for i in xrange(j):
|
||||||
|
interval = Interval(i, i+1)
|
||||||
|
iset += interval
|
||||||
|
render(iset, "In-order insertion")
|
||||||
|
|
||||||
class TestIntervalSpeed:
|
class TestIntervalSpeed:
|
||||||
@unittest.skip("this is slow")
|
@unittest.skip("this is slow")
|
||||||
@@ -300,18 +347,23 @@ class TestIntervalSpeed:
|
|||||||
import time
|
import time
|
||||||
import aplotter
|
import aplotter
|
||||||
import random
|
import random
|
||||||
|
import math
|
||||||
|
|
||||||
print
|
print
|
||||||
yappi.start()
|
yappi.start()
|
||||||
speeds = {}
|
speeds = {}
|
||||||
for j in [ 2**x for x in range(5,18) ]:
|
for j in [ 2**x for x in range(5,20) ]:
|
||||||
start = time.time()
|
start = time.time()
|
||||||
iset = IntervalSet()
|
iset = IntervalSet()
|
||||||
for i in random.sample(xrange(j),j):
|
for i in random.sample(xrange(j),j):
|
||||||
interval = Interval(i, i+1)
|
interval = Interval(i, i+1)
|
||||||
iset += interval
|
iset += interval
|
||||||
speed = (time.time() - start) * 1000000.0
|
speed = (time.time() - start) * 1000000.0
|
||||||
printf("%d: %g μs (%g μs each)\n", j, speed, speed/j)
|
printf("%d: %g μs (%g μs each, O(n log n) ratio %g)\n",
|
||||||
|
j,
|
||||||
|
speed,
|
||||||
|
speed/j,
|
||||||
|
speed / (j*math.log(j))) # should be constant
|
||||||
speeds[j] = speed
|
speeds[j] = speed
|
||||||
aplotter.plot(speeds.keys(), speeds.values(), plot_slope=True)
|
aplotter.plot(speeds.keys(), speeds.values(), plot_slope=True)
|
||||||
yappi.stop()
|
yappi.stop()
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import nilmdb
|
import nilmdb
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
import nose
|
import nose
|
||||||
from nose.tools import *
|
from nose.tools import *
|
||||||
@@ -9,8 +9,6 @@ import time
|
|||||||
|
|
||||||
from test_helpers import *
|
from test_helpers import *
|
||||||
|
|
||||||
import nilmdb.iteratorizer
|
|
||||||
|
|
||||||
def func_with_callback(a, b, callback):
|
def func_with_callback(a, b, callback):
|
||||||
callback(a)
|
callback(a)
|
||||||
callback(b)
|
callback(b)
|
||||||
@@ -27,16 +25,18 @@ class TestIteratorizer(object):
|
|||||||
eq_(self.result, "123")
|
eq_(self.result, "123")
|
||||||
|
|
||||||
# Now make it an iterator
|
# Now make it an iterator
|
||||||
it = nilmdb.iteratorizer.Iteratorizer(lambda x:
|
it = nilmdb.utils.Iteratorizer(
|
||||||
func_with_callback(1, 2, x))
|
lambda x:
|
||||||
|
func_with_callback(1, 2, x))
|
||||||
result = ""
|
result = ""
|
||||||
for i in it:
|
for i in it:
|
||||||
result += str(i)
|
result += str(i)
|
||||||
eq_(result, "123")
|
eq_(result, "123")
|
||||||
|
|
||||||
# Make sure things work when an exception occurs
|
# Make sure things work when an exception occurs
|
||||||
it = nilmdb.iteratorizer.Iteratorizer(lambda x:
|
it = nilmdb.utils.Iteratorizer(
|
||||||
func_with_callback(1, "a", x))
|
lambda x:
|
||||||
|
func_with_callback(1, "a", x))
|
||||||
result = ""
|
result = ""
|
||||||
with assert_raises(TypeError) as e:
|
with assert_raises(TypeError) as e:
|
||||||
for i in it:
|
for i in it:
|
||||||
@@ -48,7 +48,8 @@ class TestIteratorizer(object):
|
|||||||
# itself. This doesn't have a particular result in the test,
|
# itself. This doesn't have a particular result in the test,
|
||||||
# but gains coverage.
|
# but gains coverage.
|
||||||
def foo():
|
def foo():
|
||||||
it = nilmdb.iteratorizer.Iteratorizer(lambda x:
|
it = nilmdb.utils.Iteratorizer(
|
||||||
func_with_callback(1, 2, x))
|
lambda x:
|
||||||
|
func_with_callback(1, 2, x))
|
||||||
it.next()
|
it.next()
|
||||||
foo()
|
foo()
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import nilmdb
|
import nilmdb
|
||||||
|
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
from nose.tools import *
|
from nose.tools import *
|
||||||
from nose.tools import assert_raises
|
from nose.tools import assert_raises
|
||||||
@@ -28,9 +28,13 @@ class TestLayouts(object):
|
|||||||
# Some nilmdb.layout tests. Not complete, just fills in missing
|
# Some nilmdb.layout tests. Not complete, just fills in missing
|
||||||
# coverage.
|
# coverage.
|
||||||
def test_layouts(self):
|
def test_layouts(self):
|
||||||
x = nilmdb.layout.get_named("PrepData").description()
|
x = nilmdb.layout.get_named("PrepData")
|
||||||
y = nilmdb.layout.get_named("float32_8").description()
|
y = nilmdb.layout.get_named("float32_8")
|
||||||
eq_(repr(x), repr(y))
|
eq_(x.count, y.count)
|
||||||
|
eq_(x.datatype, y.datatype)
|
||||||
|
y = nilmdb.layout.get_named("float32_7")
|
||||||
|
ne_(x.count, y.count)
|
||||||
|
eq_(x.datatype, y.datatype)
|
||||||
|
|
||||||
def test_parsing(self):
|
def test_parsing(self):
|
||||||
self.real_t_parsing("PrepData", "RawData", "RawNotchedData")
|
self.real_t_parsing("PrepData", "RawData", "RawNotchedData")
|
||||||
|
53
tests/test_lrucache.py
Normal file
53
tests/test_lrucache.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import nilmdb
|
||||||
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
|
import nose
|
||||||
|
from nose.tools import *
|
||||||
|
from nose.tools import assert_raises
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from test_helpers import *
|
||||||
|
|
||||||
|
@nilmdb.utils.lru_cache(size = 3)
|
||||||
|
def foo1(n):
|
||||||
|
return n
|
||||||
|
|
||||||
|
@nilmdb.utils.lru_cache(size = 5)
|
||||||
|
def foo2(n):
|
||||||
|
return n
|
||||||
|
|
||||||
|
def foo3d(n):
|
||||||
|
foo3d.destructed.append(n)
|
||||||
|
foo3d.destructed = []
|
||||||
|
@nilmdb.utils.lru_cache(size = 3, onremove = foo3d)
|
||||||
|
def foo3(n):
|
||||||
|
return n
|
||||||
|
|
||||||
|
class TestLRUCache(object):
|
||||||
|
def test(self):
|
||||||
|
[ foo1(n) for n in [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ] ]
|
||||||
|
eq_((foo1.cache_hits, foo1.cache_misses), (6, 3))
|
||||||
|
[ foo1(n) for n in [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ] ]
|
||||||
|
eq_((foo1.cache_hits, foo1.cache_misses), (15, 3))
|
||||||
|
[ foo1(n) for n in [ 4, 2, 1, 1, 4 ] ]
|
||||||
|
eq_((foo1.cache_hits, foo1.cache_misses), (18, 5))
|
||||||
|
|
||||||
|
[ foo2(n) for n in [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ] ]
|
||||||
|
eq_((foo2.cache_hits, foo2.cache_misses), (6, 3))
|
||||||
|
[ foo2(n) for n in [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ] ]
|
||||||
|
eq_((foo2.cache_hits, foo2.cache_misses), (15, 3))
|
||||||
|
[ foo2(n) for n in [ 4, 2, 1, 1, 4 ] ]
|
||||||
|
eq_((foo2.cache_hits, foo2.cache_misses), (19, 4))
|
||||||
|
|
||||||
|
[ foo3(n) for n in [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ] ]
|
||||||
|
eq_((foo3.cache_hits, foo3.cache_misses), (6, 3))
|
||||||
|
[ foo3(n) for n in [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ] ]
|
||||||
|
eq_((foo3.cache_hits, foo3.cache_misses), (15, 3))
|
||||||
|
[ foo3(n) for n in [ 4, 2, 1, 1, 4 ] ]
|
||||||
|
eq_((foo3.cache_hits, foo3.cache_misses), (18, 5))
|
||||||
|
eq_(foo3d.destructed, [1, 3])
|
||||||
|
foo3.cache_remove(1)
|
||||||
|
eq_(foo3d.destructed, [1, 3, 1])
|
||||||
|
foo3.cache_remove_all()
|
||||||
|
eq_(foo3d.destructed, [1, 3, 1, 2, 4 ])
|
59
tests/test_mustclose.py
Normal file
59
tests/test_mustclose.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import nilmdb
|
||||||
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
|
import nose
|
||||||
|
from nose.tools import *
|
||||||
|
from nose.tools import assert_raises
|
||||||
|
|
||||||
|
from test_helpers import *
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import cStringIO
|
||||||
|
|
||||||
|
err = cStringIO.StringIO()
|
||||||
|
|
||||||
|
@nilmdb.utils.must_close(errorfile = err)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self):
|
||||||
|
fprintf(err, "Init\n")
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
fprintf(err, "Deleting\n")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
fprintf(err, "Closing\n")
|
||||||
|
|
||||||
|
@nilmdb.utils.must_close(errorfile = err)
|
||||||
|
class Bar:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TestMustClose(object):
|
||||||
|
def test(self):
|
||||||
|
|
||||||
|
# Note: this test might fail if the Python interpreter doesn't
|
||||||
|
# garbage collect the object (and call its __del__ function)
|
||||||
|
# right after a "del x".
|
||||||
|
|
||||||
|
x = Foo()
|
||||||
|
del x
|
||||||
|
eq_(err.getvalue(),
|
||||||
|
"Init\n"
|
||||||
|
"error: Foo.close() wasn't called!\n"
|
||||||
|
"Deleting\n")
|
||||||
|
|
||||||
|
err.truncate(0)
|
||||||
|
|
||||||
|
y = Foo()
|
||||||
|
y.close()
|
||||||
|
del y
|
||||||
|
eq_(err.getvalue(),
|
||||||
|
"Init\n"
|
||||||
|
"Closing\n"
|
||||||
|
"Deleting\n")
|
||||||
|
|
||||||
|
err.truncate(0)
|
||||||
|
|
||||||
|
z = Bar()
|
||||||
|
z.close()
|
||||||
|
del z
|
||||||
|
eq_(err.getvalue(), "")
|
@@ -14,6 +14,7 @@ import urllib2
|
|||||||
from urllib2 import urlopen, HTTPError
|
from urllib2 import urlopen, HTTPError
|
||||||
import Queue
|
import Queue
|
||||||
import cStringIO
|
import cStringIO
|
||||||
|
import time
|
||||||
|
|
||||||
testdb = "tests/testdb"
|
testdb = "tests/testdb"
|
||||||
|
|
||||||
@@ -39,8 +40,8 @@ class Test00Nilmdb(object): # named 00 so it runs first
|
|||||||
capture = cStringIO.StringIO()
|
capture = cStringIO.StringIO()
|
||||||
old = sys.stdout
|
old = sys.stdout
|
||||||
sys.stdout = capture
|
sys.stdout = capture
|
||||||
with nilmdb.Timer("test"):
|
with nilmdb.utils.Timer("test"):
|
||||||
nilmdb.timer.time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
sys.stdout = old
|
sys.stdout = old
|
||||||
in_("test: ", capture.getvalue())
|
in_("test: ", capture.getvalue())
|
||||||
|
|
||||||
@@ -69,12 +70,14 @@ class Test00Nilmdb(object): # named 00 so it runs first
|
|||||||
eq_(db.stream_list(layout="RawData"), [ ["/newton/raw", "RawData"] ])
|
eq_(db.stream_list(layout="RawData"), [ ["/newton/raw", "RawData"] ])
|
||||||
eq_(db.stream_list(path="/newton/raw"), [ ["/newton/raw", "RawData"] ])
|
eq_(db.stream_list(path="/newton/raw"), [ ["/newton/raw", "RawData"] ])
|
||||||
|
|
||||||
# Verify that columns were made right
|
# Verify that columns were made right (pytables specific)
|
||||||
eq_(len(db.h5file.getNode("/newton/prep").cols), 9)
|
if "h5file" in db.data.__dict__:
|
||||||
eq_(len(db.h5file.getNode("/newton/raw").cols), 7)
|
h5file = db.data.h5file
|
||||||
eq_(len(db.h5file.getNode("/newton/zzz/rawnotch").cols), 10)
|
eq_(len(h5file.getNode("/newton/prep").cols), 9)
|
||||||
assert(not db.h5file.getNode("/newton/prep").colindexed["timestamp"])
|
eq_(len(h5file.getNode("/newton/raw").cols), 7)
|
||||||
assert(not db.h5file.getNode("/newton/prep").colindexed["c1"])
|
eq_(len(h5file.getNode("/newton/zzz/rawnotch").cols), 10)
|
||||||
|
assert(not h5file.getNode("/newton/prep").colindexed["timestamp"])
|
||||||
|
assert(not h5file.getNode("/newton/prep").colindexed["c1"])
|
||||||
|
|
||||||
# Set / get metadata
|
# Set / get metadata
|
||||||
eq_(db.stream_get_metadata("/newton/prep"), {})
|
eq_(db.stream_get_metadata("/newton/prep"), {})
|
||||||
@@ -196,6 +199,6 @@ class TestServer(object):
|
|||||||
# GET instead of POST (no body)
|
# GET instead of POST (no body)
|
||||||
# (actual POST test is done by client code)
|
# (actual POST test is done by client code)
|
||||||
with assert_raises(HTTPError) as e:
|
with assert_raises(HTTPError) as e:
|
||||||
getjson("/stream/insert?path=/newton/prep")
|
getjson("/stream/insert?path=/newton/prep&start=0&end=0")
|
||||||
eq_(e.exception.code, 400)
|
eq_(e.exception.code, 400)
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import nilmdb
|
import nilmdb
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
from nose.tools import *
|
from nose.tools import *
|
||||||
from nose.tools import assert_raises
|
from nose.tools import assert_raises
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import nilmdb
|
import nilmdb
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
from nose.tools import *
|
from nose.tools import *
|
||||||
from nose.tools import assert_raises
|
from nose.tools import assert_raises
|
||||||
@@ -11,65 +11,149 @@ from nilmdb.rbtree import RBTree, RBNode
|
|||||||
from test_helpers import *
|
from test_helpers import *
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
render = False
|
# set to False to skip live renders
|
||||||
|
do_live_renders = False
|
||||||
|
def render(tree, description = "", live = True):
|
||||||
|
import renderdot
|
||||||
|
r = renderdot.RBTreeRenderer(tree)
|
||||||
|
return r.render(description, live and do_live_renders)
|
||||||
|
|
||||||
class TestRBTree:
|
class TestRBTree:
|
||||||
def test_rbtree(self):
|
def test_rbtree(self):
|
||||||
rb = RBTree()
|
rb = RBTree()
|
||||||
rb.insert(RBNode(None, 10000, 10001))
|
rb.insert(RBNode(10000, 10001))
|
||||||
rb.insert(RBNode(None, 10004, 10007))
|
rb.insert(RBNode(10004, 10007))
|
||||||
rb.insert(RBNode(None, 10001, 10002))
|
rb.insert(RBNode(10001, 10002))
|
||||||
s = rb.render_dot()
|
|
||||||
# There was a typo that gave the RBTree a loop in this case.
|
# There was a typo that gave the RBTree a loop in this case.
|
||||||
# Verify that the dot isn't too big.
|
# Verify that the dot isn't too big.
|
||||||
|
s = render(rb, live = False)
|
||||||
assert(len(s.splitlines()) < 30)
|
assert(len(s.splitlines()) < 30)
|
||||||
|
|
||||||
def test_rbtree_big(self):
|
def test_rbtree_big(self):
|
||||||
import random
|
import random
|
||||||
random.seed(1234)
|
random.seed(1234)
|
||||||
|
|
||||||
# make a set of 500 intervals, inserted in order
|
# make a set of 100 intervals, inserted in order
|
||||||
rb = RBTree()
|
rb = RBTree()
|
||||||
j = 500
|
j = 100
|
||||||
for i in xrange(j):
|
for i in xrange(j):
|
||||||
rb.insert(RBNode(None, i, i+1))
|
rb.insert(RBNode(i, i+1))
|
||||||
|
render(rb, "in-order insert")
|
||||||
# show the graph
|
|
||||||
if render:
|
|
||||||
rb.render_dot_live("in-order insert")
|
|
||||||
|
|
||||||
# remove about half of them
|
# remove about half of them
|
||||||
for i in random.sample(xrange(j),j):
|
for i in random.sample(xrange(j),j):
|
||||||
if random.randint(0,1):
|
if random.randint(0,1):
|
||||||
rb.delete(rb.find(i, i+1))
|
rb.delete(rb.find(i, i+1))
|
||||||
|
render(rb, "in-order insert, random delete")
|
||||||
|
|
||||||
# show the graph
|
# make a set of 100 intervals, inserted at random
|
||||||
if render:
|
|
||||||
rb.render_dot_live("in-order insert, random delete")
|
|
||||||
|
|
||||||
# make a set of 500 intervals, inserted at random
|
|
||||||
rb = RBTree()
|
rb = RBTree()
|
||||||
j = 500
|
j = 100
|
||||||
for i in random.sample(xrange(j),j):
|
for i in random.sample(xrange(j),j):
|
||||||
rb.insert(RBNode(None, i, i+1))
|
rb.insert(RBNode(i, i+1))
|
||||||
|
render(rb, "random insert")
|
||||||
# show the graph
|
|
||||||
if render:
|
|
||||||
rb.render_dot_live("random insert")
|
|
||||||
|
|
||||||
# remove about half of them
|
# remove about half of them
|
||||||
for i in random.sample(xrange(j),j):
|
for i in random.sample(xrange(j),j):
|
||||||
if random.randint(0,1):
|
if random.randint(0,1):
|
||||||
rb.delete(rb.find(i, i+1))
|
rb.delete(rb.find(i, i+1))
|
||||||
|
render(rb, "random insert, random delete")
|
||||||
|
|
||||||
# show the graph
|
# in-order insert of 50 more
|
||||||
if render:
|
for i in xrange(50):
|
||||||
rb.render_dot_live("random insert, random delete")
|
rb.insert(RBNode(i+500, i+501))
|
||||||
|
render(rb, "random insert, random delete, in-order insert")
|
||||||
|
|
||||||
# in-order insert of 250 more
|
def test_rbtree_basics(self):
|
||||||
for i in xrange(250):
|
rb = RBTree()
|
||||||
rb.insert(RBNode(None, i+500, i+501))
|
vals = [ 7, 14, 1, 2, 8, 11, 5, 15, 4]
|
||||||
|
for n in vals:
|
||||||
|
rb.insert(RBNode(n, n))
|
||||||
|
|
||||||
# show the graph
|
# stringify
|
||||||
if render:
|
s = ""
|
||||||
rb.render_dot_live("random insert, random delete, in-order insert")
|
for node in rb:
|
||||||
|
s += str(node)
|
||||||
|
in_("[node (None) 1", s)
|
||||||
|
eq_(str(rb.nil), "[node nil]")
|
||||||
|
|
||||||
|
# inorder traversal, successor and predecessor
|
||||||
|
last = 0
|
||||||
|
for node in rb:
|
||||||
|
assert(node.start > last)
|
||||||
|
last = node.start
|
||||||
|
successor = rb.successor(node)
|
||||||
|
if successor:
|
||||||
|
assert(rb.predecessor(successor) is node)
|
||||||
|
predecessor = rb.predecessor(node)
|
||||||
|
if predecessor:
|
||||||
|
assert(rb.successor(predecessor) is node)
|
||||||
|
|
||||||
|
# Delete node not in the tree
|
||||||
|
with assert_raises(AttributeError):
|
||||||
|
rb.delete(RBNode(1,2))
|
||||||
|
|
||||||
|
# Delete all nodes!
|
||||||
|
for node in rb:
|
||||||
|
rb.delete(node)
|
||||||
|
|
||||||
|
# Build it up again, make sure it matches
|
||||||
|
for n in vals:
|
||||||
|
rb.insert(RBNode(n, n))
|
||||||
|
s2 = ""
|
||||||
|
for node in rb:
|
||||||
|
s2 += str(node)
|
||||||
|
assert(s == s2)
|
||||||
|
|
||||||
|
def test_rbtree_find(self):
|
||||||
|
# Get a little bit of coverage for some overlapping cases,
|
||||||
|
# even though the class doesn't fully support it.
|
||||||
|
rb = RBTree()
|
||||||
|
nodes = [ RBNode(1, 5), RBNode(1, 10), RBNode(1, 15) ]
|
||||||
|
for n in nodes:
|
||||||
|
rb.insert(n)
|
||||||
|
assert(rb.find(1, 5) is nodes[0])
|
||||||
|
assert(rb.find(1, 10) is nodes[1])
|
||||||
|
assert(rb.find(1, 15) is nodes[2])
|
||||||
|
|
||||||
|
def test_rbtree_find_leftright(self):
|
||||||
|
# Now let's get some ranges in there
|
||||||
|
rb = RBTree()
|
||||||
|
vals = [ 7, 14, 1, 2, 8, 11, 5, 15, 4]
|
||||||
|
for n in vals:
|
||||||
|
rb.insert(RBNode(n*10, n*10+5))
|
||||||
|
|
||||||
|
# Check find_end_left, find_right_start
|
||||||
|
for i in range(160):
|
||||||
|
left = rb.find_left_end(i)
|
||||||
|
right = rb.find_right_start(i)
|
||||||
|
if left:
|
||||||
|
# endpoint should be more than i
|
||||||
|
assert(left.end >= i)
|
||||||
|
# all earlier nodes should have a lower endpoint
|
||||||
|
for node in rb:
|
||||||
|
if node is left:
|
||||||
|
break
|
||||||
|
assert(node.end < i)
|
||||||
|
if right:
|
||||||
|
# startpoint should be less than i
|
||||||
|
assert(right.start <= i)
|
||||||
|
# all later nodes should have a higher startpoint
|
||||||
|
for node in reversed(list(rb)):
|
||||||
|
if node is right:
|
||||||
|
break
|
||||||
|
assert(node.start > i)
|
||||||
|
|
||||||
|
def test_rbtree_intersect(self):
|
||||||
|
# Fill with some ranges
|
||||||
|
rb = RBTree()
|
||||||
|
rb.insert(RBNode(10,20))
|
||||||
|
rb.insert(RBNode(20,25))
|
||||||
|
rb.insert(RBNode(30,40))
|
||||||
|
# Just a quick test; test_interval will do better.
|
||||||
|
eq_(len(list(rb.intersect(1,100))), 3)
|
||||||
|
eq_(len(list(rb.intersect(10,20))), 1)
|
||||||
|
eq_(len(list(rb.intersect(5,15))), 1)
|
||||||
|
eq_(len(list(rb.intersect(15,15))), 1)
|
||||||
|
eq_(len(list(rb.intersect(20,21))), 1)
|
||||||
|
eq_(len(list(rb.intersect(19,21))), 2)
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import nilmdb
|
import nilmdb
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
import nose
|
import nose
|
||||||
from nose.tools import *
|
from nose.tools import *
|
||||||
@@ -57,7 +57,7 @@ class TestUnserialized(Base):
|
|||||||
class TestSerialized(Base):
|
class TestSerialized(Base):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.realfoo = Foo()
|
self.realfoo = Foo()
|
||||||
self.foo = nilmdb.serializer.WrapObject(self.realfoo)
|
self.foo = nilmdb.utils.Serializer(self.realfoo)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
del self.foo
|
del self.foo
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import nilmdb
|
import nilmdb
|
||||||
from nilmdb.printf import *
|
from nilmdb.utils.printf import *
|
||||||
|
|
||||||
import datetime_tz
|
import datetime_tz
|
||||||
|
|
||||||
|
30
timeit.sh
30
timeit.sh
@@ -1,20 +1,22 @@
|
|||||||
|
./nilmtool.py destroy /bpnilm/2/raw
|
||||||
./nilmtool.py create /bpnilm/2/raw RawData
|
./nilmtool.py create /bpnilm/2/raw RawData
|
||||||
|
|
||||||
if true; then
|
if false; then
|
||||||
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-110000 /bpnilm/2/raw
|
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-110000 -r 8000 /bpnilm/2/raw
|
||||||
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-120001 /bpnilm/2/raw
|
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-120001 -r 8000 /bpnilm/2/raw
|
||||||
else
|
else
|
||||||
for i in $(seq 2000 2050); do
|
# 170 hours, about 98 gigs uncompressed:
|
||||||
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-010001 /bpnilm/2/raw
|
for i in $(seq 2000 2016); do
|
||||||
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-020002 /bpnilm/2/raw
|
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-010001 -r 8000 /bpnilm/2/raw
|
||||||
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-030003 /bpnilm/2/raw
|
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-020002 -r 8000 /bpnilm/2/raw
|
||||||
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-040004 /bpnilm/2/raw
|
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-030003 -r 8000 /bpnilm/2/raw
|
||||||
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-050005 /bpnilm/2/raw
|
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-040004 -r 8000 /bpnilm/2/raw
|
||||||
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-060006 /bpnilm/2/raw
|
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-050005 -r 8000 /bpnilm/2/raw
|
||||||
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-070007 /bpnilm/2/raw
|
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-060006 -r 8000 /bpnilm/2/raw
|
||||||
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-080008 /bpnilm/2/raw
|
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-070007 -r 8000 /bpnilm/2/raw
|
||||||
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-090009 /bpnilm/2/raw
|
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-080008 -r 8000 /bpnilm/2/raw
|
||||||
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-100010 /bpnilm/2/raw
|
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-090009 -r 8000 /bpnilm/2/raw
|
||||||
|
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-100010 -r 8000 /bpnilm/2/raw
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user