import * as PERMUTE from './permute.js'; import * as CELLINDEX from './cellindex.js'; function index_nodes(nodes, scale) { let i = 1; for( const n of nodes ) { n["id"] = i; i++; } } function scale_nodes(nodes, scale) { for( const n of nodes ) { for( const a of [ 'x', 'y', 'z', 'w' ] ) { n[a] = scale * n[a]; } } } function dist2(n1, n2) { return (n1.x - n2.x) ** 2 + (n1.y - n2.y) ** 2 + (n1.z - n2.z) ** 2 + (n1.w - n2.w) ** 2; } export function auto_detect_edges(nodes, neighbours, debug=false) { const seen = {}; const nnodes = nodes.length; const links = []; let id = 1; for( const n1 of nodes ) { const d2 = []; for( const n2 of nodes ) { d2.push({ d2: dist2(n1, n2), id: n2.id }); } d2.sort((a, b) => a.d2 - b.d2); const closest = d2.slice(1, neighbours + 1); if( debug ) { console.log(`closest = ${closest.length}`); console.log(closest); } for( const e of closest ) { const ids = [ n1.id, e.id ]; ids.sort(); const fp = ids.join(','); if( !seen[fp] ) { seen[fp] = true; links.push({ id: id, label: 0, source: n1.id, target: e.id }); id++; } } } if( debug ) { console.log(`Found ${links.length} edges`) } return links; } // too small and simple to calculate export const cell5 = () => { const r5 = Math.sqrt(5); const r2 = Math.sqrt(2) / 2; return { name: '5-cell', nodes: [ {id:1, label: 1, x: r2, y: r2, z: r2, w: -r2 / r5 }, {id:2, label: 2, x: r2, y: -r2, z: -r2, w: -r2 / r5 }, {id:3, label: 3, x: -r2, y: r2, z: -r2, w: -r2 / r5 }, {id:4, label: 4, x: -r2, y: -r2, z: r2, w: -r2 / r5 }, {id:5, label: 5, x: 0, y: 0, z: 0, w: 4 * r2 / r5 }, ], links: [ { id:1, source:1, target: 2}, { id:2, source:1, target: 3}, { id:3, source:1, target: 4}, { id:4, source:1, target: 5}, { id:5, source:2, target: 3}, { id:6, source:2, target: 4}, { id:7, source:2, target: 5}, { id:8, source:3, target: 4}, { id:9, source:3, target: 5}, { id:10, source:4, target: 5}, ], geometry: { node_size: 0.02, link_size: 0.02 }, options: [ { name: '--' }], description: `Five tetrahedra joined at ten faces with three tetrahedra around each edge. The 5-cell is the simplest regular four-D polytope and the four-dimensional analogue of the tetrahedron. A corresponding polytope, or simplex, exists for every n-dimensional space.`, }; }; export const cell16 = () => { let nodes = PERMUTE.coordinates([1, 1, 1, 1], 0); nodes = nodes.filter((n) => n.x * n.y * n.z * n.w > 0); nodes[0].label = 1; nodes[3].label = 2; nodes[5].label = 3; nodes[6].label = 4; nodes[7].label = 1; nodes[4].label = 2; nodes[2].label = 3; nodes[1].label = 4; index_nodes(nodes); scale_nodes(nodes, 0.75); const links = auto_detect_edges(nodes, 6); return { name: '16-cell', nodes: nodes, links: links, geometry: { node_size: 0.02, link_size: 0.02 }, options: [ { name: '--' }], description: `Sixteen tetrahedra joined at 32 faces with four tetrahedra around each edge. The 16-cell is the four-dimensional analogue of the octahedron and is dual to the tesseract. Every n-dimensional space has a corresponding polytope in this family.`, }; }; export const tesseract = () => { const nodes = PERMUTE.coordinates([1, 1, 1, 1], 0); index_nodes(nodes); for( const n of nodes ) { if( n.x * n.y * n.z * n.w > 0 ) { n.label = 2; } else { n.label = 1; } } scale_nodes(nodes, Math.sqrt(2) / 2); const links = auto_detect_edges(nodes, 4); links.map((l) => { l.label = 0 }); for( const p of [ 1, 2 ] ) { const nodes16 = nodes.filter((n) => n.label === p); const links16 = auto_detect_edges(nodes16, 6); links16.map((l) => l.label = p); links.push(...links16); } return { name: 'Tesseract', nodes: nodes, links: links, geometry: { node_size: 0.02, link_size: 0.02 }, options: [ { name: 'none', links: [ 0 ] }, { name: 'one 16-cell', links: [ 0, 1 ] }, { name: 'both 16-cells', links: [ 0, 1, 2 ] }, ], description: `The most well-known four-dimensional shape, the tesseract is analogous to the cube, and is constructed by placing two cubes in parallel hyperplanes and joining their corresponding vertices. It consists of eight cubes joined at 32 face with three cubes around each edge, and is dual to the 16-cell. Every n-dimensional space has a cube analogue or measure polytope.`, }; } const CELL24_INDEXING = { x: { y: 1, z: 3, w: 2 }, y: { z: 2, w: 3 }, z: { w: 1 } }; function node_by_id(nodes, nid) { const ns = nodes.filter((n) => n.id === nid); return ns[0]; } export const cell24 = () => { const nodes = PERMUTE.coordinates([0, 0, 1, 1], 0); for( const n of nodes ) { const axes = ['x', 'y', 'z', 'w'].filter((a) => n[a] !== 0); n.label = CELL24_INDEXING[axes[0]][axes[1]]; } index_nodes(nodes); const links = auto_detect_edges(nodes, 8); links.map((l) => l.label = 0); for( const p of [ 1, 2, 3 ] ) { const nodes16 = nodes.filter((n) => n.label === p); const links16 = auto_detect_edges(nodes16, 6); links16.map((l) => l.label = p); links.push(...links16); } // links.map((l) => { // const ls = [ l.source, l.target ].map((nid) => node_by_id(nodes, nid).label); // for ( const c of [1, 2, 3] ) { // if( ! ls.includes(c) ) { // l.label = c // } // } // }); return { name: '24-cell', nodes: nodes, links: links, geometry: { node_size: 0.02, link_size: 0.02 }, base: {}, options: [ { name: 'none', links: [ 0 ] }, { name: 'one 16-cell', links: [ 0, 1 ] }, { name: 'three 16-cells', links: [ 0, 1, 2, 3 ] } ], description: `A unique object without an exact analogue in higher or lower dimensions, the 24-cell is made of twenty-four octahedra joined at 96 faces, with three around each edge. The 24-cell is self-dual.`, }; } // face detection for the 120-cell // NOTE: all of these return node ids, not nodes // return all the links which connect to a node function nodes_links(links, nodeid) { return links.filter((l) => l.source === nodeid || l.target === nodeid); } // filter to remove a link to a given id from a set of links function not_to_this(link, nodeid) { return !(link.source === nodeid || link.target === nodeid); } // given nodes n1, n2, return all neighbours of n2 which are not n1 function unmutuals(links, n1id, n2id) { const nlinks = nodes_links(links, n2id).filter((l) => not_to_this(l, n1id)); return nlinks.map((l) => { if( l.source === n2id ) { return l.target; } else { return l.source; } }) } function fingerprint(ids) { const sids = [...ids]; sids.sort(); return sids.join(','); } function auto_120cell_faces(links) { const faces = []; const seen = {}; let id = 1; for( const edge of links ) { const v1 = edge.source; const v2 = edge.target; const n1 = unmutuals(links, v2, v1); const n2 = unmutuals(links, v1, v2); const shared = []; for( const a of n1 ) { const an = unmutuals(links, v1, a); for( const d of n2 ) { const dn = unmutuals(links, v2, d); for( const x of an ) { for( const y of dn ) { if( x == y ) { shared.push([v1, a, x, d, v2]) } } } } } if( shared.length !== 3 ) { console.log(`Bad shared faces for ${edge.id} ${v1} ${v2}`); } for( const face of shared ) { const fp = fingerprint(face); if( !seen[fp] ) { faces.push({ id: id, nodes: face }); id++; seen[fp] = true; } } } return faces; } export function make_120cell_vertices() { const phi = 0.5 * (1 + Math.sqrt(5)); const r5 = Math.sqrt(5); const phi2 = phi * phi; const phiinv = 1 / phi; const phi2inv = 1 / phi2; const nodes = [ PERMUTE.coordinates([0, 0, 2, 2], 0), PERMUTE.coordinates([1, 1, 1, r5], 0), PERMUTE.coordinates([phi, phi, phi, phi2inv], 0), PERMUTE.coordinates([phiinv, phiinv, phiinv, phi2], 0), PERMUTE.coordinates([phi2, phi2inv, 1, 0], 0, true), PERMUTE.coordinates([r5, phiinv, phi, 0], 0, true), PERMUTE.coordinates([2, 1, phi, phiinv], 0, true), ].flat(); index_nodes(nodes); scale_nodes(nodes, 0.5); return nodes; } function label_nodes(nodes, ids, label) { nodes.filter((n) => ids.includes(n.id)).map((n) => n.label = label); } function label_faces_120cell(nodes, faces, cfaces, label) { const ns = new Set(); for( const fid of cfaces ) { const face = faces.filter((f)=> f.id === fid ); if( face.length > 0 ) { for ( const nid of face[0].nodes ) { ns.add(nid); } } } label_nodes(nodes, Array.from(ns), label); } function link_labels(nodes, link) { const n1 = nodes.filter((n) => n.id === link.source); const n2 = nodes.filter((n) => n.id === link.target); return [ n1[0].label, n2[0].label ]; } // version of the 120-cell where nodes are partitioned by // layer and the links follow that export const cell120_layered = (max) => { const nodes = make_120cell_vertices(); const links = auto_detect_edges(nodes, 4); nodes.map((n) => n.label = 9); // make all invisible by default for (const cstr in CELLINDEX.LAYERS120 ) { label_nodes(nodes, CELLINDEX.LAYERS120[cstr], Number(cstr)); } links.map((l) => { const labels = link_labels(nodes, l); if( labels[0] >= labels[1] ) { l.label = labels[0]; } else { l.label = labels[1]; } }); const options = []; const layers = []; for( const i of [ 0, 1, 2, 3, 4, 5, 6, 7 ] ) { layers.push(i); options.push({ name: CELLINDEX.LAYER_NAMES[i], links: [...layers], nodes: [...layers] }) } return { name: '120-cell layered', nodes: nodes, links: links, geometry: { node_size: 0.02, link_size: 0.02 }, nolink2opacity: true, options: options, description: `This version of the 120-cell lets you explore its structure by building each layer from the 'north pole' onwards.`, } } export const cell120_inscribed = () => { const nodes = make_120cell_vertices(); const links = auto_detect_edges(nodes, 4); for( const cstr in CELLINDEX.INDEX120 ) { label_nodes(nodes, CELLINDEX.INDEX120[cstr], Number(cstr)); } links.map((l) => l.label = 0); for( const p of [ 1, 2, 3, 4, 5 ]) { const nodes600 = nodes.filter((n) => n.label === p); const links600 = auto_detect_edges(nodes600, 12); links600.map((l) => l.label = p); links.push(...links600); } return { name: '120-cell', nodes: nodes, links: links, geometry: { node_size: 0.02, link_size: 0.02 }, options: [ { name: "none", links: [ 0 ]}, { name: "one inscribed 600-cell", links: [ 0, 1 ] }, { name: "five inscribed 600-cells", links: [ 0, 1, 2, 3, 4, 5 ] } ], description: `The 120-cell is the four-dimensional analogue of the dodecahedron, and consists of 120 dodecahedra joined at 720 faces, with three dodecahedra around each edge. It is dual to the 600-cell, and five 600-cells can be inscribed in its vertices.`, } } function partition_coord(i, coords, invert) { const j = invert ? -i : i; if( j >= 0 ) { return coords[j]; } return "-" + coords[-j]; } function partition_fingerprint(n, coords, invert) { const p = ['x','y','z','w'].map((a) => partition_coord(n[a], coords, invert)); const fp = p.join(','); return fp; } function label_vertex(n, coords, partition) { const fp = partition_fingerprint(n, coords, false); if( fp in partition ) { return partition[fp]; } else { const ifp = partition_fingerprint(n, coords, true); if( ifp in partition ) { return partition[ifp]; } console.log(`Map for ${fp} ${ifp} not found`); return 0; } } function map_coord(i, coords, values) { if( i >= 0 ) { return values[coords[i]]; } return -values[coords[-i]]; } export function make_600cell_vertices() { const coords = { 0: '0', 1: '1', 2: '2', 3: 't', 4: 'k' }; const t = 0.5 * (1 + Math.sqrt(5)); const values = { '0': 0, '1': 1, '2': 2, 't': t, 'k': 1 / t }; const nodes = [ PERMUTE.coordinates([0, 0, 0, 2], 0), PERMUTE.coordinates([1, 1, 1, 1], 0), PERMUTE.coordinates([3, 1, 4, 0], 0, true) ].flat(); for( const n of nodes ) { n.label = label_vertex(n, coords, CELLINDEX.PARTITION600); } for( const n of nodes ) { for( const a of [ 'x', 'y', 'z', 'w'] ) { n[a] = map_coord(n[a], coords, values); } } index_nodes(nodes); scale_nodes(nodes, 0.75); return nodes; } function get_node(nodes, id) { const ns = nodes.filter((n) => n.id === id); if( ns ) { return ns[0] } else { return undefined; } } function audit_link_labels(nodes, links) { for( const l of links ) { const n1 = get_node(nodes, l.source); const n2 = get_node(nodes, l.target); if( n1.label === n2.label ) { console.log(`link ${l.id} joins ${n1.id} ${n2.id} with label ${n2.label}`); } } } export const cell600 = () => { const nodes = make_600cell_vertices(); const links = auto_detect_edges(nodes, 12); links.map((l) => l.label = 0); for( const p of [1, 2, 3, 4, 5]) { const nodes24 = nodes.filter((n) => n.label === p); const links24 = auto_detect_edges(nodes24, 8); links24.map((l) => l.label = p); links.push(...links24); } return { name: '600-cell', nodes: nodes, links: links, geometry: { node_size: 0.02, link_size: 0.02 }, options: [ { name: "none", links: [ 0 ]}, { name: "one 24-cell", links: [ 0, 1 ] }, { name: "five 24-cells", links: [ 0, 1, 2, 3, 4, 5 ] } ], description: `The 600-cell is the four-dimensional analogue of the icosahedron, and consists of 600 tetrahedra joined at 1200 faces with five tetrahedra around each edge. It is dual to the 120-cell. Its 120 vertices can be partitioned into five sets which form the vertices of five inscribed 24-cells.`, } } export const cell600_layered = () => { const nodes = make_600cell_vertices(); const links = auto_detect_edges(nodes, 12); nodes.map((n) => n.label = 9); // make all invisible by default for (const cstr in CELLINDEX.LAYERS600 ) { label_nodes(nodes, CELLINDEX.LAYERS600[cstr], Number(cstr)); } links.map((l) => { const labels = link_labels(nodes, l); if( labels[0] >= labels[1] ) { l.label = labels[0]; } else { l.label = labels[1]; } }); const options = []; const layers = []; for( const i of [ 0, 1, 2, 3, 4, 5, 6, 7 ] ) { layers.push(i); options.push({ name: CELLINDEX.LAYER_NAMES[i], links: [...layers], nodes: [...layers] }) } return { name: '600-cell layered', nodes: nodes, links: links, geometry: { node_size: 0.02, link_size: 0.02 }, nolink2opacity: true, options: options, description: `This version of the 600-cell lets you explore its structure by building each layer from the 'north pole' onwards.`, } } export const snub24cell = () => { const nodes600 = make_600cell_vertices(); const links600 = auto_detect_edges(nodes600, 12); const nodes = nodes600.filter((n) => n.label != 1); const links = links600.filter((l) => { const sn = node_by_id(nodes, l.source); const tn = node_by_id(nodes, l.target); return sn && tn; }); links.map((l) => l.label = 0); return { name: 'Snub 24-cell', nodes: nodes, links: links, geometry: { node_size: 0.02, link_size: 0.02 }, options: [ { name: "--" } ], description: `The snub 24-cell is a semiregular polytope which connects the 24-cell with the 600-cell. It consists of 24 icosahedra and 120 tetrahedra, and is constructed by removing one of the five inscribed 24-cells from a 600-cell.` } } function make_dodecahedron_vertices() { const phi = 0.5 * (1 + Math.sqrt(5)); const phiinv = 1 / phi; const nodes = [ { x: 1, y: 1, z: 1, w: 0, label: 4 }, { x: 1, y: 1, z: -1, w: 0, label: 3 }, { x: 1, y: -1, z: 1, w: 0, label: 3 }, { x: 1, y: -1, z: -1, w: 0, label: 2 }, { x: -1, y: 1, z: 1, w: 0, label: 3 }, { x: -1, y: 1, z: -1, w: 0, label: 1 }, { x: -1, y: -1, z: 1, w: 0, label: 5 }, { x: -1, y: -1, z: -1, w: 0, label: 3 }, { x: 0, y: phi, z: phiinv, w: 0, label: 5 }, { x: 0, y: phi, z: -phiinv, w: 0 , label: 2 }, { x: 0, y: -phi, z: phiinv, w: 0, label: 4 }, { x: 0, y: -phi, z: -phiinv, w: 0 , label: 1 }, { x: phiinv, y: 0, z: phi, w: 0 , label: 2}, { x: phiinv, y: 0, z: -phi, w: 0 , label: 4}, { x: -phiinv, y: 0, z: phi, w: 0 , label: 1}, { x: -phiinv, y: 0, z: -phi, w: 0 , label: 5}, { x: phi, y: phiinv, z:0, w: 0 , label: 1}, { x: phi, y: -phiinv, z:0, w: 0 , label: 5}, { x: -phi, y: phiinv, z:0, w: 0 , label: 4}, { x: -phi, y: -phiinv, z:0, w: 0 , label: 2}, ]; index_nodes(nodes); return nodes; } export const dodecahedron = () => { const nodes = make_dodecahedron_vertices(); const links = auto_detect_edges(nodes, 3); links.map((l) => l.label = 0); for( const p of [ 1, 2, 3, 4, 5 ]) { const tetran = nodes.filter((n) => n.label === p); const tetral = auto_detect_edges(tetran, 3); tetral.map((l) => l.label = p); links.push(...tetral); } return { name: 'Dodecahedron', nodes: nodes, links: links, geometry: { node_size: 0.02, link_size: 0.02 }, options: [ { name: "none", links: [ 0 ]}, { name: "one tetrahedron", links: [ 0, 1 ] }, { name: "five tetrahedra", links: [ 0, 1, 2, 3, 4, 5 ] } ], description: `The dodecahedron is a three-dimensional polyhedron which is included here so that you can see the partition of its vertices into five interlocked tetrahedra. This structure is the basis for the partition of the 120-cell's vertices into five 600-cells.` } } export const build_all = () => { return [ dodecahedron(), cell5(), cell16(), tesseract(), cell24(), snub24cell(), cell600(), cell600_layered(), cell120_inscribed(), cell120_layered() ]; }