├── _config.yml ├── favicon.png ├── screenshot.jpg ├── modules ├── ConvexGeometry.mjs ├── SpriteText.mjs ├── HyperedgeEvent.mjs ├── TokenEvent.mjs ├── Graph3D.mjs ├── Rewriter.mjs ├── Rulial.mjs ├── HDC.mjs ├── Graph.mjs ├── ConvexHull.mjs └── Simulator.mjs ├── LICENSE └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: false 2 | -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/met4citizen/Hypergraph/HEAD/favicon.png -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/met4citizen/Hypergraph/HEAD/screenshot.jpg -------------------------------------------------------------------------------- /modules/ConvexGeometry.mjs: -------------------------------------------------------------------------------- 1 | import { BufferGeometry, Float32BufferAttribute } from 'three'; 2 | import { ConvexHull } from './ConvexHull.mjs'; 3 | 4 | class ConvexGeometry extends BufferGeometry { 5 | 6 | constructor( points ) { 7 | super(); 8 | 9 | const vertices = []; 10 | const normals = []; 11 | const convexHull = new ConvexHull().setFromPoints( points ); 12 | const faces = convexHull.faces; 13 | 14 | for ( let i = 0; i < faces.length; i ++ ) { 15 | const face = faces[ i ]; 16 | let edge = face.edge; 17 | do { 18 | const point = edge.head().point; 19 | 20 | vertices.push( point.x, point.y, point.z ); 21 | normals.push( face.normal.x, face.normal.y, face.normal.z ); 22 | 23 | edge = edge.next; 24 | } while ( edge !== face.edge ); 25 | 26 | } 27 | this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) ); 28 | this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) ); 29 | 30 | } 31 | 32 | } 33 | 34 | export { ConvexGeometry }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mika Suominen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /modules/SpriteText.mjs: -------------------------------------------------------------------------------- 1 | import { LinearFilter, Sprite, SpriteMaterial, Texture } from 'three' 2 | 3 | class SpriteText extends Sprite { 4 | 5 | constructor(text, textHeight, color) { 6 | super( new SpriteMaterial({ 7 | map: new Texture(), 8 | color: 0xffffff, 9 | transparent: true, 10 | depthTest: false, 11 | depthWrite: false 12 | }) ); 13 | 14 | this.text = `${text}`; 15 | this.textHeight = textHeight; 16 | this.color = color; 17 | 18 | this.fontFace = 'Arial'; 19 | this.fontSize = 40; // defines text resolution 20 | this.fontWeight = 'bold'; 21 | 22 | this.canvas = document.createElement('canvas'); 23 | this.texture = this.material.map; 24 | this.texture.minFilter = LinearFilter; 25 | 26 | this.update(); 27 | } 28 | 29 | update() { 30 | const ctx = this.canvas.getContext('2d'); 31 | const lines = this.text.split('\n'); 32 | const font = `${this.fontWeight} ${this.fontSize}px ${this.fontFace}`; 33 | 34 | ctx.font = font; // measure canvas with appropriate font 35 | const innerWidth = Math.max(...lines.map(line => ctx.measureText(line).width)) + 110; 36 | const innerHeight = this.fontSize * lines.length; 37 | this.canvas.width = innerWidth; 38 | this.canvas.height = innerHeight; 39 | 40 | ctx.font = font; 41 | ctx.fillStyle = this.color; 42 | ctx.textBaseline = 'bottom'; 43 | ctx.translate( 55,0 ); 44 | 45 | lines.forEach((line, index) => { 46 | const lineX = (innerWidth - ctx.measureText(line).width) / 2; 47 | const lineY = (index + 1) * this.fontSize; 48 | ctx.fillText(line, lineX, lineY); 49 | }); 50 | 51 | this.texture.image = this.canvas; 52 | this.texture.needsUpdate = true; 53 | 54 | const yScale = this.textHeight * lines.length; 55 | this.scale.set( yScale * this.canvas.width / this.canvas.height, yScale, 0); 56 | } 57 | } 58 | 59 | export { SpriteText }; 60 | -------------------------------------------------------------------------------- /modules/HyperedgeEvent.mjs: -------------------------------------------------------------------------------- 1 | import { TokenEvent } from "./TokenEvent.mjs"; 2 | 3 | /** 4 | * @class Hyperedge-Event graph 5 | * @author Mika Suominen 6 | */ 7 | class HyperedgeEvent extends TokenEvent { 8 | 9 | /** 10 | * Post-processing options. 11 | * @typedef {Object} PostProcesssingOptions 12 | * @property {boolean} bcoordinates If true, calculate branchial coordinates 13 | * @property {boolean} deduplicate If true, de-duplicate new edges 14 | * @property {boolean} merge If true, merge identical edges 15 | * @property {boolean} pathcnts If true, calculate path counts 16 | * @property {number} knn Number of nearest historical neighbours (k-NN) to calculate 17 | * @property {number} phasecutoff Hamming cutoff distance to consider identical 18 | */ 19 | 20 | /** 21 | * @typedef {number[]} Edge 22 | * @typedef {Object} Rule 23 | */ 24 | 25 | /** 26 | * @constructor 27 | */ 28 | constructor() { 29 | super(); 30 | this.L = new Map(); // Leafs 31 | this.P = new Map(); // Search patterns for leafs 32 | 33 | this.v = -1; // Current maximum vertex number 34 | 35 | this.limitid = -1; // Maximum id of the previous step 36 | this.limitevndx = 0; // Event index at the limit 37 | this.limittndx = 0; // Token index at the limit 38 | } 39 | 40 | /** 41 | * Clear. 42 | */ 43 | clear() { 44 | this.T.length = 0; 45 | this.EV.length = 0; 46 | this.id = -1; 47 | this.L.clear(); 48 | this.P.clear(); 49 | 50 | this.v = -1; 51 | 52 | this.limitid = -1; 53 | this.limitevndx = 0; 54 | this.limittndx = 0; 55 | } 56 | 57 | 58 | 59 | /** 60 | * Map negative vertices to real new vertices. 61 | * @param {Edge[]} es Array of edges with new vertices as neg numbers 62 | * @return {Edge[]} Array of real new edges. 63 | */ 64 | mapper( es ) { 65 | return es.map( e => e.map( v => ( v >= 0 ? v : (this.v - v) ) ) ); 66 | } 67 | 68 | /** 69 | * Set token as a leaf. 70 | * @param {Token} t 71 | */ 72 | setLeaf( t ) { 73 | if ( !t.leaf ) { 74 | t.leaf = true; 75 | 76 | // Add search patterns 77 | const edge = t.edge; 78 | let k = edge.join(","); 79 | let es = this.L.get( k ); 80 | es ? es.push( t ) : this.L.set( k, [ t ] ); 81 | for( let i = edge.length-1; i>=0; i-- ) { 82 | k = edge.map( (x,j) => ( j === i ? x : "*" ) ).join(","); 83 | es = this.P.get( k ); 84 | es ? es.push( t ) : this.P.set( k, [ t ] ); 85 | } 86 | } 87 | } 88 | 89 | 90 | /** 91 | * Unset token as a leaf. 92 | * @param {Token} t 93 | */ 94 | unsetLeaf( t ) { 95 | if ( t.leaf ) { 96 | t.leaf = false; 97 | 98 | // Remove search patterns 99 | const edge = t.edge; 100 | let k = edge.join(","); 101 | let es = this.L.get( k ); 102 | es.splice( es.indexOf( t ), 1 ); 103 | if ( es.length === 0 ) this.L.delete( k ); 104 | for( let i = edge.length-1; i>=0; i-- ) { 105 | k = edge.map( (x,j) => ( j === i ? x : "*" ) ).join(","); 106 | es = this.P.get( k ); 107 | es.splice( es.indexOf( t ), 1 ); 108 | if ( es.length === 0 ) this.P.delete( k ); 109 | } 110 | } 111 | } 112 | 113 | 114 | /** 115 | * Create a new event. 116 | * @param {Token[]} hits Left-hand side match 117 | * @param {Edge[]} adds Pattern of edges to add 118 | * @param {Rule} rule Rule 119 | * @param {number} step Step 120 | * @param {number} b Branch id 121 | * @return {Event} New event, null if not created. 122 | */ 123 | rewrite( hits, adds, rule, step, b ) { 124 | const ev = this.addEvent( hits ); 125 | ev.rule = rule; 126 | ev.step = step; 127 | ev.b = b; 128 | 129 | const edges = this.mapper( adds ); 130 | for( let i = 0; i < edges.length; i++ ) { 131 | // New token 132 | let t = this.addToken( ev ); 133 | t.p = adds[i]; 134 | t.edge = edges[i]; 135 | ev.child.push( t ); 136 | } 137 | 138 | return ev; 139 | } 140 | 141 | 142 | /** 143 | * Pre-process to be called just before processing matches. 144 | */ 145 | preProcess() { 146 | // Set new limits; New events/tokens will be above these limits 147 | this.limitid = this.id; 148 | this.limitevndx = this.EV.length; 149 | this.limittndx = this.T.length; 150 | } 151 | 152 | /** 153 | * Post-process to be called just after processing matches. 154 | * @generator 155 | * @param {PostProcesssingOptions} opt Post-processing options. 156 | * @return {string} Status of post-processing. 157 | */ 158 | *postProcess( opt ) { 159 | 160 | // De-dublication 161 | if ( opt.hasOwnProperty("deduplicate") ) { 162 | if ( opt.deduplicate ) { 163 | let newevs = this.EV.slice( this.limitevndx ); 164 | let total = newevs.length; 165 | while( newevs.length ) { 166 | let ev = newevs.pop(); 167 | let vs = []; 168 | ev.child.forEach( t => { 169 | t.edge = this.mapper( [ t.p ] )[0]; 170 | vs.push( ...t.edge ); 171 | this.setLeaf( t ); 172 | }); 173 | if ( ev.child.length ) { 174 | let ts = [ ev.child[0] ]; 175 | let ps = ev.child.map( t => t.p.join(",") ); 176 | for( let i = newevs.length-1; i >= 0; i-- ) { 177 | let ev2 = newevs[i]; 178 | // There has to be children to de-duplicate 179 | if ( ev2.child.length ) { 180 | let t2 = ev2.child[0]; 181 | let ps2 = ev2.child.map( t => t.p.join(",") ); 182 | // Hyperedge patterns must overlap 183 | if ( ps2.some( p => ps.includes( p ) ) ) { 184 | // All the combined tokens must be branchlike separated 185 | if ( ts.every( t => this.separation( t, t2 ) === 4 ) ) { 186 | // Use the same map to create new vertices 187 | ev2.child.forEach( t => { 188 | t.edge = this.mapper( [ t.p ] )[0]; 189 | vs.push( ...t.edge ); 190 | this.setLeaf( t ); 191 | }); 192 | newevs.splice(i,1); 193 | ts.push( t2 ); 194 | ps = [ ...new Set( [ ...ps, ...ps2 ] ) ]; 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | // Keep track of max vertex id 202 | this.v = Math.max( this.v, ...vs ); 203 | 204 | yield "DD ["+ Math.floor( ( total - newevs.length ) / total * 100 ) + "%]"; 205 | } 206 | 207 | } else { 208 | // No de-duplication; Use unique maps for each event 209 | for( let i = this.limitevndx; i < this.EV.length; i++ ) { 210 | let vs = []; 211 | this.EV[i].child.forEach( t => { 212 | t.edge = this.mapper( [ t.p ] )[0]; 213 | vs.push( ...t.edge ); 214 | this.setLeaf( t ); 215 | }); 216 | // Keep track of max vertex id 217 | this.v = Math.max( this.v, ...vs ); 218 | } 219 | } 220 | } 221 | 222 | // Merge identical new hyperedges 223 | if ( opt.hasOwnProperty("merge") && opt.merge ) { 224 | let total = this.L.size; 225 | let cnt = 0; 226 | for( let l of this.L.values() ) { 227 | let ts = l.filter( t => t.id > this.limitid ).sort( (a,b) => a.id - b.id ); 228 | for( let i=0; i=i+1; j-- ) { 231 | if ( cs.every( t => this.separation( t, ts[j] ) === 4 ) ) { 232 | cs.push( ts[j] ); 233 | ts.splice( j, 1 ); 234 | } 235 | } 236 | cs.forEach( (t,i,arr) => { 237 | if ( i ) { 238 | this.unsetLeaf( t ); 239 | this.merge( arr[0], t ); 240 | } 241 | }); 242 | } 243 | 244 | yield "MERGE ["+ Math.floor( (++cnt / total) * 100 ) + "%]"; 245 | } 246 | } 247 | 248 | // Calculate path counts 249 | if ( opt.hasOwnProperty("pathcnts") && opt.pathcnts ) { 250 | let total = this.EV.length - this.limitevndx; 251 | for( let i = this.limitevndx; i < this.EV.length; i++ ) { 252 | this.setPathcnt( this.EV[i] ); 253 | this.EV[i].child.forEach( t => this.setPathcnt(t) ); 254 | 255 | if ( i % 100 === 0 ) { 256 | yield "PATHCNT ["+ Math.floor( (i-this.limitevndx) / total * 100) + "%]"; 257 | } 258 | } 259 | } 260 | 261 | // Calculate branchial coordinates 262 | if ( opt.hasOwnProperty("bcoordinates") && opt.bcoordinates ) { 263 | let total = this.EV.length - this.limitevndx; 264 | for( let i = this.limitevndx; i < this.EV.length; i++ ) { 265 | this.setBc( this.EV[i] ); 266 | this.EV[i].child.forEach( t => this.setBc(t) ); 267 | 268 | if ( i % 100 === 0 ) { 269 | yield "BC ["+ Math.floor( (i-this.limitevndx) / total * 100) + "%]"; 270 | } 271 | } 272 | } 273 | 274 | // Calculate k-NN (for tokens only) 275 | if ( opt.hasOwnProperty("knn") && opt.knn ) { 276 | let total = this.T.length - this.limittndx; 277 | for( let i = this.limittndx; i < this.T.length; i++ ) { 278 | this.setNN( this.T[i], opt.knn, opt.phasecutoff ); 279 | 280 | if ( i % 100 === 0 ) { 281 | yield "k-NN ["+ Math.floor( (i-this.limittndx) / total * 100) + "%]"; 282 | } 283 | } 284 | } 285 | 286 | } 287 | 288 | 289 | /** 290 | * Generate all combinations of an array of arrays. 291 | * @generator 292 | * @param {Object[][]} arr Array of arrays 293 | * @return {Object[]} Combination 294 | */ 295 | *cartesian( arr ) { 296 | const inc = (t,p) => { 297 | if (p < 0) return true; // reached end of first array 298 | t[p].idx = (t[p].idx + 1) % t[p].len; 299 | return t[p].idx ? false : inc(t,p-1); 300 | } 301 | const t = arr.map( (x,i) => { return { idx: 0, len: x.length }; } ); 302 | const len = arr.length - 1; 303 | do { yield t.map( (x,i) => arr[i][x.idx] ); } while( !inc(t,len) ); 304 | } 305 | 306 | 307 | /** 308 | * Return all the possible combinations of the given a list of hyperedges. 309 | * @param {Edge[]} edges Array of edges 310 | * @param {number} int Allowed separations 311 | * @return {Token[][]} Arrays of edge objs 312 | */ 313 | hits( edges, int ) { 314 | const h = []; 315 | for( let i = 0; i x.id <= this.limitid ) ) continue; 326 | 327 | // Filter out combinations with duplicate edge ids 328 | if ( c.some( (x,i,arr) => arr.indexOf(x) !== i ) ) continue; 329 | 330 | // Filter out based on the allowed interactions 331 | if ( !this.isSeparation( c, int ) ) continue; 332 | 333 | hits.push( c ); 334 | } 335 | 336 | return hits; 337 | } 338 | 339 | /** 340 | * Find edges that match to the given wild card search pattern. 341 | * @param {Edge} p Search pattern, wild card < 0 342 | * @return {Edge[]} Matching hyperedges. 343 | */ 344 | find( p ) { 345 | let found = []; 346 | let wilds = p.reduce( (a,x) => a + ( x < 0 ? 1 : 0 ), 0 ); 347 | if ( wilds === 0 ) { 348 | // No wild cards, so we look for an exact match 349 | if ( this.L.has( p.join(",") ) ) found.push( p ); 350 | } else if ( wilds === p.length ) { 351 | // Only wild cards, so we return all edges of the given length 352 | for( const ts of this.L.values() ) { 353 | if ( ts[0].edge.length === p.length ) found.push( [...ts[0].edge] ); 354 | } 355 | } else { 356 | // Extract individual keys and find edges based on them 357 | // Filter out duplicates and get the intersection 358 | let f,k,ts; 359 | for( let i = p.length-1; i >= 0; i-- ) { 360 | if ( p[i] < 0 ) continue; 361 | k = p.map( (x,j) => ( j === i ? x : "*" )).join(","); 362 | ts = this.P.get( k ); 363 | if ( !ts ) return []; 364 | f = f ? ts.filter( t => f.includes(t) ) : ts; 365 | } 366 | // Get unique edges 367 | if ( f ) { 368 | found = Object.values( f.reduce((a,b) => { 369 | a[b.edge.join(",")] = [...b.edge]; 370 | return a; 371 | },{})); 372 | } 373 | } 374 | 375 | return found; 376 | } 377 | 378 | /** 379 | * Worldline of a given set of spatial vertices. 380 | * @param {number[]} vs An array of vertices. 381 | * @return {Edge[]} Array of causal edges. 382 | */ 383 | worldline( vs ) { 384 | let es = []; 385 | let iprev; 386 | this.EV.forEach( ev => { 387 | let adds = [ ...ev.child.map( t => t.edge ).flat() ]; 388 | if ( vs.some( x => adds.includes(x) ) ) { 389 | if ( iprev ) es.push( [iprev,ev.id] ); 390 | iprev = ev.id; 391 | } 392 | }); 393 | return es; 394 | } 395 | 396 | /** 397 | * Status. 398 | * @return {Object} Status of the hypergraph. 399 | */ 400 | status() { 401 | return { events: this.EV.length }; 402 | } 403 | 404 | } 405 | 406 | export { HyperedgeEvent }; 407 | -------------------------------------------------------------------------------- /modules/TokenEvent.mjs: -------------------------------------------------------------------------------- 1 | import { HDC } from "./HDC.mjs"; 2 | 3 | const hdc = new HDC(); 4 | 5 | /** 6 | * @class Token-Event graph 7 | * @author Mika Suominen 8 | */ 9 | class TokenEvent { 10 | 11 | /** 12 | * @typedef {Object} Token 13 | * @property {number} id Identifier 14 | * @property {Event[]} parent Parent events 15 | * @property {Event[]} child Child events 16 | * @property {Object[]} past Past causal cone 17 | * @property {Hypervector} bc Branchial coordinate 18 | */ 19 | 20 | /** 21 | * @typedef {Object} Event 22 | * @property {number} id Identifier 23 | * @property {Token[]} parent Parent tokens 24 | * @property {Token[]} child Child tokens 25 | * @property {Hypervector} bc Branchial coordinate 26 | */ 27 | 28 | /** 29 | * @constructor 30 | */ 31 | constructor() { 32 | this.T = []; // Tokens 33 | this.EV = []; // Events 34 | this.id = -1; // Maximum id 35 | } 36 | 37 | 38 | /** 39 | * Lowest common ancestors of the given arrays. 40 | * @static 41 | * @param {Set[]} s 42 | * @return {(Token|Event)[]} Lower common ancestors. 43 | */ 44 | static lca( s ) { 45 | // Intersection 46 | const is = s.reduce((a,b) => { 47 | const c = new Set(); 48 | for( const v of a ) { 49 | if ( b.has(v) ) { 50 | c.add(v); 51 | } 52 | } 53 | return c; 54 | }); 55 | 56 | // Outdegree = 0 57 | const z = [...is].filter( x => x.child.every( y => !is.has(y) ) ); 58 | 59 | return z; 60 | } 61 | 62 | /** 63 | * Add a new token. 64 | * @param {Event} ev Parent event 65 | * @return {Token} New token. 66 | */ 67 | addToken( ev ) { 68 | const t = { 69 | id: ++this.id, 70 | parent: [ ev ], 71 | child: [], 72 | past: new Set() 73 | }; 74 | 75 | // Past causal cone 76 | t.past.add( t ); 77 | if ( ev ) { 78 | t.past.add( ev ); 79 | ev.parent.forEach( x => x.past.forEach( y => t.past.add( y ) ) ); 80 | } 81 | 82 | this.T.push( t ); 83 | 84 | return t; 85 | } 86 | 87 | /** 88 | * Delete a token. 89 | * @param {Token} t 90 | */ 91 | deleteToken( t ) { 92 | let idx = this.T.indexOf( t ); 93 | if ( idx !== -1 ) { 94 | // Remove edge 95 | this.T.splice( idx, 1); 96 | 97 | // Remove from parents 98 | t.parent.forEach( ev => { 99 | ev.child.splice( ev.child.indexOf( t ), 1); 100 | }); 101 | 102 | // Delete childs 103 | t.child.forEach( ev => { 104 | if ( ev.parent.length === 1 ) { 105 | ev.parent.length = 0; 106 | this.deleteEvent( ev ); 107 | } else { 108 | ev.parent.splice( ev.parent.indexOf( t ), 1); 109 | } 110 | }); 111 | } 112 | } 113 | 114 | /** 115 | * Create a new event. 116 | * @param {Token[]} ts Parent tokens 117 | * @return {Event} New event 118 | */ 119 | addEvent( ts ) { 120 | // Add a new rewriting event 121 | const ev = { 122 | id: ++this.id, 123 | parent: ts, 124 | child: [] 125 | }; 126 | this.EV.push( ev ); 127 | 128 | // Process hit 129 | ts.forEach( t => { 130 | t.child.push( ev ); 131 | }); 132 | 133 | return ev; 134 | } 135 | 136 | /** 137 | * Undo an event. 138 | * @param {Event} ev 139 | */ 140 | deleteEvent( ev ) { 141 | let idx = this.EV.indexOf( ev ); 142 | if ( idx !== -1 ) { 143 | // Remove event 144 | this.EV.splice(idx,1); 145 | 146 | // Process parents 147 | ev.parent.forEach( t => { 148 | t.child.splice( t.child.indexOf( ev ), 1); 149 | }); 150 | 151 | // Remove childs 152 | ev.child.forEach( t => { 153 | if ( t.parent.length === 1 ) { 154 | t.parent.length = 0; 155 | this.deleteToken( t ); 156 | } else { 157 | t.parent.splice( t.parent.indexOf( ev ), 1); 158 | 159 | // Update past cone 160 | t.past = new Set(); 161 | t.past.add( t ); 162 | t.parent.forEach( x => { 163 | t.past.add( x ); 164 | x.parent.forEach( x => x.past.forEach( y => t.past.add( y ) ) ); 165 | }); 166 | } 167 | }); 168 | } 169 | } 170 | 171 | /** 172 | * Merge two tokens. 173 | * @param {Token} t1 174 | * @param {Token} t2 175 | */ 176 | merge( t1, t2 ) { 177 | // Sort based on ids 178 | if ( t1.id > t2.id ) { 179 | [ t1, t2 ] = [ t2, t1 ]; 180 | } 181 | 182 | // Switch childs 183 | t2.child.forEach( ev => { 184 | let idx = ev.parent.indexOf( t2 ); 185 | ev.parent[idx] = t1; 186 | t1.child.push( ev ); 187 | }); 188 | t2.child.length = 0; 189 | 190 | // Switch parents 191 | t2.parent.forEach( ev => { 192 | let idx = ev.child.indexOf( t2 ); 193 | ev.child[idx] = t1; 194 | t1.parent.push( ev ); 195 | }); 196 | t2.parent.length = 0; 197 | 198 | // Update the past, path count and branchial coordinate 199 | t2.past.forEach( y => t1.past.add( y ) ); 200 | t1.past.delete( t2 ); 201 | 202 | // Remove the extra token 203 | this.deleteToken( t2 ); 204 | } 205 | 206 | /** 207 | * Separation of two tokens. 208 | * @param {Token} t1 209 | * @param {Token} t2 210 | * @return {number} 0 = same, 1 = spacelike, 2 = timelike, 4 = branchlike 211 | */ 212 | separation( t1, t2 ) { 213 | if ( t1 === t2 ) return 0; // same 214 | 215 | // Lowest Common Ancestors 216 | let lca = TokenEvent.lca( [ t1.past, t2.past ] ); 217 | 218 | if ( lca.includes(t1) || lca.includes(t2) ) return 2; // timelike 219 | if ( lca.some( l => l.hasOwnProperty("past") ) ) return 4; // branchlike 220 | return 1; // spacelike 221 | } 222 | 223 | /** 224 | * Check if all given tokens are connected only through the allowed ways. 225 | * @param {Token[]} ts Array of tokens. 226 | * @param {number} int Allowed interactions. 227 | * @return {boolean} True, if connected only in allowed ways. 228 | */ 229 | isSeparation( ts, int ) { 230 | for( let i=0; i x.hasOwnProperty("past") ) ) { 257 | // branchlike separated, probability to see t is 0 258 | return 0; 259 | } else { 260 | let a = 0; 261 | let b = 0; 262 | for( const x of s.keys() ) { 263 | if ( x === tref ) { 264 | continue; 265 | } else if ( x === t ) { 266 | a = a + 1; 267 | b = b + 1; 268 | continue; 269 | } else { 270 | lca = TokenEvent.lca( [ tref.past, x.past ] ); 271 | if ( lca.every( x => !x.hasOwnProperty("past") ) ) { 272 | lca = TokenEvent.lca( [ t.past, x.past ] ); 273 | if ( lca.every( x => !x.hasOwnProperty("past") ) ) { 274 | b = b + 1; 275 | } 276 | } 277 | } 278 | } 279 | return(b>0?a/b:1) 280 | } 281 | 282 | // calculate probability 283 | /* let n = 0; // Count of t all spacelike separated pairs (tref,x) 284 | let cnt = 0; // Count of times t is spacelike separated to pair (tref,x) 285 | for( const x of s.keys() ) { 286 | if ( x === tref ) continue; 287 | if ( x === t ) { 288 | n = n + 1; 289 | cnt = cnt + 1; 290 | continue; 291 | } 292 | lca = TokenEvent.lca( [ tref.past, x.past ] ); 293 | if ( lca.some( x => x.hasOwnProperty("past") ) ) continue; 294 | let pairpast = new Set( [ ...tref.past, ...x.past ] ); 295 | n = n + 1; 296 | lca = TokenEvent.lca( [ t.past, pairpast ] ); 297 | if ( lca.some( x => x.hasOwnProperty("past") ) ) continue; 298 | cnt = cnt + 1; 299 | } 300 | return (n>0 ? cnt / n : 1); */ 301 | } 302 | 303 | /** 304 | * Calculate and set the path count of the given token/event. 305 | * @param {(Token|Event)} x Token/event 306 | * @param {boolean} [reset=false] If true, recalculate and reset. 307 | */ 308 | setPathcnt( x, reset=false ) { 309 | if ( !reset && x.hasOwnProperty("pathcnt") ) return; // Already set 310 | if ( x.parent.length === 0 ) { 311 | // No parents -> 1 312 | x.pathcnt = 1; 313 | } else { 314 | // Ensure all parents have been calculated 315 | x.parent.forEach( p => { 316 | if ( !p.hasOwnProperty("pathcnt") ) this.setPathcnt( p ); 317 | }); 318 | if ( x.hasOwnProperty("past") ) { 319 | // This is a token -> sum of the parents path counts 320 | x.pathcnt = x.parent.reduce( (a,y) => a + y.pathcnt, 0 ); 321 | } else { 322 | // This is an event -> minimun of the parents' path counts 323 | x.pathcnt = Math.min( ...x.parent.map( y => y.pathcnt ) ); 324 | } 325 | } 326 | } 327 | 328 | /** 329 | * Calculate and set the branchial coordinate of the given token/event. 330 | * @param {(Token|Event)} x 331 | * @param {boolean} [reset=false] If true, recalculate and reset. 332 | */ 333 | setBc( x, reset = false ) { 334 | if ( !reset && x.hasOwnProperty("bc") ) return; // Already set 335 | 336 | // Make sure all parents have coordinates 337 | x.parent.forEach( y => { 338 | if (!y.hasOwnProperty("bc") ) this.setBc( y ); 339 | }); 340 | 341 | // Find out parent coordinates 342 | let bcs; 343 | if ( x.parent.length === 0 ) { 344 | bcs = [ hdc.random() ]; // Initial event, use random 345 | } else if ( x.parent.length === 1 ) { 346 | bcs = [ x.parent[0].bc ]; // Single parent, inherit 347 | } else { 348 | bcs = x.parent.map( y => y.bc ); // Use immediate parents by default 349 | const ps = x.hasOwnProperty("past") ? [ ...new Set( x.parent.map( y => y.parent ).flat() ) ] : x.parent; 350 | if ( ps.length ) { 351 | const lca = TokenEvent.lca( ps.map( y => y.past ) ); 352 | if ( lca.length ) { 353 | bcs = lca.map( y => y.bc ); // Lowest common ancestors 354 | } 355 | } 356 | } 357 | 358 | // Use majority rule 359 | if ( bcs.length === 1 || bcs.every( y => y === bcs[0] ) ) { 360 | x.bc = bcs[0]; 361 | } else { 362 | x.bc = hdc.maj( bcs ); 363 | } 364 | 365 | // If overlaps in rewrites, separate based on random coordinate 366 | if ( !x.hasOwnProperty("past") && x.parent.some( y => y.child.length > 1 ) ) { 367 | x.bc = hdc.maj( [ x.bc, hdc.random() ] ); 368 | } 369 | } 370 | 371 | /** 372 | * Calculate and set k Nearest Neighbours (k-NN). 373 | * Note: Only calculated for tokens. 374 | * @param {Token} x 375 | * @param {number} k Number of nearest neighbours to calculate. 376 | * @param {number} cutoff Below this Hamming distance, consider the same 377 | * @param {boolean} [reset=false] If true, recalculate and reset. 378 | */ 379 | setNN( x, k, cutoff, reset = false ) { 380 | if ( !reset && x.hasOwnProperty("nn") ) return; // Already set 381 | 382 | let limit = Infinity; 383 | let nn = new Array(k).fill().map( _ => { 384 | return { t:null, d: limit }; 385 | }); 386 | let ndx = this.T.indexOf(x); 387 | for( let i=ndx-1; i>=0; i-- ) { 388 | if ( !this.T[i].hasOwnProperty("nn") ) { 389 | this.setNN( this.T[i], k, reset, cutoff ); 390 | } 391 | 392 | // Identical tokens 393 | if ( x.bc === this.T[i].bc ) { 394 | if ( this.T[i].nn[0].d < cutoff ) { 395 | nn = this.T[i].nn; 396 | } else { 397 | nn[0].t = this.T[i]; 398 | nn[0].d = 0; 399 | for( let j=1; j>> 1) & 0x55555555); 415 | n = (n & 0x33333333) + ((n >>> 2) & 0x33333333); 416 | d += ((n + (n >>> 4) & 0xF0F0F0F) * 0x1010101) >>> 24; 417 | } 418 | if ( d < cutoff ) { 419 | for( let j=1; j a.d - b.d ); 430 | limit = nn[k-1].d; 431 | } 432 | } 433 | x.nn = nn; 434 | } 435 | 436 | } 437 | 438 | export { TokenEvent }; 439 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hypergraph Rewriting System 2 | 3 | 4 | 5 | **Run it: https://met4citizen.github.io/Hypergraph/** 6 | 7 | Hypergraph Rewriting System able to visualize both 8 | single-way and multiway evolutions in 3D. 9 | 10 | The app uses 11 | [3d Force-Directed Graph](https://github.com/vasturiano/3d-force-graph) 12 | for representing graph structures, 13 | [ThreeJS](https://github.com/mrdoob/three.js/)/WebGL for 3D rendering and 14 | [d3-force-3d](https://github.com/vasturiano/d3-force-3d) for the force engine. 15 | 16 | ## Introduction 17 | 18 | A hypergraph is a generalization of a regular graph. Whereas an edge 19 | typically connects only two nodes, a hyperedge can join any number of nodes. 20 | In a Hypergraph Rewriting System some initial hypergraph is transformed 21 | incrementally by making a series of updates that follow some abstract 22 | rewriting rule. 23 | 24 | As an example, consider an abstract rewriting rule 25 | `(x,x,y)(y,z,u)->(x,v,u)(y,v,z)(v,v,u)`. Wherever and whenever a subhypergraph 26 | having the form of the left-hand side pattern `(x,x,y)(y,z,u)` is found in the 27 | hypergraph, it is replaced with a new subhypergraph having the form of the 28 | right-hand side pattern `(x,v,u)(y,v,z)(v,v,u)` introducing a new node `v`. 29 | 30 | Sometimes matches overlap. For example, when using the previous rule 31 | with the initial state `(1,1,2)(2,2,3)(3,3,4)` there are two overlapping 32 | matches `(x=1,y=2,z=2,u=3)` and `(x=2,y=3,z=3,u=4)` (critical pair). One way 33 | to resolve the conflict is to pick one of the two according to some 34 | ordering scheme and ignore the other (single-way evolution). Another approach 35 | is to rewrite both by allowing the system to branch (multiway evolution). 36 | 37 | As the multiway system branches and diverges (quantum mechanics), the 38 | probability of ending up in some particular end state is related to the number 39 | of different evolutionary paths to that state (path counting). However, there 40 | can also be rules that make branches merge (critical pair completion). 41 | This branching and merging makes certain end states more/less likely 42 | (constructive/destructive interference). In the end we (observers) are likely 43 | to find ourselves in the part of the system in which the branches always merge 44 | (confluence) and the system converges (classical mechanics). 45 | 46 | For more information about hypergraph rewriting systems and their potential to 47 | represent fundamental physics visit 48 | [The Wolfram Physics Project](https://www.wolframphysics.org) website. 49 | According to their 50 | [technical documents](https://www.wolframphysics.org/technical-documents/) 51 | certain models reproduce key features of both relativity and quantum 52 | mechanics. 53 | 54 | If you are interested in quantum mechanics and graph theory, you might also 55 | want to take a look at my spin-off projects 56 | [BigraphQM](https://github.com/met4citizen/BigraphQM) and 57 | [CliqueVM](https://github.com/met4citizen/CliqueVM). 58 | For a philosophical take on these ideas see my blog post 59 | [The Game of Algorithms](https://metacity.blogspot.com/). 60 | 61 | ## Rules 62 | 63 | Click `RULE` to modify the rewriting rule, change its settings, and run 64 | the rewriting process. 65 | 66 | The system supports several rules separated with a semicolon `;` or written 67 | on separate lines. The two sides of any one-way rule must be separated 68 | with an arrow `->`. The separator `==` can be used as a shortcut for a two-way 69 | setup in which both directions (the rule and its inverse) are possible. 70 | 71 | Hyperedge patterns can be described by using numbers, characters or words. 72 | Several types of parentheses are also supported. For example, a rule 73 | `[{x,y}{x,z}]->[{x,y}{x,w}{y,w}{z,w}]` is considered valid and can be 74 | validated and converted to the default number format by clicking `Scan`. 75 | 76 | The system also supports a filter/negation `\`. As an example, the rule 77 | `(1)(1,2)\(2)->(1)(1,2)(2)` is applied only if there is no unary edge `(2)`. 78 | If the branchlike interactions are allowed, the check is made relative to all 79 | possible branches of history. 80 | 81 | A rule without any right-hand side, such as `(1,1,1)(1,1,1)`, is used as the 82 | initial graph. An alternative way to define an initial state is to use 83 | some predefined function: 84 | 85 | Initial graph | Description 86 | --- | --- 87 | `complete(n)` | Complete graph with `n` vertices so that each vertex is connected to every other vertex. 88 | `grid(d1,d2,...)` | Grid with sides of length `d1`, `d2`, and so on. The number of parameters defines the dimension of the grid. For example, `grid(2,4,5)` creates a 3-dimensional 2x4x5 grid. 89 | `line(n)` | Line with `n` vertices. 90 | `points(n)` | `n` unconnected unary edges. 91 | `prerun(n)` | Pre-run the current rule for `n` events in one branch (singleway). The leaves of the result are used as an initial state. 92 | `random(n,d,nedges)` | Random graph with `n` vertices so that each vertex is sprinkled randomly in `d` dimensional space and has at least `nedges` connections. 93 | `rule('rule',n)` | Run rewriting rule `rule` for maximum `n` events in one branch (singleway). The leaves of the result are used as an initial state. 94 | `sphere(n)` | Fibonacci sphere with `n` vertices. 95 | 96 | By using options `twoway`, `oneway` and/or `inverse`, each edge produced can 97 | be made a two-way edge, sorted or reversed. It is also possible to define some 98 | specific branch, or a combination of branches, for the initial state. As an 99 | example, `(1,1)(1,1)/7`, would specify branches 1-3 (the sum of the first 100 | three bits 1+2+4). By default, the initial state is set for all the tracked 101 | branches. 102 | 103 | If the initial state is not specified, the left-hand side pattern of the first 104 | rule is used, but with only a single node. For example, a rule 105 | `(1,2)(1,3)->(1,2)(1,4)(2,4)(3,4)` gives an initial state `(1,1)(1,1)`. 106 | 107 | The `Evolution` option defines which kind of evolution is to be simulated: 108 | 109 | Evolution | Description 110 | --- | --- 111 | `1`,`2`,`4` | Single-way system with 1/2/4 branches. By default, random event order is used to resolve overlaps for each branch. If the `WM` option is set, Wolfram model's standard event order is used for branch 1 and its reverse for branch 2. 112 | `FULL` | Full multiway system. All the matches are instantiated and four branches tracked. 113 | 114 | The `Interactions` option defines the possible interactions between hyperedges. 115 | Any combination of the three possible interactions can be selected. 116 | 117 | Interactions | Description 118 | --- | --- 119 | `SPACE` | Allow interactions between spacelike separated hyperedges. Two edges are spacelike separated if their lowest common ancestors are all updating events. In practice this should always be selected, because the nodes in a typical initial state are spacelike separated. 120 | `TIME` | Allow interaction between timelike separated hyperedges. Two edges are timelike separated if either one of them is an ancestors of the other one, that is, inside the other's past causal cone. 121 | `BRANCH` | Allow interaction between branchlike separated hyperedges. Two edges are branchlike separated if any of their lowest common ancestors is a hyperedge. 122 | 123 | Other options: 124 | 125 | Option | Description 126 | --- | --- 127 | `WM` | Wolfram Model. If set, the first branch uses Wolfram Model's standard event order (LeastRecentEdge + RuleOrdering + RuleIndex) and the second branch its reverse. By default the setting is off and all tracked branches use random event ordering. 128 | `RO` | Rule order (index). Regardless of other settings, always try to apply the events in the order in which the rules have been specified. By default the setting is off and the individual rules are allowed to mix. 129 | `DD` | De-duplicate. The overlapping new hyperedges on different branches are de-duplicated at the end of each step. This allows branches to merge. EXPERIMENTAL, FUNCTIONALITY LIKELY TO CHANGE. 130 | 131 | 132 | ## Simulation/Observer 133 | 134 | Simulation can be run in three different modes: `Space`, `Time` or `Phase`. 135 | 136 | - In `Space` mode the system shows the evolution of the spatial hypergraph. 137 | According to the Wolfram Model, the spatial hypergraph represents 138 | a spacelike state of the universe with nodes as "atoms of space". 139 | - In `Time` mode the system builds up the transitive reduction of the causal 140 | graph. In this view nodes represent updating events and directed 141 | edges their causal relations. According to the Wolfram Model, the flux of 142 | causal edges through spacelike and timelike hypersurfaces is related to 143 | energy and momentum respectively. 144 | - In `Phase` mode the hyper-dimensional multiway space is projected in 3D by 145 | using Hamming distances and k-NN algorithm (see [Appendix A](#appendix-a): 146 | Coordinatization of Local Multiway System by using Hyper-dimensional Vectors). 147 | According to Wolfram Model, positions in so-called "branchial" space are 148 | related to quantum phase. 149 | 150 | Media buttons let you reset the mode, start/pause the simulation and 151 | skip to the end. Whenever the system has branches, the first four 152 | branches can be shown separately or in any combination. If `Past` is selected, 153 | the full history of the local multiway system is shown in space mode. 154 | By default only the leaf edges of the system are visible. 155 | 156 | The two sliders change the visual appearance of the graph by tuning the 157 | parameters of the underlying force engine. Note: Changing the viewpoint 158 | or the forces do not in any way change the multiway system itself only 159 | how it is visualized on the screen. 160 | 161 | 162 | ## Highlighting 163 | 164 | Subhypergraphs can be highlighted by clicking `RED`/`BLUE` and using one or 165 | more of the following commands: 166 | 167 | Command | Highlighted | Status Bar 168 | --- | --- | --- 169 | `curv(x,y)` | Two n-dimensional balls of radius one and the shortest path between their centers. | Curvature based on Ollivier-Ricci (1-Wasserstein) distance. 170 | `dim([x],[radius])` | N-dimensional ball with an origin `x` (random, if not specified) and radius `r` (automatically scaled if not specified). | The effective dimension `d` based on nearby n-ball volumes fitted to `r^d`. 171 | `geodesic(x,y,[dir],[rev],[all])`

`dir` = directed edges
`rev` = reverse direction
`all` = all shortest paths | Shortest path(s) between two nodes.

| Path distance as the number of edges. 172 | `lightcone(x,length)` | Lightcone centered at node `x` with size `length`. `TIME` mode only. | Size of the cones as the number of edges. 173 | `phase(x,y)` | Multiway distance between two nodes. | Multiway distance 0-10,240. Mid-point 5,120 corresponds to orthogonal vectors (phase difference PI). 174 | `nball(x,radius,[dir],[rev])`

`dir` = directed edges
`rev` = reverse direction | N-dimensional ball is a set of nodes and edges within a distance `radius` from a given node `x`. | Volume as the number of edges. 175 | `nsphere(x,radius,[dir],[rev])`

`dir` = directed edges
`rev` = reverse direction | N-dimensional sphere/hypersurface within a distance `radius` from a given node `x`. | Area as the number of nodes. 176 | `random(x,distance,[dir],[rev])`

`dir` = directed edges
`rev` = reverse direction | Random walk starting from a specific node `x` with maximum `distance`. | Path distance as the number of edges. 177 | `surface(x,y)` | Space-like hypersurface based on a range of nodes. | Volume as the number of nodes. 178 | `worldline(x,...)` | Time-like curve of space-like node/nodes. `TIME` mode only. | Distance as the number of edges. 179 | `(x,y)(y,z)`
`(x,y)(y,z)\(z)->(x,y)` | Subhypergraphs matching the given rule-based pattern. The right hand side pattern can be used to specify which part of the match is highlighted. `SPACE` mode only. | The number of rule-based matches. 180 | 181 | The node parameters can be specified with identifiers you can find by 182 | hovering the mouse pointer over nodes. It is also possible to use 183 | variables `x` and `y`. To set variable `x` click on any node and to set `y` 184 | use right click. 185 | 186 | 187 | ## Scalar Fields 188 | 189 | Click `GRAD` to visualize predefined scalar fields. Relative intensity of 190 | the field is represented by different hues of colour from light blue (lowest) 191 | to green to yellow (mid) to orange to red (highest). Field values are 192 | calculated for each edge and the colours of the vertices represent 193 | the mean of their edges. 194 | 195 | Scalar Field | Description 196 | --- | --- 197 | `branch` | Branch id. With two branches the main colours are blue and red. With four branches blue, green, orange and red. For shared edges the colour is in the middle of the spectrum. 198 | `created` | Creation time from oldest to newest. 199 | `curvature` | Ollivier-Ricci curvature. NOTE: Calculating curvature is a CPU intensive task. When used in real-time it will slow down the animation. 200 | `degree` | The mean of incoming and outgoing edges. 201 | `energy` | The mean of updated edges. 202 | `mass` | The part of `energy` in which the right hand side edges connect pre-existing vertices. 203 | `momentum` | The part of `energy` in which the right hand side edges have new vertices. 204 | `pathcnt` | The number of paths leading to specific edge. 205 | `phase(x)` | Multiway distance to a given token `x`. Multiway coordinates are calculated using 10,240-dimensional dense bipolar hypervectors. Distances close to 5,120, that is, 1/2 of the dimension, can be considered orthogonal. NOTE: Hyperdimensional computing is CPU intensive, so when used in real-time it will slow down the animation. 206 | `probability` | Normalized path count for each edge in each step. 207 | `step` | Rewriting step. 208 | 209 | The value range can be limited by giving lower and higher limits as 210 | parameters. For example, `branch(2,4)` shows branches 2-4. A limit can also 211 | be given as a percentage. For example, `energy(50%,100%)` highlights 212 | only upper half of the full range. 213 | 214 | ## Notes 215 | 216 | The aim of this project has been to learn some basic concepts and 217 | ideas related to hypergraphs and hypergraph rewriting. Whereas the Wolfram 218 | physics project has been a great inspiration, this project is not directly 219 | associated with it, doesn't use any code from it, and doesn't claim to be 220 | compatible with the Wolfram Model. 221 | 222 | As a historical note, the idea of "atoms of space" is not a new one. It already 223 | appears in the old Greek tradition of atomism (*atomos*, "uncuttable") started 224 | by Democritus (ca. 460–370 BCE). In the Hellenistic period the idea was revived 225 | by Epicurus (341–270 BCE) and put in a poetic form by Lucretius (ca. 99–55 BCE). 226 | Unfortunately, starting from the Early Middle Ages, atomism was mostly 227 | forgotten in the Western world until Lucretius' *De Rerum Natura* and other 228 | atomist teachings were rediscovered in the 14th century. 229 | 230 | > “The atoms come together in different order and position, like letters, 231 | > which, though they are few, yet, by being placed together in different ways, 232 | > produce innumerable words.” 233 | > -- Epicurus (according to Lactantius) 234 | 235 | ## Appendix A 236 | 237 | ### Coordinatization of Local Multiway System using Hyperdimensional Vectors 238 | 239 | The following procedure utilizes Pentti Kanerva's work on Hyperdimensional 240 | computing [1]. HDC is based on the fact that in hyperdimensional space 241 | (>10,000-D) a randomly chosen vector (hypervector) is quasi-orthogonal to 242 | all previously generated hypervectors. 243 | 244 | - For each rewriting rule, generate a random 10,240-dimensional dense bipolar 245 | {-1,1} seed vector. 246 | 247 | - Let the coordinate of the initial event in a local multiway system be a 248 | randomly generated 10,240-dimensional dense bipolar {-1,1} vector. 249 | 250 | - Let the coordinate of each new token/event be the sum of its parents' 251 | coordinates so that the 'sum' is the element-wise majority with random bipolar 252 | values for ties. 253 | 254 | - Whenever the parents are timelike/branchlike separated, instead of summing up 255 | their own coordinates, use the coordinates of their lowest common ancestors. 256 | 257 | - Whenever there is a new branch so that two or more events use an overlapping 258 | set of tokens, separate the events in orthogonal branches by adding random 259 | hypervectors to their previously calculated coordinates. 260 | 261 | - The distance between the coordinates of any two tokens/events is the 262 | Hamming distance between their hypervectors. If the distance is close to 263 | 1/2 of the used dimension, the two hypervectors are quasi-orthogonal. 264 | 265 | The local multiway coordinates can be projected into some lower dimension by 266 | first calculating the distance matrix and then doing multidimensional scaling. 267 | Al alternative way to do the projection is to build a graph in which 268 | each node represents a group of close-by tokens/events. These groups can then 269 | be connected to their 1-3 nearest neighbours by using a k-NN algorithm. 270 | 271 | *[1] Kanerva, P. (2019). Computing with High-Dimensional Vectors. IEEE Design & Test, 36(3):7–14.* 272 | -------------------------------------------------------------------------------- /modules/Graph3D.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | BufferGeometry, BufferAttribute, Sprite, SpriteMaterial, Texture, 3 | MeshBasicMaterial, Mesh, DoubleSide, Vector3 4 | } from 'three' 5 | import { ConvexGeometry } from './ConvexGeometry.mjs'; 6 | 7 | import _3dForceGraph from 'forcegraph'; 8 | import { Graph } from "./Graph.mjs"; 9 | 10 | /** 11 | * @class Graph3D 12 | * @author Mika Suominen 13 | */ 14 | class Graph3D extends Graph { 15 | 16 | /** 17 | * Creates an instance of Hypergraph. 18 | * @param {Object} canvas DOM element of the canvas 19 | * @constructor 20 | */ 21 | constructor( element ) { 22 | super(); 23 | this.element = element; 24 | this.FG = _3dForceGraph({ rendererConfig: { antialias: true, precision: "lowp" }}); 25 | this.HS = []; // Array of highlighted hypersurfaces 26 | this.HL = []; // Array of extra highlighted link objs 27 | this.view = 1; // View, 1 = space, 2 = time 28 | 29 | this.spaceStyles = [ 30 | { nColor: "black", lColor: "grey", nVal: 4, lWidth: 3, bgColor: "white", nRelSize: 3 }, // 0 defaults 31 | { nColor: "purple", lColor: "hotpink", nVal: 7, lWidth: 6, fill: "hotpink", opacity: 0.2 }, // 1 Red 32 | { nColor: "blue", lColor: "deepskyblue", nVal: 7, lWidth: 6, fill: "deepskyblue", opacity: 0.2 }, // 2 Blue 33 | { nColor: "darkblue", lColor: "darkblue", nVal: 7, lWidth: 6 }, // 3 Red + Blue 34 | { nColor: "lightgrey", lColor: "lightgrey", fill: "#A0D0D6", opacity: 0.3, nVal: 4, lWidth: 0, ring: "#000000", ringOpacity: 0.6 } // 4 Multiedge 35 | ]; 36 | 37 | this.timeStyles = [ 38 | { nColor: "black", lColor: "grey", nVal: 6, lWidth: 6, bgColor: "white", nRelSize: 4 }, // 0 defaults 39 | { nColor: "purple", lColor: "hotpink", nVal: 8, lWidth: 9, fill: "hotpink", opacity: 0.2 }, // 1 Red 40 | { nColor: "blue", lColor: "deepskyblue", nVal: 8, lWidth: 9, fill: "deepskyblue", opacity: 0.2 }, // 2 Blue 41 | { nColor: "darkblue", lColor: "darkblue", nVal: 8, lWidth: 9 } // 3 Red + Blue 42 | ]; 43 | 44 | this.phaseStyles = [ 45 | { nColor: "black", lColor: "grey", nVal: 4, lWidth: 3, bgColor: "white", nRelSize: 5 }, // 0 defaults 46 | { nColor: "purple", lColor: "hotpink", nVal: 7, lWidth: 6, fill: "hotpink", opacity: 0.2 }, // 1 Red 47 | { nColor: "blue", lColor: "deepskyblue", nVal: 7, lWidth: 6, fill: "deepskyblue", opacity: 0.2 }, // 2 Blue 48 | { nColor: "darkblue", lColor: "darkblue", nVal: 7, lWidth: 6 } // 3 Red + Blue 49 | ]; 50 | 51 | // Material for hyperedges 52 | this.hyperedgematerial = new MeshBasicMaterial( { 53 | color: this.spaceStyles[4].fill, 54 | transparent: true, 55 | opacity: this.spaceStyles[4].opacity, 56 | side: DoubleSide, 57 | depthTest: false, 58 | depthWrite: false 59 | }); 60 | 61 | // Circle for unary edges 62 | let canvas = document.createElement( 'canvas' ); 63 | canvas.width = 256; 64 | canvas.height = 256; 65 | let ctx = canvas.getContext( '2d' ); 66 | ctx.lineWidth = 26; 67 | ctx.strokeStyle = "#ffffff"; 68 | ctx.beginPath(); 69 | ctx.arc( 128, 128, 96, 0, 2 * Math.PI, true ); 70 | ctx.closePath(); 71 | ctx.stroke(); 72 | let texture = new Texture( canvas ); 73 | texture.needsUpdate = true; 74 | this.circlematerial = new SpriteMaterial({ 75 | opacity: this.spaceStyles[4].ringOpacity, 76 | map: texture, 77 | color: 0xffffff, 78 | transparent: true, 79 | depthTest: false, 80 | depthWrite: false 81 | }); 82 | 83 | // Setup force graph 84 | this.FG( element ) 85 | .forceEngine('d3') 86 | .numDimensions( 3 ) 87 | .showNavInfo( false ) 88 | .enablePointerInteraction( true ) 89 | .backgroundColor( this.spaceStyles[0].bgColor ) 90 | .nodeLabel( d => `${ d.id }` ) 91 | .nodeOpacity( 0.9 ) 92 | .linkOpacity( 0.9 ) 93 | .cooldownTime( 5000 ) 94 | .nodeVisibility( true ) 95 | .linkVisibility( true ) 96 | .onEngineTick( Graph3D.onEngineTick.bind(this) ) 97 | .linkThreeObjectExtend( false ); 98 | 99 | } 100 | 101 | /** 102 | * Make ternaries of an array, used for triangulation. First point fixed. 103 | * @static 104 | * @param {number[]} arr Array of numbers 105 | * @return {number[][]} Array of ternaries of numbers. 106 | */ 107 | static triangulate( arr ) { 108 | const result = []; 109 | for ( let i = 0; i < (arr.length - 2); i++ ) result.push( [ arr[0], arr[i+1], arr[i+2] ] ); 110 | return result; 111 | } 112 | 113 | /** 114 | * Return colour gradient 115 | * @static 116 | * @param {number} grad Value from 0 to 1 117 | * @return {string} RGB colour 118 | */ 119 | static colorGradient(grad) { 120 | const low = [ 32, 255, 255 ]; // RGB 121 | const mid = [ 255, 255, 0 ]; 122 | const hi = [ 255, 32, 32 ]; 123 | 124 | let c1 = grad < 0.5 ? low : mid; 125 | let c2 = grad < 0.5 ? mid : hi; 126 | let fade = grad < 0.5 ? 2 * grad : 2 * grad - 1; 127 | 128 | let c = c1.map( (x,i) => Math.floor( x + (c2[i] - x) * fade )); 129 | return 'rgb(' + c.join(",") + ')'; 130 | } 131 | 132 | /** 133 | * Update hyperedges and hypersurfaces 134 | * @param {Object} o Link object 135 | * @param {Object} coord Start and end 136 | * @param {Object} l Link 137 | * @return {boolean} False. 138 | */ 139 | static linkPositionUpdate( o, coord, l ) { 140 | if ( !l.hyperedge ) return false; 141 | 142 | if ( l.hyperedge.length === 1 ) { 143 | Object.assign(o.position, coord.start); 144 | let f = 20 + Math.min( 20, Math.pow(l.scale,3/5) * 10 ); 145 | o.scale.set( f, f, 0 ); 146 | if ( l.hasOwnProperty("grad") ) { 147 | o.material.color.set( Graph3D.colorGradient( l.grad ) ); 148 | } else { 149 | o.material.color.set( this.spaceStyles[4].ring ); 150 | } 151 | } else { 152 | const p = o.geometry.attributes.position; 153 | let i = 0; 154 | Graph3D.triangulate( l.hyperedge ).forEach( t => { 155 | if ( t[0] !== t[1] && t[0] !== t[2] && t[1] !== t[2] ) { 156 | t.forEach( v => { 157 | p.array[ i++ ] = v.x; 158 | p.array[ i++ ] = v.y; 159 | p.array[ i++ ] = v.z; 160 | }); 161 | } 162 | }); 163 | if ( l.hasOwnProperty("grad") ) { 164 | o.material.color.set( Graph3D.colorGradient( l.grad ) ); 165 | } else { 166 | o.material.color.set( this.spaceStyles[4].fill ); 167 | } 168 | p.needsUpdate = true; 169 | } 170 | return true; 171 | } 172 | 173 | /** 174 | * Update hypersurfaces 175 | */ 176 | static onEngineTick() { 177 | this.HS.forEach( hs => { 178 | const ps = []; 179 | hs.vs.forEach( v => { 180 | if ( !isNaN(v.x) ) ps.push( new Vector3( v.x, v.y, v.z ) ); 181 | }); 182 | if ( ps.length > 3) { 183 | hs.mesh.geometry.dispose(); 184 | hs.mesh.geometry = new ConvexGeometry( ps ); 185 | hs.mesh.geometry.attributes.position.needsUpdate = true; 186 | } 187 | }); 188 | } 189 | 190 | /** 191 | * Custom link objects. 192 | * @param {Object} l Link 193 | * @return {Object} ThreeJS Object3D obj or false. 194 | */ 195 | static linkThreeObject( l ) { 196 | if ( !l.hyperedge ) return false; // Not a custom obj 197 | if ( l.hyperedge.length === 1 ) { 198 | let sprite = new Sprite( this.circlematerial.clone() ); 199 | return sprite; 200 | } else { 201 | const cs = []; 202 | Graph3D.triangulate( l.hyperedge ).forEach( t => { 203 | if ( t[0] !== t[1] && t[0] !== t[2] && t[1] !== t[2] ) { 204 | t.forEach( v => cs.push( v.x, v.y, v.z ) ); 205 | } 206 | }); 207 | if ( cs.length === 0 ) return false; // Use regular edge 208 | const geom = new BufferGeometry(); 209 | const positions = new Float32Array( cs ); 210 | const normals = new Float32Array( cs.length ); 211 | geom.setAttribute( 'position', new BufferAttribute( positions, 3 ) ); 212 | geom.setAttribute( 'normal', new BufferAttribute( normals, 3 ) ); 213 | geom.computeVertexNormals(); 214 | return new Mesh(geom, this.hyperedgematerial.clone() ); 215 | } 216 | } 217 | 218 | /** 219 | * Update 3d force directed graph size. 220 | * @param {number} width New window width 221 | * @param {number} height New window height 222 | */ 223 | size( width, height ) { 224 | this.FG.width( width ); 225 | this.FG.height( height ); 226 | } 227 | 228 | /** 229 | * Clear graph. 230 | */ 231 | clear() { 232 | // Dispose hypersurfaces 233 | this.HS.forEach( hs => { 234 | this.FG.scene().remove( hs.mesh ); 235 | hs.mesh.geometry.dispose(); 236 | hs.mesh.material.dispose(); 237 | hs.mesh = undefined; 238 | }); 239 | this.HS.length = 0; 240 | this.HL.length = 0; 241 | 242 | // Clear graph 243 | super.clear(); 244 | } 245 | 246 | /** 247 | * Reset graph and set view. 248 | * @param {number} view Mode, 1 = space, 2 = time 249 | */ 250 | reset( view ) { 251 | // Clear graph 252 | this.clear(); 253 | 254 | // Set view "SPACE" 255 | if ( view === 1 ) { 256 | this.view = 1; 257 | 258 | this.FG 259 | .numDimensions( 3 ) 260 | .dagMode( null ) 261 | .backgroundColor( this.spaceStyles[0].bgColor ) 262 | .nodeLabel( n => `${ n.id }` ) 263 | .nodeRelSize( this.spaceStyles[0].nRelSize ) 264 | .nodeVal( n => (n.big ? 8 : 1 ) * this.spaceStyles[n.style].nVal ) 265 | .nodeColor( n => (n.hasOwnProperty("grad") && !n.style) ? Graph3D.colorGradient( n.grad ) : this.spaceStyles[n.style].nColor ) 266 | .linkWidth( l => this.spaceStyles[l.style].lWidth ) 267 | .linkColor( l => (l.hasOwnProperty("grad") && !l.style) ? Graph3D.colorGradient( l.grad ) : this.spaceStyles[l.style].lColor ) 268 | .linkCurvature( 'curvature' ) 269 | .linkCurveRotation( 'rotation' ) 270 | .linkDirectionalArrowLength(0) 271 | .linkPositionUpdate( Graph3D.linkPositionUpdate.bind(this) ) 272 | .linkThreeObject( Graph3D.linkThreeObject.bind(this) ) 273 | .nodeThreeObject( null ); 274 | 275 | // Set forces 276 | this.FG.d3Force("link").iterations( 15 ); 277 | this.FG.d3Force("link").strength( l => { 278 | let refs = Math.min(l.source.refs, l.target.refs) + 1; 279 | return 1 / refs; 280 | }); 281 | this.FG.d3Force("link").distance( 50 ); 282 | this.FG.d3Force("center").strength( 1 ); 283 | this.FG.d3Force("charge").strength( -600 ); 284 | this.FG.d3Force("charge").distanceMin( 20 ); 285 | this.force(50,50); 286 | } else if ( view === 2 ) { // Set view "TIME" 287 | this.view = 2; 288 | 289 | this.FG 290 | .numDimensions( 3 ) 291 | .dagMode( "td" ) 292 | .backgroundColor( this.timeStyles[0].bgColor ) 293 | .nodeLabel( n => `${ n.id }` ) 294 | .nodeRelSize( this.timeStyles[0].nRelSize ) 295 | .nodeVal( n => (n.big ? 8 : 1 ) * this.timeStyles[n.style].nVal ) 296 | .nodeColor( n => (n.hasOwnProperty("grad") && !n.style) ? Graph3D.colorGradient( n.grad ) : this.timeStyles[n.style].nColor ) 297 | .linkWidth( l => this.timeStyles[l.style].lWidth ) 298 | .linkColor( l => (l.hasOwnProperty("grad") && !l.style) ? Graph3D.colorGradient( l.grad ) : this.timeStyles[l.style].lColor ) 299 | .linkCurvature( 0 ) 300 | .linkCurveRotation( 0 ) 301 | .linkDirectionalArrowLength( 20 ) 302 | .linkDirectionalArrowRelPos(1) 303 | .linkPositionUpdate( null ) 304 | .linkThreeObject( null ) 305 | .nodeThreeObject( null ); 306 | 307 | // Set forces 308 | this.FG.d3Force("link").iterations( 2 ); 309 | this.FG.d3Force("link").strength( l => { 310 | let refs = 4 * (Math.min(l.source.refs, l.target.refs) + 1); 311 | return 1 / refs; 312 | }); 313 | this.FG.d3Force("link").distance( 10 ); 314 | this.FG.d3Force("center").strength( 0.1 ); 315 | this.FG.d3Force("charge").strength( -200 ); 316 | this.FG.d3Force("charge").distanceMin( 1 ); 317 | this.force(50,50); 318 | } else if ( view === 3 ) { 319 | this.view = 3; 320 | 321 | this.FG 322 | .numDimensions( 3 ) 323 | .dagMode( null ) 324 | .backgroundColor( this.phaseStyles[0].bgColor ) 325 | .nodeLabel( n => `${ n.id }` ) 326 | .nodeRelSize( this.phaseStyles[0].nRelSize ) 327 | .nodeVal( n => (n.big ? 8 : 4 ) * n.refs ) 328 | .nodeColor( n => (n.hasOwnProperty("grad") && !n.style) ? Graph3D.colorGradient( n.grad ) : this.phaseStyles[n.style].nColor ) 329 | .linkWidth( l => this.phaseStyles[l.style].lWidth ) 330 | .linkColor( l => (l.hasOwnProperty("grad") && !l.style) ? Graph3D.colorGradient( l.grad ) : this.phaseStyles[l.style].lColor ) 331 | .linkCurvature( 0 ) 332 | .linkCurveRotation( 0 ) 333 | .linkDirectionalArrowLength(0) 334 | .linkPositionUpdate( null ) 335 | .linkThreeObject( null ) 336 | .nodeThreeObject( null ); 337 | 338 | // Set forces 339 | this.FG.d3Force("link").iterations( 15 ); 340 | this.FG.d3Force("link").strength( l => { 341 | let refs = (Math.min(l.source.refs, l.target.refs) + (10240-l.w)/10240); 342 | return 1 / refs; 343 | }); 344 | this.FG.d3Force("link").distance( 50 ); 345 | this.FG.d3Force("center").strength( 1 ); 346 | this.FG.d3Force("charge").strength( -600 ); 347 | this.FG.d3Force("charge").distanceMin( 20 ); 348 | } 349 | } 350 | 351 | 352 | /** 353 | * Change force dynamics. 354 | * @param {number} dist Distance 0-100 355 | * @param {number} decay Decay 0-100 356 | */ 357 | force( dist, decay ) { 358 | if ( this.view === 1 ) { 359 | if ( dist >= 0 && dist <= 100 ) { 360 | this.FG.d3Force("link").distance( dist ); 361 | this.FG.d3Force("charge").strength( -10 * (dist + 10) ); 362 | } 363 | if ( decay >=0 && decay <=100 ) { 364 | this.FG.d3VelocityDecay( decay / 100 ); 365 | } 366 | } else if ( this.view === 2 ){ 367 | if ( dist >= 0 && dist <= 100 ) { 368 | this.FG.dagLevelDistance( (dist * dist) / 2 + 1 ); 369 | this.FG.d3Force("link").distance( dist/10 ); 370 | this.FG.d3Force("charge").strength( -300 ); 371 | } 372 | if ( decay >=0 && decay <=100 ) { 373 | this.FG.d3VelocityDecay( decay / 100 ); 374 | } 375 | } else if ( this.view === 3 ) { 376 | if ( dist >= 0 && dist <= 100 ) { 377 | this.FG.d3Force("link").distance( dist ); 378 | this.FG.d3Force("charge").strength( -10 * (dist + 10) ); 379 | } 380 | if ( decay >=0 && decay <=100 ) { 381 | this.FG.d3VelocityDecay( decay / 100 ); 382 | } 383 | } 384 | } 385 | 386 | 387 | /** 388 | * Refresh graph. 389 | */ 390 | refresh() { 391 | this.FG.graphData( { nodes: this.nodes, links: this.links } ); 392 | } 393 | 394 | /** 395 | * Highlight nodes/edges. 396 | * @param {Object} subgraph Edges, nodes and points to highlight. 397 | * @param {number} style Style to use in highlighting. 398 | * @param {boolean} surface If true, fill hypersurfaces. 399 | * @param {boolean} background If false, show only highlighted nodes/edges. 400 | */ 401 | setHighlight( subgraph, style, surface = true, background = true ) { 402 | // Big Vertices 403 | subgraph['p'].forEach( id => { 404 | let v = this.V.get( id ); 405 | if ( v ) { 406 | v.big = true; 407 | v.style |= style; 408 | } 409 | }); 410 | 411 | // Vertices and hypersurfaces connecting them 412 | subgraph['v'].forEach( ids => { 413 | const vs = [], points = []; 414 | let p; 415 | ids.forEach( id => { 416 | let v = this.V.get( id ); 417 | if ( v && !vs.includes(v) ) { 418 | v.style |= style; 419 | if ( v.x ) points.push( new Vector3( v.x, v.y, v.z ) ); 420 | vs.push( v ); 421 | } 422 | }); 423 | if ( surface && points.length > 3 ) { 424 | const geom = new ConvexGeometry( points ); 425 | const mesh = new Mesh(geom, this.hyperedgematerial.clone() ); 426 | mesh.material.color.set( this.spaceStyles[style].fill ); 427 | // TODO: opaque set? 428 | this.FG.scene().add( mesh ); 429 | const hs = { 430 | vs: vs, 431 | mesh: mesh, 432 | style: style 433 | }; 434 | this.HS.push( hs ); 435 | } 436 | }); 437 | 438 | // Hyperedges 439 | subgraph['e'].forEach( es => { 440 | es.forEach( e => { 441 | let vprev = this.V.get( e[0] ); 442 | if ( vprev ) { 443 | vprev.style |= style; 444 | for( let i=1; i v.target.includes(l) ); 449 | ls.forEach( l => l.style |= style ); 450 | if ( ls.length === 0 ) { 451 | // Not found, add new link 452 | const hl = { 453 | source: vprev, target: v, 454 | style: style 455 | }; 456 | this.links.push( hl ); 457 | this.HL.push( hl ); 458 | } 459 | vprev = v; 460 | } 461 | } 462 | } 463 | }); 464 | }); 465 | 466 | // Show/hide background graph and update 467 | if ( background ) { 468 | this.FG.nodeVisibility( true ).linkVisibility( true ); 469 | } else { 470 | this.FG.nodeVisibility( "style" ).linkVisibility( "style" ); 471 | } 472 | 473 | } 474 | 475 | /** 476 | * Clear highlight style. 477 | * @param {number} style Style to be removed. 478 | */ 479 | clearHighlight( style ) { 480 | // Remove hypersurfaces 481 | for( let i=this.HS.length-1; i>=0; i-- ) { 482 | let hs = this.HS[i]; 483 | if ( hs.style & style ) { 484 | this.FG.scene().remove( hs.mesh ); 485 | hs.mesh.geometry.dispose(); 486 | hs.mesh.material.dispose(); 487 | hs.mesh = undefined; 488 | this.HS.splice(i,1); 489 | } 490 | } 491 | 492 | // Remove extra highlight links 493 | for( let i=this.HL.length-1; i>=0; i-- ) { 494 | const hl = this.HL[i]; 495 | if ( hl.style & style ) { 496 | this.links.splice( this.links.indexOf( hl ), 1); 497 | this.HL.splice(i,1); 498 | } 499 | } 500 | 501 | // Reset node styles 502 | this.nodes.forEach( n => { 503 | delete n.big; 504 | n.style &= ~style; 505 | }); 506 | 507 | // Reset link styles 508 | this.links.forEach( l => { 509 | l.style &= ~style; 510 | }); 511 | 512 | this.FG.nodeVisibility( true ).linkVisibility( true ); 513 | } 514 | 515 | /** 516 | * Set gradient colours based on field 'grad' 517 | */ 518 | setField() { 519 | this.refresh(); 520 | } 521 | 522 | /** 523 | * Clear gradient 524 | */ 525 | clearField() { 526 | // Clear nodes 527 | this.nodes.forEach( n => { 528 | delete n.grad; 529 | }); 530 | 531 | // Clear links 532 | this.links.forEach( l => { 533 | delete l.grad; 534 | }); 535 | 536 | this.refresh(); 537 | } 538 | 539 | /** 540 | * Status. 541 | * @return {Object} 542 | */ 543 | status() { 544 | return { ...super.status() }; 545 | } 546 | 547 | } 548 | 549 | export { Graph3D }; 550 | -------------------------------------------------------------------------------- /modules/Rewriter.mjs: -------------------------------------------------------------------------------- 1 | import { Rulial } from "./Rulial.mjs"; 2 | import { HyperedgeEvent } from "./HyperedgeEvent.mjs"; 3 | 4 | /** 5 | * @class Hypergraph Rewriting System. 6 | * @author Mika Suominen 7 | */ 8 | class Rewriter { 9 | 10 | /** 11 | * Rewriting rule. 12 | * @typedef {Object} Pattern 13 | */ 14 | 15 | /** 16 | * Rewriting options. 17 | * @typedef {Object} Options 18 | * @property {number} evolution Number of branches to evolve, 0 = full multiway 19 | * @property {number} interactions Combination of allowed separations; 1=spacelike, 2=timelike, 4=branchlike 20 | * @property {number} maxevents Maximum number of events 21 | * @property {number} maxsteps Maximum number of steps 22 | * @property {number} maxtokens Maximum number of tokens 23 | * @property {number} timeslot Processing unit in msec 24 | * @property {boolean} noduplicates If true, duplicate edges in rules are ignored 25 | * @property {boolean} pathcnts If true, calculate path counts 26 | * @property {boolean} bcoordinates If true, calculate branchial coordinates 27 | * @property {number} knn Number of nearest historical neighbours (k-NN) to calculate 28 | * @property {number} phasecutoff Hamming cutoff distance to consider the same 29 | * @property {boolean} deduplicate If true, de-duplicate new edges 30 | * @property {boolean} merge If true, merge identical edges 31 | * @property {boolean} wolfram If true, use Wolfram default order/reverse for branches 1/2 32 | * @property {boolean} rulendx If true, order based on rule index 33 | */ 34 | 35 | /** 36 | * Callback for rewriting progress update. 37 | * @callback progressfn 38 | * @param {numeric} eventcnt Number of events processed. 39 | */ 40 | 41 | /** 42 | * Callback for rewriting finished. 43 | * @callback finishedfn 44 | */ 45 | 46 | /** 47 | * Creates an instance of Rewriter. 48 | * @constructor 49 | */ 50 | constructor() { 51 | this.rulial = new Rulial(); // Rulial foliation 52 | this.multiway = new HyperedgeEvent(); // Multiway system 53 | this.M = []; // LHS hits as maps 54 | this.reset(); 55 | } 56 | 57 | /** 58 | * Reset instance. 59 | * @param {Options} [opt=null] 60 | */ 61 | reset( opt = null ) { 62 | opt = opt || {}; 63 | this.rulial.clear(); 64 | 65 | this.step = -1; 66 | this.M.length = 0; 67 | this.eventcnt = 0; 68 | this.opt = { 69 | evolution: 1, 70 | interactions: 5, 71 | timeslot: 250, 72 | maxsteps: Infinity, 73 | maxevents: Infinity, 74 | maxtokens: Infinity, 75 | noduplicates: false, 76 | pathcnts: true, 77 | bcoordinates: true, 78 | knn: 3, 79 | phasecutoff: 200, 80 | deduplicate: false, 81 | merge: true, 82 | wolfram: false, 83 | rulendx: false 84 | }; 85 | Object.assign( this.opt, opt ); 86 | if ( this.opt.maxevents === Infinity && 87 | this.opt.maxsteps === Infinity && 88 | this.opt.maxtokens === Infinity ) { 89 | // If no max limits set, use default limits 90 | this.opt.maxevents = (this.opt.evolution || 4) * 1000; 91 | this.opt.maxtokens = 20000; 92 | } 93 | this.multiway.clear(); 94 | 95 | this.timerid = null; // Timer 96 | this.rewritedelay = 50; // Timer delay in msec 97 | this.progressfn = null; // Callback for rewrite progress 98 | this.finishedfn = null; // Callback for rewrite finished 99 | 100 | this.progress = { // Real-time statistics about rewriting process 101 | progress: 0, 102 | step: "0", 103 | matches: "0", 104 | events: "0", 105 | post: "" 106 | }; 107 | this.interrupt = false; // If true, user stopped the rewriting process 108 | } 109 | 110 | /** 111 | * Map subgraph pattern to real subgraph using 'map'. 112 | * @param {Pattern[]} ps Patterns to map 113 | * @param {number[]} map Map from pattern to real vertices 114 | * @return {Edge[]} Real subgraph. 115 | */ 116 | mapper( ps, map ) { 117 | return ps.map( p => p.map( v => ( v < map.length ? map[v] : ( map.length - v ) - 1 ) ) ); 118 | } 119 | 120 | /** 121 | * Test whether an edge matches the given pattern. 122 | * @param {Edge} edge Hyperedge to test 123 | * @param {Edge} p Pattern to test against 124 | * @return {boolean} True if the edge matches the pattern 125 | */ 126 | isMatch( edge, p ) { 127 | if ( edge.length !== p.length ) return false; 128 | for( let i=p.length-1; i>0; i-- ) { 129 | let x = p.indexOf( p[i] ); 130 | if ( x !== i && edge[x] !== edge[i] ) return false; 131 | } 132 | return true; 133 | } 134 | 135 | /** 136 | * Get the number of of neg hits. 137 | * @param {Rule} rule 138 | * @param {number[]} map 139 | * @param {Edge[]} [ignore=null] Hit to ignore 140 | * @return {number} Number of hits 141 | */ 142 | negs( rule, map, ignore ) { 143 | let map0 = rule.negmap.slice(); 144 | map.forEach( (x,i) => map0[i] = x ); 145 | let mapsNext = [ map0 ]; 146 | 147 | for( let j = 0; j < rule.neg.length; j++ ) { 148 | let pattern = rule.neg[j]; 149 | let maps = mapsNext; 150 | mapsNext = []; 151 | 152 | // Iterate all mapping hypotheses 153 | for( let k = maps.length-1; k >= 0; k-- ) { 154 | let edges = this.multiway.find( this.mapper( [ pattern ], maps[k] )[0] ); 155 | for (let l = edges.length-1; l >= 0; l-- ) { 156 | if ( !this.isMatch( edges[l], pattern ) ) continue; 157 | let map = maps[k]; 158 | for(let n = pattern.length - 1; n >= 0; n-- ) map[ pattern[n] ] = edges[l][n]; 159 | mapsNext.push( [ ...map ] ); 160 | } 161 | } 162 | } 163 | 164 | let fullrule = [ ...rule.lhs, ...rule.neg ]; 165 | let cnt = 0; 166 | for( let k = mapsNext.length-1; k >=0; k-- ) { 167 | let hits = this.multiway.hits( this.mapper( fullrule, mapsNext[k] ), this.opt.interactions ); 168 | cnt += hits.length; 169 | 170 | // Ignore hits with tokens from 'ignore' 171 | if ( ignore ) { 172 | hits.forEach( h => { 173 | if ( h.length === ignore.length && h.every( (t,i) => t === ignore[i] ) ) { 174 | cnt--; 175 | } 176 | }); 177 | } 178 | } 179 | 180 | return cnt; 181 | } 182 | 183 | /** 184 | * Find possible mappings between rule pattern 'lhs' and the hypergraph. 185 | * @generator 186 | */ 187 | *findMatches() { 188 | // Clear previous matches 189 | this.M.length = 0; 190 | 191 | // Check each edge for hit 192 | for( let e of this.multiway.L.values() ) { 193 | let edge = e[0].edge; 194 | 195 | // Go through all the rules 196 | for( let i=0; i < this.rulial.rules.length; i++ ) { 197 | let rule = this.rulial.rules[i]; 198 | 199 | // Allowed interactions for this rule 200 | let interactions = this.opt.interactions; 201 | if ( rule.hasOwnProperty("opt") && rule.opt.length > 0 ) { 202 | if ( rule.opt === "c" ) { 203 | interactions = 5; // Completions spacelike+branchlike 204 | } else { 205 | let x = parseInt(rule.opt); 206 | if ( x > 0 && x < 7 ) interactions = x; 207 | } 208 | } 209 | 210 | // Next rule, if the edge doesn't match the rule 211 | if ( !this.isMatch( edge, rule.lhs[0] ) ) continue; 212 | 213 | // Map based on this edge 214 | let map0 = rule.lhsmap.slice(); 215 | for( let n = edge.length - 1; n >=0; n-- ) map0[ rule.lhs[0][n] ] = edge[n]; 216 | 217 | // Go through all the other parts of the lhs rule 218 | let mapsNext = [ map0 ]; 219 | let len = rule.lhs.length; 220 | for( let j = 1; j < len; j++ ) { 221 | let pattern = rule.lhs[j]; 222 | let maps = mapsNext; 223 | mapsNext = []; 224 | 225 | // Iterate all mapping hypotheses 226 | for( let k = maps.length-1; k >= 0; k-- ) { 227 | let edges = this.multiway.find( this.mapper( [ pattern ], maps[k] )[0] ); 228 | for (let l = edges.length-1; l >= 0; l-- ) { 229 | if ( !this.isMatch( edges[l], pattern ) ) continue; 230 | let map = maps[k]; 231 | for(let n = pattern.length - 1; n >= 0; n-- ) map[ pattern[n] ] = edges[l][n]; 232 | mapsNext.push( [ ...map ] ); 233 | } 234 | } 235 | } 236 | 237 | // Filter out hypotheses that match 'neg' 238 | if ( rule.hasOwnProperty("neg") ) { 239 | mapsNext = mapsNext.filter( map => !this.negs( rule, map ) ); 240 | } 241 | 242 | // Replicate according to the final results 243 | for( let k = mapsNext.length-1; k >= 0; k-- ) { 244 | let hits = this.multiway.hits( this.mapper( rule.lhs, mapsNext[k] ), interactions ); 245 | for( let l = hits.length-1; l >= 0; l-- ) { 246 | this.M.push( { 247 | hit: hits[l], 248 | map: mapsNext[k], 249 | rule: rule 250 | } ); 251 | } 252 | } 253 | } 254 | 255 | yield; 256 | } 257 | 258 | } 259 | 260 | /** 261 | * Generate random sequence of indeces. 262 | */ 263 | orderRandom() { 264 | // Shuffle matches 265 | let arr = [...Array(this.M.length)].map((_,i) => i); 266 | for (let i = arr.length - 1; i > 0; i--) { 267 | const j = Math.floor(Math.random() * (i + 1)); 268 | [arr[i], arr[j]] = [arr[j], arr[i]]; 269 | } 270 | return arr; 271 | } 272 | 273 | /** 274 | * Generate indices in the order of default Wolfram Model ordering 275 | * (LeastRecentEdge + RuleOrdering + RuleIndex) 276 | */ 277 | orderWM() { 278 | // Pre-calculate values to sort by 279 | this.M.forEach( m => { 280 | let ids = m.hit.map( (e,i) => e.id ); 281 | m.sort2 = Array.from( Array(ids.length).keys() ).sort( (x,y) => ids[x] - ids[y] ); 282 | m.sort1 = ids.sort( (a,b) => a - b ).reverse(); 283 | }); 284 | 285 | // Sort indices 286 | let arr = this.orderRandom(); 287 | arr.sort( (a,b) => { 288 | const am = this.M[a]; 289 | const bm = this.M[b]; 290 | const len = Math.min( am.sort1.length, bm.sort1.length ); 291 | for(let i = 0; i < len; i++ ) { 292 | if ( am.sort1[i] !== bm.sort1[i] ) return am.sort1[i] - bm.sort1[i]; 293 | } 294 | for(let i = 0; i < len; i++ ) { 295 | if ( am.sort2[i] !== bm.sort2[i] ) return am.sort2[i] - bm.sort2[i]; 296 | } 297 | return am.rule.id - bm.rule.id; 298 | }); 299 | return arr; 300 | } 301 | 302 | 303 | /** 304 | * Process the given rewriting rule 'lhs' 'rhs' using the given 305 | * array of mappings 'maps'. 306 | * @generator 307 | * @param {number} b Branch 2^id 308 | */ 309 | *processMatches( b ) { 310 | 311 | // Processing order 312 | let order; 313 | if ( this.opt.wolfram && b === 1 ) { 314 | order = this.orderWM(); 315 | } else if ( this.opt.wolfram && b === 2 ) { 316 | order = this.orderWM().reverse(); 317 | } else { 318 | order = this.orderRandom(); 319 | } 320 | 321 | // Rule ordering 322 | if ( this.opt.rulendx && this.rulial.rules.length > 1 ) { 323 | order.sort( (a,b) => this.M[a].rule.id - this.M[b].rule.id ); 324 | } 325 | 326 | // Process matches 327 | for( let idx of order ) { 328 | let m = this.M[idx]; 329 | 330 | // Allowed interactions for this rule 331 | let interactions = this.opt.interactions; 332 | let isCompletion = false; 333 | if ( m.rule.hasOwnProperty("opt") && m.rule.opt.length > 0 ) { 334 | if ( m.rule.opt === "c" ) { 335 | isCompletion = true; 336 | interactions = 5; // Completions between branches 337 | } else { 338 | let x = parseInt(m.rule.opt); 339 | if ( x > 0 && x < 7 ) interactions = x; 340 | } 341 | } 342 | 343 | if ( b ) { 344 | // If no duplicates set, test only with no duplicates 345 | let hit = m.hit; 346 | if ( this.opt.noduplicates ) { 347 | hit = hit.filter( (_,i) => !m.rule.lhsdup[i] ); 348 | } 349 | 350 | // None of the edges can't already be used by b 351 | let bsc = hit.map( t => t.child.reduce( (a,x) => a | x.b, 0) ); 352 | if ( bsc.some( x => x & b ) ) continue; 353 | 354 | 355 | let bsp = m.hit.map( t => t.parent.reduce( (a,x) => a | x.b, 0) ); 356 | if ( isCompletion ) { 357 | // If completion, there must be only tracked branches but several different ones 358 | if ( bsp.some( x => x === 0 ) ) continue; 359 | if ( bsp.every( (x,_,arr) => x === arr[0] ) ) continue; 360 | } else { 361 | // If branchlike allowed, one of the edges must be accessible to b 362 | // Otherwise, all the edges must be accessible to b 363 | if ( interactions & 4 ) { 364 | if ( bsp.every( x => !(x & b) ) ) continue; 365 | } else { 366 | if ( bsp.some( x => !(x & b) ) ) continue; 367 | } 368 | } 369 | 370 | // If the match have already been instantiated, reuse it 371 | if ( m.ev ) { 372 | m.ev.b |= b; 373 | continue; 374 | } 375 | } else { 376 | // Ignore completions of the tracked branches 377 | if ( isCompletion ) continue; 378 | 379 | // Ignore matches that have already been instantiated 380 | if ( m.ev ) continue; 381 | } 382 | 383 | // Edges to add 384 | // If no duplicates set, do not add duplicates 385 | let add = this.mapper( m.rule.rhs, m.map ); 386 | if ( this.opt.noduplicates ) { 387 | add = add.filter( (_,i) => !m.rule.rhsdup[i] ); 388 | } 389 | 390 | // Rewrite 391 | m.ev = this.multiway.rewrite( m.hit, add, m.rule, this.step, b ); 392 | 393 | this.eventcnt++; 394 | 395 | // Break when limit reached 396 | if ( this.eventcnt >= this.opt.maxevents || 397 | (this.multiway.T.length+this.multiway.EV.length) >= this.opt.maxtokens ) break; 398 | 399 | yield; 400 | } 401 | 402 | } 403 | 404 | /** 405 | * Post-process matches by unwriting negs and updating leafs. 406 | * @generator 407 | */ 408 | *postProcessMatches() { 409 | 410 | // Determine vertices 411 | let g = this.multiway.postProcess( { 412 | deduplicate: this.opt.deduplicate, 413 | merge: this.opt.merge 414 | }); 415 | let vs = g.next(); 416 | while( !vs.done ) { 417 | this.progress.post = vs.value || ""; 418 | yield; 419 | vs = g.next(); 420 | } 421 | 422 | // Post-process 'neg's 423 | if ( this.rulial.rules.some( x => x.hasOwnProperty("neg") ) ) { 424 | // Test for negs and remove overlapping matches 425 | const rm = []; 426 | const total = this.M.length; 427 | for( let idx = total-1; idx >= 0; idx-- ) { 428 | let m = this.M[idx]; 429 | if ( m.ev && m.rule.neg && this.negs( m.rule, m.map, m.hit ) ) { 430 | rm.push( m ); 431 | } 432 | this.progress.post = "Negs ["+ Math.floor( (total - idx) / total * 100 ) + "%]"; 433 | yield; 434 | } 435 | rm.forEach( m => { 436 | this.multiway.deleteEvent( m.ev ); 437 | delete m.ev; 438 | this.eventcnt--; 439 | }); 440 | } 441 | 442 | // Finalize multiway system 443 | g = this.multiway.postProcess( { 444 | pathcnts: this.opt.pathcnts, 445 | bcoordinates: this.opt.bcoordinates, 446 | knn: this.opt.knn, 447 | phasecutoff: this.opt.phasecutoff 448 | }); 449 | vs = g.next(); 450 | while( !vs.done ) { 451 | this.progress.post = vs.value || ""; 452 | yield; 453 | vs = g.next(); 454 | } 455 | 456 | this.progress.post = ""; 457 | } 458 | 459 | /** 460 | * Rewrite. 461 | * @generator 462 | */ 463 | *rewrite() { 464 | 465 | let start = Date.now(); 466 | do { 467 | // New step 468 | this.step++; 469 | this.progress.step = "" + this.step; 470 | this.progress.matches = "" + 0; 471 | 472 | // Find matches 473 | let g = this.findMatches(); 474 | while ( !this.interrupt && !g.next().done ) { 475 | if ( (Date.now() - start) > this.opt.timeslot ) { 476 | this.progress.matches = "" + this.M.length; 477 | yield; 478 | start = Date.now(); 479 | } 480 | } 481 | this.progress.matches = "" + this.M.length; 482 | 483 | // Break if no new hits 484 | if ( this.M.length === 0 ) break; 485 | 486 | 487 | // Pre-process multiway system 488 | this.multiway.preProcess(); 489 | 490 | // Generator(s) to process 491 | let gs = []; 492 | for( let i=0; i< (this.opt.evolution || 4); i++ ) { 493 | gs.push( this.processMatches( 1 << i ) ); 494 | } 495 | 496 | // Full multiway 497 | if ( this.opt.evolution === 0 ) { 498 | gs.push( this.processMatches( 0 ) ); 499 | } 500 | 501 | // Process matches 502 | let oldeventcnt = this.eventcnt; 503 | let vs = gs.map( x => x.next() ); 504 | while ( !this.interrupt && vs.some( x => !x.done ) ) { 505 | if ( (Date.now() - start) > this.opt.timeslot ) { 506 | this.progress.events = "" + this.eventcnt; 507 | yield; 508 | start = Date.now(); 509 | } 510 | vs = gs.map( x => x.next() ); 511 | } 512 | this.progress.events = "" + this.eventcnt; 513 | 514 | // Post-process matches 515 | g = this.postProcessMatches(); 516 | while ( !g.next().done ) { 517 | if ( (Date.now() - start) > this.opt.timeslot ) { 518 | yield; 519 | start = Date.now(); 520 | } 521 | } 522 | 523 | // Break if no new rewrites 524 | if ( this.eventcnt === oldeventcnt ) break; 525 | 526 | } while( 527 | !this.interrupt && 528 | (this.step < this.opt.maxsteps) && 529 | (this.eventcnt < this.opt.maxevents) && 530 | ((this.multiway.T.length+this.multiway.EV.length) < this.opt.maxtokens) 531 | ); 532 | } 533 | 534 | /** 535 | * Timer. 536 | * @param {Object} g Generator for rewrite 537 | */ 538 | timer(g) { 539 | if ( g.next().done ) { 540 | this.interrupt = false; 541 | if ( this.finishedfn ) { 542 | this.finishedfn(); 543 | } 544 | } else { 545 | if ( this.progressfn ) { 546 | this.progress.progress = Math.max( 547 | (this.eventcnt / this.opt.maxevents) || 0, 548 | (this.step / this.opt.maxsteps) || 0, 549 | ((this.multiway.T.length+this.multiway.EV.length) / this.opt.maxtokens) || 0 550 | ); 551 | this.progressfn( this.progress ); 552 | } 553 | this.timerid = setTimeout( this.timer.bind(this), this.rewritedelay, g ); 554 | } 555 | } 556 | 557 | /** 558 | * Run abstract rewriting rules. 559 | * @param {Rules} rulestr Rewriting rules as a string 560 | * @param {Options} opt 561 | * @param {progressfn} [progressfn=null] Progress update callback function 562 | * @param {finishedfn} [finishedfn=null] Rewriting finished callback function 563 | */ 564 | run( rulestr, opt, progressfn = null, finishedfn = null ) { 565 | 566 | // Set parameters 567 | this.reset( opt ); 568 | this.rulial.setRule( rulestr ); 569 | this.progressfn = progressfn; 570 | this.finishedfn = finishedfn; 571 | 572 | // Add initial edges for all tracked branches 573 | let b = ( 1 << ( this.opt.evolution || 4 ) ) - 1; 574 | this.rulial.initial.forEach( init => { 575 | let ev = this.multiway.rewrite( [], init.edges, null, ++this.step, init.b || b ); 576 | }); 577 | let f = this.multiway.postProcess( { 578 | deduplicate: false, 579 | merge: false, 580 | pathcnts: this.opt.pathcnts, 581 | bcoordinates: this.opt.bcoordinates, 582 | knn: this.opt.knn, 583 | phasecutoff: this.opt.phasecutoff 584 | }); // No de-duplication 585 | while ( !f.next().done ); 586 | 587 | // Start rewriting process; timeout if either of the callback fns set 588 | let g = this.rewrite(); 589 | if ( this.progressfn || this.finishedfn ) { 590 | this.timerid = setTimeout( this.timer.bind(this), this.rewritedelay, g ); 591 | } else { 592 | while ( !g.next().done ); 593 | } 594 | } 595 | 596 | /** 597 | * Cancel rewriting process. 598 | */ 599 | cancel() { 600 | this.progress.user = "Stopping..."; 601 | this.interrupt = true; 602 | } 603 | 604 | /** 605 | * Report status. 606 | */ 607 | status() { 608 | return { ...this.multiway.status() }; 609 | } 610 | 611 | } 612 | 613 | 614 | export { Rewriter }; 615 | -------------------------------------------------------------------------------- /modules/Rulial.mjs: -------------------------------------------------------------------------------- 1 | import { HDC } from "./HDC.mjs"; 2 | import { Rewriter } from "./Rewriter.mjs"; 3 | 4 | const hdc = new HDC(); 5 | 6 | /** 7 | * @class The space of all rewriting rules. 8 | * @author Mika Suominen 9 | * @author Tuomas Sorakivi 10 | */ 11 | class Rulial { 12 | 13 | /** 14 | * Creates an instance of Rulial. 15 | * @constructor 16 | */ 17 | constructor() { 18 | this.rules = []; // rewriting rules 19 | this.commands = []; // commands 20 | this.initial = []; // initial graphs 21 | } 22 | 23 | /** 24 | * Clear the Rulial for reuse. 25 | */ 26 | clear() { 27 | this.rules.length = 0; // rewriting rules 28 | this.commands.length = 0; // commands 29 | this.initial.length = 0; // initial graphs 30 | } 31 | 32 | /** 33 | * Parse command string (can be ; separated) 34 | * @static 35 | * @param {string} str 36 | * @return {Object[]} [ { cmd: "", params: ["1","2"] }, ... ] 37 | */ 38 | static parseCommands( str ) { 39 | // Extract quoted strings 40 | let req = /(["'])((?:\\\1|(?:(?!\1)).)*)(\1)/g; 41 | let q = str.match( req ) || []; 42 | let qfn = () => { return q.shift(); }; 43 | 44 | str = str.replace( req, "''" ).toLowerCase() 45 | .replace( /\}\}\,\{\{/g, "}};{{") 46 | .replace( /\{|\[/g , "(" ).replace( /}|]/g , ")" ) 47 | .replace( /(\()+/g , "(" ).replace( /(\))+/g , ")" ) 48 | .replace( /(;)+/g , ";" ).replace( /;$/g ,"" ) 49 | .replace( /[^()a-z0-9,=;>\.\-\\\/'%]+/g , "" ); 50 | 51 | let cs = []; 52 | str.split(";").forEach( s => { 53 | let x = s.match( /^([^(]+)/ ); 54 | let c = x ? x[0] : ""; 55 | let ps = [], opt = ""; 56 | if ( c === '' ) { 57 | x = s.match( /\/([^/]*)$/ ); 58 | opt = x ? x[1] : ""; 59 | if (opt.length) s = s.slice(0, -(opt.length+1) ); 60 | ps.push( s.replace( /''/g, qfn ) ); 61 | } else { 62 | x = s.match( /\/([^/]*)$/ ); 63 | opt = x ? x[1] : ""; 64 | x = s.match( /\((.*)\)/s ); 65 | if ( x && x[1].length ) { 66 | ps = x[1].split(",").map( t => t.replace( /''/g, qfn ) ); 67 | } 68 | } 69 | cs.push( { cmd: c, params: ps, opt: opt } ); 70 | }); 71 | 72 | return cs; 73 | } 74 | 75 | /** 76 | * Parse rules. 77 | * @static 78 | * @param {string[]} str Array of rule strings 79 | * @return {Object[]} [ { lhs, rhs, neg, delmask, addmask}, ... ] 80 | */ 81 | static parseRules( str ) { 82 | const rules = []; 83 | let sfn = (e) => { return e.split(","); }; 84 | let jfn = (e) => { return e.join(","); }; 85 | 86 | str.forEach( s => { 87 | let c = s.toLowerCase() 88 | .replace( /\{|\[/g , "(" ).replace( /}|]/g , ")" ) 89 | .replace( /(\()+/g , "(" ).replace( /(\))+/g , ")" ) 90 | .replace( /(;)+/g , ";" ).replace( /;$/g ,"" ) 91 | .replace( /(=)+/g , "=" ).replace( /[^()a-z0-9,=;>\\\/]+/g , "" ); 92 | 93 | let o = [ [], [], [],[] ]; // lhs, rhs, lhsneg, rhsneg 94 | let i = 0, rhs = 0, neg = 0, twoway = false; 95 | while ( i < c.length ) { 96 | switch( c[i] ) { 97 | case '(': 98 | let x = c.substring(i).match( /\(([^)]+)\)/s ); 99 | if ( x ) { 100 | o[ neg + rhs ].push(x[1]); 101 | i += x[0].length - 1; 102 | } 103 | break; 104 | case '\\': neg = 2; break; 105 | case '=': twoway = true; 106 | case '>': neg = 0; rhs = 1; 107 | } 108 | i++; 109 | } 110 | if ( rhs ) { 111 | const r = { lhs: o[0].map( sfn ),rhs: o[1].map( sfn ) }; 112 | if ( o[2].length ) r.neg = o[2].map( sfn ); 113 | rules.push( r ); 114 | if ( twoway ) { 115 | const r2 = { lhs: o[1].map( sfn ), rhs: o[0].map( sfn ) }; 116 | if ( o[3].length ) r2.neg = o[3].map( sfn ); 117 | rules.push( r2 ); 118 | } 119 | } 120 | }); 121 | 122 | // Post-process 123 | for( let i=rules.length-1; i >=0; i-- ) { 124 | let r = rules[i]; 125 | 126 | // Normalize 127 | const u = [ ...new Set( Object.values(r).flat().flat() ) ]; 128 | Object.keys(r).forEach( k => { 129 | for( let j=0; j < r[k].length; j++ ) { 130 | for( let l=0; l < r[k][j].length; l++) r[k][j][l] = u.indexOf( r[k][j][l] ); 131 | } 132 | }); 133 | 134 | // Map templates 135 | r.lhsmap = [...new Set( r.lhs.flat() )].fill( -1 ); 136 | if ( r.neg ) r.negmap = [...new Set( [...r.lhs.flat(),...r.neg.flat()])].fill( -1 ); 137 | let lhsall = r.lhs.map( e => e.join(",")); 138 | let rhsall = r.rhs.map( e => e.join(",")); 139 | r.lhsdup = lhsall.map( e => rhsall.includes(e) ); 140 | r.rhsdup = rhsall.map( e => lhsall.includes(e) ); 141 | 142 | // Calculate physical Values 143 | r.energy = r.lhs.length + r.rhs.length; // Hypothesis: energy is # of edges 144 | let maxlhsv = r.lhsmap.length - 1; 145 | let masses = r.rhs.filter( e => e.every( v => v <= maxlhsv ) ); 146 | let massratio = masses.length ? (masses.length / r.rhs.length) : 1; 147 | r.mass = r.energy * massratio; 148 | r.momentum = r.energy * ( 1 - massratio ); 149 | let lhsrevs = r.lhs.map( e => e.slice().reverse().join(",") ); 150 | r.spin = lhsrevs.reduce( (a,b) => a + rhsall.includes(b) ? 1 : 0, 0 ); 151 | 152 | // Branchial coordinate (basis) 153 | r.bc = hdc.random(); 154 | 155 | } 156 | 157 | return rules; 158 | } 159 | 160 | 161 | /** 162 | * Return euclidean distance between two points in n-dimension 163 | * @param {number} a Coordinates for point A 164 | * @param {number} b Coordinates for point B 165 | * @return {number} Distance. 166 | */ 167 | static d(a,b) { 168 | let sum = 0; 169 | for (let i = a.length - 1; i >= 0; i--) { 170 | sum += Math.pow(a[i] - b[i], 2); 171 | } 172 | return Math.sqrt(sum); 173 | } 174 | 175 | /** 176 | * Return the distance of two points on a Fibonacci sphere radius = 1 177 | * @param {number} i Index of point 1 178 | * @param {number} j Index of point 2 179 | * @param {number} n Total number of points 1 180 | * @param {number} m Total number of points 2 181 | * @return {number} Distance on a unit sphere 182 | */ 183 | static fibonacciD(i, j, n, m) { 184 | // Point on a spherical Fibonacci lattice 185 | function p(x,n) { 186 | let phi = Math.PI * (3.0 - Math.sqrt(5.0)) // golden angle in radians 187 | let y = 1 - ( x / (n-1)) * 2; 188 | let d = Math.sqrt( 1 - y*y ); 189 | return [ Math.cos(phi*x)*d, y ,Math.sin(phi*x)*d ]; 190 | } 191 | return Rulial.d( p(i,n), p(j,m) ); 192 | } 193 | 194 | 195 | /** 196 | * Creates a graph from sprinkled points in n-dimensional euclidean space 197 | * @param {number[][]} manifold Array of point coordinates 198 | * @param {number} distance Distance limit 199 | * @return {number[][]} Edges 200 | */ 201 | manifoldToGraph( manifold, distance = 1.01 ) { 202 | let edges = []; 203 | for ( let i = manifold.length-1; i>0; i--) { 204 | for( let j = i-1; j>=0; j-- ) { 205 | if ( Math.abs( Rulial.d( manifold[i], manifold[j] )) < distance ) { 206 | // Random direction of the edge 207 | Math.random() > 0.5 ? edges.push( [i,j] ) : edges.push( [j,i] ); 208 | } 209 | } 210 | } 211 | return edges; 212 | } 213 | 214 | /** 215 | * Produces a random sprinkling of points 216 | * @param {number} n Number of vertices 217 | * @return {number[][]} Edges 218 | */ 219 | points( n ) { 220 | if ( (n < 1) || (n > 10000) ) 221 | throw new RangeError("[Points] Number of points must be between 1-10000."); 222 | return new Array(n).fill().map((_,i) => [i]); 223 | } 224 | 225 | /** 226 | * Produces a random sprinkling of points into a flat grid of 227 | * given dimension and number of vertices. 228 | * @param {number[]} ls Arrays of edge sizes [ dx, dy, dz, ... ] 229 | * @return {number[][]} Edges 230 | */ 231 | grid( ls ) { 232 | let n = ls.reduce( (a,b) => a * b, 1); 233 | if ( (n < 1) || (n > 10000) ) 234 | throw new RangeError("[Grid] Number of points must be between 1-10000."); 235 | 236 | let sizes = ls.reduce( (a,x,i,arr) => { 237 | a.push( i ? a[i-1]*arr[i-1] : 1 ); 238 | return a; 239 | }, []); 240 | 241 | let manifold = Array(n).fill().map( (_,i) => { 242 | return sizes.map( (s,j) => Math.floor( i / s ) % ls[j] ); 243 | }); 244 | 245 | return this.manifoldToGraph( manifold ); 246 | } 247 | 248 | /** 249 | * Produces a random graph of n vertices using sprinkling 250 | * @param {number} n Number of vertices 251 | * @param {number} dimension Mininum number of edges per vertix 252 | * @param {number} connections Mininum number of edges per vertix 253 | * @param {number} mode Mode, 0=n-cube, 1=n-ball 254 | * @param {number} exp If TRUE use exponential distribution 255 | * @return {number[][]} Edges 256 | */ 257 | random( n, dimension, connections ) { 258 | if ( (n < 10) || (n > 1000) ) 259 | throw new RangeError("[Random] Number of points must be between 10-1000."); 260 | if ( (dimension < 1) || (dimension > 20) ) 261 | throw new RangeError("[Random] Dimension must be between 1-20."); 262 | if ( (connections < 1) || (connections > 100) ) 263 | throw new RangeError("[Random] Connections must be between 1-100."); 264 | 265 | // Sprinkling 266 | let points = []; 267 | let alledges = []; 268 | let origo = Array( dimension ).fill(0); 269 | for ( let i = 0; i < n; i++ ) { 270 | // Random point; if mode==1 filter out points outside the n-ball 271 | let point = Array( dimension ).fill().map(() => 2 * Math.random() - 1 ); 272 | points.push( point ); 273 | 274 | // All possible edges using random direction 275 | for ( let j = 0; j < i; j++ ) { 276 | let dist = Rulial.d( points[i], points[j] ); 277 | let edge = Math.random() > 0.5 ? [i,j] : [j,i]; // Random direction 278 | alledges.push( { edge: edge, dist: dist } ); 279 | } 280 | } 281 | 282 | // Sort, min length first 283 | alledges.sort( (a,b) => a.dist - b.dist ); 284 | 285 | // Ensure all vertices get at least two links 286 | let linkcnt = Array(n).fill(0); 287 | let edges = []; 288 | for ( let i = 0; i < alledges.length; i++ ) { 289 | let a = alledges[i].edge[0]; 290 | let b = alledges[i].edge[1]; 291 | if ( (linkcnt[ a ] < connections ) && (linkcnt[ b ] < connections ) ) { 292 | linkcnt[ a ]++; linkcnt[ b ]++; 293 | edges.push( [ a,b ]); 294 | } 295 | } 296 | 297 | return edges; 298 | } 299 | 300 | /** 301 | * Produces a complete graph with n vertices 302 | * @param {number} n Number of vertices 303 | * @param {number} d The number of egdes between vertices 304 | * @param {number} surface If true, connect only the surface 305 | * @return {number[][]} Edges 306 | */ 307 | complete( n, d = 1, surface = false ) { 308 | let fibDistRef = 0.0; // Reference distance on a Fibonacci sphere 309 | if ( surface ) { 310 | fibDistRef = 1.1 * Rulial.fibonacciD(1,2,n,n); 311 | if ( (n < 5) || (n > 1000) ) 312 | throw new RangeError("[Sphere] Number of vertices must be between 5-1000."); 313 | } else { 314 | if ( (n < 1) || (n > 100) ) 315 | throw new RangeError("[Complete] Number of vertices must be between 1-100."); 316 | } 317 | 318 | let edges = []; 319 | let v = n; 320 | for( let i=0; i fibDistRef ) continue; 324 | 325 | // Random direction of the edge 326 | let [ a,b ] = Math.random() > 0.5 ? [i,j] : [j,i]; 327 | 328 | let c = [ a, ...Array(d-1).fill().map( x => v++ ), b ]; 329 | for( let k = 0; k < c.length-1; k++ ) { 330 | edges.push( [ c[k], c[k+1 ] ] ); 331 | } 332 | } 333 | } 334 | 335 | return edges; 336 | } 337 | 338 | /** 339 | * Produces initial graph from rule. 340 | * @param {string} str Rule string 341 | * @param {number} n Maximum number of events. 342 | * @return {number[][]} Edges 343 | */ 344 | rule( str, n ) { 345 | if ( (n < 10) || (n > 1000) ) 346 | throw new RangeError("[Rule] Max # of events must be between 10-1000."); 347 | 348 | // Run the given rule in singleway spacelike mode for n events 349 | let h = new Rewriter(); 350 | h.run( str, { 351 | evolution: 1, 352 | interactions: 1, 353 | maxevents: n, 354 | deduplicate: false, 355 | merge: false, 356 | pathcnts: false, 357 | bcoordinates: false, 358 | knn: 0 359 | }); 360 | let leafs = h.multiway.T.filter( t => t.child.length === 0 ).map( t => t.edge ); 361 | 362 | return leafs; 363 | } 364 | 365 | /** 366 | * Parse rule/command from a string. 367 | * @param {string} str Rule/command string 368 | */ 369 | setRule( str ) { 370 | // Reset 371 | this.clear(); 372 | 373 | // Check if empty 374 | if ( str.length === 0 ) throw new RangeError("Given rule is empty."); 375 | 376 | // Parse commands and rules 377 | [ this.rules, this.commands ] = Rulial.parseCommands( str ).reduce( (a,b) => { 378 | if ( b.cmd === '' ) { 379 | let r = Rulial.parseRules( b.params ); 380 | if ( r.length ) { 381 | r.forEach( x => x.opt = b.opt ); 382 | a[0].push( ...r ); // rule 383 | } else { 384 | a[1].push(b); // initial state 385 | } 386 | } else { 387 | a[1].push(b); // command 388 | } 389 | return a; 390 | }, [[],[]]); 391 | 392 | // Set ids 393 | this.rules.forEach( (r,i) => r.id = i ); 394 | 395 | // Process commands 396 | let idx = this.commands.findIndex( x => x.cmd === "prerun" ); 397 | if ( idx !== -1 ) { 398 | // Process pre-run and other commands with it 399 | let c = this.commands[idx]; 400 | let cmd = c.cmd; 401 | let p = c.params; 402 | if ( p.length < 1 ) throw new TypeError("Prerun: Invalid number of parameters."); 403 | let branch = c.opt.length ? parseInt(c.opt) : 0; 404 | if ( branch < 0 || branch > 16 ) throw new RangeError("Option '/': Branch must be between 0-16."); 405 | let rule = this.getRule(";"); 406 | rule = rule.replace( /;prerun[^;]+/g, "" ); // filter out to avoid recursion 407 | let edges = this.rule( rule, parseInt(p[0]) || 10 ); 408 | this.initial.push( { edges: edges, b: branch } ); 409 | } else { 410 | // Process commands 411 | let negedges = []; // Edges to be removed from the final set 412 | this.commands.forEach( c => { 413 | let cmd = c.cmd; 414 | let p = c.params; 415 | let branch = c.opt.length ? parseInt(c.opt) : 0; 416 | if ( branch < 0 || branch > 16 ) throw new RangeError("Option '/': Branch must be between 0-16."); 417 | let edges = []; 418 | 419 | switch( cmd ) { 420 | case "": // initial graph 421 | let [pos,neg] = p[0].split("\\"); 422 | if ( pos ) { 423 | edges = pos.split("(").map( p => [ ...p.replace( /[^-a-z0-9,\.]+/g, "" ).split(",") ] ).slice(1); 424 | } 425 | if ( neg ) { 426 | negedges.push( { edges: neg.split("(").map( p => p.replace( /[^-a-z0-9,\.]+/g, "" ) ).slice(1), b: branch } ); 427 | } 428 | break; 429 | case "rule": 430 | if ( p.length < 2 ) throw new TypeError("Rule: Invalid number of parameters."); 431 | edges = this.rule( p[0].slice(1, -1) || "", parseInt(p[1]) || 10 ); 432 | break; 433 | case "points": 434 | if ( p.length < 1 ) throw new TypeError("Points: Invalid number of parameters."); 435 | edges = this.points( parseInt(p[0]) || 1 ); 436 | break; 437 | case "line": 438 | if ( p.length < 1 ) throw new TypeError("Line: Invalid number of parameters."); 439 | edges = this.grid( [ parseInt(p[0]) || 1 ] ); 440 | break; 441 | case "grid": case "ngrid": 442 | if ( p.length < 1 ) throw new TypeError("Grid: Invalid number of parameters."); 443 | edges = this.grid( p.map( x => parseInt(x) ).filter(Boolean) ); 444 | break; 445 | case "sphere": 446 | if ( p.length < 1 ) throw new TypeError("Sphere: Invalid number of parameters."); 447 | edges = this.complete( parseInt(p[0]) || 10, 1, true ); 448 | break; 449 | case "random": 450 | if ( p.length < 3 ) throw new TypeError("Random: Invalid number of parameters."); 451 | edges = this.random( parseInt(p[0]) || 10, parseInt(p[1]) || 3, parseInt(p[2]) || 3 ); 452 | break; 453 | case "complete": 454 | if ( p.length < 1 ) throw new TypeError("Complete: Invalid number of parameters."); 455 | edges = this.complete( parseInt(p[0]) || 10 ); 456 | break; 457 | default: 458 | throw new TypeError( "Unknown command: " + c.cmd ); 459 | } 460 | 461 | // If oneway option specified, sort edges 462 | if ( p.includes("oneway") ) { 463 | edges = edges.map( x => x.sort( (a,b) => a - b ) ); 464 | } 465 | 466 | // If twoway option specified, make each edge a two-way edge 467 | if ( p.includes("twoway") ) { 468 | edges = edges.map( x => [x,x.slice().reverse()] ).flat(); 469 | } 470 | 471 | // If inverse option specified, invert each edge 472 | if ( p.includes("inverse") ) { 473 | edges = edges.map( x => x.reverse() ); 474 | } 475 | 476 | this.initial.push( { edges: edges, b: branch } ); 477 | }); 478 | 479 | // Remove negative edges 480 | negedges.forEach( x => { 481 | this.initial.forEach( y => { 482 | if ( x.b === 0 || y.b === 0 || (x.b & y.b) ) { 483 | y.edges = y.edges.filter( z => !x.edges.includes( z.join(",") ) ); 484 | } 485 | }) 486 | }); 487 | 488 | } 489 | 490 | // Use first lhs as the initial state, if not specified 491 | // Note: replace all vertices with pattern 1, e.g. (1,2) => (1,1) 492 | if ( this.rules.length && this.initial.length === 0 ) { 493 | let edges = this.rules[0].lhs.map( e => e.map( x => 1 )); 494 | this.initial.push( { edges: edges, b: 0 } ); 495 | } 496 | 497 | // Normalize initial states 498 | let u = []; 499 | this.initial.forEach( i => { 500 | let v = i.edges.flat().sort((a,b) => a-b ).map(String); 501 | u = [ ...new Set( [...u, ...v ] ) ] 502 | i.edges = i.edges.map( e => e.map(String) ); 503 | }); 504 | this.initial.forEach( i => { 505 | i.edges = i.edges.map( e => e.map( x => u.indexOf( x ) ) ); 506 | }) 507 | 508 | if ( this.initial.length === 0 ) { 509 | throw new TypeError("Parsing the rule failed."); 510 | } 511 | 512 | } 513 | 514 | /** 515 | * Return rule as a string. 516 | * @param {string} [sep="
"] Separator for rules 517 | * @return {string} Rule as a string. 518 | */ 519 | getRule( sep = "
" ) { 520 | let s = ""; 521 | let ef = (e) => { return s += "(" + e.map( v => v + 1 ).join(",") + ")"; }; 522 | 523 | // Rules 524 | this.rules.forEach( r => { 525 | if ( s.length ) s += sep; 526 | r.lhs.forEach( ef ); 527 | if ( r.hasOwnProperty("neg") && r.neg.length > 0 ) { 528 | s += "\\"; 529 | r.neg.forEach( ef ); 530 | } 531 | s += "->"; 532 | if ( r.rhs.length === 0 ) { 533 | s += "()"; 534 | } else { 535 | r.rhs.forEach( ef ); 536 | } 537 | if ( r.opt && r.opt.length > 0 ) s += "/" + r.opt; 538 | }); 539 | 540 | // Commands and initial graphs 541 | this.commands.forEach( c => { 542 | if ( s.length ) s += sep; 543 | s += (c.cmd === "") ? c.params : (c.cmd + "(" + c.params.join(",") + ")"); 544 | if ( c.opt && c.opt.length > 0 ) s += "/" + c.opt; 545 | }); 546 | 547 | return s; 548 | } 549 | 550 | } 551 | 552 | export { Rulial }; 553 | -------------------------------------------------------------------------------- /modules/HDC.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2025 Mika Suominen 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | 26 | /** 27 | * Hyperdimensional computing using dense binary hypervectors. 28 | * 29 | * @class 30 | * @author Mika Suominen 31 | * 32 | * The general idea of HDC is based on Pentti Kanerva's work on 33 | * Hyperdimensional computing [1]. 34 | * 35 | * [1] Kanerva, P. (2019). Computing with High-Dimensional Vectors. 36 | * IEEE Design & Test, 36(3):7–14. 37 | */ 38 | 39 | class HDC { 40 | 41 | /** 42 | * @typedef {Uint32Array} Hypervector Dense binary hypervector 43 | */ 44 | 45 | /** 46 | * Creates an instance of the class. 47 | * 48 | * @constructor 49 | * @param {number} [dimension=10240] Dimension of the hypervectors, must be a multiple of 32. 50 | * @param {number} [checks=false] If true, checks all the input parameters. 51 | */ 52 | constructor(dimension=10240, checks=false) { 53 | 54 | // Dimension and Uint32Array size 55 | this.dimension = dimension; 56 | if (this.dimension < 32 || (this.dimension % 32 !== 0) ) { 57 | throw new Error('Dimension must a multiple of 32.'); 58 | } 59 | this.arraySize = this.dimension / 32; 60 | 61 | // Checks 62 | this.isChecks = !!checks; 63 | 64 | // Check for crypto and Buffer 65 | this.isCrypto = !!globalThis.crypto; 66 | this.isBuffer = typeof Buffer !== 'undefined'; 67 | 68 | // Precompute look-up tables 69 | this.bitcountLookup = new Uint8Array(256); 70 | this.b64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 71 | this.b64Lookup = new Uint8Array(256); 72 | this.binarystringLookup = new Array(256); 73 | for( let i = 0; i < 256; i++ ) { 74 | this.bitcountLookup[i] = (i & 1) + this.bitcountLookup[i >> 1]; 75 | this.b64Lookup[this.b64Chars.charCodeAt(i)] = i; 76 | this.binarystringLookup[i] = i.toString(2).padStart(8, '0'); 77 | } 78 | 79 | } 80 | 81 | /** 82 | * Check whether the parameter is a valid hypervector 83 | * with correct dimension. 84 | * 85 | * @param {Hypervector} v Hypervector candidate 86 | * @return {boolean} If true, this is a valid hypervector. 87 | */ 88 | isHypervector(v) { 89 | return (v instanceof Uint32Array && v.length === this.arraySize); 90 | } 91 | 92 | /** 93 | * Check if two hypervectors are equal. 94 | * 95 | * @param {Hypervector} v1 Hypervector 1 96 | * @param {Hypervector} v2 Hypervector 1 97 | * @return {boolean} If true, hypervectors are equal. 98 | */ 99 | isEqual(v1, v2) { 100 | if ( this.isChecks ) { 101 | if ( !this.isHypervector(v1) ) { 102 | throw new Error('First parameter is not a valid hypervector.'); 103 | } 104 | if ( !this.isHypervector(v2) ) { 105 | throw new Error('Second parameter is not a valid hypervector.'); 106 | } 107 | } 108 | 109 | if (v1 === v2) return true; // same reference 110 | const N = this.arraySize; 111 | for (let i = 0; i < N; i++) { 112 | if (v1[i] !== v1[i]) return false; 113 | } 114 | return true; 115 | } 116 | 117 | /** 118 | * Clones the hypervector. 119 | * 120 | * @param {Hypervector} v 121 | * @param {Hypervector} [r=null] Pre-allocated result vector. 122 | * @return {Hypervector} 123 | */ 124 | copy(v, r=null) { 125 | if ( this.isChecks ) { 126 | if ( !this.isHypervector(v) ) { 127 | throw new Error('Not a valid hypervector.'); 128 | } 129 | if ( r && !this.isHypervector(r) ) { 130 | throw new Error('Result is not a valid hypervector.'); 131 | } 132 | } 133 | 134 | if ( r ) { 135 | r.set(v); 136 | return r; 137 | } else { 138 | return new Uint32Array(v); 139 | } 140 | } 141 | 142 | /** 143 | * Create a new empty hypervector (all zeros). 144 | * 145 | * @return {Hypervector} 146 | */ 147 | empty() { 148 | return new Uint32Array(this.arraySize); 149 | } 150 | 151 | /** 152 | * Invert hypervector. 153 | * 154 | * @param {Hypervector} v 155 | * @param {Hypervector} [r=null] Pre-allocated result vector. 156 | * @return {Hypervector} Inverted hypervector. 157 | */ 158 | invert(v, r=null) { 159 | if ( this.isChecks ) { 160 | if ( !this.isHypervector(v) ) { 161 | throw new Error('Not a valid hypervector.'); 162 | } 163 | if ( r && !this.isHypervector(r) ) { 164 | throw new Error('Result is not a valid hypervector.'); 165 | } 166 | } 167 | 168 | const N = this.arraySize; 169 | const w = r || new Uint32Array(N); 170 | for (let i = 0; i < N; i++) w[i] = ~v[i]; 171 | return w; 172 | } 173 | 174 | /** 175 | * Create a new random hypervector. 176 | * 177 | * @param {Hypervector} [r=null] Pre-allocated result vector. 178 | * @return {Hypervector} 179 | */ 180 | random(r=null) { 181 | if ( this.isChecks ) { 182 | if ( r && !this.isHypervector(r) ) { 183 | throw new Error('Result is not a valid hypervector.'); 184 | } 185 | } 186 | 187 | const N = this.arraySize; 188 | const v = r || new Uint32Array(N); 189 | 190 | if ( this.isCrypto ) { 191 | globalThis.crypto.getRandomValues(v); 192 | } else { 193 | for (let i = 0; i < N; i++) v[i] = Math.random() * 0x100000000; 194 | } 195 | return v; 196 | } 197 | 198 | /** 199 | * Performs a bitwise XOR across TWO hypervectors. 200 | * 201 | * In HDC, XOR corresponds to binding, which combines 202 | * two hypervectors into a new one. Binding is invertible 203 | * (xor(a, xor(a, b)) = b). 204 | * 205 | * @param {Hypervector} v1 Hypervector 1 206 | * @param {Hypervector} v2 Hypervector 2 207 | * @param {Hypervector} [r=null] Pre-allocated result vector. 208 | * @return {Hypervector} Result hypervector. 209 | */ 210 | xor(v1, v2, r=null) { 211 | if ( this.isChecks ) { 212 | if ( !this.isHypervector(v1) ) { 213 | throw new Error('First parameter is not a valid hypervector.'); 214 | } 215 | if ( !this.isHypervector(v2) ) { 216 | throw new Error('Second parameter is not a valid hypervector.'); 217 | } 218 | if ( r && !this.isHypervector(r) ) { 219 | throw new Error('Result is not a valid hypervector.'); 220 | } 221 | } 222 | 223 | // Result 224 | const N = this.arraySize; 225 | const w = r || new Uint32Array(v1); 226 | 227 | for (let j = 0; j < N; j+=4) { 228 | 229 | // Chunked for efficiency 230 | w[j] ^= v2[j]; 231 | w[j + 1] ^= v2[j + 1]; 232 | w[j + 2] ^= v2[j + 2]; 233 | w[j + 3] ^= v2[j + 3]; 234 | 235 | } 236 | 237 | return w; 238 | } 239 | 240 | 241 | /** 242 | * Performs a bitwise XOR across MULTIPLE hypervectors. 243 | * 244 | * @param {Hypervector[]} vs Array of hypervectors. 245 | * @param {Hypervector} [r=null] Pre-allocated result vector. 246 | * @return {Hypervector} Result hypervector. 247 | */ 248 | xor2(vs, r=null) { 249 | if ( this.isChecks ) { 250 | if ( !Array.isArray(vs) ) { 251 | throw new Error('Parameter is not an array.'); 252 | } 253 | if ( !vs.length ) { 254 | throw new Error('Parameter is an empty array.'); 255 | } 256 | if ( vs.some( x => !this.isHypervector(x) ) ) { 257 | throw new Error('Parameter includes an element that is not a valid hypervector.'); 258 | } 259 | if ( r && !this.isHypervector(r) ) { 260 | throw new Error('Result is not a valid hypervector.'); 261 | } 262 | } 263 | 264 | // Result 265 | const n = vs.length; 266 | const N = this.arraySize; 267 | let w; 268 | if ( r ) { 269 | r.set(vs[0]); 270 | w = r; 271 | } else { 272 | w = new Uint32Array(vs[0]); 273 | } 274 | 275 | for (let i = 1; i < n; i++) { 276 | const v = vs[i]; 277 | for (let j = 0; j < N; j+=4) { 278 | 279 | // Chunked for efficiency 280 | w[j] ^= v[j]; 281 | w[j + 1] ^= v[j + 1]; 282 | w[j + 2] ^= v[j + 2]; 283 | w[j + 3] ^= v[j + 3]; 284 | 285 | } 286 | } 287 | 288 | return w; 289 | } 290 | 291 | /** 292 | * Computes the bitwise majority across multiple hypervectors. 293 | * 294 | * In HDC, MAJ represents bundling (or superposition) that 295 | * combines multiple hypervectors into a single prototype. 296 | * Each bit is set to the most common value among inputs. 297 | * 298 | * @param {Hypervector[]} vs Array of hypervectors 299 | * @param {Hypervector} [r=null] Pre-allocated result vector 300 | * @return {Hypervector} Result hypervector. 301 | */ 302 | maj(vs, r=null) { 303 | if ( this.isChecks ) { 304 | if ( !Array.isArray(vs) ) { 305 | throw new Error('Parameter is not an array'); 306 | } 307 | 308 | if ( !vs.length ) { 309 | throw new Error('Parameter is an empty array.'); 310 | } 311 | 312 | if ( vs.some( x => !this.isHypervector(x) ) ) { 313 | throw new Error('Parameter includes an element that is not a valid hypervector.'); 314 | } 315 | 316 | if ( r && !this.isHypervector(r) ) { 317 | throw new Error('Result is not a valid hypervector.'); 318 | } 319 | } 320 | 321 | const n = vs.length; 322 | const N = this.arraySize; 323 | const w = r || new Uint32Array(N); 324 | 325 | // Solve ties with a random hypervector, is even number of elements 326 | const isEven = (n % 2 === 0); 327 | let tieBreaker; 328 | if ( isEven ) { 329 | tieBreaker = this.random(); 330 | } 331 | 332 | // Threshold planes 333 | const threshold = n >> 1; // Integer division by 2 334 | const numPlanes = Math.ceil(Math.log2(n + 1)); // Number of planes 335 | const ts = new Uint32Array(numPlanes); 336 | for (let k = 0; k < numPlanes; k++) { 337 | if (threshold & (1 << k)) ts[k] = 0xFFFFFFFF; 338 | } 339 | 340 | // Calculate majority for each word 341 | for (let j = 0; j < N; j++) { 342 | 343 | // Bit-parallel addition 344 | let ps = new Uint32Array(numPlanes); 345 | for (let i = 0; i < n; i++) { 346 | let c = vs[i][j]; 347 | let cPrev = c; 348 | for( let k=0; k < numPlanes; k++ ) { 349 | c = ps[k] & cPrev; 350 | ps[k] ^= cPrev; 351 | if (c === 0) break; 352 | cPrev = c; 353 | } 354 | } 355 | 356 | // Bit-parallel comparison 357 | let gt = 0; // Mask of bits where sum > threshold 358 | let eq = 0xFFFFFFFF; // Mask of bits still equal so far 359 | 360 | for (let k = numPlanes - 1; k >= 0; k--) { 361 | const p = ps[k]; 362 | const t = ts[k]; 363 | gt |= (p & eq) & ~t; // Bits greater than threshold at this plane 364 | eq &= ~(p ^ t); // Bits equal at this plane 365 | if (eq === 0) break; 366 | } 367 | 368 | // Finalize per-bit majority result 369 | w[j] = gt >>> 0; 370 | if ( isEven && eq ) { 371 | w[j] |= eq & tieBreaker[j]; 372 | } 373 | 374 | } 375 | 376 | return w; 377 | } 378 | 379 | 380 | /** 381 | * Performs a cyclic rotation (permutation) of the bits in a hypervector. 382 | * 383 | * In HDC, ROT corresponds to the permutation operation, often used 384 | * to encode order or positional information in sequences. 385 | * 386 | * @param {Hypervector} v 387 | * @param {number} [shift=1] Number of rotations to LEFT, negative to RIGHT 388 | * @param {Hypervector} [r=null] Pre-allocated result vector 389 | * @return {Hypervector} Rotated hypervector. 390 | */ 391 | rot( v, shift=1, r=null ) { 392 | if ( this.isChecks ) { 393 | if ( !this.isHypervector(v) ) { 394 | throw new Error('Not a valid hypervector.'); 395 | } 396 | 397 | if ( !Number.isInteger(shift) ) { 398 | throw new Error('Rotation must an integer value'); 399 | } 400 | 401 | if ( r && !this.isHypervector(r) ) { 402 | throw new Error('Result is not a valid hypervector.'); 403 | } 404 | } 405 | 406 | // Result 407 | const w = r || new Uint32Array(this.arraySize); 408 | 409 | // Normalize shift to the vector's total bit length 410 | shift = ((shift % this.dimension) + this.dimension) % this.dimension; // make 0..totalBits-1 411 | 412 | // No rotation 413 | if (shift === 0) { 414 | w.set(v); 415 | return w; 416 | } 417 | 418 | // Calculate how many full words and leftover bits to rotate 419 | const N = this.arraySize; 420 | const wordN = (shift >>> 5) % N; 421 | const bitN = shift & 31; 422 | const invN = 32 - bitN; 423 | 424 | let src1 = wordN; 425 | let src2 = (src1 + 1) % N; 426 | 427 | // If bitN = 0, we only rotate by whole words (faster path) 428 | if (bitN === 0) { 429 | for (let i = 0; i < N; i += 4) { 430 | w[i] = v[src1]; 431 | if (++src1 === N) src1 = 0; 432 | w[i + 1] = v[src1]; 433 | if (++src1 === N) src1 = 0; 434 | w[i + 2] = v[src1]; 435 | if (++src1 === N) src1 = 0; 436 | w[i + 3] = v[src1]; 437 | if (++src1 === N) src1 = 0; 438 | } 439 | return w; 440 | } 441 | 442 | // General case: combine bits across word boundaries 443 | for (let i = 0; i < N; i += 4) { 444 | w[i] = (v[src1] << bitN) | (v[src2] >>> invN); 445 | if (++src1 === N) src1 = 0; 446 | if (++src2 === N) src2 = 0; 447 | 448 | w[i + 1] = (v[src1] << bitN) | (v[src2] >>> invN); 449 | if (++src1 === N) src1 = 0; 450 | if (++src2 === N) src2 = 0; 451 | 452 | w[i + 2] = (v[src1] << bitN) | (v[src2] >>> invN); 453 | if (++src1 === N) src1 = 0; 454 | if (++src2 === N) src2 = 0; 455 | 456 | w[i + 3] = (v[src1] << bitN) | (v[src2] >>> invN); 457 | if (++src1 === N) src1 = 0; 458 | if (++src2 === N) src2 = 0; 459 | } 460 | 461 | return w; 462 | 463 | } 464 | 465 | /** 466 | * Computes the Hamming distance between TWO binary hypervectors. 467 | * 468 | * In HDC, D serves as the similarity metric (distance) used 469 | * for classification and retrieval. A smaller distance implies 470 | * higher similarity. 471 | * 472 | * @param {Hypervector} v1 Hypervector 1 473 | * @param {Hypervector} v2 Hypervector 2 474 | * @return {number} Distance. 475 | */ 476 | d( v1, v2 ) { 477 | if ( this.isChecks ) { 478 | if ( !this.isHypervector(v1) ) { 479 | throw new Error('First parameter is not a valid hypervector.'); 480 | } 481 | if ( !this.isHypervector(v2) ) { 482 | throw new Error('Second parameter is not a valid hypervector.'); 483 | } 484 | } 485 | 486 | const N = this.arraySize; 487 | const p = this.bitcountLookup; 488 | let hdist = 0; 489 | for( let i=0; i>> 8) & 0xFF] + p[(x >>> 16) & 0xFF] + p[(x >>> 24)]; 492 | } 493 | 494 | return hdist; 495 | } 496 | 497 | /** 498 | * Computes the Hamming distance between MULTIPLE binary hypervectors. 499 | * 500 | * @param {Hypervector} v Hypervector 501 | * @param {Hypervector[]} vs Array of hypervectors 502 | * @param {number[]} [rs=null] Pre-allocated result array 503 | * @return {number[]} Distances. 504 | */ 505 | d2( v, vs, rs=null ) { 506 | if ( this.isChecks ) { 507 | if ( !this.isHypervector(v) ) { 508 | throw new Error('First paramter is not a valid hypervector.'); 509 | } 510 | 511 | if ( !Array.isArray(vs) ) { 512 | throw new Error('Second parameter is not an array.'); 513 | } 514 | 515 | if ( !vs.length ) { 516 | throw new Error('Second parameter is an empty array.'); 517 | } 518 | 519 | if ( vs.some( x => !this.isHypervector(x) ) ) { 520 | throw new Error('Second parameter includes an element that is not a valid hypervector.'); 521 | } 522 | 523 | if ( rs ) { 524 | if ( !Array.isArray(rs) ) { 525 | throw new Error('Result is not an array.'); 526 | } 527 | if ( rs.length !== vs.length ) { 528 | throw new Error('Result array is not the right size.'); 529 | } 530 | } 531 | } 532 | 533 | const n = vs.length; 534 | const N = this.arraySize; 535 | const p = this.bitcountLookup; 536 | const r = rs || new Array(n); 537 | 538 | for (let j=0; j>> 8) & 0xFF] + p[(x >>> 16) & 0xFF] + p[(x >>> 24)]; 544 | } 545 | r[j] = hdist; 546 | } 547 | 548 | return r; 549 | } 550 | 551 | /** 552 | * Get bit value. 553 | * 554 | * @param {Hypervector} v Hypervector 555 | * @param {number} index Index 556 | * @return {boolean} If true, bit is set 557 | */ 558 | getBit(v, index) { 559 | if ( this.isChecks ) { 560 | if ( !this.isHypervector(v) ) { 561 | throw new Error('Not a valid hypervector.'); 562 | } 563 | } 564 | 565 | const elementIndex = Math.floor(index / 32); 566 | const bitIndex = 31 - (index % 32); 567 | 568 | if (elementIndex >= v.length) { 569 | throw new RangeError("Index out of range"); 570 | } 571 | 572 | return (v[elementIndex] & (1 << bitIndex)) !== 0; 573 | } 574 | 575 | /** 576 | * Set bit value. 577 | * 578 | * @param {Hypervector} v Hypervector 579 | * @param {number} index Index 580 | * @param {boolean} value Value 581 | */ 582 | setBit(v, index, value) { 583 | if ( this.isChecks ) { 584 | if ( !this.isHypervector(v) ) { 585 | throw new Error('Not a valid hypervector.'); 586 | } 587 | } 588 | 589 | const elementIndex = Math.floor(index / 32); 590 | const bitIndex = 31 - (index % 32); 591 | 592 | if (elementIndex >= v.length) { 593 | throw new RangeError("Index out of range"); 594 | } 595 | 596 | const mask = 1 << bitIndex; 597 | if (value) { 598 | v[elementIndex] |= mask; // set bit to 1 599 | } else { 600 | v[elementIndex] &= ~mask; // clear bit to 0 601 | } 602 | } 603 | 604 | /** 605 | * Encode hypervector as Base64 string. 606 | * 607 | * @param {Hypervector} v Hypervector 608 | * @return {string} Base64 encoded string. 609 | */ 610 | b64Encode( v ) { 611 | if ( this.isChecks ) { 612 | if ( !this.isHypervector(v) ) { 613 | throw new Error('Not a valid hypervector.'); 614 | } 615 | } 616 | 617 | let bytes = new Uint8Array(v.buffer); 618 | 619 | let s; 620 | if ( this.isBuffer ) { 621 | s = Buffer.from(bytes).toString('base64'); 622 | } else { 623 | const chars = this.b64Chars; 624 | s = ''; 625 | let i; 626 | 627 | for (i = 0; i + 2 < bytes.length; i += 3) { 628 | const triplet = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; 629 | s += chars[(triplet >> 18) & 0x3F]; 630 | s += chars[(triplet >> 12) & 0x3F]; 631 | s += chars[(triplet >> 6) & 0x3F]; 632 | s += chars[triplet & 0x3F]; 633 | } 634 | 635 | // Handle padding 636 | const remaining = bytes.length - i; 637 | if (remaining === 1) { 638 | const triplet = bytes[i] << 16; 639 | s += chars[(triplet >> 18) & 0x3F]; 640 | s += chars[(triplet >> 12) & 0x3F]; 641 | s += '=='; 642 | } else if (remaining === 2) { 643 | const triplet = (bytes[i] << 16) | (bytes[i + 1] << 8); 644 | s += chars[(triplet >> 18) & 0x3F]; 645 | s += chars[(triplet >> 12) & 0x3F]; 646 | s += chars[(triplet >> 6) & 0x3F]; 647 | s += '='; 648 | } 649 | } 650 | 651 | return s; 652 | } 653 | 654 | /** 655 | * Decode Base64 string as hypervector. 656 | * 657 | * @param {string} s Base64 encoded string 658 | * @return {Hypervector} Hypervector. 659 | */ 660 | b64Decode( s ) { 661 | if ( this.isChecks ) { 662 | if ( typeof s !== "string" ) { 663 | throw new Error('Parameter is not a string.'); 664 | } 665 | } 666 | 667 | let bytes; 668 | if ( this.isBuffer ) { 669 | bytes = Buffer.from( s, "base64" ); 670 | } else { 671 | const lookup = this.b64Lookup; 672 | const clean = s.replace(/=+$/, ''); 673 | const len = clean.length; 674 | bytes = new Uint8Array((len * 3 / 4) | 0); 675 | let byteIndex = 0; 676 | 677 | for (let i = 0; i < len; i += 4) { 678 | const sextet1 = lookup[clean.charCodeAt(i)]; 679 | const sextet2 = lookup[clean.charCodeAt(i + 1)]; 680 | const sextet3 = lookup[clean.charCodeAt(i + 2)] || 0; 681 | const sextet4 = lookup[clean.charCodeAt(i + 3)] || 0; 682 | 683 | const triple = (sextet1 << 18) | (sextet2 << 12) | (sextet3 << 6) | sextet4; 684 | 685 | if (byteIndex < bytes.length) bytes[byteIndex++] = (triple >> 16) & 0xFF; 686 | if (byteIndex < bytes.length) bytes[byteIndex++] = (triple >> 8) & 0xFF; 687 | if (byteIndex < bytes.length) bytes[byteIndex++] = triple & 0xFF; 688 | } 689 | } 690 | 691 | // Hypervector 692 | let v = new Uint32Array( 693 | bytes.buffer, 694 | bytes.byteOffset, 695 | bytes.byteLength / Uint32Array.BYTES_PER_ELEMENT 696 | ); 697 | if ( !this.isHypervector(v) ) { 698 | throw new Error('Not a valid hypervector.'); 699 | } 700 | 701 | return v; 702 | } 703 | 704 | /** 705 | * Encode hypervector as binary string. 706 | * 707 | * @param {Hypervector} v Hypervector 708 | * @return {string} Binary string 709 | */ 710 | bitEncode( v ) { 711 | if ( this.isChecks ) { 712 | if ( !this.isHypervector(v) ) { 713 | throw new Error('Not a valid hypervector.'); 714 | } 715 | } 716 | 717 | const N = this.arraySize; 718 | const r = Array( 4 * N ); 719 | const p = this.binarystringLookup; 720 | let j = 0; 721 | for (let i = 0; i < N; i++) { 722 | const x = v[i]; 723 | r[j++] = p[(x >>> 24) & 0xFF]; 724 | r[j++] = p[(x >>> 16) & 0xFF]; 725 | r[j++] = p[(x >>> 8) & 0xFF]; 726 | r[j++] = p[x & 0xFF]; 727 | } 728 | 729 | return r.join(''); 730 | } 731 | 732 | /** 733 | * Decode binary string as hypervector. 734 | * 735 | * @param {string} s Binary string 736 | * @return {Hypervector} Hypervector. 737 | */ 738 | bitDecode( s ) { 739 | if ( this.isChecks ) { 740 | if ( typeof s !== "string" ) { 741 | throw new Error('Not a string.'); 742 | } 743 | if ( s.length !== (this.arraySize * 32) ) { 744 | throw new Error('Invalid length.'); 745 | } 746 | } 747 | 748 | const N = this.arraySize; 749 | const v = new Uint32Array(N); 750 | for (let i = 0; i < N; i++) { 751 | const chunk = s.slice(i * 32, i * 32 + 32); 752 | v[i] = parseInt(chunk, 2) >>> 0; // convert to unsigned 32-bit 753 | } 754 | 755 | return v; 756 | } 757 | 758 | } 759 | 760 | export { HDC }; 761 | -------------------------------------------------------------------------------- /modules/Graph.mjs: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @class Graph 4 | * @author Mika Suominen 5 | */ 6 | class Graph { 7 | 8 | /** 9 | * @typedef {number[]} Edge 10 | * @typedef {number[]} Pattern 11 | * @typedef {Object} Node 12 | * @typedef {Object} Token 13 | */ 14 | 15 | /** 16 | * Creates an instance of Hypergraph. 17 | * @constructor 18 | */ 19 | constructor() { 20 | this.nodes = []; 21 | this.links = []; 22 | this.L = new Map(); // Leafs 23 | this.P = new Map(); // Search patterns for leafs 24 | this.V = new Map(); // Map vertex id to node object 25 | this.T = new Map(); // Map token to array of links 26 | } 27 | 28 | /** 29 | * Clear the hypergraph for reuse. 30 | */ 31 | clear() { 32 | this.nodes.length = 0; 33 | this.links.length = 0; 34 | this.L.clear(); 35 | this.P.clear(); 36 | this.V.clear(); 37 | this.T.clear(); 38 | } 39 | 40 | 41 | /** 42 | * Calculate the mean of array elements. 43 | * @static 44 | * @param {number[]} arr Array of numbers 45 | * @return {number} The mean. 46 | */ 47 | static mean( arr ) { 48 | return arr.reduce( (a,b) => a + b, 0 ) / arr.length; 49 | } 50 | 51 | /** 52 | * Calculate the median of array numbers. 53 | * @static 54 | * @param {number[]} arr Array of numbers 55 | * @return {number} The median. 56 | */ 57 | static median( arr ) { 58 | const mid = Math.floor( arr.length / 2 ); 59 | const nums = [ ...arr ].sort( (a, b) => a - b ); 60 | return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2; 61 | } 62 | 63 | 64 | /** 65 | * Add a multiway hyperedge. 66 | * @param {Token} t Token. 67 | * @param {number} [view=1] 1=space, 2=time, 3=phase 68 | * @return {Node[]} Array of nodes. 69 | */ 70 | add( t, view = 1 ) { 71 | // Add the edge 72 | if ( this.T.has(t) ) return []; // Already added 73 | 74 | // Add search patterns 75 | const edge = t.edge; 76 | let k = edge.join(","); 77 | let es = this.L.get( k ); 78 | if ( es ) { 79 | if ( view === 2 ) return []; // Edge already exists, return (transitive closure) 80 | if ( view === 3 ) { 81 | if ( edge.length === 1 ) { 82 | this.V.get( edge[0] ).refs++; // Increase node size 83 | } 84 | return []; // Edge already exists, return (transitive closure) 85 | } 86 | } 87 | es ? es.push( t ) : this.L.set( k, [ t ] ); 88 | for( let i = edge.length-1; i>=0; i-- ) { 89 | k = edge.map( (x,j) => ( j === i ? x : "*" ) ).join(","); 90 | es = this.P.get( k ); 91 | es ? es.push( t ) : this.P.set( k, [ t ] ); 92 | } 93 | 94 | // New edge object 95 | const o = []; 96 | this.T.set( t, o ); 97 | 98 | // Calculate position 99 | let p = { x: 0, y: 0, z: 0 }, cnt = 0; 100 | let u = [ ...new Set( t.edge ) ]; 101 | u.forEach( id => { 102 | let v = this.V.get( id ); 103 | if ( v && v.x ) { 104 | p.x += v.x; 105 | p.y += v.y; 106 | p.z += v.z; 107 | cnt++; 108 | } 109 | }); 110 | if ( cnt ) { 111 | p.x /= cnt; 112 | p.y /= cnt; 113 | p.z /= cnt; 114 | } 115 | 116 | // Add vertices and links 117 | const vs = []; 118 | let vprev 119 | for( let i = 0; i < edge.length; i++ ) { 120 | let id = edge[i]; 121 | let v = this.V.get( id ); 122 | if ( v ) { 123 | v.refs++; 124 | v.t.push( t ); 125 | if ( view === 1 ) v.bc = t.bc; // In space view, update vertex bc to latest 126 | } else { 127 | // New vertex 128 | v = { 129 | id: id, 130 | refs: 1, 131 | in: [], out: [], 132 | source: [], target: [], 133 | style: 0, 134 | x: (p.x + (view === 2 ? (Math.sign(p.x)*Math.random()) : ((Math.random()-0.5)/100))), 135 | y: (p.y + (view === 2 ? (Math.sign(p.y)*Math.random()) : ((Math.random()-0.5)/100))), 136 | z: (p.z + (view === 2 ? (10*Math.sign(p.z)*Math.random()) : ((Math.random()-0.5)/100))), 137 | bc: (view === 1 ? t.bc : t.mw[i].bc), 138 | mw: (view === 1 ? null : t.mw[i]), 139 | t: [t] 140 | }; 141 | this.V.set( id, v ); 142 | this.nodes.push( v ); 143 | } 144 | 145 | vs.push( v ); 146 | 147 | // Modify adjacency arrays 148 | if ( i < (edge.length-1) ) v.out.push( edge[i+1] ); 149 | if ( i > 0 ) { 150 | v.in.push( edge[i-1] ); 151 | 152 | // Curvature 153 | let curv = 0.5; 154 | let ls = vprev.source.filter( l => v.target.includes(l) && !l.hasOwnProperty("meshes") ); 155 | if ( ls.length === 0 ) { 156 | // first link, keep straight 157 | if ( vprev !== v ) { 158 | curv = 0; 159 | } 160 | } else { 161 | let rot = 2 * Math.PI / ( ls.length + 1); 162 | ls.forEach( (l,j) => { 163 | l.curvature = 0.5; 164 | l.rotation = ( j + 1 ) * rot; 165 | }); 166 | } 167 | 168 | // New link 169 | const l = { 170 | source: vprev, 171 | target: v, 172 | style: 0, 173 | curvature: curv, 174 | rotation: 0, 175 | bc: ( view === 1 ? t.bc : t.mw[ t.mw.length -1 ].bc ), 176 | w: ( t.hasOwnProperty("w") ? t.w : 1 ) // weight 177 | }; 178 | vprev.source.push( l ); 179 | v.target.push( l ); 180 | this.links.push( l ); 181 | o.push( l ); 182 | } 183 | vprev = v; 184 | } 185 | 186 | // Hyperedges (in space mode only) 187 | if ( view === 1 && (vs.length === 1 || vs.length > 2) ) { 188 | const hl = { 189 | source: vs[ vs.length - 1 ], 190 | target: vs[0], 191 | hyperedge: vs, 192 | style: 4 193 | }; 194 | if ( vs.length === 1 ) { 195 | hl.scale = 0; 196 | vs[ vs.length - 1 ].source.push( hl ); 197 | vs[0].target.push( hl ); 198 | } 199 | this.links.push( hl ); 200 | o.push( hl ); 201 | 202 | // Rescale rings 203 | if ( vs.length === 1 ) { 204 | let ls = vs[0].target.filter( l => l.hasOwnProperty("hyperedge") && l.hyperedge.length === 1 ); 205 | ls.forEach( (l,i) => l.scale = i ); 206 | } 207 | } 208 | 209 | return vs; 210 | } 211 | 212 | /** 213 | * Delete multiway edge. 214 | * @param {Token} t Token. 215 | */ 216 | del( t ) { 217 | if ( !this.T.has( t ) ) return; // Already deleted 218 | 219 | // Delete links 220 | this.T.get( t ).forEach( l => { 221 | let idx = l.source.source.indexOf( l ); 222 | if ( idx !== -1 ) l.source.source.splice( idx, 1 ); 223 | idx = l.target.target.indexOf( l ); 224 | if ( idx !== -1 ) l.target.target.splice( idx, 1 ); 225 | 226 | if ( l.hyperedge && l.hyperedge.length === 1 ) { 227 | // Rescale rings 228 | let ls = l.target.target.filter( k => k.hasOwnProperty("hyperedge") && k.hyperedge.length === 1 ); 229 | ls.forEach( (l,i) => l.scale = i ); 230 | } else { 231 | // Restore curvature 232 | let ls = l.source.source.filter( x => l.target.target.includes(x) && !x.hasOwnProperty("meshes") ); 233 | if ( ls.length === 1 ) { 234 | if ( l.source !== l.target ) { 235 | ls[0].curvature = 0; 236 | } 237 | ls[0].rotation = 0; 238 | } else if ( ls.length > 1 ) { 239 | let rot = 2 * Math.PI / ls.length; 240 | ls.forEach( (l,j) => { 241 | l.curvature = 0.5; 242 | l.rotation = j * rot; 243 | }); 244 | } 245 | } 246 | this.links.splice( this.links.indexOf( l ), 1 ); 247 | }); 248 | 249 | // Remove search patterns 250 | const edge = t.edge; 251 | let k = edge.join(","); 252 | let es = this.L.get( k ); 253 | es.splice( es.indexOf( t ), 1 ); 254 | if ( es.length === 0 ) this.L.delete( k ); 255 | for( let i = edge.length-1; i>=0; i-- ) { 256 | k = edge.map( (x,j) => ( j === i ? x : "*" ) ).join(","); 257 | es = this.P.get( k ); 258 | es.splice( es.indexOf( t ), 1 ); 259 | if ( es.length === 0 ) this.P.delete( k ); 260 | } 261 | 262 | // Delete vertices 263 | for( let i = edge.length - 1; i >= 0; i-- ) { 264 | const id = edge[i]; 265 | const v = this.V.get( id ); 266 | if ( !v ) continue; // Already deleted, ignore 267 | if ( i > 0 ) v.in.splice( v.in.indexOf( edge[i-1] ), 1 ); 268 | if ( i < (edge.length-1) ) v.out.splice( v.out.indexOf( edge[i+1] ), 1 ); 269 | v.t.splice( v.t.indexOf( t ), 1 ) 270 | v.refs--; 271 | if ( v.refs <= 0 ) { 272 | this.V.delete( edge[i] ); 273 | this.nodes.splice( this.nodes.findIndex( v => v.id === id ), 1 ); 274 | } 275 | } 276 | 277 | // Remove token 278 | this.T.delete( t ); 279 | 280 | } 281 | 282 | /** 283 | * Generate all combinations of an array of arrays. 284 | * @generator 285 | * @param {Object[][]} arr Array of arrays 286 | * @return {Object[]} Combination 287 | */ 288 | *cartesian( arr ) { 289 | const inc = (t,p) => { 290 | if (p < 0) return true; // reached end of first array 291 | t[p].idx = (t[p].idx + 1) % t[p].len; 292 | return t[p].idx ? false : inc(t,p-1); 293 | } 294 | const t = arr.map( (x,i) => { return { idx: 0, len: x.length }; } ); 295 | const len = arr.length - 1; 296 | do { yield t.map( (x,i) => arr[i][x.idx] ); } while( !inc(t,len) ); 297 | } 298 | 299 | /** 300 | * Return all possible combinations of the given a list of hyperedges. 301 | * @param {Edge[]} edges Array of edges 302 | * @param {number} mode 0=no qm, 1=only qm, 2=both 303 | * @return {Token[][]} Arrays of tokens 304 | */ 305 | hits( edges, mode = 0 ) { 306 | const h = []; 307 | for( let i = 0; i arr.indexOf(x) !== i ) ) continue; 318 | hits.push( c ); 319 | } 320 | 321 | return hits; 322 | } 323 | 324 | /** 325 | * Find edges that match to the given wild card search pattern. 326 | * @param {Pattern} p Search pattern, wild card < 0 327 | * @return {Edge[]} Matching hyperedges. 328 | */ 329 | find( p ) { 330 | let found = []; 331 | let wilds = p.reduce( (a,b) => a + ( b<0 ? 1 : 0 ), 0 ); 332 | if ( wilds === 0 ) { 333 | // Pattern has no wild cards, so we look for an exact match 334 | if ( this.L.has( p.join(",") ) ) found.push( p ); 335 | } else if ( wilds === p.length ) { 336 | // All wild cards, so we return all edges of the given length 337 | for( const ts of this.L.values() ) { 338 | if ( ts[0].edge.length === p.length ) found.push( [...ts[0].edge] ); 339 | } 340 | } else { 341 | // Extract individual keys and find edges based on them 342 | // Filter out duplicates and get the intersection 343 | let f,k,ts; 344 | for( let i = p.length-1; i >= 0; i-- ) { 345 | if ( p[i] < 0 ) continue; 346 | k = p.map( (x,j) => ( j === i ? x : "*" )).join(","); 347 | ts = this.P.get( k ); 348 | if ( !ts ) return []; 349 | f = f ? ts.filter( t => f.includes(t) ) : ts; 350 | } 351 | // Get unique edges 352 | if ( f ) { 353 | found = Object.values( f.reduce((a,b) => { 354 | a[b.edge.join(",")] = [...b.edge]; 355 | return a; 356 | },{})); 357 | } 358 | } 359 | 360 | return found; 361 | } 362 | 363 | 364 | /** 365 | * BFS generator function. 366 | * @generator 367 | * @param {Node} v Root vertex of the bfs 368 | * @param {boolean} [dir=false] Use directed edges 369 | * @param {boolean} [rev=false] Reverse the order of directed edges 370 | * @yields {Node[]} The next leafs. 371 | */ 372 | *bfs( v, dir = false, rev = false ) { 373 | if ( !this.V.has( v ) ) return; // Node not found 374 | let searching = [ v ], visited = []; 375 | while( searching.length > 0 ) { 376 | // Yield the process; client can filter the search set 377 | let override = yield searching; 378 | if ( override ) searching = override; 379 | const leafs = []; 380 | for( const x of searching) { 381 | const w = this.V.get( x ); 382 | if ( !dir || rev ) leafs.push( ...w.in ); 383 | if ( !dir || !rev ) leafs.push( ...w.out ); 384 | } 385 | visited = [ ...new Set( [ ...visited, ...searching ] ) ]; // Set Union 386 | searching = [ ...new Set( leafs ) ].filter( x => !visited.includes(x) ); // Set Difference 387 | } 388 | } 389 | 390 | /** 391 | * Random walk never visiting any vertex twice. 392 | * @param {Node} v Root vertex of the walk 393 | * @param {number} [distance=Infinity] Maximum distance 394 | * @param {boolean} [dir=false] Use directed edges 395 | * @param {boolean} [rev=false] Reverse the order of directed edges 396 | * @return {Hyperedge[]} True if array has duplicates. 397 | */ 398 | random( v, distance = Infinity, dir = false, rev = false ) { 399 | const path = [], gen = this.bfs( v, dir, rev ); 400 | let d = 0, a = gen.next().value; 401 | while( ++d <= distance ) { 402 | const m = gen.next( a ); 403 | if ( m.done ) break; 404 | const b = m.value[ Math.floor( Math.random() * m.value.length ) ]; 405 | if ( dir || this.V.get( a[0] ).out.includes( b) ) { 406 | path.push( [ a[0], b ] ); 407 | } else { 408 | path.push( [ b, a[0] ] ); 409 | } 410 | 411 | a = [ b ]; 412 | } 413 | return path; 414 | } 415 | 416 | /** 417 | * Tree. 418 | * @param {Node} root Root of the tree 419 | * @param {boolean} [dir=false] Use directed edges 420 | * @param {boolean} [rev=false] Reverse the order of directed edges 421 | * @param {Node[]} breaks Array of vertices on which to stop 422 | * @param {distance} distance Maximum length of the tree 423 | * @return {Node[][]} Array of vertex layers of the tree 424 | */ 425 | tree( root, dir = false, rev = false, breaks = [], distance = Infinity ) { 426 | const tree = []; 427 | let d = 0; 428 | for( const v of this.bfs( root, dir, rev ) ) { 429 | tree.push(v); 430 | if ( ++d > distance || breaks.some( x => v.includes(x) ) ) break; 431 | } 432 | return tree; 433 | } 434 | 435 | /** 436 | * Shortest path from vertex 'a' to vertex 'b' using BFS. 437 | * @param {Node} v1 First vertex 438 | * @param {Node} v2 Second vertex 439 | * @param {boolean} [dir=false] Use directed edges 440 | * @param {boolean} [rev=false] Reverse the order of directed edges 441 | * @param {boolean} [all=false] Return all shortest paths 442 | * @return {Edge[]} Shortest path(s) as an array of hyperedges 443 | */ 444 | geodesic( v1, v2, dir = false, rev = false, all = false ) { 445 | const genA = this.bfs( v1, dir, rev ), treeA = []; 446 | const genB = this.bfs( v2, dir, !rev ), treeB = []; 447 | let m, n = { value: [] }; 448 | 449 | // Find the collision point 450 | while( true ) { 451 | m = genA.next(); 452 | if ( m.done ) return []; // root/leaf not connected 453 | if ( m.value.some( x => n.value.includes( x ) ) ) break; 454 | treeA.push( m.value ); 455 | n = genB.next(); 456 | if ( n.done ) return []; // root/leaf not connected 457 | if ( n.value.some( x => m.value.includes( x ) ) ) break; 458 | treeB.push( n.value ); 459 | } 460 | 461 | // Set the mid-point 462 | const path = new Array( treeA.length + treeB.length ).fill().map( () => new Array() ); 463 | path[ treeB.length ] = m.value.filter( x => n.value.includes(x) ); 464 | if ( !all ) path[ treeB.length ] = [ path[ treeB.length ][0] ]; // 1st path only 465 | const edges = new Array( path.length - 1 ).fill().map( () => new Array() ); 466 | 467 | // Fill-in the 'path' and 'edges' from the mid-point using intersections 468 | for( let i = treeB.length - 1; i >= 0; i-- ) { 469 | n = genB.next( path[ i+1 ] ); 470 | path[ i ] = treeA[i].filter( x => n.value.includes(x) ); 471 | 472 | for( let j = path[i+1].length - 1; j >= 0; j-- ) { 473 | const v = this.V.get( path[i+1][j] ); 474 | for( let k = path[i].length - 1; k >= 0; k-- ) { 475 | if ( ( !dir || !rev ) && v.in.includes( path[i][k] ) ) { 476 | edges[ i ].push( [ path[i][k], path[i+1][j] ] ); 477 | if ( !all ) { path[ i ] = [ path[i][k] ]; break; } 478 | } 479 | if ( ( !dir || rev ) && v.out.includes( path[i][k] ) ) { 480 | edges[ i ].push( [ path[i+1][j], path[i][k] ] ); 481 | if ( !all ) { path[ i ] = [ path[i][k] ]; break; } 482 | } 483 | } 484 | } 485 | } 486 | for( let i = treeB.length + 1; i < path.length; i++ ) { 487 | m = genA.next( path[ i-1 ] ); 488 | path[ i ] = treeB[ path.length - 1 - i ].filter( x => m.value.includes(x) ); 489 | 490 | for( let j = path[i-1].length - 1; j >= 0; j-- ) { 491 | const v = this.V.get( path[i-1][j] ); 492 | for( let k = path[i].length - 1; k >= 0; k-- ) { 493 | if ( ( !dir || !rev ) && v.out.includes( path[i][k] ) ) { 494 | edges[ i-1 ].push( [ path[i-1][j], path[i][k] ] ); 495 | if ( !all ) { path[ i ] = [ path[i][k] ]; break; } 496 | } 497 | if ( ( !dir || rev ) && v.in.includes( path[i][k] ) ) { 498 | edges[ i-1 ].push( [ path[i][k], path[i-1][j] ] ); 499 | if ( !all ) { path[ i ] = [ path[i][k] ]; break; } 500 | } 501 | } 502 | } 503 | } 504 | return edges; 505 | } 506 | 507 | /** 508 | * N-dimensional ball. 509 | * @param {Node} center Center vertex of the n-ball 510 | * @param {Node} radius Radius of the n-ball 511 | * @param {boolean} [dir=false] Use directed edges 512 | * @param {boolean} [rev=false] Reverse the order of directed edges 513 | * @return {Edge[]} Array of edges inside the n-ball 514 | */ 515 | nball( center, radius, dir = false, rev = false ) { 516 | // Start from the root and get the distance tree up to the distance 'radius' 517 | const vs = this.tree( center, dir, rev, [], Math.abs(radius) ).flat(); 518 | const edges = []; 519 | for( let i = vs.length - 1; i >= 0; i-- ) { 520 | const v = this.V.get( vs[i] ); 521 | for( let j = vs.length - 1; j >= 0; j-- ) { 522 | if ( ( !dir || !rev ) && v.in.includes( vs[j] ) ) edges.push( [ vs[j], vs[i] ] ); 523 | if ( ( !dir || rev ) && v.out.includes( vs[j] ) ) edges.push( [ vs[i], vs[j] ] ); 524 | } 525 | } 526 | return edges; 527 | } 528 | 529 | /** 530 | * N-sphere. 531 | * @param {Node} center Center vertex of the n-sphere 532 | * @param {Node} radius Radius of the n-sphere 533 | * @param {boolean} [dir=false] Use directed edges 534 | * @param {boolean} [rev=false] Reverse the order of directed edges 535 | * @return {Node[]} Array of vertexes on the n-sphere 536 | */ 537 | nsphere( center, radius, dir = false, rev = false ) { 538 | // Start from the root and get the distance tree up to the distance 'radius' 539 | const tree = this.tree( center, dir, rev, [], Math.abs(radius) ); 540 | const d = tree.length - 1; 541 | if ( d < Math.abs(radius) ) return []; // N-sphere with the given radius not found 542 | return tree[ d ]; 543 | } 544 | 545 | /** 546 | * Minimum distance between two vertices. 547 | * @param {Node} v1 First vertex 548 | * @param {Node} v2 Second vertex 549 | * @param {boolean} [dir=false] Use directed edges 550 | * @param {boolean} [rev=false] Reverse the order of directed edges 551 | * @return {number} Number of step from 'a' to 'b', -1 if not connected 552 | */ 553 | dist( v1, v2, dir = false, rev = false ) { 554 | const genA = this.bfs( v1, dir, rev ); 555 | const genB = this.bfs( v2, dir, !rev ); 556 | let m, n = { value: [] }, d = -1; 557 | // Bidirectional BFS 558 | while( true ) { 559 | m = genA.next(); 560 | if ( m.done ) return -1; // Not connected 561 | if ( m.value.some( x => n.value.includes( x ) ) ) return d; 562 | d++; 563 | n = genB.next(); 564 | if ( n.done ) return -1; // Not connected 565 | if ( n.value.some( x => m.value.includes( x ) ) ) return d; 566 | d++; 567 | } 568 | } 569 | 570 | /** 571 | * Hypersurface. 572 | * @param {Node} v1 Starting point in space/time 573 | * @param {Node} v2 Ending point in space/time 574 | * @return {Node[]} Hypersurface 575 | */ 576 | surface( v1, v2 ) { 577 | const vertices = []; 578 | for( let i = v1; i <= v2; i++ ) { 579 | if ( this.V.has(i) ) vertices.push( i ); 580 | } 581 | return vertices; 582 | } 583 | 584 | /** 585 | * Light cones. 586 | * @param {Node} moment Single point in space and time 587 | * @param {number} length Size of the cones 588 | * @param {boolean} [past=true] Include past light cone 589 | * @param {boolean} [future=true] Include future light cone 590 | * @return {Edge[]} Light cone. 591 | */ 592 | lightcone( moment, length, past = true, future = true ) { 593 | let pastcone = [], futurecone = []; 594 | if ( past ) { 595 | let s = this.nsphere( moment, length, true, true ); 596 | s.forEach( v => { 597 | pastcone.push( ...this.geodesic( v, moment, true, false, true ).flat() ); 598 | }); 599 | } 600 | if ( future ) { 601 | let s = this.nsphere( moment, length, true, false ); 602 | s.forEach( v => { 603 | futurecone.push( ...this.geodesic( v, moment, true, true, true ).flat() ); 604 | }); 605 | } 606 | return { past: [ ...new Set( pastcone ) ], future: [ ...new Set( futurecone ) ] }; 607 | } 608 | 609 | 610 | /** 611 | * Computes the optimal transport matrix and returns sinkhorn distance 612 | * (i.e. optimal transport) using the Sinkhorn-Knopp algorithm 613 | * @param {numbers[][]} dm Cost/distance matrix 614 | * @param {numbers[]} [a=null] Marginal A, unif distibution by default 615 | * @param {numbers[]} [b=null] Marginal B, unif distibution by default 616 | * @param {number} [lam=10] Strength of the entropic regularization 617 | * @param {number} [epsilon=1e-8] Convergence parameter 618 | * @return {Object} Optimal transport matrix and sinkhorn distance. 619 | */ 620 | sinkhorn = function( dm, a = null, b = null, lam = 10, epsilon = 1e-8 ) { 621 | const m = dm.length; 622 | const n = dm[0].length; 623 | if ( !a ) a = new Array( m ).fill( 1/m ); 624 | if ( !b ) b = new Array( n ).fill( 1/n ); 625 | if ( a.length !== m || b.length !== n ) throw new Error("Dimensions don't match."); 626 | const P = new Array( m ).fill().map( x => new Array( n ).fill( 0 ) ); 627 | let Psum = 0; 628 | for( let i = m-1; i >= 0; i-- ) 629 | for( let j = n-1; j >= 0; j-- ) { 630 | P[i][j] = Math.exp( -lam * dm[i][j] ); 631 | Psum += P[i][j]; 632 | } 633 | let u = new Array( n ).fill(0); // row sums 634 | for( let i = m-1; i >= 0; i-- ) 635 | for( let j = n-1; j >= 0; j-- ) { 636 | P[i][j] /= Psum; 637 | u[j] += P[i][j]; 638 | } 639 | let du = new Array( n ); // row sums diff between iterations 640 | let v = new Array( m ); // column sums 641 | 642 | // Normalize matrix by scaling rows/columns until no significant change 643 | do { 644 | du.fill(0); 645 | v.fill(0); 646 | for( let i = m-1; i >= 0; i-- ) 647 | for( let j = n-1; j >= 0; j-- ) { 648 | du[j] += P[i][j]; 649 | P[i][j] *= b[j] / u[j]; // scale the rows 650 | v[i] += P[i][j] 651 | } 652 | u.fill(0); 653 | for( let i = m-1; i >= 0; i-- ) 654 | for( let j = n-1; j >= 0; j-- ) { 655 | P[i][j] *= a[i] / v[i]; // scale the columns 656 | u[j] += P[i][j]; 657 | du[j] -= P[i][j]; 658 | } 659 | } 660 | while ( Math.max.apply( null, du.map( Math.abs ) ) > epsilon ); 661 | 662 | // Calculate sinkhorn distance 663 | let dist = 0; 664 | for( let i = m-1; i >= 0; i-- ) 665 | for( let j = n-1; j >= 0; j-- ) 666 | dist += P[i][j] * dm[i][j]; 667 | return { ot: P, dist: dist }; 668 | } 669 | 670 | /** 671 | * Curvature based on Ollivier-Ricci (1-Wasserstein) distance. 672 | * @param {Node} v1 First vertex 673 | * @param {Node} v2 Second vertex 674 | * @param {number} [radius=1] Radius 675 | * @param {boolean} [dir=false] Use directed edges 676 | * @return {number} Ollivier-Ricci distance. 677 | */ 678 | orc( v1, v2, radius = 1, dir = false ) { 679 | let ns1, ns2; 680 | try { 681 | ns1 = this.nsphere( v1, Math.abs(radius), dir, true ); 682 | ns2 = this.nsphere( v2, Math.abs(radius), dir, false ); 683 | if ( ns1.length === 0 || ns2.length === 0 ) return 0; 684 | } 685 | catch( e ) { 686 | return 0; 687 | } 688 | 689 | // Construct distance matrix 690 | const dm = new Array( ns1.length ).fill().map( x => new Array( ns2.length ) ); 691 | for( let i = ns1.length - 1; i >= 0; i-- ) 692 | for( let j = ns2.length - 1; j >= 0; j-- ) 693 | dm[i][j] = this.dist( ns1[i], ns2[j], dir ); 694 | 695 | // Calculate Wasserstein-1 distance using sinkhorn-knopp algorithm 696 | return ( 1 - this.sinkhorn( dm )[ "dist" ] / this.dist( v1, v2, dir ) ); 697 | } 698 | 699 | /** 700 | * Status. 701 | * @return {Object} Status of the hypergraph. 702 | */ 703 | status() { 704 | return { nodes: this.nodes.length, edges: this.links.length }; 705 | } 706 | 707 | } 708 | 709 | export { Graph }; 710 | -------------------------------------------------------------------------------- /modules/ConvexHull.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | Line3, 3 | Plane, 4 | Triangle, 5 | Vector3 6 | } from 'three'; 7 | 8 | /** 9 | * Ported from: https://github.com/maurizzzio/quickhull3d/ by Mauricio Poppe (https://github.com/maurizzzio) 10 | */ 11 | 12 | const Visible = 0; 13 | const Deleted = 1; 14 | 15 | const _v1 = new Vector3(); 16 | const _line3 = new Line3(); 17 | const _plane = new Plane(); 18 | const _closestPoint = new Vector3(); 19 | const _triangle = new Triangle(); 20 | 21 | class ConvexHull { 22 | 23 | constructor() { 24 | 25 | this.tolerance = - 1; 26 | 27 | this.faces = []; // the generated faces of the convex hull 28 | this.newFaces = []; // this array holds the faces that are generated within a single iteration 29 | 30 | // the vertex lists work as follows: 31 | // 32 | // let 'a' and 'b' be 'Face' instances 33 | // let 'v' be points wrapped as instance of 'Vertex' 34 | // 35 | // [v, v, ..., v, v, v, ...] 36 | // ^ ^ 37 | // | | 38 | // a.outside b.outside 39 | // 40 | this.assigned = new VertexList(); 41 | this.unassigned = new VertexList(); 42 | 43 | this.vertices = []; // vertices of the hull (internal representation of given geometry data) 44 | 45 | } 46 | 47 | setFromPoints( points ) { 48 | 49 | if ( Array.isArray( points ) !== true ) { 50 | 51 | console.error( 'THREE.ConvexHull: Points parameter is not an array.' ); 52 | 53 | } 54 | 55 | if ( points.length < 4 ) { 56 | 57 | console.error( 'THREE.ConvexHull: The algorithm needs at least four points.' ); 58 | 59 | } 60 | 61 | this.makeEmpty(); 62 | 63 | for ( let i = 0, l = points.length; i < l; i ++ ) { 64 | 65 | this.vertices.push( new VertexNode( points[ i ] ) ); 66 | 67 | } 68 | 69 | this.compute(); 70 | 71 | return this; 72 | 73 | } 74 | 75 | setFromObject( object ) { 76 | 77 | const points = []; 78 | 79 | object.updateMatrixWorld( true ); 80 | 81 | object.traverse( function ( node ) { 82 | 83 | const geometry = node.geometry; 84 | 85 | if ( geometry !== undefined ) { 86 | 87 | if ( geometry.isGeometry ) { 88 | 89 | console.error( 'THREE.ConvexHull no longer supports Geometry. Use THREE.BufferGeometry instead.' ); 90 | return; 91 | 92 | } else if ( geometry.isBufferGeometry ) { 93 | 94 | const attribute = geometry.attributes.position; 95 | 96 | if ( attribute !== undefined ) { 97 | 98 | for ( let i = 0, l = attribute.count; i < l; i ++ ) { 99 | 100 | const point = new Vector3(); 101 | 102 | point.fromBufferAttribute( attribute, i ).applyMatrix4( node.matrixWorld ); 103 | 104 | points.push( point ); 105 | 106 | } 107 | 108 | } 109 | 110 | } 111 | 112 | } 113 | 114 | } ); 115 | 116 | return this.setFromPoints( points ); 117 | 118 | } 119 | 120 | containsPoint( point ) { 121 | 122 | const faces = this.faces; 123 | 124 | for ( let i = 0, l = faces.length; i < l; i ++ ) { 125 | 126 | const face = faces[ i ]; 127 | 128 | // compute signed distance and check on what half space the point lies 129 | 130 | if ( face.distanceToPoint( point ) > this.tolerance ) return false; 131 | 132 | } 133 | 134 | return true; 135 | 136 | } 137 | 138 | intersectRay( ray, target ) { 139 | 140 | // based on "Fast Ray-Convex Polyhedron Intersection" by Eric Haines, GRAPHICS GEMS II 141 | 142 | const faces = this.faces; 143 | 144 | let tNear = - Infinity; 145 | let tFar = Infinity; 146 | 147 | for ( let i = 0, l = faces.length; i < l; i ++ ) { 148 | 149 | const face = faces[ i ]; 150 | 151 | // interpret faces as planes for the further computation 152 | 153 | const vN = face.distanceToPoint( ray.origin ); 154 | const vD = face.normal.dot( ray.direction ); 155 | 156 | // if the origin is on the positive side of a plane (so the plane can "see" the origin) and 157 | // the ray is turned away or parallel to the plane, there is no intersection 158 | 159 | if ( vN > 0 && vD >= 0 ) return null; 160 | 161 | // compute the distance from the ray’s origin to the intersection with the plane 162 | 163 | const t = ( vD !== 0 ) ? ( - vN / vD ) : 0; 164 | 165 | // only proceed if the distance is positive. a negative distance means the intersection point 166 | // lies "behind" the origin 167 | 168 | if ( t <= 0 ) continue; 169 | 170 | // now categorized plane as front-facing or back-facing 171 | 172 | if ( vD > 0 ) { 173 | 174 | // plane faces away from the ray, so this plane is a back-face 175 | 176 | tFar = Math.min( t, tFar ); 177 | 178 | } else { 179 | 180 | // front-face 181 | 182 | tNear = Math.max( t, tNear ); 183 | 184 | } 185 | 186 | if ( tNear > tFar ) { 187 | 188 | // if tNear ever is greater than tFar, the ray must miss the convex hull 189 | 190 | return null; 191 | 192 | } 193 | 194 | } 195 | 196 | // evaluate intersection point 197 | 198 | // always try tNear first since its the closer intersection point 199 | 200 | if ( tNear !== - Infinity ) { 201 | 202 | ray.at( tNear, target ); 203 | 204 | } else { 205 | 206 | ray.at( tFar, target ); 207 | 208 | } 209 | 210 | return target; 211 | 212 | } 213 | 214 | intersectsRay( ray ) { 215 | 216 | return this.intersectRay( ray, _v1 ) !== null; 217 | 218 | } 219 | 220 | makeEmpty() { 221 | 222 | this.faces = []; 223 | this.vertices = []; 224 | 225 | return this; 226 | 227 | } 228 | 229 | // Adds a vertex to the 'assigned' list of vertices and assigns it to the given face 230 | 231 | addVertexToFace( vertex, face ) { 232 | 233 | vertex.face = face; 234 | 235 | if ( face.outside === null ) { 236 | 237 | this.assigned.append( vertex ); 238 | 239 | } else { 240 | 241 | this.assigned.insertBefore( face.outside, vertex ); 242 | 243 | } 244 | 245 | face.outside = vertex; 246 | 247 | return this; 248 | 249 | } 250 | 251 | // Removes a vertex from the 'assigned' list of vertices and from the given face 252 | 253 | removeVertexFromFace( vertex, face ) { 254 | 255 | if ( vertex === face.outside ) { 256 | 257 | // fix face.outside link 258 | 259 | if ( vertex.next !== null && vertex.next.face === face ) { 260 | 261 | // face has at least 2 outside vertices, move the 'outside' reference 262 | 263 | face.outside = vertex.next; 264 | 265 | } else { 266 | 267 | // vertex was the only outside vertex that face had 268 | 269 | face.outside = null; 270 | 271 | } 272 | 273 | } 274 | 275 | this.assigned.remove( vertex ); 276 | 277 | return this; 278 | 279 | } 280 | 281 | // Removes all the visible vertices that a given face is able to see which are stored in the 'assigned' vertext list 282 | 283 | removeAllVerticesFromFace( face ) { 284 | 285 | if ( face.outside !== null ) { 286 | 287 | // reference to the first and last vertex of this face 288 | 289 | const start = face.outside; 290 | let end = face.outside; 291 | 292 | while ( end.next !== null && end.next.face === face ) { 293 | 294 | end = end.next; 295 | 296 | } 297 | 298 | this.assigned.removeSubList( start, end ); 299 | 300 | // fix references 301 | 302 | start.prev = end.next = null; 303 | face.outside = null; 304 | 305 | return start; 306 | 307 | } 308 | 309 | } 310 | 311 | // Removes all the visible vertices that 'face' is able to see 312 | 313 | deleteFaceVertices( face, absorbingFace ) { 314 | 315 | const faceVertices = this.removeAllVerticesFromFace( face ); 316 | 317 | if ( faceVertices !== undefined ) { 318 | 319 | if ( absorbingFace === undefined ) { 320 | 321 | // mark the vertices to be reassigned to some other face 322 | 323 | this.unassigned.appendChain( faceVertices ); 324 | 325 | 326 | } else { 327 | 328 | // if there's an absorbing face try to assign as many vertices as possible to it 329 | 330 | let vertex = faceVertices; 331 | 332 | do { 333 | 334 | // we need to buffer the subsequent vertex at this point because the 'vertex.next' reference 335 | // will be changed by upcoming method calls 336 | 337 | const nextVertex = vertex.next; 338 | 339 | const distance = absorbingFace.distanceToPoint( vertex.point ); 340 | 341 | // check if 'vertex' is able to see 'absorbingFace' 342 | 343 | if ( distance > this.tolerance ) { 344 | 345 | this.addVertexToFace( vertex, absorbingFace ); 346 | 347 | } else { 348 | 349 | this.unassigned.append( vertex ); 350 | 351 | } 352 | 353 | // now assign next vertex 354 | 355 | vertex = nextVertex; 356 | 357 | } while ( vertex !== null ); 358 | 359 | } 360 | 361 | } 362 | 363 | return this; 364 | 365 | } 366 | 367 | // Reassigns as many vertices as possible from the unassigned list to the new faces 368 | 369 | resolveUnassignedPoints( newFaces ) { 370 | 371 | if ( this.unassigned.isEmpty() === false ) { 372 | 373 | let vertex = this.unassigned.first(); 374 | 375 | do { 376 | 377 | // buffer 'next' reference, see .deleteFaceVertices() 378 | 379 | const nextVertex = vertex.next; 380 | 381 | let maxDistance = this.tolerance; 382 | 383 | let maxFace = null; 384 | 385 | for ( let i = 0; i < newFaces.length; i ++ ) { 386 | 387 | const face = newFaces[ i ]; 388 | 389 | if ( face.mark === Visible ) { 390 | 391 | const distance = face.distanceToPoint( vertex.point ); 392 | 393 | if ( distance > maxDistance ) { 394 | 395 | maxDistance = distance; 396 | maxFace = face; 397 | 398 | } 399 | 400 | if ( maxDistance > 1000 * this.tolerance ) break; 401 | 402 | } 403 | 404 | } 405 | 406 | // 'maxFace' can be null e.g. if there are identical vertices 407 | 408 | if ( maxFace !== null ) { 409 | 410 | this.addVertexToFace( vertex, maxFace ); 411 | 412 | } 413 | 414 | vertex = nextVertex; 415 | 416 | } while ( vertex !== null ); 417 | 418 | } 419 | 420 | return this; 421 | 422 | } 423 | 424 | // Computes the extremes of a simplex which will be the initial hull 425 | 426 | computeExtremes() { 427 | 428 | const min = new Vector3(); 429 | const max = new Vector3(); 430 | 431 | const minVertices = []; 432 | const maxVertices = []; 433 | 434 | // initially assume that the first vertex is the min/max 435 | 436 | for ( let i = 0; i < 3; i ++ ) { 437 | 438 | minVertices[ i ] = maxVertices[ i ] = this.vertices[ 0 ]; 439 | 440 | } 441 | 442 | min.copy( this.vertices[ 0 ].point ); 443 | max.copy( this.vertices[ 0 ].point ); 444 | 445 | // compute the min/max vertex on all six directions 446 | 447 | for ( let i = 0, l = this.vertices.length; i < l; i ++ ) { 448 | 449 | const vertex = this.vertices[ i ]; 450 | const point = vertex.point; 451 | 452 | // update the min coordinates 453 | 454 | for ( let j = 0; j < 3; j ++ ) { 455 | 456 | if ( point.getComponent( j ) < min.getComponent( j ) ) { 457 | 458 | min.setComponent( j, point.getComponent( j ) ); 459 | minVertices[ j ] = vertex; 460 | 461 | } 462 | 463 | } 464 | 465 | // update the max coordinates 466 | 467 | for ( let j = 0; j < 3; j ++ ) { 468 | 469 | if ( point.getComponent( j ) > max.getComponent( j ) ) { 470 | 471 | max.setComponent( j, point.getComponent( j ) ); 472 | maxVertices[ j ] = vertex; 473 | 474 | } 475 | 476 | } 477 | 478 | } 479 | 480 | // use min/max vectors to compute an optimal epsilon 481 | 482 | this.tolerance = 3 * Number.EPSILON * ( 483 | Math.max( Math.abs( min.x ), Math.abs( max.x ) ) + 484 | Math.max( Math.abs( min.y ), Math.abs( max.y ) ) + 485 | Math.max( Math.abs( min.z ), Math.abs( max.z ) ) 486 | ); 487 | 488 | return { min: minVertices, max: maxVertices }; 489 | 490 | } 491 | 492 | // Computes the initial simplex assigning to its faces all the points 493 | // that are candidates to form part of the hull 494 | 495 | computeInitialHull() { 496 | 497 | const vertices = this.vertices; 498 | const extremes = this.computeExtremes(); 499 | const min = extremes.min; 500 | const max = extremes.max; 501 | 502 | // 1. Find the two vertices 'v0' and 'v1' with the greatest 1d separation 503 | // (max.x - min.x) 504 | // (max.y - min.y) 505 | // (max.z - min.z) 506 | 507 | let maxDistance = 0; 508 | let index = 0; 509 | 510 | for ( let i = 0; i < 3; i ++ ) { 511 | 512 | const distance = max[ i ].point.getComponent( i ) - min[ i ].point.getComponent( i ); 513 | 514 | if ( distance > maxDistance ) { 515 | 516 | maxDistance = distance; 517 | index = i; 518 | 519 | } 520 | 521 | } 522 | 523 | const v0 = min[ index ]; 524 | const v1 = max[ index ]; 525 | let v2; 526 | let v3; 527 | 528 | // 2. The next vertex 'v2' is the one farthest to the line formed by 'v0' and 'v1' 529 | 530 | maxDistance = 0; 531 | _line3.set( v0.point, v1.point ); 532 | 533 | for ( let i = 0, l = this.vertices.length; i < l; i ++ ) { 534 | 535 | const vertex = vertices[ i ]; 536 | 537 | if ( vertex !== v0 && vertex !== v1 ) { 538 | 539 | _line3.closestPointToPoint( vertex.point, true, _closestPoint ); 540 | 541 | const distance = _closestPoint.distanceToSquared( vertex.point ); 542 | 543 | if ( distance > maxDistance ) { 544 | 545 | maxDistance = distance; 546 | v2 = vertex; 547 | 548 | } 549 | 550 | } 551 | 552 | } 553 | 554 | // 3. The next vertex 'v3' is the one farthest to the plane 'v0', 'v1', 'v2' 555 | 556 | maxDistance = - 1; 557 | _plane.setFromCoplanarPoints( v0.point, v1.point, v2.point ); 558 | 559 | for ( let i = 0, l = this.vertices.length; i < l; i ++ ) { 560 | 561 | const vertex = vertices[ i ]; 562 | 563 | if ( vertex !== v0 && vertex !== v1 && vertex !== v2 ) { 564 | 565 | const distance = Math.abs( _plane.distanceToPoint( vertex.point ) ); 566 | 567 | if ( distance > maxDistance ) { 568 | 569 | maxDistance = distance; 570 | v3 = vertex; 571 | 572 | } 573 | 574 | } 575 | 576 | } 577 | 578 | const faces = []; 579 | 580 | if ( _plane.distanceToPoint( v3.point ) < 0 ) { 581 | 582 | // the face is not able to see the point so 'plane.normal' is pointing outside the tetrahedron 583 | 584 | faces.push( 585 | Face.create( v0, v1, v2 ), 586 | Face.create( v3, v1, v0 ), 587 | Face.create( v3, v2, v1 ), 588 | Face.create( v3, v0, v2 ) 589 | ); 590 | 591 | // set the twin edge 592 | 593 | for ( let i = 0; i < 3; i ++ ) { 594 | 595 | const j = ( i + 1 ) % 3; 596 | 597 | // join face[ i ] i > 0, with the first face 598 | 599 | faces[ i + 1 ].getEdge( 2 ).setTwin( faces[ 0 ].getEdge( j ) ); 600 | 601 | // join face[ i ] with face[ i + 1 ], 1 <= i <= 3 602 | 603 | faces[ i + 1 ].getEdge( 1 ).setTwin( faces[ j + 1 ].getEdge( 0 ) ); 604 | 605 | } 606 | 607 | } else { 608 | 609 | // the face is able to see the point so 'plane.normal' is pointing inside the tetrahedron 610 | 611 | faces.push( 612 | Face.create( v0, v2, v1 ), 613 | Face.create( v3, v0, v1 ), 614 | Face.create( v3, v1, v2 ), 615 | Face.create( v3, v2, v0 ) 616 | ); 617 | 618 | // set the twin edge 619 | 620 | for ( let i = 0; i < 3; i ++ ) { 621 | 622 | const j = ( i + 1 ) % 3; 623 | 624 | // join face[ i ] i > 0, with the first face 625 | 626 | faces[ i + 1 ].getEdge( 2 ).setTwin( faces[ 0 ].getEdge( ( 3 - i ) % 3 ) ); 627 | 628 | // join face[ i ] with face[ i + 1 ] 629 | 630 | faces[ i + 1 ].getEdge( 0 ).setTwin( faces[ j + 1 ].getEdge( 1 ) ); 631 | 632 | } 633 | 634 | } 635 | 636 | // the initial hull is the tetrahedron 637 | 638 | for ( let i = 0; i < 4; i ++ ) { 639 | 640 | this.faces.push( faces[ i ] ); 641 | 642 | } 643 | 644 | // initial assignment of vertices to the faces of the tetrahedron 645 | 646 | for ( let i = 0, l = vertices.length; i < l; i ++ ) { 647 | 648 | const vertex = vertices[ i ]; 649 | 650 | if ( vertex !== v0 && vertex !== v1 && vertex !== v2 && vertex !== v3 ) { 651 | 652 | maxDistance = this.tolerance; 653 | let maxFace = null; 654 | 655 | for ( let j = 0; j < 4; j ++ ) { 656 | 657 | const distance = this.faces[ j ].distanceToPoint( vertex.point ); 658 | 659 | if ( distance > maxDistance ) { 660 | 661 | maxDistance = distance; 662 | maxFace = this.faces[ j ]; 663 | 664 | } 665 | 666 | } 667 | 668 | if ( maxFace !== null ) { 669 | 670 | this.addVertexToFace( vertex, maxFace ); 671 | 672 | } 673 | 674 | } 675 | 676 | } 677 | 678 | return this; 679 | 680 | } 681 | 682 | // Removes inactive faces 683 | 684 | reindexFaces() { 685 | 686 | const activeFaces = []; 687 | 688 | for ( let i = 0; i < this.faces.length; i ++ ) { 689 | 690 | const face = this.faces[ i ]; 691 | 692 | if ( face.mark === Visible ) { 693 | 694 | activeFaces.push( face ); 695 | 696 | } 697 | 698 | } 699 | 700 | this.faces = activeFaces; 701 | 702 | return this; 703 | 704 | } 705 | 706 | // Finds the next vertex to create faces with the current hull 707 | 708 | nextVertexToAdd() { 709 | 710 | // if the 'assigned' list of vertices is empty, no vertices are left. return with 'undefined' 711 | 712 | if ( this.assigned.isEmpty() === false ) { 713 | 714 | let eyeVertex, maxDistance = 0; 715 | 716 | // grap the first available face and start with the first visible vertex of that face 717 | 718 | const eyeFace = this.assigned.first().face; 719 | let vertex = eyeFace.outside; 720 | 721 | // now calculate the farthest vertex that face can see 722 | 723 | do { 724 | 725 | const distance = eyeFace.distanceToPoint( vertex.point ); 726 | 727 | if ( distance > maxDistance ) { 728 | 729 | maxDistance = distance; 730 | eyeVertex = vertex; 731 | 732 | } 733 | 734 | vertex = vertex.next; 735 | 736 | } while ( vertex !== null && vertex.face === eyeFace ); 737 | 738 | return eyeVertex; 739 | 740 | } 741 | 742 | } 743 | 744 | // Computes a chain of half edges in CCW order called the 'horizon'. 745 | // For an edge to be part of the horizon it must join a face that can see 746 | // 'eyePoint' and a face that cannot see 'eyePoint'. 747 | 748 | computeHorizon( eyePoint, crossEdge, face, horizon ) { 749 | 750 | // moves face's vertices to the 'unassigned' vertex list 751 | 752 | this.deleteFaceVertices( face ); 753 | 754 | face.mark = Deleted; 755 | 756 | let edge; 757 | 758 | if ( crossEdge === null ) { 759 | 760 | edge = crossEdge = face.getEdge( 0 ); 761 | 762 | } else { 763 | 764 | // start from the next edge since 'crossEdge' was already analyzed 765 | // (actually 'crossEdge.twin' was the edge who called this method recursively) 766 | 767 | edge = crossEdge.next; 768 | 769 | } 770 | 771 | do { 772 | 773 | const twinEdge = edge.twin; 774 | const oppositeFace = twinEdge.face; 775 | 776 | if ( oppositeFace.mark === Visible ) { 777 | 778 | if ( oppositeFace.distanceToPoint( eyePoint ) > this.tolerance ) { 779 | 780 | // the opposite face can see the vertex, so proceed with next edge 781 | 782 | this.computeHorizon( eyePoint, twinEdge, oppositeFace, horizon ); 783 | 784 | } else { 785 | 786 | // the opposite face can't see the vertex, so this edge is part of the horizon 787 | 788 | horizon.push( edge ); 789 | 790 | } 791 | 792 | } 793 | 794 | edge = edge.next; 795 | 796 | } while ( edge !== crossEdge ); 797 | 798 | return this; 799 | 800 | } 801 | 802 | // Creates a face with the vertices 'eyeVertex.point', 'horizonEdge.tail' and 'horizonEdge.head' in CCW order 803 | 804 | addAdjoiningFace( eyeVertex, horizonEdge ) { 805 | 806 | // all the half edges are created in ccw order thus the face is always pointing outside the hull 807 | 808 | const face = Face.create( eyeVertex, horizonEdge.tail(), horizonEdge.head() ); 809 | 810 | this.faces.push( face ); 811 | 812 | // join face.getEdge( - 1 ) with the horizon's opposite edge face.getEdge( - 1 ) = face.getEdge( 2 ) 813 | 814 | face.getEdge( - 1 ).setTwin( horizonEdge.twin ); 815 | 816 | return face.getEdge( 0 ); // the half edge whose vertex is the eyeVertex 817 | 818 | 819 | } 820 | 821 | // Adds 'horizon.length' faces to the hull, each face will be linked with the 822 | // horizon opposite face and the face on the left/right 823 | 824 | addNewFaces( eyeVertex, horizon ) { 825 | 826 | this.newFaces = []; 827 | 828 | let firstSideEdge = null; 829 | let previousSideEdge = null; 830 | 831 | for ( let i = 0; i < horizon.length; i ++ ) { 832 | 833 | const horizonEdge = horizon[ i ]; 834 | 835 | // returns the right side edge 836 | 837 | const sideEdge = this.addAdjoiningFace( eyeVertex, horizonEdge ); 838 | 839 | if ( firstSideEdge === null ) { 840 | 841 | firstSideEdge = sideEdge; 842 | 843 | } else { 844 | 845 | // joins face.getEdge( 1 ) with previousFace.getEdge( 0 ) 846 | 847 | sideEdge.next.setTwin( previousSideEdge ); 848 | 849 | } 850 | 851 | this.newFaces.push( sideEdge.face ); 852 | previousSideEdge = sideEdge; 853 | 854 | } 855 | 856 | // perform final join of new faces 857 | 858 | firstSideEdge.next.setTwin( previousSideEdge ); 859 | 860 | return this; 861 | 862 | } 863 | 864 | // Adds a vertex to the hull 865 | 866 | addVertexToHull( eyeVertex ) { 867 | 868 | const horizon = []; 869 | 870 | this.unassigned.clear(); 871 | 872 | // remove 'eyeVertex' from 'eyeVertex.face' so that it can't be added to the 'unassigned' vertex list 873 | 874 | this.removeVertexFromFace( eyeVertex, eyeVertex.face ); 875 | 876 | this.computeHorizon( eyeVertex.point, null, eyeVertex.face, horizon ); 877 | 878 | this.addNewFaces( eyeVertex, horizon ); 879 | 880 | // reassign 'unassigned' vertices to the new faces 881 | 882 | this.resolveUnassignedPoints( this.newFaces ); 883 | 884 | return this; 885 | 886 | } 887 | 888 | cleanup() { 889 | 890 | this.assigned.clear(); 891 | this.unassigned.clear(); 892 | this.newFaces = []; 893 | 894 | return this; 895 | 896 | } 897 | 898 | compute() { 899 | 900 | let vertex; 901 | 902 | this.computeInitialHull(); 903 | 904 | // add all available vertices gradually to the hull 905 | 906 | while ( ( vertex = this.nextVertexToAdd() ) !== undefined ) { 907 | 908 | this.addVertexToHull( vertex ); 909 | 910 | } 911 | 912 | this.reindexFaces(); 913 | 914 | this.cleanup(); 915 | 916 | return this; 917 | 918 | } 919 | 920 | } 921 | 922 | // 923 | 924 | class Face { 925 | 926 | constructor() { 927 | 928 | this.normal = new Vector3(); 929 | this.midpoint = new Vector3(); 930 | this.area = 0; 931 | 932 | this.constant = 0; // signed distance from face to the origin 933 | this.outside = null; // reference to a vertex in a vertex list this face can see 934 | this.mark = Visible; 935 | this.edge = null; 936 | 937 | } 938 | 939 | static create( a, b, c ) { 940 | 941 | const face = new Face(); 942 | 943 | const e0 = new HalfEdge( a, face ); 944 | const e1 = new HalfEdge( b, face ); 945 | const e2 = new HalfEdge( c, face ); 946 | 947 | // join edges 948 | 949 | e0.next = e2.prev = e1; 950 | e1.next = e0.prev = e2; 951 | e2.next = e1.prev = e0; 952 | 953 | // main half edge reference 954 | 955 | face.edge = e0; 956 | 957 | return face.compute(); 958 | 959 | } 960 | 961 | getEdge( i ) { 962 | 963 | let edge = this.edge; 964 | 965 | while ( i > 0 ) { 966 | 967 | edge = edge.next; 968 | i --; 969 | 970 | } 971 | 972 | while ( i < 0 ) { 973 | 974 | edge = edge.prev; 975 | i ++; 976 | 977 | } 978 | 979 | return edge; 980 | 981 | } 982 | 983 | compute() { 984 | 985 | const a = this.edge.tail(); 986 | const b = this.edge.head(); 987 | const c = this.edge.next.head(); 988 | 989 | _triangle.set( a.point, b.point, c.point ); 990 | 991 | _triangle.getNormal( this.normal ); 992 | _triangle.getMidpoint( this.midpoint ); 993 | this.area = _triangle.getArea(); 994 | 995 | this.constant = this.normal.dot( this.midpoint ); 996 | 997 | return this; 998 | 999 | } 1000 | 1001 | distanceToPoint( point ) { 1002 | 1003 | return this.normal.dot( point ) - this.constant; 1004 | 1005 | } 1006 | 1007 | } 1008 | 1009 | // Entity for a Doubly-Connected Edge List (DCEL). 1010 | 1011 | class HalfEdge { 1012 | 1013 | 1014 | constructor( vertex, face ) { 1015 | 1016 | this.vertex = vertex; 1017 | this.prev = null; 1018 | this.next = null; 1019 | this.twin = null; 1020 | this.face = face; 1021 | 1022 | } 1023 | 1024 | head() { 1025 | 1026 | return this.vertex; 1027 | 1028 | } 1029 | 1030 | tail() { 1031 | 1032 | return this.prev ? this.prev.vertex : null; 1033 | 1034 | } 1035 | 1036 | length() { 1037 | 1038 | const head = this.head(); 1039 | const tail = this.tail(); 1040 | 1041 | if ( tail !== null ) { 1042 | 1043 | return tail.point.distanceTo( head.point ); 1044 | 1045 | } 1046 | 1047 | return - 1; 1048 | 1049 | } 1050 | 1051 | lengthSquared() { 1052 | 1053 | const head = this.head(); 1054 | const tail = this.tail(); 1055 | 1056 | if ( tail !== null ) { 1057 | 1058 | return tail.point.distanceToSquared( head.point ); 1059 | 1060 | } 1061 | 1062 | return - 1; 1063 | 1064 | } 1065 | 1066 | setTwin( edge ) { 1067 | 1068 | this.twin = edge; 1069 | edge.twin = this; 1070 | 1071 | return this; 1072 | 1073 | } 1074 | 1075 | } 1076 | 1077 | // A vertex as a double linked list node. 1078 | 1079 | class VertexNode { 1080 | 1081 | constructor( point ) { 1082 | 1083 | this.point = point; 1084 | this.prev = null; 1085 | this.next = null; 1086 | this.face = null; // the face that is able to see this vertex 1087 | 1088 | } 1089 | 1090 | } 1091 | 1092 | // A double linked list that contains vertex nodes. 1093 | 1094 | class VertexList { 1095 | 1096 | constructor() { 1097 | 1098 | this.head = null; 1099 | this.tail = null; 1100 | 1101 | } 1102 | 1103 | first() { 1104 | 1105 | return this.head; 1106 | 1107 | } 1108 | 1109 | last() { 1110 | 1111 | return this.tail; 1112 | 1113 | } 1114 | 1115 | clear() { 1116 | 1117 | this.head = this.tail = null; 1118 | 1119 | return this; 1120 | 1121 | } 1122 | 1123 | // Inserts a vertex before the target vertex 1124 | 1125 | insertBefore( target, vertex ) { 1126 | 1127 | vertex.prev = target.prev; 1128 | vertex.next = target; 1129 | 1130 | if ( vertex.prev === null ) { 1131 | 1132 | this.head = vertex; 1133 | 1134 | } else { 1135 | 1136 | vertex.prev.next = vertex; 1137 | 1138 | } 1139 | 1140 | target.prev = vertex; 1141 | 1142 | return this; 1143 | 1144 | } 1145 | 1146 | // Inserts a vertex after the target vertex 1147 | 1148 | insertAfter( target, vertex ) { 1149 | 1150 | vertex.prev = target; 1151 | vertex.next = target.next; 1152 | 1153 | if ( vertex.next === null ) { 1154 | 1155 | this.tail = vertex; 1156 | 1157 | } else { 1158 | 1159 | vertex.next.prev = vertex; 1160 | 1161 | } 1162 | 1163 | target.next = vertex; 1164 | 1165 | return this; 1166 | 1167 | } 1168 | 1169 | // Appends a vertex to the end of the linked list 1170 | 1171 | append( vertex ) { 1172 | 1173 | if ( this.head === null ) { 1174 | 1175 | this.head = vertex; 1176 | 1177 | } else { 1178 | 1179 | this.tail.next = vertex; 1180 | 1181 | } 1182 | 1183 | vertex.prev = this.tail; 1184 | vertex.next = null; // the tail has no subsequent vertex 1185 | 1186 | this.tail = vertex; 1187 | 1188 | return this; 1189 | 1190 | } 1191 | 1192 | // Appends a chain of vertices where 'vertex' is the head. 1193 | 1194 | appendChain( vertex ) { 1195 | 1196 | if ( this.head === null ) { 1197 | 1198 | this.head = vertex; 1199 | 1200 | } else { 1201 | 1202 | this.tail.next = vertex; 1203 | 1204 | } 1205 | 1206 | vertex.prev = this.tail; 1207 | 1208 | // ensure that the 'tail' reference points to the last vertex of the chain 1209 | 1210 | while ( vertex.next !== null ) { 1211 | 1212 | vertex = vertex.next; 1213 | 1214 | } 1215 | 1216 | this.tail = vertex; 1217 | 1218 | return this; 1219 | 1220 | } 1221 | 1222 | // Removes a vertex from the linked list 1223 | 1224 | remove( vertex ) { 1225 | 1226 | if ( vertex.prev === null ) { 1227 | 1228 | this.head = vertex.next; 1229 | 1230 | } else { 1231 | 1232 | vertex.prev.next = vertex.next; 1233 | 1234 | } 1235 | 1236 | if ( vertex.next === null ) { 1237 | 1238 | this.tail = vertex.prev; 1239 | 1240 | } else { 1241 | 1242 | vertex.next.prev = vertex.prev; 1243 | 1244 | } 1245 | 1246 | return this; 1247 | 1248 | } 1249 | 1250 | // Removes a list of vertices whose 'head' is 'a' and whose 'tail' is b 1251 | 1252 | removeSubList( a, b ) { 1253 | 1254 | if ( a.prev === null ) { 1255 | 1256 | this.head = b.next; 1257 | 1258 | } else { 1259 | 1260 | a.prev.next = b.next; 1261 | 1262 | } 1263 | 1264 | if ( b.next === null ) { 1265 | 1266 | this.tail = a.prev; 1267 | 1268 | } else { 1269 | 1270 | b.next.prev = a.prev; 1271 | 1272 | } 1273 | 1274 | return this; 1275 | 1276 | } 1277 | 1278 | isEmpty() { 1279 | 1280 | return this.head === null; 1281 | 1282 | } 1283 | 1284 | } 1285 | 1286 | export { ConvexHull }; 1287 | -------------------------------------------------------------------------------- /modules/Simulator.mjs: -------------------------------------------------------------------------------- 1 | import { Rulial } from "./Rulial.mjs"; 2 | import { Rewriter } from "./Rewriter.mjs"; 3 | import { Graph3D } from "./Graph3D.mjs"; 4 | import { HDC } from "./HDC.mjs"; 5 | import { SpriteText } from "./SpriteText.mjs"; 6 | 7 | const hdc = new HDC(); 8 | 9 | /** 10 | * @class User interface for rewriter 11 | * @author Mika Suominen 12 | * @author Tuomas Sorakivi 13 | */ 14 | class Simulator extends Rewriter { 15 | 16 | /** 17 | * Reference frame 18 | * @typedef {Object} RefFrame 19 | * @property {number} view Viewmode, 1 = space (default), 2 = time 20 | * @property {boolean} leaves If true, show only leaves of the multiway system 21 | * @property {number} branches Field for branches to show, 0 = all 22 | */ 23 | 24 | /** 25 | * Creates an instance of Simulator. 26 | * @param {Object} element DOM element of the canvas 27 | * @param {Object} status DOM element of the status 28 | * @constructor 29 | */ 30 | constructor(canvas,status) { 31 | super(); 32 | this.G = new Graph3D(canvas); 33 | this.dom = status; // DOM element for status 34 | 35 | this.maxtokenid = -1; // Max token id shown in phase view 36 | 37 | this.pos = 0; // Event log position 38 | this.playpos = 0; // Play position 39 | this.playing = false; // Is play on? 40 | this.stopfn = null; // stop callback function 41 | 42 | // Observer's reference frame 43 | this.observer = { 44 | view: 1, // space view 45 | leaves: true, // show only leaves 46 | branches: 0 // show all branches 47 | }; 48 | 49 | this.H = new Map(); // Highlights 50 | this.F = null; // Fields 51 | 52 | // Variables x and y 53 | this.x = 0; 54 | this.y = 0; 55 | this.G.FG 56 | .onNodeClick( Simulator.onNodeClick.bind(this) ) 57 | .onNodeRightClick( Simulator.onNodeRightClick.bind(this) ) 58 | .nodeThreeObjectExtend( true ); 59 | } 60 | 61 | /** 62 | * Click on node, set x 63 | * @param {Object} n Node object 64 | * @param {Object} event Event object 65 | */ 66 | static onNodeClick( n, event) { 67 | this.x = n.id; 68 | this.G.FG.nodeThreeObject( Simulator.nodeThreeObject.bind(this) ); 69 | this.refresh(); 70 | } 71 | 72 | /** 73 | * Right click on node, set y 74 | * @param {Object} n Node object 75 | * @param {Object} event Event object 76 | */ 77 | static onNodeRightClick( n, event) { 78 | this.y = n.id; 79 | this.G.FG.nodeThreeObject( Simulator.nodeThreeObject.bind(this) ); 80 | this.refresh(); 81 | } 82 | 83 | /** 84 | * Display x and/or y. 85 | * @param {Object} n Node object 86 | */ 87 | static nodeThreeObject( n ) { 88 | if ( n.id === this.x ) { 89 | if ( n.id === this.y ) { 90 | n.big = true; 91 | return new SpriteText("x=y",26,"DarkSlateGray"); 92 | } else { 93 | n.big = true; 94 | return new SpriteText("x",26,"DarkSlateGray"); 95 | } 96 | } else if ( n.id === this.y ) { 97 | n.big = true; 98 | return new SpriteText("y",26,"DarkSlateGray"); 99 | } else { 100 | n.big = false; 101 | return false; 102 | } 103 | } 104 | 105 | /** 106 | * Check rewriting rule by passing it to algorithmic parser. 107 | * @param {string} rulestr Rewriting rule in string format. 108 | * @return {string} Rewriting rule in standard string format. 109 | */ 110 | validateRule( rulestr ) { 111 | let r = new Rulial(); 112 | r.setRule( rulestr ); 113 | return r.getRule(); 114 | } 115 | 116 | /** 117 | * Set observer's reference frame 118 | * @param {RefFrame} rf 119 | */ 120 | setRefFrame( rf ) { 121 | if ( rf.hasOwnProperty("view") ) { 122 | let initlen; 123 | switch( rf.view ) { 124 | case "time": 125 | this.observer.view = 2; 126 | initlen = 1; 127 | break; 128 | case "phase": 129 | this.observer.view = 3; 130 | initlen = this.rulial.initial.length; 131 | this.maxtokenid = -1; 132 | break; 133 | default: // space 134 | this.observer.view = 1; 135 | initlen = this.rulial.initial.length; 136 | } 137 | 138 | // Stop animation and set position to start 139 | this.stop(); 140 | this.pos = 0; 141 | this.playpos = 0; 142 | 143 | // Reset graph 144 | this.G.reset( this.observer.view ); 145 | 146 | // First additions 147 | this.tick( initlen ); 148 | } 149 | 150 | if ( ( rf.hasOwnProperty("branches") && rf.branches !== this.observer.branches ) || 151 | ( rf.hasOwnProperty("leaves") && rf.leaves !== this.observer.leaves ) ) { 152 | let update = false; 153 | if ( rf.hasOwnProperty("branches") ) { 154 | this.observer.branches = rf.branches; 155 | update = true; 156 | } 157 | if ( rf.hasOwnProperty("leaves") ) { 158 | this.observer.leaves = rf.leaves; 159 | if ( this.observer.view === 1 ) update = true; 160 | } 161 | if ( update ) { 162 | switch( this.observer.view ) { 163 | case 1: // Space 164 | for( let i=0; i 0 ) { 171 | let rm = []; 172 | for( let t of this.G.T.keys() ) { 173 | if ( t.mw.some( ev => !( ev.b & this.observer.branches ) ) ) rm.push( t ); 174 | } 175 | rm.forEach( this.G.del, this.G ); 176 | } 177 | 178 | // Forward 179 | for( let i = 0; i < this.pos; i++ ) { 180 | this.processCausalEvent( this.multiway.EV[ i ] ); 181 | } 182 | break; 183 | } 184 | } 185 | this.refresh(); 186 | } 187 | 188 | } 189 | 190 | /** 191 | * Refresh UI. 192 | */ 193 | refresh() { 194 | this.processField(); 195 | this.processHighlights(); 196 | if ( this.dom ) { 197 | let s = this.status(); 198 | let str = ""; 199 | Object.keys(s).forEach( k => { 200 | str += ''+k+''+s[k]+''; 201 | }); 202 | this.dom.innerHTML = str; 203 | } 204 | this.G.refresh(); // Refresh graph 205 | } 206 | 207 | /** 208 | * Show of hide edges in spatial graph. 209 | * @param {Object} ev Event reference 210 | * @param {boolean} [reverse=false] If true, reverse the process of add and remove nodes 211 | * @return {boolean} True, if some was made 212 | */ 213 | processSpatialEvent( ev, reverse = false ) { 214 | const tokens = [ ...ev.child, ...ev.parent ]; 215 | 216 | let change = false; 217 | let bfn = (a,x) => a | (x.id <= ev.id ? x.b :0); 218 | if ( this.observer.branches ) { 219 | tokens.forEach( t => { 220 | let b = t.parent.reduce( bfn, 0 ); 221 | if ( this.observer.leaves ) { 222 | b &= ~t.child.reduce( bfn, 0 ); 223 | } 224 | if ( b & this.observer.branches ) { 225 | if ( !this.G.T.has( t ) && !reverse) { 226 | this.G.add(t,1); 227 | change = true; 228 | } else if ( this.G.T.has( t ) && reverse ) { 229 | this.G.del(t); 230 | change = true; 231 | } 232 | } else { 233 | if ( this.G.T.has( t ) && !reverse) { 234 | this.G.del(t); 235 | change = true; 236 | } else if ( !this.G.T.has( t ) && reverse) { 237 | this.G.add(t,1); 238 | change = true; 239 | } 240 | } 241 | }); 242 | } else { 243 | tokens.forEach( t => { 244 | if ( this.observer.leaves && t.child.some( x => x.id <= ev.id ) ) { 245 | if ( this.G.T.has( t ) && !reverse) { 246 | this.G.del(t); 247 | change = true; 248 | } 249 | else if ( !this.G.T.has( t ) && reverse) { 250 | this.G.add(t,1); 251 | change = true; 252 | } 253 | } else { 254 | if ( !this.G.T.has( t ) && !reverse) { 255 | this.G.add(t,1); 256 | change = true; 257 | } else if ( this.G.T.has( t ) && reverse) { 258 | this.G.del(t); 259 | change = true; 260 | } 261 | } 262 | }); 263 | } 264 | 265 | return change; 266 | } 267 | 268 | /** 269 | * Show of hide edges in causal graph. 270 | * @param {Object} ev Event reference 271 | * @return {boolean} True, if change was made 272 | */ 273 | processCausalEvent( ev ) { 274 | let change = false; 275 | if ( this.observer.branches && !( ev.b & this.observer.branches ) ) return change; 276 | if ( ev.parent.length === 0 ) { 277 | this.G.add( { mw: [ ev ], edge: [ ev.id ] }, 2 ); 278 | change = true; 279 | } else { 280 | let pev = [ ...new Set( ev.parent.map( x => x.parent ).flat() ) ]; 281 | pev.forEach( x => { 282 | if ( x.id < ev.id && ( !this.observer.branches || (x.b & this.observer.branches)) ) { 283 | this.G.add( { mw: [ x, ev ], edge: [ x.id, ev.id ] }, 2 ); 284 | change = true; 285 | } 286 | }); 287 | } 288 | return change; 289 | } 290 | 291 | /** 292 | * Show of hide edges in phase graph. 293 | * @param {Object} ev Event reference 294 | * @return {boolean} True, if change was made 295 | */ 296 | processPhaseEvent( ev ) { 297 | let change = false; 298 | ev.child.forEach( t => { 299 | if ( t.id > this.maxtokenid ) { 300 | if ( t.nn[0].t === null ) { 301 | this.G.add( { mw: [ t ], edge: [ t.id ], w: 1 }, 3 ); 302 | } else { 303 | if ( t.nn[0].d < this.opt.phasecutoff ) { 304 | this.G.add( { mw: [ t.nn[0].t ], edge: [ t.nn[0].t.id ], w: 1 }, 3 ); 305 | } else { 306 | this.G.add( { mw: [ t.nn[0].t, t ], edge: [ t.nn[0].t.id, t.id ], w: t.nn[0].d }, 3 ); 307 | if ( t.nn[1].d < 2560 ) { 308 | this.G.add( { mw: [ t.nn[1].t, t ], edge: [ t.nn[1].t.id, t.id ], w: t.nn[1].d }, 3 ); 309 | } 310 | if ( t.nn[2].d < 2560 ) { 311 | this.G.add( { mw: [ t.nn[2].t, t ], edge: [ t.nn[2].t.id, t.id ], w: t.nn[2].d }, 3 ); 312 | } 313 | } 314 | } 315 | this.maxtokenid = t.id; 316 | change = true; 317 | } 318 | }); 319 | return change; 320 | } 321 | 322 | /** 323 | * Process events. 324 | * @param {number} [steps=1] Number of steps to process 325 | * @param {boolean} [reverse=false] If true, reverse the process of add and remove nodes 326 | * @return {boolean} True there are more events to process. 327 | */ 328 | tick( steps = 1, reverse = false ) { 329 | while ( steps > 0 && (( reverse && this.pos > 0) || ( !reverse && this.pos < this.multiway.EV.length))) { 330 | let ev = reverse ? this.multiway.EV[ this.pos - 1 ] : this.multiway.EV[ this.pos ]; 331 | let changed = false; 332 | switch( this.observer.view ) { 333 | case 1: 334 | changed = this.processSpatialEvent( ev, reverse); 335 | break; 336 | case 2: 337 | changed = this.processCausalEvent( ev ); 338 | break; 339 | case 3: 340 | changed = this.processPhaseEvent( ev ); 341 | } 342 | if ( changed ) { 343 | steps--; 344 | if ( reverse ) { 345 | this.playpos = 0; 346 | } else { 347 | this.playpos++; 348 | } 349 | } 350 | if (reverse) { 351 | this.pos--; 352 | } else { 353 | this.pos++; 354 | } 355 | } 356 | this.refresh(); 357 | 358 | return reverse ? (this.pos > 1) : (this.pos < this.multiway.EV.length); 359 | } 360 | 361 | /** 362 | * Timed update process. 363 | */ 364 | update() { 365 | const steps = Math.min( 15, Math.ceil( ( this.playpos + 1 ) / 10) ); 366 | if ( this.tick( steps ) ) { 367 | if ( this.playing ) setTimeout( this.update.bind(this), 250 ); 368 | } else { 369 | this.stop(); 370 | if ( this.stopfn ) this.stopfn(); 371 | } 372 | } 373 | 374 | /** 375 | * Callback for animation end. 376 | * @callback stopcallbackfn 377 | */ 378 | 379 | /** 380 | * Play animation. 381 | * @param {stopcallbackfn} stopcallbackfn Animation stopped callback function 382 | */ 383 | play( stopcallbackfn = null ) { 384 | this.G.FG.enablePointerInteraction( false ); 385 | this.stopfn = stopcallbackfn; 386 | this.playpos = 0; 387 | this.playing = true; 388 | this.update(); 389 | } 390 | 391 | /** 392 | * Stop animation. 393 | */ 394 | stop() { 395 | this.playing = false; 396 | this.G.FG.enablePointerInteraction( true ); 397 | } 398 | 399 | /** 400 | * Skip to the end of the animation. 401 | */ 402 | final() { 403 | this.stop(); 404 | this.G.force(-1,10); 405 | this.tick( this.multiway.EV.length ); 406 | } 407 | 408 | 409 | /** 410 | * Highlight nodes/edges. 411 | * @param {string} str Commands in string format 412 | * @param {number} style Style to use in highlighting. 413 | * @param {Object} element DOM element for text results. 414 | * @param {boolean} surface If true, fill hypersurfaces. 415 | * @param {boolean} background If false, show only highlighted nodes/edges. 416 | */ 417 | setHighlight( str, style, element, surface = true, background = true ) { 418 | // Parse command string for rules and commands 419 | let [ rules, cmds ] = Rulial.parseCommands( str ).reduce( (a,b) => { 420 | if ( b.cmd === '' ) { 421 | let r = Rulial.parseRules( b.params ); 422 | if ( r.length === 0 ) { 423 | r = Rulial.parseRules( [ b.params[0] + "->" + b.params[0] ] ); 424 | } 425 | if ( r.length ) { 426 | a[0].push( ...r ); // rule 427 | } 428 | } else { 429 | a[1].push(b); // command 430 | } 431 | return a; 432 | }, [[],[]]); 433 | 434 | // Highlight object 435 | const o = { 436 | cmds: cmds, 437 | rules: rules, 438 | style: style, 439 | element: element, 440 | surface: surface, 441 | background: background 442 | }; 443 | 444 | this.G.clearHighlight( style ); 445 | let result = this.calculateHighlight( cmds, rules ); 446 | this.G.setHighlight( result, style, surface, background ); 447 | 448 | if ( element ) { 449 | element.textContent = '['+result.r.join("|")+']'; 450 | } 451 | 452 | this.H.set( style, o ); 453 | } 454 | 455 | /** 456 | * Process existing highlights. 457 | */ 458 | processHighlights() { 459 | for( let h of this.H.values() ) { 460 | try { 461 | this.G.clearHighlight( h.style ); 462 | let result = this.calculateHighlight( h.cmds, h.rules ); 463 | this.G.setHighlight( result, h.style, h.surface, h.background ); 464 | if ( h.element ) { 465 | h.element.textContent = '['+result.r.join("|")+']'; 466 | } 467 | } 468 | catch(e) { 469 | if ( h.element ) { 470 | h.element.textContent = '[Error]'; 471 | } 472 | } 473 | } 474 | } 475 | 476 | /** 477 | * Run commands given in string format. 478 | * @param {Object[]} cmds Commands 479 | * @param {Object[]} rules Patterns to highlight 480 | * @return {Object} Edges, vertices, points and results. 481 | */ 482 | calculateHighlight( cmds, rules ) { 483 | const v = [], e = [], p = [], r = []; 484 | 485 | if ( cmds ) { 486 | cmds.forEach( (c,i) => { 487 | let ps = c.params.map( x => { 488 | if ( x === 'x' ) return this.x; 489 | if ( x === 'y' ) return this.y; 490 | return x; 491 | }); 492 | 493 | 494 | let ret; 495 | 496 | switch( c.cmd ) { 497 | case "geodesic": case "line": case "path": 498 | if ( ps.length < 2 ) throw new TypeError("Geodesic: Invalid number of parameters."); 499 | p.push( parseInt(ps[0]), parseInt(ps[1]) ); 500 | ret = this.G.geodesic( parseInt(ps[0]), parseInt(ps[1]), ps.includes("dir"), ps.includes("rev"), ps.includes("all") ).flat(); 501 | r.push( ret.length ); 502 | e.push( ret ); 503 | break; 504 | 505 | case "phase": 506 | if ( ps.length < 2 ) throw new TypeError("Phase: Invalid number of parameters."); 507 | let a = this.G.V.get( parseInt(ps[0]) ); 508 | let b = this.G.V.get( parseInt(ps[1]) ); 509 | if ( a && b ) { 510 | p.push( parseInt(ps[0]), parseInt(ps[1]) ); 511 | r.push( hdc.d( a.bc,b.bc ) ); 512 | ret = this.G.geodesic( parseInt(ps[0]), parseInt(ps[1]), ps.includes("dir"), ps.includes("rev"), ps.includes("all") ).flat(); 513 | e.push( ret ); 514 | } 515 | break; 516 | 517 | case "curv": case "curvature": 518 | if ( ps.length < 2 ) throw new TypeError("Curv: Invalid number of parameters."); 519 | p.push( parseInt(ps[0]), parseInt(ps[1]) ); 520 | let curv = this.G.orc( parseInt(ps[0]), parseInt(ps[1]) ); 521 | curv = curv.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 }); 522 | r.push( curv ); 523 | e.push( this.G.geodesic( parseInt(ps[0]), parseInt(ps[1]), ps.includes("dir"), ps.includes("rev"), ps.includes("all") ).flat() ); 524 | v.push( this.G.nsphere( parseInt(ps[0]), 1 ) ); 525 | v.push( this.G.nsphere( parseInt(ps[1]), 1 ) ); 526 | break; 527 | 528 | case "dim": case "dimension": 529 | let origin = this.G.nodes[ Math.floor( this.G.nodes.length / 2 ) ].id; 530 | if ( ps.length ) origin = parseInt(ps[0]); 531 | let tree = this.G.tree( origin ); 532 | let radius = Math.max(1,Math.floor( tree.length/4 )); 533 | if ( ps.length > 1 ) radius = parseInt(ps[1]); 534 | if ( (radius+3) < tree.length ) { 535 | let dfn = (rad) => (Math.log(tree.slice(0,rad+1).flat().length)/Math.log(rad)); 536 | let rads = Array(4).fill().map((x,j) => j+radius); 537 | let dim = rads.reduce((a,x) => a+dfn(x),0) / rads.length; 538 | ret = this.G.nball( origin, radius+1 ); 539 | p.push( origin ); 540 | r.push( dim.toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 }) ); 541 | e.push( ret ); 542 | v.push( [ ...new Set( ret.flat() ) ] ); 543 | } 544 | break; 545 | 546 | case "nsphere": case "sphere": 547 | if ( ps.length < 2 ) throw new TypeError("Nsphere: Invalid number of parameters."); 548 | p.push( parseInt(ps[0]) ); 549 | ret = this.G.nsphere( parseInt(ps[0]), parseInt(ps[1]), ps.includes("dir"), ps.includes("rev") ); 550 | r.push( ret.length ); 551 | v.push( ret ); 552 | break; 553 | 554 | case "nball": case "ball": case "tree": 555 | if ( ps.length < 2 ) throw new TypeError("Nball: Invalid number of parameters."); 556 | p.push( parseInt(ps[0]) ); 557 | ret = this.G.nball( parseInt(ps[0]), parseInt(ps[1]), ps.includes("dir"), ps.includes("rev") ); 558 | r.push( ret.length ); 559 | e.push( ret ); 560 | v.push( [ ...new Set( ret.flat() ) ] ); 561 | break; 562 | 563 | case "random": case "walk": 564 | if ( ps.length < 2 ) throw new TypeError("Random: Invalid number of parameters."); 565 | p.push( parseInt(ps[0]) ); 566 | ret = this.G.random( parseInt(ps[0]), parseInt(ps[1]), ps.includes("dir"), ps.includes("rev") ); 567 | r.push( ret.length ); 568 | e.push( ret ); 569 | break; 570 | 571 | case "worldline": case "timeline": 572 | if ( this.observer.view === 2 ) { 573 | if ( ps.length < 1 ) throw new TypeError("Worldline: Invalid number of parameters."); 574 | let maxv = this.G.nodes.length; 575 | ret = this.multiway.worldline( [ ...ps.map( x => parseInt(x) ) ] ).filter( e => { 576 | return (e[0] < maxv) && (e[1] < maxv); 577 | }); 578 | r.push( ret.length ); 579 | e.push( ret ); 580 | if ( ret.length ) { 581 | p.push( ret[0][0], ret[ ret.length - 1][1] ); 582 | } 583 | } 584 | break; 585 | 586 | case "lightcone": 587 | if ( this.observer.view === 2 ) { 588 | if ( ps.length < 2 ) throw new TypeError("Lightcone: Invalid number of parameters."); 589 | p.push( parseInt(ps[0]) ); 590 | ret = this.G.lightcone( parseInt(ps[0]), parseInt(ps[1]) ); 591 | r.push( ret["past"].length + ret["future"].length ); 592 | e.push( [ ...ret["past"], ...ret["future"] ] ); 593 | v.push( [ ...new Set( ret["past"].flat() ) ] ); 594 | v.push( [ ...new Set( ret["future"].flat() ) ] ); 595 | } 596 | break; 597 | 598 | case "surface": case "hypersurface": 599 | if ( ps.length < 2 ) throw new TypeError("Surface: Invalid number of parameters."); 600 | ret = this.G.surface( parseInt(ps[0]), parseInt(ps[1]) ); 601 | r.push( ret.length ); 602 | v.push( ret ); 603 | break; 604 | 605 | default: 606 | throw new TypeError( "Unknown command: " + c.cmd ); 607 | } 608 | 609 | }); 610 | } 611 | 612 | // Rules 613 | if ( rules && rules.length>0 && this.observer.view === 1 ) { 614 | let rw = new Rewriter(); 615 | rw.rulial.rules = rules; // Rules 616 | rw.multiway = this.G; // Multiway system to search from 617 | let g = rw.findMatches(); 618 | while( !g.next().done ); // Find matches 619 | r.push( rw.M.length ); 620 | for( let m of rw.M ) { 621 | let ls = m.hit.filter( (_,i) => m.rule.lhsdup[i] ).map( t => this.G.T.get(t).map( l => [l.source.id,l.target.id] ) ).flat(); 622 | e.push( ls ); 623 | } 624 | } 625 | 626 | return { e: e, v: v, p: p, r: r }; 627 | 628 | } 629 | 630 | /** 631 | * Clear highlight of the given style. 632 | * @param {number} style 633 | */ 634 | clearHighlight( style ) { 635 | let o = this.H.get( style ); 636 | if ( !o ) return; 637 | 638 | if ( o.element ) { 639 | o.element.textContent = ''; 640 | } 641 | 642 | this.G.clearHighlight( style ); 643 | 644 | this.H.delete( style ); 645 | } 646 | 647 | /** 648 | * Set gradient colours based on fields 649 | * @param {string} str Fields separated with semicolon 650 | * @param {Object} element DOM element for text results. 651 | * @param {Object} opt Options 652 | */ 653 | setField( str, element, opt ) { 654 | opt = opt || {}; 655 | 656 | // Parse command string 657 | let cmds = Rulial.parseCommands( str ); 658 | 659 | // Field object 660 | this.F = { 661 | cmds: cmds, 662 | element: element, 663 | opt: opt 664 | }; 665 | 666 | // Calculate & display 667 | this.G.clearField(); 668 | let result = this.calculateField( cmds, opt ); 669 | this.G.setField(); 670 | 671 | // Display numeric results 672 | if ( element ) { 673 | element.textContent = '['+result.join("|")+']'; 674 | } 675 | 676 | } 677 | 678 | /** 679 | * Set gradient colours based on fields 680 | * @param {string} cmds Fields separated with semicolon 681 | * @param {Object} opt Options 682 | * @return {string[]} Text results 683 | */ 684 | calculateField( cmds, opt ) { 685 | const grad = new Map(); 686 | const results = []; 687 | 688 | cmds.forEach( (c,i) => { 689 | let tmp = new Map(); 690 | let scaleZero = false; 691 | let digits = 1; 692 | let min, max; 693 | 694 | // Reference point 695 | let ref; 696 | let pndx = 0; 697 | if ( ( c.cmd === 'phase' || c.cmd === 'probability' ) && c.params[pndx] ) { 698 | let id; 699 | if ( c.params[pndx] === 'x' ) { 700 | id = this.x; 701 | } else if ( c.params[pndx] === 'y' ) { 702 | id = this.y; 703 | } else { 704 | id = parseInt( c.params[pndx] ); 705 | } 706 | ref = this.G.V.get( id ); 707 | pndx++; 708 | } 709 | 710 | // Limits 711 | let minp, minv; 712 | if ( c.params[pndx] ) { 713 | if ( c.params[pndx].slice(-1)==="%" ) { 714 | minp = parseFloat( c.params[pndx] ) / 100; 715 | } else { 716 | minv = parseFloat( c.params[pndx] ); 717 | min = minv; 718 | } 719 | pndx++; 720 | } 721 | let maxp, maxv; 722 | if ( c.params[pndx] ) { 723 | if ( c.params[pndx].slice(-1)==="%" ) { 724 | maxp = parseFloat( c.params[pndx] ) / 100; 725 | } else { 726 | maxv = parseFloat( c.params[pndx] ); 727 | max = maxv; 728 | } 729 | } 730 | 731 | // Function to set value 732 | let setfn = (e,val) => { 733 | if (e && (typeof minv === 'undefined' || val>=minv) && 734 | (typeof maxv === 'undefined' || val<=maxv) ) tmp.set(e,val); 735 | }; 736 | 737 | switch( c.cmd ) { 738 | case "created": 739 | for ( const [t,ls] of this.G.T.entries() ) { 740 | if ( ls.length ) { 741 | let val = this.G.links.indexOf(ls[0]); 742 | if ( val !== -1 ) setfn( t, val ); 743 | } 744 | } 745 | break; 746 | 747 | case "branch": 748 | let bits = [...Array(this.opt.evolution || 4)].map( (_,i) => i ); 749 | let nums = bits.map( x => Math.pow(2,x) ); 750 | let posid = this.pos > 0 ? this.multiway.EV[ this.pos-1 ].id : 0; 751 | if ( this.observer.view === 1 ) { 752 | for ( const t of this.G.T.keys() ) { 753 | let b = t.parent.reduce( (a,x) => a | (x.id <= posid ? x.b : 0), 0); 754 | if ( b > 0 ) { 755 | let val = bits.filter( (x,i) => b & nums[i] ); 756 | setfn( t, val.reduce( (a,x) => a+x, 0 )/val.length + 1 ); 757 | } 758 | } 759 | } else if ( this.observer.view === 2 ) { 760 | for ( const t of this.G.T.keys() ) { 761 | let b = t.mw.reduce( (a,x) => a | x.b, 0 ); 762 | if ( b > 0 ) { 763 | let val = bits.filter( (x,i) => b & nums[i] ); 764 | setfn( t, val.reduce( (a,x) => a+x, 0 )/val.length + 1 ); 765 | } 766 | } 767 | } else if ( this.observer.view === 3 ) { 768 | for ( const t of this.G.T.keys() ) { 769 | let b = t.mw[ t.mw.length - 1 ].parent.reduce( (a,x) => a | x.b, 0 ); 770 | if ( b > 0 ) { 771 | let val = bits.filter( (x,i) => b & nums[i] ); 772 | setfn( t, val.reduce( (a,x) => a+x, 0 )/val.length + 1 ); 773 | } 774 | } 775 | } 776 | min = min || 1; 777 | max = max || this.opt.evolution || 4; 778 | break; 779 | 780 | case "degree": 781 | for ( const [t,ls] of this.G.T.entries() ) { 782 | if ( ls.length ) { 783 | let val = ls.reduce( (a,l) => a + l.source.source.length + 784 | l.source.target.length + l.target.source.length + 785 | l.target.target.length, 0 ) / 2; 786 | setfn( t, val ); 787 | } 788 | } 789 | break; 790 | 791 | case "energy": case "mass": case "momentum": 792 | if ( this.observer.view === 1 ) { 793 | for ( const t of this.G.T.keys() ) { 794 | let evs = [ ...t.parent, ...t.child ].filter( x => x.rule ); 795 | if ( evs.length ) setfn( t, evs.reduce( (a,x) => a + x.rule[c.cmd],0 ) / evs.length ); 796 | } 797 | } else if ( this.observer.view === 2 ) { 798 | for ( const t of this.G.T.keys() ) { 799 | let ev = t.mw[ t.mw.length-1 ]; 800 | if ( ev.rule ) setfn( t, ev.rule[c.cmd] ); 801 | } 802 | } else if ( this.observer.view === 3 ) { 803 | for ( const t of this.G.T.keys() ) { 804 | let evs = [ ...t.mw[ t.mw.length-1 ].parent, ...t.mw[ t.mw.length-1 ].child ].filter( x => x.rule ); 805 | if ( evs.length ) setfn( t, evs.reduce( (a,x) => a + x.rule[c.cmd],0 ) / evs.length ); 806 | } 807 | } 808 | break; 809 | 810 | case "step": 811 | if ( this.observer.view === 1 ) { 812 | for ( const t of this.G.T.keys() ) { 813 | setfn( t, t.parent.reduce( (a,x) => a + x.step,0 ) / t.parent.length ); 814 | } 815 | } else if ( this.observer.view === 2 ) { 816 | for ( const t of this.G.T.keys() ) { 817 | setfn( t, t.mw[ t.mw.length-1 ].step ); 818 | } 819 | } else if ( this.observer.view === 3 ) { 820 | for ( const t of this.G.T.keys() ) { 821 | setfn( t, t.mw[ t.mw.length -1 ].parent.reduce( (a,x) => a + x.step,0 ) / t.mw[ t.mw.length -1 ].parent.length ); 822 | } 823 | } 824 | break; 825 | 826 | case "pathcnt": 827 | if ( this.observer.view === 1 ) { 828 | for ( const t of this.G.T.keys() ) { 829 | setfn( t, t.pathcnt ); 830 | } 831 | } else if ( this.observer.view === 2 || this.observer.view === 3 ) { 832 | for ( const t of this.G.T.keys() ) { 833 | setfn( t, t.mw[ t.mw.length-1 ].pathcnt ); 834 | } 835 | } 836 | break; 837 | 838 | case "phase": 839 | if ( ref ) { 840 | if ( this.observer.view === 1 ) { 841 | for ( const t of this.G.T.keys() ) { 842 | setfn( t, hdc.d( t.bc, ref.bc ) ); 843 | } 844 | } else if ( this.observer.view === 2 || this.observer.view === 3 ) { 845 | for ( const t of this.G.T.keys() ) { 846 | setfn( t, hdc.d( t.mw[ t.mw.length-1 ].bc, ref.bc ) ); 847 | } 848 | } 849 | } 850 | break; 851 | 852 | case "probability": 853 | if ( ref ) { 854 | let tref = ref.t[ ref.t.length - 1 ]; 855 | if ( this.observer.view === 1 ) { 856 | for ( const t of this.G.T.keys() ) { 857 | setfn( t, this.multiway.probability( tref, t, this.G.T ) ); 858 | } 859 | } else if ( this.observer.view === 2 ) { 860 | /* for ( const t of this.G.T.keys() ) { 861 | let ev = t.mw[ t.mw.length-1 ]; 862 | let pc = ev.pathcnt; 863 | setfn( t, pc / s[ ev.step ] ); 864 | } */ 865 | } else if ( this.observer.view === 3 ) { 866 | /* for ( const t of this.G.T.keys() ) { 867 | setfn( t, t.mw[ t.mw.length -1 ].pathcnt / sum ); 868 | } */ 869 | } 870 | digits = 4; 871 | } 872 | break; 873 | 874 | case "curvature": 875 | for ( const [t,ls] of this.G.T.entries() ) { 876 | if ( ls.length && ls[0].source !== ls[0].target ) { 877 | let orc = this.G.orc( ls[0].source.id, ls[0].target.id ); 878 | if ( isNaN(orc) || !isFinite(orc) ) continue; 879 | setfn( t, orc ); 880 | } 881 | } 882 | scaleZero = true; 883 | digits = 2; 884 | break; 885 | 886 | default: 887 | throw new TypeError( "Unknown command: " + c.cmd ); 888 | } 889 | 890 | // Min, max and scaling factors 891 | min = min || Math.min( ...tmp.values() ); 892 | max = max || Math.max( ...tmp.values() ); 893 | let scaleNeg = 1, scalePos = 1; 894 | 895 | // Results 896 | results.push( [ 897 | min.toLocaleString(undefined, { maximumFractionDigits: digits, minimumFractionDigits: 0 }), 898 | max.toLocaleString(undefined, { maximumFractionDigits: digits, minimumFractionDigits: 0 }) 899 | ].join("<") ); 900 | 901 | if ( scaleZero ) { 902 | // Scale zero to midpoint 903 | let limit = Math.max( Math.abs(min), Math.abs(max) ); 904 | if ( min < 0 ) scaleNeg = limit / Math.abs(min); 905 | if ( max > 0 ) scalePos = limit / Math.abs(max); 906 | min = -limit; 907 | max = limit; 908 | } 909 | 910 | // Normalize 911 | let delta = max - min; 912 | for ( const [key, value] of tmp.entries() ) { 913 | let v = (value > 0 ? scalePos : scaleNeg ) * value; 914 | let norm = ( (min===max) ? 0.5 : ( (v - min) / delta ) ); 915 | 916 | // Limits 917 | if ( (minp && normmaxp) ) continue; 918 | 919 | // Add to final 920 | if ( grad.has( key ) ) { 921 | grad.get( key ).push( norm ); 922 | } else { 923 | grad.set( key, [ norm ] ); 924 | } 925 | } 926 | 927 | }); 928 | 929 | // Set gradient as a mean of the normalized value 930 | for (const [key, value] of grad.entries()) { 931 | let mean = value.reduce( (a,b) => a + b, 0 ) / value.length; 932 | this.G.T.get( key ).forEach( l => l.grad = mean ); 933 | } 934 | 935 | // Calculate values for nodes 936 | this.G.nodes.forEach( n => { 937 | let links = [ ...n.source, ...n.target ]; 938 | if ( links.every( l => l.hasOwnProperty("grad") ) ) { 939 | n.grad = links.reduce( (a,b) => a + b.grad, 0) / links.length; 940 | } 941 | }); 942 | 943 | // Smooth 944 | if ( opt.smooth ) { 945 | for( const l of this.G.links ) { 946 | if ( l.hasOwnProperty("grad") && l.source.hasOwnProperty("grad") && l.target.hasOwnProperty("grad") ) { 947 | l.grad = ( 2 * l.grad + l.source.grad + l.target.grad ) / 4; 948 | } 949 | } 950 | } 951 | 952 | return results; 953 | } 954 | 955 | /** 956 | * Process existing highlights. 957 | */ 958 | processField() { 959 | if ( !this.F ) return; 960 | 961 | try { 962 | this.G.clearField(); 963 | let result = this.calculateField( this.F.cmds, this.F.opt ); 964 | this.G.setField(); 965 | 966 | if ( this.F.element ) { 967 | this.F.element.textContent = '['+result.join("|")+']'; 968 | } 969 | } 970 | catch(e) { 971 | if ( this.F.element ) { 972 | this.F.element.textContent = '[Error]'; 973 | } 974 | } 975 | } 976 | 977 | 978 | /** 979 | * Clear gradient 980 | */ 981 | clearField() { 982 | if ( this.F ) { 983 | if ( this.F.element ) { 984 | this.F.element.textContent = ''; 985 | } 986 | delete this.F; 987 | } 988 | this.G.clearField(); 989 | } 990 | 991 | /** 992 | * Report status. 993 | * @return {Object} Status of the Multiway3D. 994 | */ 995 | status() { 996 | return { ...this.G.status(), events: this.pos+"/"+this.multiway.EV.length }; 997 | } 998 | 999 | } 1000 | 1001 | export { Simulator }; 1002 | --------------------------------------------------------------------------------