import { base64ToArray } from "./utilities"; import { Vector3, Matrix4 } from "./WebGL/math"; import { VASP } from "./parsers/VASP"; import { CUBE } from "./parsers/CUBE"; import { inflate } from "pako"; interface VolumeDataOptions { negate?: boolean; normalize?: boolean; }; /** * $3Dmol.VolumeData stores volumetric data. This includes file parsing * functionality. * * @class * @param {string} str - volumetric data * @param {string} format - format of supplied data (cube, dx, vasp); append .gz if compressed * @param {Object} options - normalize (zero mean, unit variance), negate */ export class VolumeData { unit = { x: 1, y: 1, z: 1 }; // scale of each voxel origin = { x: 0, y: 0, z: 0 }; // origin (bottom "left", not center) size = { x: 0, y: 0, z: 0 }; // number of voxels in each direction data = new Float32Array([]); // actual floating point data, arranged // x->y->z matrix: any = null; //if set must transform data inversematrix: Matrix4|null = null; dimensionorder: any; isbinary = new Set(['ccp4','CCP4']); constructor(str: any, format: string, options?: VolumeDataOptions) { format = format.toLowerCase(); if (/\.gz$/.test(format)) { //unzip gzipped files format = format.replace(/\.gz$/, ''); try { if ((this as any)[format] && this.isbinary.has(format)) { if (typeof (str) == "string") { //assume base64 encoded str = base64ToArray(str); } str = inflate(str); } else { str = new TextDecoder("utf-8").decode(inflate(str)); } } catch (err) { console.log(err); } } if ((this as any)[format]) { if (this.isbinary.has(format) && typeof (str) == "string") { str = base64ToArray(str); } (this as any)[format](str); } if (options) { if (options.negate) { for (let i = 0, n = this.data.length; i < n; i++) { this.data[i] = -this.data[i]; } } if (options.normalize) { var total = 0.0; for (let i = 0, n = this.data.length; i < n; i++) { total += this.data[i]; } var mean = total / this.data.length; total = 0; for (let i = 0, n = this.data.length; i < n; i++) { var diff = this.data[i] - mean; total += diff * diff; //variance is ave of squared difference with mean } var variance = total / this.data.length; //console.log("Computed variance: "+variance); //now normalize for (let i = 0, n = this.data.length; i < n; i++) { this.data[i] = (this.data[i] - mean) / variance; } } } } /** * @function $3Dmol.VolumeData.getIndex * @param {number} x,y,z - the coordinates * @returns - index into flat array closest to provided coordinate; -1 if invalid */ getIndex(x: number, y: number, z: number) { if (this.matrix) { //all transformation is done through matrix multiply if (this.inversematrix == null) { this.inversematrix = new Matrix4().getInverse(this.matrix); } var pt = new Vector3(x, y, z); pt = pt.applyMatrix4(this.inversematrix as Matrix4); x = pt.x; y = pt.y; z = pt.z; } else { //use simple origin/unit transform x -= this.origin.x; y -= this.origin.y; z -= this.origin.z; x /= this.unit.x; y /= this.unit.y; z /= this.unit.z; } x = Math.round(x); y = Math.round(y); z = Math.round(z); if (x < 0 || x >= this.size.x) return -1; if (y < 0 || y >= this.size.y) return -1; if (z < 0 || z >= this.size.z) return -1; return x * this.size.y * this.size.z + y * this.size.z + z; }; /** * @function $3Dmol.VolumeData.getVal * @param {number} x,y,z - the coordinates * @returns - value closest to provided coordinate; zero if coordinate invalid */ getVal(x: number, y: number, z: number) { let i = this.getIndex(x, y, z); if (i < 0) return 0; return this.data[i]; }; getCoordinates = function (index: number) { var x = index / (this.size.y * this.size.z); var y = index % (this.size.y * this.size.z); var z = index % this.size.z; x *= this.unit.x; y *= this.unit.y; z *= this.unit.z; x += this.origin.x; y += this.origin.y; z += this.origin.z; return { x: x, y: y, z: z }; }; /* * parse vasp data * Essentially this parser converts the CHGCAR data into * cube data. It has been adapted from 'chg2cube.pl' found in * http://theory.cm.utexas.edu/vtsttools/ */ vasp = function (str: string) { var lines = str.replace(/^\s+/, "").split(/[\n\r]/); var atomicData = VASP(str)[0]; var natoms = atomicData.length; if (natoms == 0) { console.log("No good formating of CHG or CHGCAR file, not atomic information provided in the file."); this.data = []; return; } // Assume atomic units // var unittype = "bohr/hartree"; var l_units = 1.889725992; var e_units = 0.036749309; // copied from $3Dmol.Parsers.vasp var convFactor = parseFloat(lines[1]); // This is how Vasp reads in the basis We need the l_units in order to // compute the volume of the cell. Afterwards to obtain the axis for the // voxels we have to remove this unit and divide by the number of voxels in // each dimension var v: string[]; v = lines[2].replace(/^\s+/, "").split(/\s+/); var xVec = new Vector3(parseFloat(v[0]), parseFloat(v[1]), parseFloat(v[2])).multiplyScalar(convFactor * l_units); v = lines[3].replace(/^\s+/, "").split(/\s+/); var yVec = new Vector3(parseFloat(v[0]), parseFloat(v[1]), parseFloat(v[2])).multiplyScalar(convFactor * l_units); v = lines[4].replace(/^\s+/, "").split(/\s+/); var zVec = new Vector3(parseFloat(v[0]), parseFloat(v[1]), parseFloat(v[2])).multiplyScalar(convFactor * l_units); // correct volume for non-orthognal box (expansion by minors) var vol = xVec.x * (yVec.y * zVec.z - zVec.y * yVec.z) - yVec.x * (xVec.y * zVec.z - zVec.y * xVec.z) + zVec.x * (xVec.y * yVec.z - yVec.y * xVec.z); vol = Math.abs(vol) / (Math.pow(l_units, 3)); var vol_scale = 1.0 / (vol); //This Only for CHGCAR files // We splice the structure information // 2 (header) + 3 (vectors) + 2 (atoms) + 1 (vaspMode) + natoms (coords) + 1 (blank line) lines.splice(0, 2 + 3 + 2 + 1 + natoms + 1); var lineArr = lines[0].replace(/^\s+/, "").replace(/\s+/g, " ").split(" "); var nX = Math.abs(parseFloat(lineArr[0])); var nY = Math.abs(parseFloat(lineArr[1])); var nZ = Math.abs(parseFloat(lineArr[2])); var origin = this.origin = new Vector3(0, 0, 0); this.size = { x: nX, y: nY, z: nZ }; this.unit = new Vector3(xVec.x, yVec.y, zVec.z); // resize the vectors accordingly xVec = xVec.multiplyScalar(1 / (l_units * nX)); yVec = yVec.multiplyScalar(1 / (l_units * nY)); zVec = zVec.multiplyScalar(1 / (l_units * nZ)); if (xVec.y != 0 || xVec.z != 0 || yVec.x != 0 || yVec.z != 0 || zVec.x != 0 || zVec.y != 0) { //need a transformation matrix this.matrix = new Matrix4(xVec.x, yVec.x, zVec.x, 0, xVec.y, yVec.y, zVec.y, 0, xVec.z, yVec.z, zVec.z, 0, 0, 0, 0, 1); //include translation in matrix this.matrix = this.matrix.multiplyMatrices(this.matrix, new Matrix4().makeTranslation(origin.x, origin.y, origin.z)); //all translation and scaling done by matrix, so reset origin and unit this.origin = new Vector3(0, 0, 0); this.unit = new Vector3(1, 1, 1); } lines.splice(0, 1); // Remove the dimension line var raw = lines.join(" "); raw = raw.replace(/^\s+/, ''); var rawArray = raw.split(/[\s\r]+/); rawArray.splice(nX * nY * nZ + 1); var preConvertedData = Float32Array.from(rawArray, parseFloat); //We still have to format it to get the density for (var i = 0; i < preConvertedData.length; i++) { preConvertedData[i] = preConvertedData[i] * vol_scale * e_units; } this.data = preConvertedData; //console.log(xVec); //console.log(yVec); //console.log(zVec); //console.log(this.unit); //console.log(this.origin); //console.log(this.matrix); //console.log(this.data); }; // parse dx data - does not support all features of the file format dx = function (str: string) { var lines = str.split(/[\n\r]+/); var m: string[]; var recounts = /gridpositions\s+counts\s+(\d+)\s+(\d+)\s+(\d+)/; var reorig = /^origin\s+(\S+)\s+(\S+)\s+(\S+)/; var redelta = /^delta\s+(\S+)\s+(\S+)\s+(\S+)/; var follows = /data follows/; var i = 0; for (i = 0; i < lines.length; i++) { var line = lines[i]; if ((m = recounts.exec(line))) { var nX = parseInt(m[1]); var nY = parseInt(m[2]); var nZ = parseInt(m[3]); this.size = { x: nX, y: nY, z: nZ }; } else if ((m = redelta.exec(line))) { var xunit = parseFloat(m[1]); if (parseFloat(m[2]) != 0 || parseFloat(m[3]) != 0) { console.log("Non-orthogonal delta matrix not currently supported in dx format"); } i += 1; line = lines[i]; m = redelta.exec(line); if (m == null) { console.log("Parse error in dx delta matrix"); return; } var yunit = parseFloat(m[2]); if (parseFloat(m[1]) != 0 || parseFloat(m[3]) != 0) { console.log("Non-orthogonal delta matrix not currently supported in dx format"); } i += 1; line = lines[i]; m = redelta.exec(line); if (m == null) { console.log("Parse error in dx delta matrix"); return; } var zunit = parseFloat(m[3]); if (parseFloat(m[1]) != 0 || parseFloat(m[2]) != 0) { console.log("Non-orthogonal delta matrix not currently supported in dx format"); } this.unit = new Vector3(xunit, yunit, zunit); } else if ((m = reorig.exec(line))) { var xorig = parseFloat(m[1]); var yorig = parseFloat(m[2]); var zorig = parseFloat(m[3]); this.origin = new Vector3(xorig, yorig, zorig); } else if ((m = follows.exec(line))) { break; } } i += 1; if (!this.size || !this.origin || !this.unit || !this.size) { console.log("Error parsing dx format"); return; } var raw = lines.splice(i).join(" "); var rawArray = raw.split(/[\s\r]+/); this.data = Float32Array.from(rawArray, parseFloat); }; // parse cube data cube(str: string) { var lines = str.split(/\r?\n/); if (lines.length < 6) return; var cryst = CUBE(str, {}).modelData[0].cryst; var lineArr = lines[2].replace(/^\s+/, "").replace(/\s+/g, " ").split(" "); var atomsnum = parseFloat(lineArr[0]); //includes sign, which indicates presence of oribital line in header var natoms = Math.abs(atomsnum); this.origin = cryst.origin; this.size = cryst.size; this.unit = cryst.unit; this.matrix = cryst.matrix4; var headerlines = 6; if (atomsnum < 0) headerlines++; //see: http://www.ks.uiuc.edu/Research/vmd/plugins/molfile/cubeplugin.html var raw = lines.splice(natoms + headerlines).join(" "); raw = raw.replace(/^\s+/, ''); var rawArray = raw.split(/[\s\r]+/); this.data = Float32Array.from(rawArray, parseFloat); }; //parse cp4 files ccp4(bin: Int8Array) { // http://www.ccp4.ac.uk/html/maplib.html#description //code from ngl: https://github.com/arose/ngl/blob/master/js/ngl/parser.js var header:any = {}; bin = new Int8Array(bin); var intView = new Int32Array(bin.buffer, 0, 56); var floatView = new Float32Array(bin.buffer, 0, 56); var dv = new DataView(bin.buffer); // 53 MAP Character string 'MAP ' to identify file type header.MAP = String.fromCharCode( dv.getUint8(52 * 4), dv.getUint8(52 * 4 + 1), dv.getUint8(52 * 4 + 2), dv.getUint8(52 * 4 + 3) ); // 54 MACHST Machine stamp indicating machine type which wrote file // 17 and 17 for big-endian or 68 and 65 for little-endian header.MACHST = [dv.getUint8(53 * 4), dv.getUint8(53 * 4 + 1)]; // swap byte order when big endian if (header.MACHST[0] === 17 && header.MACHST[1] === 17) { var n = bin.byteLength; for (var i = 0; i < n; i += 4) { dv.setFloat32(i, dv.getFloat32(i), true); } } header.NX = intView[0]; // NC - columns (fastest changing) header.NY = intView[1]; // NR - rows header.NZ = intView[2]; // NS - sections (slowest changing) // mode // 0 image : signed 8-bit bytes range -128 to 127 // 1 image : 16-bit halfwords // 2 image : 32-bit reals // 3 transform : complex 16-bit integers // 4 transform : complex 32-bit reals // 6 image : unsigned 16-bit range 0 to 65535 // 16 image: unsigned char * 3 (for rgb data, non-standard) // // Note: Mode 2 is the normal mode used in the CCP4 programs. // Other modes than 2 and 0 may NOT WORK header.MODE = intView[3]; // start header.NXSTART = intView[4]; // NCSTART - first column header.NYSTART = intView[5]; // NRSTART - first row header.NZSTART = intView[6]; // NSSTART - first section // intervals header.MX = intView[7]; // intervals along x header.MY = intView[8]; // intervals along y header.MZ = intView[9]; // intervals along z // cell length (Angstroms in CCP4) header.xlen = floatView[10]; header.ylen = floatView[11]; header.zlen = floatView[12]; // cell angle (Degrees) header.alpha = floatView[13]; header.beta = floatView[14]; header.gamma = floatView[15]; // axis correspondence (1,2,3 for X,Y,Z) header.MAPC = intView[16]; // column header.MAPR = intView[17]; // row header.MAPS = intView[18]; // section // density statistics header.DMIN = floatView[19]; header.DMAX = floatView[20]; header.DMEAN = floatView[21]; // space group number 0 or 1 (default=0) header.ISPG = intView[22]; // number of bytes used for symmetry data (0 or 80) header.NSYMBT = intView[23]; // Flag for skew transformation, =0 none, =1 if foll header.LSKFLG = intView[24]; // 26-34 SKWMAT Skew matrix S (in order S11, S12, S13, S21 etc) if // LSKFLG .ne. 0. // 35-37 SKWTRN Skew translation t if LSKFLG != 0. // Skew transformation is from standard orthogonal // coordinate frame (as used for atoms) to orthogonal // map frame, as Xo(map) = S * (Xo(atoms) - t) // 38 future use (some of these are used by the MSUBSX routines // . " in MAPBRICK, MAPCONT and FRODO) // . " (all set to zero by default) // . " // 52 " // 50-52 origin in X,Y,Z used for transforms header.originX = floatView[49]; header.originY = floatView[50]; header.originZ = floatView[51]; // 53 MAP Character string 'MAP ' to identify file type // => see top of this parser // 54 MACHST Machine stamp indicating machine type which wrote file // => see top of this parser // Rms deviation of map from mean density header.ARMS = floatView[54]; // 56 NLABL Number of labels being used // 57-256 LABEL(20,10) 10 80 character text labels (ie. A4 format) //console.log("Map has min,mean,average,rmsddv: "+header.DMIN+","+header.DMAX+","+header.DMEAN+","+header.ARMS); //create transformation matrix, code mostly copied from ngl var h = header; var basisX: Array = [ h.xlen, 0, 0 ]; var basisY: Array = [ h.ylen * Math.cos(Math.PI / 180.0 * h.gamma), h.ylen * Math.sin(Math.PI / 180.0 * h.gamma), 0 ]; var basisZ: Array = [ h.zlen * Math.cos(Math.PI / 180.0 * h.beta), h.zlen * ( Math.cos(Math.PI / 180.0 * h.alpha) - Math.cos(Math.PI / 180.0 * h.gamma) * Math.cos(Math.PI / 180.0 * h.beta) ) / Math.sin(Math.PI / 180.0 * h.gamma), 0 ]; basisZ[2] = Math.sqrt( h.zlen * h.zlen * Math.sin(Math.PI / 180.0 * h.beta) * Math.sin(Math.PI / 180.0 * h.beta) - basisZ[1] * basisZ[1] ); var basis: Array = [0, basisX, basisY, basisZ]; var nxyz: Array = [0, h.MX, h.MY, h.MZ]; var mapcrs: Array = [0, h.MAPC, h.MAPR, h.MAPS]; this.matrix = new Matrix4(); this.matrix.set( basis[mapcrs[1]][0] / nxyz[mapcrs[1]], basis[mapcrs[2]][0] / nxyz[mapcrs[2]], basis[mapcrs[3]][0] / nxyz[mapcrs[3]], 0, basis[mapcrs[1]][1] / nxyz[mapcrs[1]], basis[mapcrs[2]][1] / nxyz[mapcrs[2]], basis[mapcrs[3]][1] / nxyz[mapcrs[3]], 0, basis[mapcrs[1]][2] / nxyz[mapcrs[1]], basis[mapcrs[2]][2] / nxyz[mapcrs[2]], basis[mapcrs[3]][2] / nxyz[mapcrs[3]], 0, 0, 0, 0, 1 ); //include translation in matrix, NXSTART etc are an offset in grid space this.matrix = this.matrix.multiplyMatrices( this.matrix, new Matrix4().makeTranslation( h.NXSTART + h.originX, h.NYSTART + h.originY, h.NZSTART + h.originZ) ); //all translation and scaling done by matrix, so reset origin and unit this.origin = new Vector3(0, 0, 0); this.unit = new Vector3(1, 1, 1); this.size = { x: header.NX, y: header.NY, z: header.NZ }; this.dimensionorder = [header.MAPC, header.MAPR, header.MAPS]; var data = new Float32Array(bin.buffer, 1024 + header.NSYMBT); //data must by (slowest changing) x,y,z (fastest changing) var NX = header.NX, NY = header.NY, NZ = header.NZ; this.data = new Float32Array(NX * NY * NZ); for (let i = 0; i < NX; i++) { for (let j = 0; j < NY; j++) { for (let k = 0; k < NZ; k++) { //should I be concerned that I'm not using mapc? this.data[((i * NY) + j) * NZ + k] = data[((k * NY) + j) * NX + i]; } } } }; };