map01bf@625
|
1 #!/usr/bin/env python
|
map01bf@625
|
2 # encoding: utf-8
|
map01bf@625
|
3 """
|
map01bf@625
|
4 pyadb.py
|
map01bf@625
|
5
|
map01bf@625
|
6 public access and class structure for python audioDb api bindings.
|
map01bf@625
|
7
|
map01bf@628
|
8
|
map01bf@628
|
9
|
map01bf@625
|
10 Created by Benjamin Fields on 2009-09-22.
|
map01bf@625
|
11 Copyright (c) 2009 Goldsmith University of London.
|
map01bf@625
|
12 """
|
map01bf@625
|
13
|
map01bf@625
|
14 import sys
|
map01bf@625
|
15 import os, os.path
|
map01bf@625
|
16 import unittest
|
map01bf@625
|
17 import _pyadb
|
map01bf@625
|
18
|
map01bf@717
|
19 ADB_HEADER_FLAG_L2NORM = 0x1 #annoyingly I can't find a means
|
map01bf@717
|
20 ADB_HEADER_FLAG_POWER = 0x4 #around defining these flag definitions
|
map01bf@717
|
21 ADB_HEADER_FLAG_TIMES = 0x20 #as they aren't even exported to the
|
map01bf@717
|
22 ADB_HEADER_FLAG_REFERENCES = 0x40 #api, so this is the only way to get them.
|
map01bf@625
|
23
|
map01bf@625
|
24 class Usage(Exception):
|
map01bf@628
|
25 """error to indicate that a method has been called with incorrect args"""
|
map01bf@628
|
26 def __init__(self, msg):
|
map01bf@628
|
27 self.msg = msg
|
map01bf@625
|
28
|
map01bf@628
|
29 class Pyadb(object):
|
map01bf@628
|
30 """Pyadb class. Allows for creation, access, insertion and query of an audioDB vector matching database."""
|
map01bf@628
|
31 validConfigTerms = {"seqLength":int, "seqStart":int, "exhaustive":bool,
|
map01bf@628
|
32 "falsePositives":bool, "accumulation":str, "distance":str, "npoints":int,
|
map01bf@628
|
33 "ntracks":int, "includeKeys":list, "excludeKeys":list, "radius":float, "absThres":float,
|
map01bf@628
|
34 "relThres":float, "durRatio":float, "hopSize":int, "resFmt":str}
|
map01bf@625
|
35 def __init__(self, path, mode='w'):
|
map01bf@625
|
36 self.path = path
|
map01bf@628
|
37 self.configQuery = {}
|
map01bf@625
|
38 if not (mode=='w' or mode =='r'):
|
map01bf@625
|
39 raise(ValueError, "if specified, mode must be either\'r\' or \'w\'.")
|
map01bf@625
|
40 if os.path.exists(path):
|
map01bf@625
|
41 self._db = _pyadb._pyadb_open(path, mode)
|
map01bf@625
|
42 else:
|
map01bf@625
|
43 self._db = _pyadb._pyadb_create(path,0,0,0)
|
map01bf@625
|
44 self._updateDBAttributes()
|
map01bf@625
|
45 return
|
map01bf@625
|
46
|
map01bf@625
|
47 def insert(self, featFile=None, powerFile=None, timesFile=None, featData=None, powerData=None, timesData=None, key=None):
|
map01bf@717
|
48 """
|
map01bf@717
|
49 Insert features into database. Can be done with data provided directly or by giving a path to a binary fftExtract style feature file. If power and/or timing is engaged in the database header, it must be provided (via the same means as the feature) or a Usage exception will be raised. Power files should be of the same binary type as features. Times files should be the ascii number length of time in seconds from the begining of the file to segment start, one per line.
|
map01bf@717
|
50 If providing data directly, featData should be a numpy array with shape= (number of Dimensions, number of Vectors)
|
map01bf@717
|
51 """
|
map01bf@628
|
52 #While python style normally advocates leaping before looking, these check are nessecary as
|
map01bf@625
|
53 #it is very difficult to assertain why the insertion failed once it has been called.
|
map01bf@625
|
54 if (self.hasPower and (((featFile) and powerFile==None) or ((featData) and powerData==None))):
|
map01bf@625
|
55 raise(Usage, "The db you are attempting an insert on (%s) expects power and you either\
|
map01bf@625
|
56 haven't provided any or have done so in the wrong format."%self.path)
|
map01bf@625
|
57 if (self.hasTimes and (((timesFile) and timesFile==None) or ((timesData) and timesData==None))):
|
map01bf@625
|
58 raise(Usage, "The db you are attempting an insert on (%s) expects times and you either\
|
map01bf@625
|
59 haven't provided any or have done so in the wrong format."%self.path)
|
map01bf@625
|
60 args = {"db":self._db}
|
map01bf@625
|
61 if featFile:
|
map01bf@625
|
62 args["features"] = featFile
|
map01bf@717
|
63 elif (featData != None):
|
map01bf@625
|
64 args["features"] = featData
|
map01bf@625
|
65 else:
|
map01bf@625
|
66 raise(Usage, "Must provide some feature data!")
|
map01bf@625
|
67 if self.hasPower:
|
map01bf@625
|
68 if featFile:
|
map01bf@625
|
69 args["power"]=powerFile
|
map01bf@625
|
70 elif featData:
|
map01bf@625
|
71 pass
|
map01bf@625
|
72 if self.hasTimes:
|
map01bf@625
|
73 if featFile:
|
map01bf@625
|
74 args["times"]=timesFile
|
map01bf@625
|
75 elif timesData:
|
map01bf@625
|
76 pass
|
map01bf@625
|
77 if key:
|
map01bf@625
|
78 args["key"]=str(key)
|
map01bf@625
|
79 if featFile:
|
map01bf@625
|
80 if not _pyadb._pyadb_insertFromFile(**args):
|
map01bf@717
|
81 raise(RuntimeError, "Insertion from file failed for an unknown reason.")
|
map01bf@625
|
82 else:
|
map01bf@625
|
83 self._updateDBAttributes()
|
map01bf@625
|
84 return
|
map01bf@717
|
85 elif (featData != None):
|
map01bf@717
|
86 if (len(args["features"].shape) == 1) : args["features"] = args["features"].reshape((args["features"].shape[0],1))
|
map01bf@717
|
87 args["nDim"], args["nVect"] = args["features"].shape
|
map01bf@717
|
88 args["features"] = args["features"].flatten()
|
map01bf@717
|
89 print "args: " + str(args)
|
map01bf@717
|
90 if not _pyadb._pyadb_insertFromArray(**args):
|
map01bf@717
|
91 raise(RuntimeError, "Direct data insertion failed for an unknown reason.")
|
map01bf@717
|
92 else:
|
map01bf@717
|
93 self._updateDBAttributes()
|
map01bf@717
|
94 return
|
map01bf@628
|
95
|
map01bf@628
|
96 def configCheck(self, scrub=False):
|
map01bf@628
|
97 """examine self.configQuery dict. For each key encouters confirm it is in the validConfigTerms list and if appropriate, type check. If scrub is False, leave unexpected keys and values alone and return False, if scrub try to correct errors (attempt type casts and remove unexpected entries) and continue. If self.configQuery only contains expected keys with correctly typed values, return True. See Pyadb.validConfigTerms for allowed keys and types. Note also that include/exclude key lists memebers or string switched are not verified here, but rather when they are converted to const char * in the C api call and if malformed, an error will be rasied from there. Valid keys and values in queryconfig:
|
map01bf@628
|
98 {seqLength : Int Sequence Length, \n\
|
map01bf@628
|
99 seqStart : Int offset from start for key, \n\
|
map01bf@628
|
100 exhaustive : boolean - True for exhaustive (false by default),\n\
|
map01bf@628
|
101 falsePositives: boolean - True to keep fps (false by defaults),\n\
|
map01bf@628
|
102 accumulation : [\"db\"|\"track\"|\"one2one\"] (\"db\" by default),\n\
|
map01bf@628
|
103 distance : [\"dot\"|\"eucNorm\"|\"euclidean\"] (\"dot\" by default),\n\
|
map01bf@628
|
104 npoints : int number of points per track,\n\
|
map01bf@628
|
105 ntracks : max number of results returned in db accu mode,\n\
|
map01bf@628
|
106 includeKeys : list of strings to include (use all by default),\n\
|
map01bf@628
|
107 excludeKeys : list of strings to exclude (none by default),\n\
|
map01bf@628
|
108 radius : double of nnRadius (1.0 default, overrides npoints if specified),\n\
|
map01bf@628
|
109 absThres : double absolute power threshold (db must have power),\n\
|
map01bf@628
|
110 relThres : double relative power threshold (db must have power),\n\
|
map01bf@628
|
111 durRatio : double time expansion/compresion ratio,\n\
|
map01bf@628
|
112 hopSize : int hopsize (1 by default)])->resultDict\n\
|
map01bf@628
|
113 resFmt : [\"list\"|\"dict\"](\"dict\" by default)}"""
|
mas01mj@630
|
114 for key in self.configQuery.keys():
|
map01bf@628
|
115 if key not in Pyadb.validConfigTerms.keys():
|
mas01mj@630
|
116 if not scrub: return False
|
map01bf@632
|
117 print "scrubbing %s from query config."%str(key)
|
mas01mj@630
|
118 del self.configQuery[key]
|
mas01mj@630
|
119 if not isinstance(self.configQuery[key], Pyadb.validConfigTerms[key]):
|
mas01mj@630
|
120 if not scrub: return False
|
mas01mj@630
|
121 self.configQuery[key] = Pyadb.validConfigTerms[key](self.configQuery[key])#hrm, syntax?
|
mas01mj@630
|
122 return True
|
map01bf@628
|
123
|
map01bf@628
|
124 #
|
map01bf@628
|
125
|
map01bf@632
|
126 def query(self, key=None, featData=None, strictConfig=True):
|
map01bf@628
|
127 """query the database. Query parameters as defined in self.configQuery. For details on this consult the doc string in the configCheck method."""
|
map01bf@628
|
128 if not self.configCheck():
|
map01bf@628
|
129 if strictConfig:
|
map01bf@628
|
130 raise ValueError("configQuery dict contains unsupported terms and strict configure mode is on.\n\
|
map01bf@628
|
131 Only keys found in Pyadb.validConfigTerms may be defined")
|
map01bf@628
|
132 else:
|
map01bf@632
|
133 print "configQuery dict contains unsupported terms and strict configure mode is off.\n\
|
map01bf@632
|
134 Only keys found in Pyadb.validConfigTerms should be defined. Removing invalid terms and proceeding..."
|
map01bf@628
|
135 self.configCheck(scrub=True)
|
map01bf@628
|
136 if ((not key and not featData) or (key and featData)):
|
map01bf@628
|
137 raise Usage("query require either key or featData to be defined, you have defined both or neither.")
|
map01bf@628
|
138 if key:
|
map01bf@628
|
139 result = _pyadb._pyadb_queryFromKey(self._db, key, **self.configQuery)
|
map01bf@628
|
140 elif featData:
|
map01bf@628
|
141 raise NotImplementedError("direct data query not yet implemented. Sorry.")
|
mas01mj@630
|
142 return Pyadb.Result(result, self.configQuery)
|
map01bf@625
|
143
|
map01bf@638
|
144 def status(self):
|
map01bf@638
|
145 '''update attributes and return them as a dict'''
|
map01bf@638
|
146 self._updateDBAttributes()
|
map01bf@638
|
147 return { "numFiles" : self.numFiles,
|
map01bf@638
|
148 "dims" : self.dims,
|
map01bf@638
|
149 "dudCount" : self.dudCount,
|
map01bf@638
|
150 "nullCount": self.nullCount,
|
map01bf@638
|
151 "length" : self.length,
|
map01bf@638
|
152 "data_region_size" : self.data_region_size,
|
map01bf@638
|
153 "l2Normed" : self.l2Normed,
|
map01bf@638
|
154 "hasPower" : self.hasPower,
|
map01bf@638
|
155 "hasTimes" : self.hasTimes,
|
map01bf@638
|
156 "usesRefs" : self.usesRefs}
|
map01bf@625
|
157 ###internal methods###
|
map01bf@625
|
158 def _updateDBAttributes(self):
|
map01bf@625
|
159 '''run _pyadb_status to fill/update the database level flags and info'''
|
map01bf@625
|
160 rawFlags = long(0)
|
map01bf@625
|
161 (self.numFiles,
|
map01bf@625
|
162 self.dims,
|
map01bf@625
|
163 self.dudCount,
|
map01bf@625
|
164 self.nullCount,
|
map01bf@625
|
165 rawFlags,
|
map01bf@625
|
166 self.length,
|
map01bf@625
|
167 self.data_region_size) = _pyadb._pyadb_status(self._db)
|
map01bf@625
|
168 self.l2Normed = bool(rawFlags & ADB_HEADER_FLAG_L2NORM)
|
map01bf@625
|
169 self.hasPower = bool(rawFlags & ADB_HEADER_FLAG_POWER)
|
map01bf@625
|
170 self.hasTimes = bool(rawFlags & ADB_HEADER_FLAG_TIMES)
|
map01bf@625
|
171 self.usesRefs = bool(rawFlags & ADB_HEADER_FLAG_REFERENCES)
|
map01bf@625
|
172 return
|
map01bf@625
|
173
|
map01bf@628
|
174 class Result(object):
|
map01bf@628
|
175 def __init__(self, rawData, currentConfig):
|
map01bf@628
|
176 self.rawData = rawData
|
map01bf@628
|
177 if "resFmt" in currentConfig:
|
map01bf@628
|
178 self.type = currentConfig["resFmt"]
|
map01bf@628
|
179 else:
|
map01bf@628
|
180 self.type = "dict"
|
map01bf@628
|
181 def __str__(self):
|
mas01mj@631
|
182 return str(self.rawData)
|
map01bf@628
|
183 def __repr__(self):
|
mas01mj@631
|
184 return repr(self.rawData)
|
map01bf@625
|
185
|
map01bf@625
|
186 class untitledTests(unittest.TestCase):
|
map01bf@625
|
187 def setUp(self):
|
map01bf@625
|
188 pass
|
map01bf@625
|
189
|
map01bf@625
|
190
|
map01bf@625
|
191 if __name__ == '__main__':
|
mas01mj@630
|
192 unittest.main()
|