maia-hash
Version:
Hashing and fingerprinting functions supporting various applications by Music Artificial Intelligence Algorithms, Inc.
418 lines (392 loc) • 13.3 kB
JavaScript
// Imports
const fs = require("fs")
const path = require("path")
const mu = require("maia-util")
// import PointSet from './PointSet'
export default class OntimePitchHasher {
constructor(_mapPath) {
if (_mapPath !== undefined) {
this.map = require(_mapPath)
} else {
this.map = {}
}
}
contains(aKey) {
return this.map[aKey]
}
// The expected format is with ontime in the first dimension and pitch in the
// second dimension of pts. It is assumed that pts is sorted
// lexicographically.
create_hash_entries(
pts, cumuTime, fnam, mode = "duples", insertMode = "increment and file with fnams",
tMin = 0.1, tMax = 10, pMin = 1, pMax = 12, folder = __dirname
) {
const npts = pts.length
// console.log("npts:", npts)
let nh = 0
switch (mode) {
case "duples":
for (let i = 0; i < npts - 1; i++) {
const v0 = pts[i]
let j = i + 1
while (j < npts) {
const v1 = pts[j]
const td = v1[0] - v0[0]
const apd = Math.abs(v1[1] - v0[1])
// console.log("i:", i, "j:", j)
// Decide whether to make a hash entry.
if (td > tMin && td < tMax && apd >= pMin && apd <= pMax) {
// Make a hash entry, something like "±pdtd"
const he = this.create_hash_entry(
[v1[1] - v0[1], td], mode,
cumuTime + v0[0], fnam,
tMin, tMax
)
this.insert(he)
nh++
} // End whether to make a hash entry.
if (td >= tMax) {
j = npts - 1
}
j++
} // End while.
} // for (let i = 0;
break
case "triples":
for (let i = 0; i < npts - 2; i++) {
const v0 = pts[i]
let j = i + 1
while (j < npts - 1) {
const v1 = pts[j]
const td1 = v1[0] - v0[0]
const apd1 = Math.abs(v1[1] - v0[1])
// console.log("i:", i, "j:", j)
// Decide whether to proceed to v1 and v2.
if (td1 > tMin && td1 < tMax && apd1 >= pMin && apd1 <= pMax) {
let k = j + 1
while (k < npts) {
const v2 = pts[k]
const td2 = v2[0] - v1[0]
const apd2 = Math.abs(v2[1] - v1[1])
// console.log("j:", j, "k:", k)
// Decide whether to make a hash entry.
if (td2 > tMin && td2 < tMax && apd2 >= pMin && apd2 <= pMax) {
// Make a hash entry, something like "±pd1±pd2tdr"
const he = this.create_hash_entry(
[v1[1] - v0[1], v2[1] - v1[1], td2 / td1], mode,
cumuTime + v0[0], fnam,
tMin, tMax
)
this.insert(he, insertMode, folder)
nh++
} // End whether to make a hash entry.
if (td2 >= tMax) {
k = npts - 1
}
k++
} // End k while.
}
if (td1 >= tMax) {
j = npts - 2
}
j++
} // End j while.
} // for (let i = 0;
break
default:
console.log("Should not get to default in create_hash_entries() switch.")
}
return nh
}
create_hash_entry(vals, mode, ctime, fnam, tMin, tMax) {
let str = "", isInteger
switch (mode) {
case "duples":
// Sense-check pitch difference.
const apd = Math.abs(vals[0])
if (apd >= 100 || Math.round(vals[0]) !== vals[0]) {
console.log("Unexpected pitch difference:", vals[0])
console.log("Returning.")
return
}
if (vals[0] >= 0) {
str += "+"
} else {
str += "-"
}
if (apd < 10) {
str += "0"
}
str += apd
// Sense-check time difference.
isInteger = Math.round(vals[1]) === vals[1]
if (vals[1] >= tMax || vals[1] < tMin) {
console.log("Unexpected time difference:", vals[1])
console.log("Returning.")
return
}
// Round time difference to 1 d.p. and append to str.
str += Math.round(10 * vals[1]) / 10
if (isInteger) {
str += ".0"
}
break
case "triples":
// Sense-check pitch difference.
vals.slice(0, 2).forEach(function (v, idx) {
const apd = Math.abs(v)
if (apd >= 100 || Math.round(v) !== v) {
console.log("Unexpected pitch difference:", v, idx)
console.log("Returning.")
return
}
if (v >= 0) {
str += "+"
} else {
str += "-"
}
if (apd < 10) {
str += "0"
}
str += apd
})
// Sense-check time difference ratio.
if (vals[2] >= tMax / tMin || vals[2] < tMin / tMax) {
console.log("Unexpected time difference:", vals[2])
console.log("Returning.")
return
}
// If ratio less than 1, invert and give it a negative sign so that such
// values are as accurately represented as positive values.
// console.log("vals[2] before inversion:", vals[2])
let sign = "+"
if (vals[2] < 1) {
vals[2] = 1 / vals[2]
sign = "-"
}
// console.log("vals[2] after inversion:", vals[2])
str += sign
// Round time difference ratio to 1 d.p. and append to str.
const dp1 = Math.round(10 * vals[2]) / 10
isInteger = Math.round(dp1) === dp1
// console.log("isInteger:", isInteger)
str += dp1
if (isInteger) {
str += ".0"
}
// console.log("str:", str)
break
default:
console.log("Should not get to default in create_hash_entry() switch.")
}
return {
"hash": str,
"ctimes": [ctime],
"fnams": [fnam]
}
}
// This method is inefficient and could be improved. I don't think it's worth
// obtaining the name of each piece when most of the entries of countBins
// are zero. Therefore, I've sliced it to topN.
get_piece_names(countBins, ctimes, fnams, binSize, topN = 100){
let out = []
// "out" contains the index of bin,
// and it is sorted based on the corresponding number of hash entries contained in "hist".
for (let i = 0; i < countBins.length; i++) {
out.push(i)
}
out.sort(function (a, b) {
return countBins[b] - countBins[a]
})
out = out.slice(0, topN)
return out.map((idx) => {
for (let i = 0; i < ctimes.length; i++){
if (idx*binSize <= ctimes[i]){
return {
"winningPiece": fnams[i - 1], "edge": idx * binSize, "count": countBins[idx]
}
}
}
})
}
insert(hashEntry, method = "hash and lookup", dir) {
const key = hashEntry.hash
const lookup = this.contains(key)
switch (method) {
case "hash and lookup":
if (lookup !== undefined) {
// Extend ctimes and fnams arrays.
lookup.ctimes.push(hashEntry.ctimes[0])
lookup.fnams.push(hashEntry.fnams[0])
} else {
delete hashEntry.hash
this.map[key] = hashEntry
}
break
case "increment and file with fnams":
if (lookup !== undefined) {
this.map[key].increment++
} else {
this.map[key] = {
"increment": 1,
"log": fs.openSync(path.join(dir, key + ".json"), "a")
}
}
fs.writeSync(
this.map[key].log,
JSON.stringify(
[
Math.round(100 * hashEntry.ctimes[0]) / 100,
hashEntry.fnams[0]
]
) + ","
)
break
case "increment and file":
if (lookup !== undefined) {
this.map[key].increment++
} else {
this.map[key] = {
"increment": 1,
"log": fs.openSync(
path.join(dir, key + ".json"), "a"
// {"flags": "a"}
)
}
}
const content = JSON.stringify(Math.round(100 * hashEntry.ctimes[0]) / 100) + "," // 82.3MB
// const content = JSON.stringify(Math.round(10 * hashEntry.ctimes[0]) / 10) + "," // 72.MB
// const content = JSON.stringify(hashEntry.ctimes[0]) + "," // 162.9MB
fs.writeSync(this.map[key].log, content)
// this.map[key].log.write(content)
// fs.writeFileSync(
// path.join(dir, key + ".json"),
// JSON.stringify(
// [
// Math.round(10*hashEntry.ctimes[0])/10,
// hashEntry.fnams[0]
// ]
// )
// + ",",
// { "flag": "a" }
// )
break
default:
console.log("Should not get to default in insert()!")
}
}
// The expected format is with time in the first dimension and pitch in the
// second dimension of pts. It is assumed that pts is sorted
// lexicographically.
match_hash_entries(
pts, mode = "duples", tMin, tMax, pMin, pMax, ctimes, binSize, folder = __dirname
) {
let uninh = new Set()
const bins = Math.ceil(ctimes[ctimes.length - 1] / binSize);
let countBins = new Array(bins).fill(0).map(() => {
return new Set()
})
pts = pts.slice(0, 80)
const npts = pts.length
let nh = 0
switch (mode) {
case "duples":
for (let i = 0; i < npts - 1; i++) {
const v0 = pts[i]
let j = i + 1
while (j < npts) {
const v1 = pts[j]
const td = v1[0] - v0[0]
const apd = Math.abs(v1[1] - v0[1])
// console.log("i:", i, "j:", j)
// Decide whether to make a hash entry.
if (td > tMin && td < tMax && apd >= pMin && apd <= pMax) {
// Make a hash entry, something like "±pdtd"
const he = this.create_hash_entry(
[v1[1] - v0[1], td], mode, v0[0]
)
// console.log("he:", he)
// Is there a match?
const lookup = this.contains(he.hash)
if (lookup !== undefined) {
// There's a match!
lookup.ctimes.forEach(function (ctime) {
tInDset.push(ctime)
tInQuery.push(he.ctimes[0])
})
}
nh++
} // End whether to make a hash entry.
if (td >= tMax) {
j = npts - 1
}
j++
} // End while.
} // for (let i = 0;
break
case "triples":
loop1:
for (let i = 0; i < npts - 2; i++) {
const v0 = pts[i]
let j = i + 1
while (j < npts - 1) {
const v1 = pts[j]
const td1 = v1[0] - v0[0]
const apd1 = Math.abs(v1[1] - v0[1])
// console.log("i:", i, "j:", j)
// Decide whether to proceed to v1 and v2.
if (td1 > tMin && td1 < tMax && apd1 >= pMin && apd1 <= pMax) {
let k = j + 1
while (k < npts) {
const v2 = pts[k]
const td2 = v2[0] - v1[0]
const apd2 = Math.abs(v2[1] - v1[1])
// console.log("j:", j, "k:", k)
// Decide whether to make a hash entry.
if (td2 > tMin && td2 < tMax && apd2 >= pMin && apd2 <= pMax) {
const he = this.create_hash_entry(
[v1[1] - v0[1], v2[1] - v1[1], td2 / td1], mode, v0[0]
)
if (fs.existsSync(path.join(folder, he.hash + ".json"))) {
const lookupStr = fs.readFileSync(
path.join(folder, he.hash + ".json"), "utf8"
).slice(0, -1)
let lookup = JSON.parse("[" + lookupStr + "]")
lookup.forEach((value) => {
let dif = value - he.ctimes[0]
if (dif >= 0 && dif <= ctimes[ctimes.length - 1]) {
countBins[Math.floor(dif / binSize)].add(he.hash)
}
})
}
uninh.add(he.hash)
nh++
if (nh > 5000) {
break loop1
}
} // End whether to make a hash entry.
if (td2 >= tMax) {
k = npts - 1
}
k++
} // End k while.
}
if (td1 >= tMax) {
j = npts - 2
}
j++
} // End j while.
} // for (let i = 0;
break
default:
console.log("Should not get to default in match_hash_entries() switch.")
}
return {
"nosHashes": nh,
"uninosHashes": uninh.size,
"countBins": countBins.map((value => {
return value.size
}))
}
}
}