StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics
https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
446 lines
10 KiB
446 lines
10 KiB
"use strict"
|
|
|
|
//High level idea:
|
|
// 1. Use Clarkson's incremental construction to find convex hull
|
|
// 2. Point location in triangulation by jump and walk
|
|
|
|
module.exports = incrementalConvexHull
|
|
|
|
var orient = require("robust-orientation")
|
|
var compareCell = require("simplicial-complex").compareCells
|
|
|
|
function compareInt(a, b) {
|
|
return a - b
|
|
}
|
|
|
|
function Simplex(vertices, adjacent, boundary) {
|
|
this.vertices = vertices
|
|
this.adjacent = adjacent
|
|
this.boundary = boundary
|
|
this.lastVisited = -1
|
|
}
|
|
|
|
Simplex.prototype.flip = function() {
|
|
var t = this.vertices[0]
|
|
this.vertices[0] = this.vertices[1]
|
|
this.vertices[1] = t
|
|
var u = this.adjacent[0]
|
|
this.adjacent[0] = this.adjacent[1]
|
|
this.adjacent[1] = u
|
|
}
|
|
|
|
function GlueFacet(vertices, cell, index) {
|
|
this.vertices = vertices
|
|
this.cell = cell
|
|
this.index = index
|
|
}
|
|
|
|
function compareGlue(a, b) {
|
|
return compareCell(a.vertices, b.vertices)
|
|
}
|
|
|
|
function bakeOrient(d) {
|
|
var code = ["function orient(){var tuple=this.tuple;return test("]
|
|
for(var i=0; i<=d; ++i) {
|
|
if(i > 0) {
|
|
code.push(",")
|
|
}
|
|
code.push("tuple[", i, "]")
|
|
}
|
|
code.push(")}return orient")
|
|
var proc = new Function("test", code.join(""))
|
|
var test = orient[d+1]
|
|
if(!test) {
|
|
test = orient
|
|
}
|
|
return proc(test)
|
|
}
|
|
|
|
var BAKED = []
|
|
|
|
function Triangulation(dimension, vertices, simplices) {
|
|
this.dimension = dimension
|
|
this.vertices = vertices
|
|
this.simplices = simplices
|
|
this.interior = simplices.filter(function(c) {
|
|
return !c.boundary
|
|
})
|
|
|
|
this.tuple = new Array(dimension+1)
|
|
for(var i=0; i<=dimension; ++i) {
|
|
this.tuple[i] = this.vertices[i]
|
|
}
|
|
|
|
var o = BAKED[dimension]
|
|
if(!o) {
|
|
o = BAKED[dimension] = bakeOrient(dimension)
|
|
}
|
|
this.orient = o
|
|
}
|
|
|
|
var proto = Triangulation.prototype
|
|
|
|
//Degenerate situation where we are on boundary, but coplanar to face
|
|
proto.handleBoundaryDegeneracy = function(cell, point) {
|
|
var d = this.dimension
|
|
var n = this.vertices.length - 1
|
|
var tuple = this.tuple
|
|
var verts = this.vertices
|
|
|
|
//Dumb solution: Just do dfs from boundary cell until we find any peak, or terminate
|
|
var toVisit = [ cell ]
|
|
cell.lastVisited = -n
|
|
while(toVisit.length > 0) {
|
|
cell = toVisit.pop()
|
|
var cellVerts = cell.vertices
|
|
var cellAdj = cell.adjacent
|
|
for(var i=0; i<=d; ++i) {
|
|
var neighbor = cellAdj[i]
|
|
if(!neighbor.boundary || neighbor.lastVisited <= -n) {
|
|
continue
|
|
}
|
|
var nv = neighbor.vertices
|
|
for(var j=0; j<=d; ++j) {
|
|
var vv = nv[j]
|
|
if(vv < 0) {
|
|
tuple[j] = point
|
|
} else {
|
|
tuple[j] = verts[vv]
|
|
}
|
|
}
|
|
var o = this.orient()
|
|
if(o > 0) {
|
|
return neighbor
|
|
}
|
|
neighbor.lastVisited = -n
|
|
if(o === 0) {
|
|
toVisit.push(neighbor)
|
|
}
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
proto.walk = function(point, random) {
|
|
//Alias local properties
|
|
var n = this.vertices.length - 1
|
|
var d = this.dimension
|
|
var verts = this.vertices
|
|
var tuple = this.tuple
|
|
|
|
//Compute initial jump cell
|
|
var initIndex = random ? (this.interior.length * Math.random())|0 : (this.interior.length-1)
|
|
var cell = this.interior[ initIndex ]
|
|
|
|
//Start walking
|
|
outerLoop:
|
|
while(!cell.boundary) {
|
|
var cellVerts = cell.vertices
|
|
var cellAdj = cell.adjacent
|
|
|
|
for(var i=0; i<=d; ++i) {
|
|
tuple[i] = verts[cellVerts[i]]
|
|
}
|
|
cell.lastVisited = n
|
|
|
|
//Find farthest adjacent cell
|
|
for(var i=0; i<=d; ++i) {
|
|
var neighbor = cellAdj[i]
|
|
if(neighbor.lastVisited >= n) {
|
|
continue
|
|
}
|
|
var prev = tuple[i]
|
|
tuple[i] = point
|
|
var o = this.orient()
|
|
tuple[i] = prev
|
|
if(o < 0) {
|
|
cell = neighbor
|
|
continue outerLoop
|
|
} else {
|
|
if(!neighbor.boundary) {
|
|
neighbor.lastVisited = n
|
|
} else {
|
|
neighbor.lastVisited = -n
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
return cell
|
|
}
|
|
|
|
proto.addPeaks = function(point, cell) {
|
|
var n = this.vertices.length - 1
|
|
var d = this.dimension
|
|
var verts = this.vertices
|
|
var tuple = this.tuple
|
|
var interior = this.interior
|
|
var simplices = this.simplices
|
|
|
|
//Walking finished at boundary, time to add peaks
|
|
var tovisit = [ cell ]
|
|
|
|
//Stretch initial boundary cell into a peak
|
|
cell.lastVisited = n
|
|
cell.vertices[cell.vertices.indexOf(-1)] = n
|
|
cell.boundary = false
|
|
interior.push(cell)
|
|
|
|
//Record a list of all new boundaries created by added peaks so we can glue them together when we are all done
|
|
var glueFacets = []
|
|
|
|
//Do a traversal of the boundary walking outward from starting peak
|
|
while(tovisit.length > 0) {
|
|
//Pop off peak and walk over adjacent cells
|
|
var cell = tovisit.pop()
|
|
var cellVerts = cell.vertices
|
|
var cellAdj = cell.adjacent
|
|
var indexOfN = cellVerts.indexOf(n)
|
|
if(indexOfN < 0) {
|
|
continue
|
|
}
|
|
|
|
for(var i=0; i<=d; ++i) {
|
|
if(i === indexOfN) {
|
|
continue
|
|
}
|
|
|
|
//For each boundary neighbor of the cell
|
|
var neighbor = cellAdj[i]
|
|
if(!neighbor.boundary || neighbor.lastVisited >= n) {
|
|
continue
|
|
}
|
|
|
|
var nv = neighbor.vertices
|
|
|
|
//Test if neighbor is a peak
|
|
if(neighbor.lastVisited !== -n) {
|
|
//Compute orientation of p relative to each boundary peak
|
|
var indexOfNeg1 = 0
|
|
for(var j=0; j<=d; ++j) {
|
|
if(nv[j] < 0) {
|
|
indexOfNeg1 = j
|
|
tuple[j] = point
|
|
} else {
|
|
tuple[j] = verts[nv[j]]
|
|
}
|
|
}
|
|
var o = this.orient()
|
|
|
|
//Test if neighbor cell is also a peak
|
|
if(o > 0) {
|
|
nv[indexOfNeg1] = n
|
|
neighbor.boundary = false
|
|
interior.push(neighbor)
|
|
tovisit.push(neighbor)
|
|
neighbor.lastVisited = n
|
|
continue
|
|
} else {
|
|
neighbor.lastVisited = -n
|
|
}
|
|
}
|
|
|
|
var na = neighbor.adjacent
|
|
|
|
//Otherwise, replace neighbor with new face
|
|
var vverts = cellVerts.slice()
|
|
var vadj = cellAdj.slice()
|
|
var ncell = new Simplex(vverts, vadj, true)
|
|
simplices.push(ncell)
|
|
|
|
//Connect to neighbor
|
|
var opposite = na.indexOf(cell)
|
|
if(opposite < 0) {
|
|
continue
|
|
}
|
|
na[opposite] = ncell
|
|
vadj[indexOfN] = neighbor
|
|
|
|
//Connect to cell
|
|
vverts[i] = -1
|
|
vadj[i] = cell
|
|
cellAdj[i] = ncell
|
|
|
|
//Flip facet
|
|
ncell.flip()
|
|
|
|
//Add to glue list
|
|
for(var j=0; j<=d; ++j) {
|
|
var uu = vverts[j]
|
|
if(uu < 0 || uu === n) {
|
|
continue
|
|
}
|
|
var nface = new Array(d-1)
|
|
var nptr = 0
|
|
for(var k=0; k<=d; ++k) {
|
|
var vv = vverts[k]
|
|
if(vv < 0 || k === j) {
|
|
continue
|
|
}
|
|
nface[nptr++] = vv
|
|
}
|
|
glueFacets.push(new GlueFacet(nface, ncell, j))
|
|
}
|
|
}
|
|
}
|
|
|
|
//Glue boundary facets together
|
|
glueFacets.sort(compareGlue)
|
|
|
|
for(var i=0; i+1<glueFacets.length; i+=2) {
|
|
var a = glueFacets[i]
|
|
var b = glueFacets[i+1]
|
|
var ai = a.index
|
|
var bi = b.index
|
|
if(ai < 0 || bi < 0) {
|
|
continue
|
|
}
|
|
a.cell.adjacent[a.index] = b.cell
|
|
b.cell.adjacent[b.index] = a.cell
|
|
}
|
|
}
|
|
|
|
proto.insert = function(point, random) {
|
|
//Add point
|
|
var verts = this.vertices
|
|
verts.push(point)
|
|
|
|
var cell = this.walk(point, random)
|
|
if(!cell) {
|
|
return
|
|
}
|
|
|
|
//Alias local properties
|
|
var d = this.dimension
|
|
var tuple = this.tuple
|
|
|
|
//Degenerate case: If point is coplanar to cell, then walk until we find a non-degenerate boundary
|
|
for(var i=0; i<=d; ++i) {
|
|
var vv = cell.vertices[i]
|
|
if(vv < 0) {
|
|
tuple[i] = point
|
|
} else {
|
|
tuple[i] = verts[vv]
|
|
}
|
|
}
|
|
var o = this.orient(tuple)
|
|
if(o < 0) {
|
|
return
|
|
} else if(o === 0) {
|
|
cell = this.handleBoundaryDegeneracy(cell, point)
|
|
if(!cell) {
|
|
return
|
|
}
|
|
}
|
|
|
|
//Add peaks
|
|
this.addPeaks(point, cell)
|
|
}
|
|
|
|
//Extract all boundary cells
|
|
proto.boundary = function() {
|
|
var d = this.dimension
|
|
var boundary = []
|
|
var cells = this.simplices
|
|
var nc = cells.length
|
|
for(var i=0; i<nc; ++i) {
|
|
var c = cells[i]
|
|
if(c.boundary) {
|
|
var bcell = new Array(d)
|
|
var cv = c.vertices
|
|
var ptr = 0
|
|
var parity = 0
|
|
for(var j=0; j<=d; ++j) {
|
|
if(cv[j] >= 0) {
|
|
bcell[ptr++] = cv[j]
|
|
} else {
|
|
parity = j&1
|
|
}
|
|
}
|
|
if(parity === (d&1)) {
|
|
var t = bcell[0]
|
|
bcell[0] = bcell[1]
|
|
bcell[1] = t
|
|
}
|
|
boundary.push(bcell)
|
|
}
|
|
}
|
|
return boundary
|
|
}
|
|
|
|
function incrementalConvexHull(points, randomSearch) {
|
|
var n = points.length
|
|
if(n === 0) {
|
|
throw new Error("Must have at least d+1 points")
|
|
}
|
|
var d = points[0].length
|
|
if(n <= d) {
|
|
throw new Error("Must input at least d+1 points")
|
|
}
|
|
|
|
//FIXME: This could be degenerate, but need to select d+1 non-coplanar points to bootstrap process
|
|
var initialSimplex = points.slice(0, d+1)
|
|
|
|
//Make sure initial simplex is positively oriented
|
|
var o = orient.apply(void 0, initialSimplex)
|
|
if(o === 0) {
|
|
throw new Error("Input not in general position")
|
|
}
|
|
var initialCoords = new Array(d+1)
|
|
for(var i=0; i<=d; ++i) {
|
|
initialCoords[i] = i
|
|
}
|
|
if(o < 0) {
|
|
initialCoords[0] = 1
|
|
initialCoords[1] = 0
|
|
}
|
|
|
|
//Create initial topological index, glue pointers together (kind of messy)
|
|
var initialCell = new Simplex(initialCoords, new Array(d+1), false)
|
|
var boundary = initialCell.adjacent
|
|
var list = new Array(d+2)
|
|
for(var i=0; i<=d; ++i) {
|
|
var verts = initialCoords.slice()
|
|
for(var j=0; j<=d; ++j) {
|
|
if(j === i) {
|
|
verts[j] = -1
|
|
}
|
|
}
|
|
var t = verts[0]
|
|
verts[0] = verts[1]
|
|
verts[1] = t
|
|
var cell = new Simplex(verts, new Array(d+1), true)
|
|
boundary[i] = cell
|
|
list[i] = cell
|
|
}
|
|
list[d+1] = initialCell
|
|
for(var i=0; i<=d; ++i) {
|
|
var verts = boundary[i].vertices
|
|
var adj = boundary[i].adjacent
|
|
for(var j=0; j<=d; ++j) {
|
|
var v = verts[j]
|
|
if(v < 0) {
|
|
adj[j] = initialCell
|
|
continue
|
|
}
|
|
for(var k=0; k<=d; ++k) {
|
|
if(boundary[k].vertices.indexOf(v) < 0) {
|
|
adj[j] = boundary[k]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Initialize triangles
|
|
var triangles = new Triangulation(d, initialSimplex, list)
|
|
|
|
//Insert remaining points
|
|
var useRandom = !!randomSearch
|
|
for(var i=d+1; i<n; ++i) {
|
|
triangles.insert(points[i], useRandom)
|
|
}
|
|
|
|
//Extract boundary cells
|
|
return triangles.boundary()
|
|
} |