diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 62428486..e34ad511 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [10.x, 12.x, 14.x, 16.x] + node-version: [12.x, 14.x, 16.x] steps: - name: Checkout this repository diff --git a/src/material.js b/src/material.js index 4be60059..aa39ed4a 100644 --- a/src/material.js +++ b/src/material.js @@ -171,8 +171,8 @@ export class Material extends HasMetadataNamedDefaultableInMemoryEntity { cell: this.Lattice.vectorArrays, }); } - - /** High-level access to unique elements from material instead of basis. + + /** High-level access to unique elements from material instead of basis. * * @return {String[]} */ diff --git a/src/parsers/enums.js b/src/parsers/enums.js new file mode 100644 index 00000000..e4178c5e --- /dev/null +++ b/src/parsers/enums.js @@ -0,0 +1,14 @@ +export const STRUCTURAL_INFORMATION_FORMATS = { + JSON: "json", + POSCAR: "poscar", + CIF: "cif", + QE: "qe", + XYZ: "xyz", + UNKNOWN: "unknown", +}; + +export const APPLICATIONS = { + ESPRESSO: "espresso", + VASP: "vasp", + UNKNOWN: "unknown", +}; diff --git a/src/parsers/espresso/parser.js b/src/parsers/espresso/parser.js new file mode 100644 index 00000000..815e326b --- /dev/null +++ b/src/parsers/espresso/parser.js @@ -0,0 +1,249 @@ +import { ATOMIC_COORD_UNITS, coefficients } from "@exabyte-io/code.js/dist/constants"; +import { mix } from "mixwith"; + +import { primitiveCell } from "../../cell/primitive_cell"; +import { Lattice } from "../../lattice/lattice"; +import math from "../../math"; +import { MaterialParser } from "../structure"; +import { FortranParserMixin } from "../utils/fortran"; +import { IBRAV_TO_LATTICE_TYPE_MAP, regex } from "./settings"; + +export class ESPRESSOMaterialParser extends mix(MaterialParser).with(FortranParserMixin) { + parse(content) { + this.data = this.fortranParseNamelists(content); + return this.parseMaterial(); + } + + /** + * @summary Return unit cell parameters from CELL_PARAMETERS card + * @returns {{cell: Number[][], units: String}} + */ + getCell() { + const text = this.data.cards; + let cell = {}; + if (this.data.system === undefined) + throw new Error("No &SYSTEM section found in input this.data."); + if (this.data.system.ibrav === undefined) throw new Error("ibrav is required in &SYSTEM."); + + if (this.data.system.ibrav === 0) { + const match = regex.cellParameters.exec(text); + if (match) { + const units = match[1]; + const values = match.slice(2, 11); + // creating matrix 3 by 3 of numbers from 9 strings + const vectors = Array.from({ length: 3 }, (_, i) => + values.slice(i * 3, i * 3 + 3).map(Number), + ); + cell = { cell: vectors, units }; + // TODO: implement type detection, now defaults to TRI + cell.type = "TRI"; + return cell; + } + } else { + cell = this.ibravToCellConfig(); + return cell; + } + throw new Error("Couldn't read cell parameters"); + } + + /** + * @summary Return elements from ATOMIC_SPECIES card + * @returns {{id: Number, value: String}[]} + */ + getElements() { + const text = this.data.cards; + const atomicPositionsMatches = Array.from(text.matchAll(regex.atomicPositions)); + return atomicPositionsMatches.map((match, index) => ({ + id: index, + value: match[1], + })); + } + + /** + * @summary Return atomic positions from ATOMIC_POSITIONS card + * @returns {{id: Number, value: Number[]}[]} + */ + getCoordinates() { + const text = this.data.cards; + const atomicPositionsMatches = Array.from(text.matchAll(regex.atomicPositions)); + const { scalingFactor } = this.getCoordinatesUnitsScalingFactor(); + return atomicPositionsMatches.map((match, index) => ({ + id: index, + value: match.slice(2, 5).map((value) => parseFloat(value) * scalingFactor), + })); + } + + /** + * @summary Return atomic constraints from ATOMIC_POSITIONS card + * @returns {{id: Number, value: Boolean[]}[]} + */ + getConstraints() { + const text = this.data.cards; + const atomicPositionsMatches = Array.from(text.matchAll(regex.atomicPositions)); + + const constraints = atomicPositionsMatches.reduce((acc, match, index) => { + const value = match + .slice(5, 8) + .filter((constraint) => constraint !== undefined) + .map((constraint) => constraint === "1"); // expect only 0 or 1 as valid values + + acc.push({ + id: index, + value, + }); + + return acc; + }, []); + + // If all constraints are empty, return an empty array + if (constraints.every((constraint) => constraint.value.length === 0)) { + return []; + } + + return constraints; + } + + /** + * @summary Return atomic coordinates units from ATOMIC_POSITIONS card + * @returns {String} + */ + getUnits() { + return this.getCoordinatesUnitsScalingFactor().units; + } + + /** + * @summary Return material name from CONTROL card + * If not present, later will be generated from the formula in materialConfig object + * @returns {String} + */ + getName() { + return this.data.control.title; + } + + /** + * @summary Returns cell config from ibrav and celldm(i) parameters + * + * QE docs: https://www.quantum-espresso.org/Doc/INPUT_PW.html#ibrav + * "If ibrav /= 0, specify EITHER [ celldm(1)-celldm(6) ] + * OR [ A, B, C, cosAB, cosAC, cosBC ] + * but NOT both. The lattice parameter "alat" is set to + * alat = celldm(1) (in a.u.) or alat = A (in Angstrom);" + * + * @returns {{cell: Number[][], type: String}} + */ + ibravToCellConfig() { + const { system } = this.data; + const { celldm } = system; + let { a, b, c } = system; + if (celldm && a) { + throw new Error("Both celldm and A are given"); + } else if (!celldm && !a) { + throw new Error("Missing celldm(1)"); + } + + const type = this.ibravToCellType(); + [a, b, c] = celldm ? this.getLatticeConstants() : [a, b, c]; + const [alpha, beta, gamma] = this.getLatticeAngles(); + + const lattice = new Lattice({ + a, + b, + c, + alpha, + beta, + gamma, + type, + }); + const cell = primitiveCell(lattice); + return { cell, type }; + } + + /** + * @summary Converts ibrav value to cell type according to Quantum ESPRESSO docs + * https://www.quantum-espresso.org/Doc/INPUT_PW.html#ibrav + * @returns {String} + */ + ibravToCellType() { + const { ibrav } = this.data.system; + const type = IBRAV_TO_LATTICE_TYPE_MAP[ibrav]; + if (type === undefined) { + throw new Error(`Invalid ibrav value: ${ibrav}`); + } + return type; + } + + /** + * @summary Calculates cell parameters from celldm(i) or A, B, C parameters depending on which are present. Specific to Quantum ESPRESSO. + * @returns {Number[]} + * */ + getLatticeConstants() { + const { celldm } = this.data.system; + // celldm indices shifted -1 from fortran list representation. In QE input file celldm(1) list starts with 1, but parsed starting with 0. + const a = celldm[0] * coefficients.BOHR_TO_ANGSTROM; // celldm(1) is a in bohr + const b = celldm[1] * celldm[0] * coefficients.BOHR_TO_ANGSTROM; // celldm(2) is b/a + const c = celldm[2] * celldm[0] * coefficients.BOHR_TO_ANGSTROM; // celldm(3) is c/a + return [a, b, c]; + } + + /** + * @summary Calculates cell angles from celldm(i) or cosAB, cosAC, cosBC parameters. Specific to Quantum ESPRESSO. + * @returns {Array} + * */ + getLatticeAngles() { + const { celldm, cosbc, cosac, cosab } = this.data.system; + let alpha, beta, gamma; + if (cosbc) alpha = math.acos(cosbc); + if (cosac) beta = math.acos(cosac); + if (cosab) gamma = math.acos(cosab); + + // Case for some of the cell types in QE docs + // celldm indices shifted -1 from fortran list representation. In QE input file celdm(1) array starts with 1, but parsed starting with 0. + if (celldm && celldm[3]) { + gamma = math.acos(celldm[3]); + } + + // Specific case for hexagonal cell in QE docs + // celldm indices shifted -1 from fortran list representation. In QE input file celdm(1) array starts with 1, but parsed starting with 0. + if (celldm && celldm[3] && celldm[4] && celldm[5]) { + alpha = math.acos(celldm[3]); + beta = math.acos(celldm[4]); + gamma = math.acos(celldm[5]); + } + + // Convert radians to degrees which are used in lattice definitions + [alpha, beta, gamma] = [alpha, beta, gamma].map((x) => + x === undefined ? x : (x * 180) / math.PI, + ); + return [alpha, beta, gamma]; + } + + /** + * @summary Return units and scaling factor according to Quantum ESPRESSO 7.2 docs + * @returns {{units: String, scalingFactor: Number}} + */ + getCoordinatesUnitsScalingFactor() { + const units = this.data.cards.match(regex.atomicPositionsUnits)[1]; + let scalingFactor = 1.0; + let _units; + switch (units) { + case "alat": + _units = ATOMIC_COORD_UNITS.crystal; + break; + case "bohr": + scalingFactor = coefficients.BOHR_TO_ANGSTROM; + _units = ATOMIC_COORD_UNITS.cartesian; + break; + case "angstrom": + _units = ATOMIC_COORD_UNITS.cartesian; + break; + case "crystal": + _units = ATOMIC_COORD_UNITS.crystal; + break; + case "crystal_sg": + throw new Error("crystal_sg not supported yet"); + default: + throw new Error(`Units ${units} not supported`); + } + return { units: _units, scalingFactor }; + } +} diff --git a/src/parsers/espresso/settings.js b/src/parsers/espresso/settings.js new file mode 100644 index 00000000..32905297 --- /dev/null +++ b/src/parsers/espresso/settings.js @@ -0,0 +1,55 @@ +import { LATTICE_TYPE } from "../../lattice/types"; +import { regex as commonRegex } from "../utils/settings"; + +const { double } = commonRegex.general; +export const regex = { + espressoFingerprint: /&CONTROL|&SYSTEM|ATOMIC_SPECIES/i, + atomicSpecies: new RegExp( + "([A-Z][a-z]?)\\s+" + // element symbol Aa + `(${double})\\s` + // mass + "(\\S*)\\s*" + // potential source file name + "(?=\\n)", // end of line + "gm", + ), + atomicPositionsUnits: new RegExp( + "ATOMIC_POSITIONS\\s+" + // start of card + "\\(?" + // optional parentheses + "(\\w+)" + // units + "\\)?", // end of optional parentheses + ), + atomicPositions: new RegExp( + `^\\s*([A-Z][a-z]*)\\s+` + // atomic element symbol + `(${double})\\s+(${double})\\s+(${double})` + // atomic coordinates + `(?:\\s+(0|1)\\s+(0|1)\\s+(0|1))?(?=\\s*\\n)`, // atomic constraints + "gm", + ), + cellParameters: new RegExp( + `CELL_PARAMETERS\\s*(?:\\(?(\\w+)\\)?)?\\n` + + `^\\s*(${double})\\s+(${double})\\s+(${double})\\s*\\n` + + `^\\s*(${double})\\s+(${double})\\s+(${double})\\s*\\n` + + `^\\s*(${double})\\s+(${double})\\s+(${double})\\s*\\n`, + "gm", + ), +}; + +export const IBRAV_TO_LATTICE_TYPE_MAP = { + 1: LATTICE_TYPE.CUB, + 2: LATTICE_TYPE.FCC, + 3: LATTICE_TYPE.BCC, + "-3": LATTICE_TYPE.BCC, + 4: LATTICE_TYPE.HEX, + 5: LATTICE_TYPE.RHL, + "-5": LATTICE_TYPE.RHL, + 6: LATTICE_TYPE.TET, + 7: LATTICE_TYPE.BCT, + 8: LATTICE_TYPE.ORC, + 9: LATTICE_TYPE.ORCC, + "-9": LATTICE_TYPE.ORCC, + 10: LATTICE_TYPE.ORCF, + 11: LATTICE_TYPE.ORCI, + 12: LATTICE_TYPE.MCL, + "-12": LATTICE_TYPE.MCL, + 13: LATTICE_TYPE.MCLC, + "-13": LATTICE_TYPE.MCLC, + 14: LATTICE_TYPE.TRI, +}; diff --git a/src/parsers/init.js b/src/parsers/init.js new file mode 100644 index 00000000..26bb4cc4 --- /dev/null +++ b/src/parsers/init.js @@ -0,0 +1,10 @@ +export class BaseParser { + constructor(options) { + this.options = options; + } + + // eslint-disable-next-line class-methods-use-this + parse() { + throw new Error("parse() is implemented in children"); + } +} diff --git a/src/parsers/native_format_parsers.js b/src/parsers/native_format_parsers.js index 5e3e58a7..4f2b9365 100644 --- a/src/parsers/native_format_parsers.js +++ b/src/parsers/native_format_parsers.js @@ -1,26 +1,20 @@ +import { STRUCTURAL_INFORMATION_FORMATS } from "./enums"; +import { ESPRESSOMaterialParser } from "./espresso/parser"; import Poscar from "./poscar"; - -const NATIVE_FORMAT = { - JSON: "json", - POSCAR: "poscar", - CIF: "cif", - PWX: "pwx", - XYZ: "xyz", - UNKNOWN: "unknown", -}; - /** * @summary Detects the format of the input string * @throws {Error} - If the input string is unknown format * @param {string} text - input string to detect format - * @returns {NATIVE_FORMAT} - Format of the input string + * @returns {string} - Format of the input string */ function detectFormat(text) { + // TODO: replace with actual detection function const jsonRegex = /^\s*\{/; - if (jsonRegex.test(text)) return NATIVE_FORMAT.JSON; - if (Poscar.isPoscar(text)) return NATIVE_FORMAT.POSCAR; - - return NATIVE_FORMAT.UNKNOWN; + const espressoRegex = /^\s*ATOMIC_SPECIES/m; + if (jsonRegex.test(text)) return STRUCTURAL_INFORMATION_FORMATS.JSON; + if (Poscar.isPoscar(text)) return STRUCTURAL_INFORMATION_FORMATS.POSCAR; + if (espressoRegex.test(text)) return STRUCTURAL_INFORMATION_FORMATS.QE; + return STRUCTURAL_INFORMATION_FORMATS.UNKNOWN; } /** @@ -32,12 +26,17 @@ function detectFormat(text) { function convertFromNativeFormat(text) { const format = detectFormat(text); + // TODO: replace with parsers factory switch (format) { - case NATIVE_FORMAT.JSON: + case STRUCTURAL_INFORMATION_FORMATS.JSON: return JSON.parse(text); - case NATIVE_FORMAT.POSCAR: + case STRUCTURAL_INFORMATION_FORMATS.POSCAR: return Poscar.fromPoscar(text); - case NATIVE_FORMAT.UNKNOWN: + case STRUCTURAL_INFORMATION_FORMATS.QE: + // eslint-disable-next-line no-case-declarations + const parser = new ESPRESSOMaterialParser(); + return parser.parse(text, "material"); + case STRUCTURAL_INFORMATION_FORMATS.UNKNOWN: throw new Error(`Unknown format`); // TODO: add more formats default: diff --git a/src/parsers/structure.js b/src/parsers/structure.js new file mode 100644 index 00000000..278c8577 --- /dev/null +++ b/src/parsers/structure.js @@ -0,0 +1,72 @@ +import { ConstrainedBasis } from "../basis/constrained_basis"; +import { Lattice } from "../lattice/lattice"; +import { BaseParser } from "./init"; + +export class MaterialParser extends BaseParser { + /* eslint-disable class-methods-use-this */ + + parse(content, property_name = "material") { + if (property_name !== "material") throw new Error("Implemented for material only"); + return this.parseMaterial(); + } + + /** + * @summary Parses structural information from a string. + * @returns {Object} - materialConfig object + * */ + parseMaterial() { + this.cell = this.getCell(); + this.elements = this.getElements(); + this.coordinates = this.getCoordinates(); + this.constraints = this.getConstraints(); + this.units = this.getUnits(); + this.name = this.getName(); + + const lattice = Lattice.fromVectors({ + a: this.cell.cell[0], + b: this.cell.cell[1], + c: this.cell.cell[2], + type: this.cell.type, + }); + + const basis = new ConstrainedBasis({ + elements: this.elements, + coordinates: this.coordinates, + units: this.units, + type: this.cell.type, + cell: lattice.vectorArrays, + constraints: this.constraints, + }); + + return { + lattice: lattice.toJSON(), + basis: basis.toJSON(), + name: this.name, + isNonPeriodic: false, + }; + } + + getCell() { + throw new Error("Implement in children"); + } + + getElements() { + throw new Error("Implement in children"); + } + + getCoordinates() { + throw new Error("Implement in children"); + } + + getConstraints() { + throw new Error("Implement in children"); + } + + getUnits() { + throw new Error("Implement in children"); + } + + getName() { + throw new Error("Implement in children"); + } +} diff --git a/src/parsers/utils/fortran.js b/src/parsers/utils/fortran.js new file mode 100644 index 00000000..d5488653 --- /dev/null +++ b/src/parsers/utils/fortran.js @@ -0,0 +1,132 @@ +import { regex } from "./settings"; + +const typeParsers = { + [Number]: (value) => parseFloat(value), + [String]: (value) => value, + [Boolean]: (value) => value === "true", +}; + +export const FortranParserMixin = (superclass) => + class extends superclass { + /** + * Parses Fortran namelists and cards data from a string. + * + * @summary Parses Fortran namelists and cards data from a QE input file string. + * @param {String} content - The text to parse. + * @throws {Error} If no namelist data is found in `text`. + * @throws {Error} If no cards data is found in `text`. + * @returns {Object} An object containing the parsed namelist and cards data. The exact structure of this object will depend on the structure of the namelist and cards data in `text`. + */ + fortranParseNamelists(content) { + let output = {}; + try { + output = this.fortranExtractNamelistData(content); + } catch (err) { + throw new Error("Incorrect fortran file"); + } + + const match = regex.fortran.cards.exec(content); + // eslint-disable-next-line prefer-destructuring + output.cards = match[0]; + return output; + } + + /** + * @summary Extracts namelist data from a string. + * @param {String} text + * @returns {Object} + */ + fortranExtractNamelistData(text) { + const namelistNameRegex = /^&(\w+)/gm; + const matches = Array.from(text.matchAll(namelistNameRegex)); + const namelistNames = matches.map((match) => match[1].toLowerCase()); + const namelists = {}; + + namelistNames.forEach((namelistName) => { + const namelistsRegex = regex.fortran.namelists(namelistName); + const namelistData = text.match(namelistsRegex)[2]; + namelists[namelistName] = this.fortranExtractKeyValuePairs(namelistData); + }); + return namelists; + } + + /** + * @summary Extracts an array of the key value pairs from a Fortran namelist. + * @param {String} data - namelist data + * @returns {Object} + * + * @example + * for input data: + * ecutrho = 4.8000000000d+02 + * ecutwfc = 6.0000000000d+01 + * ibrav = 4 + * celldm(1) = 4.7478008 + * celldm(3) = 3.0676560682 + * nat = 4 + * nosym = .false. + * ntyp = 2 + * occupations = 'fixed' + * + * should return object: + * { + * ecutrho: 480, + * ecutwfc: 60, + * ibrav: 4, + * celldm: [4.7478008, null, 3.0676560682], + * nat: 4, + * nosym: false, + * ntyp: 2, + * occupations: 'fixed' + * } + */ + fortranExtractKeyValuePairs(data) { + const pairTypes = [ + // TODO: add support for a number in form of 1.234D-56 -- current solution parses it as 1.234 + { regexPattern: regex.fortran.numberKeyValue, type: Number }, + { regexPattern: regex.fortran.stringKeyValue, type: String }, + { regexPattern: regex.fortran.booleanKeyValue, type: Boolean }, + // TODO: Change regex and capturing to accommodate for: Fortran lists assigned multiple values inline: list = 1,2,3 -- current implementation doesn't capture that + { regexPattern: regex.fortran.numberArrayKeyValue, type: Number, isArray: true }, + { regexPattern: regex.fortran.stringArrayKeyValue, type: String, isArray: true }, + { regexPattern: regex.fortran.booleanArrayKeyValue, type: Boolean, isArray: true }, + ]; + + return pairTypes.reduce((output, { regexPattern, type, isArray }) => { + this.fortranExtractKeyValuePair(data, regexPattern, type, isArray).forEach( + ([key, value]) => { + if (isArray) { + output[key] = output[key] || []; + // eslint-disable-next-line prefer-destructuring + output[key][value[0] - 1] = value[1]; // to start arrays from index 0, while Fortran lists start from 1 + } else { + output[key] = value; + } + }, + ); + return output; + }, {}); + } + + /** + * Extracts key-value pairs from a string data using provided regex pattern and type. + * If isArray is set to true, treats the key-value pair as an array. + * + * @param {String} data - The string data to extract key-value pairs from. + * @param {RegExp} regexPattern - The regex pattern to use for extracting. + * @param {Function | NumberConstructor} type - The type of the value. + * @param {Boolean} [isArray=false] - Whether to treat the value as an array. + * + * @returns {Array} The extracted pairs. Each pair is represented as an array, + * where the first element is the key and the second element is the value. + * If isArray is true, the value is an array where the first element + * is the index of the Fortran array element and the second element is the value. + * @throws {Error} If an invalid type is provided. + */ + // eslint-disable-next-line class-methods-use-this + fortranExtractKeyValuePair(data, regexPattern, type, isArray = false) { + const parser = typeParsers[type]; + return Array.from(data.matchAll(regexPattern)).map(([, key, index, value]) => + isArray ? [key, [parseInt(index, 10), parser(value)]] : [key, parser(index)], + ); + } + }; diff --git a/src/parsers/utils/settings.js b/src/parsers/utils/settings.js new file mode 100644 index 00000000..4b04e914 --- /dev/null +++ b/src/parsers/utils/settings.js @@ -0,0 +1,85 @@ +import s from "underscore.string"; + +const fortranNamelistRegex = + "&" + // Start with an ampersand + "%s" + // Namelist name placeholder + "((?:\\s|\\S)*?)" + // Matches any sequence of space or non-space characters + "\\/"; // Ends with a slash + +const fortranCardsRegex = + // "^\\s*\\/" + // Slash at the start of a line with any leading spaces + "(?![\\s\\S]*^\\/)" + // Negative lookahead for a slash at the beginning of the next line + "([\\s\\S]*)"; // Capture all characters till end + +const keyValueRegex = + "^\\s*" + // Key name at the start of a line with any leading spaces + "%s" + // Key name placeholder + "\\s*=\\s*" + // Equal sign with any leading and trailing spaces + "%s" + // Value placeholder + "\\s*\\n"; // Ends with a newline character +// TODO: Change regex and capturing to accommodate for: Fortran lists assigned multiple values inline: list = 1,2,3 -- current implementation doesn't capture that +const fortranArrayRegex = + "^\\s*" + // Array name at the start of a line with any leading spaces + "%s" + // Array name + "\\(" + // Array index opening parentheses + "%s" + // Array index + "\\)" + // Array index closing parentheses + "\\s*=\\s*" + // Equal sign with any leading and trailing spaces + "%s" + // Value placeholder + "\\s*\\n"; // Ends with a newline character + +const fortranDoubleRegex = + "([-+]?" + // Optional leading sign + "\\d*" + // Zero or more digits before the decimal point + "\\.?" + // Optional decimal point + "\\d*" + // Zero or more digits after the decimal point + "(?:[EeDd][+-]?\\d+)?" + // Optional exponent part + ")"; + +const fortranStringRegex = + "'" + // Starting single quote + "([\\w.\\-\\+\\/ ]*)" + // Matches alphanumeric, period, hyphen, plus, slash, and space characters + "'"; // Ending single quote + +const fortranBooleanRegex = + "\\." + // Starting period + "(true|false)" + // Matches either "true" or "false" surrounded by periods + "\\."; // Ending period +const stringRegex = "([+\\w.\\-\\/]*)"; // Matches alphanumeric, plus, period, hyphen, and slash characters + +const doubleRegex = + "[-+]?" + // Optional leading sign + "\\d*" + // Zero or more digits before the decimal point + "\\.?" + // Optional decimal point + "\\d*" + // Zero or more digits after the decimal point + "(?:[Ee][+-]?\\d+)?"; // Optional exponent part, + +export const regex = { + general: { + double: doubleRegex, + string: stringRegex, + }, + fortran: { + stringKeyValue: new RegExp(s.sprintf(keyValueRegex, stringRegex, fortranStringRegex), "gm"), + numberKeyValue: new RegExp(s.sprintf(keyValueRegex, stringRegex, fortranDoubleRegex), "gm"), + booleanKeyValue: new RegExp( + s.sprintf(keyValueRegex, stringRegex, fortranBooleanRegex), + "gm", + ), + numberArrayKeyValue: new RegExp( + s.sprintf(fortranArrayRegex, stringRegex, "(\\d+)", fortranDoubleRegex), + "gm", + ), + stringArrayKeyValue: new RegExp( + s.sprintf(fortranArrayRegex, stringRegex, "(\\d+)", fortranStringRegex), + "gm", + ), + booleanArrayKeyValue: new RegExp( + s.sprintf(fortranArrayRegex, stringRegex, "(\\d+)", fortranBooleanRegex), + "gm", + ), + namelists: (namelistName) => + new RegExp(s.sprintf(fortranNamelistRegex, `(${namelistName.toUpperCase()})`)), + cards: new RegExp(fortranCardsRegex, "m"), + }, +}; diff --git a/tests/enums.js b/tests/enums.js index 62843214..2fbafd5d 100644 --- a/tests/enums.js +++ b/tests/enums.js @@ -37,3 +37,18 @@ export const GraphenePoscar = readFile(path.join(FIXTURES_DIR, "Graphene.poscar" export const NiHex = readJSONFile(path.join(FIXTURES_DIR, "Ni-hex.json")); export const NiHexPoscar = readFile(path.join(FIXTURES_DIR, "Ni-hex.poscar")); export const SiHex = readJSONFile(path.join(FIXTURES_DIR, "Si-hex.json")); + +export const FortranFile1 = readFile(path.join(FIXTURES_DIR, "/parsers/utils/fortran-file-1.in")); +export const FortranFile1JSON = readJSONFile( + path.join(FIXTURES_DIR, "/parsers/utils/fortran-file-1.json"), +); +export const BNHexIbravPWSCFInput = readFile( + path.join(FIXTURES_DIR, "/parsers/espresso/BN-hex-ibrav-pwscf.in"), +); +export const BNHexPWSCF = readFile(path.join(FIXTURES_DIR, "/parsers/espresso/BN-hex-pwscf.in")); +export const BNHex = readJSONFile(path.join(FIXTURES_DIR, "/parsers/espresso/BN-hex.json")); + +export const NiCubIbravAPWSCFInput = readFile( + path.join(FIXTURES_DIR, "/parsers/espresso/Ni-cub-ibrav-a-pwscf.in"), +); +export const NiCub = readJSONFile(path.join(FIXTURES_DIR, "/parsers/espresso/Ni-cub.json")); diff --git a/tests/fixtures/parsers/espresso/BN-hex-ibrav-pwscf.in b/tests/fixtures/parsers/espresso/BN-hex-ibrav-pwscf.in new file mode 100644 index 00000000..c5355255 --- /dev/null +++ b/tests/fixtures/parsers/espresso/BN-hex-ibrav-pwscf.in @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:333adea16f2284c67618707088fbf9627e976f65b42b2b883cf846b30941c78b +size 966 diff --git a/tests/fixtures/parsers/espresso/BN-hex-pwscf.in b/tests/fixtures/parsers/espresso/BN-hex-pwscf.in new file mode 100644 index 00000000..644fb625 --- /dev/null +++ b/tests/fixtures/parsers/espresso/BN-hex-pwscf.in @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84e6969c1140e353a8a93f7e31b2c7e26405f74cba6b31898797fc693c042a97 +size 1092 diff --git a/tests/fixtures/parsers/espresso/BN-hex.json b/tests/fixtures/parsers/espresso/BN-hex.json new file mode 100644 index 00000000..ad7e90f5 --- /dev/null +++ b/tests/fixtures/parsers/espresso/BN-hex.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b985f9c27521c67dff18a79e68ab3346a5a99f070bdfed1de12eefc4eab659ea +size 2074 diff --git a/tests/fixtures/parsers/espresso/Ni-cub-ibrav-a-pwscf.in b/tests/fixtures/parsers/espresso/Ni-cub-ibrav-a-pwscf.in new file mode 100644 index 00000000..44b9a71a --- /dev/null +++ b/tests/fixtures/parsers/espresso/Ni-cub-ibrav-a-pwscf.in @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b0cf30c6b8ffa1ef098967e09c7e2775306534d3fb34b0a2dd260223c5a9263 +size 1047 diff --git a/tests/fixtures/parsers/espresso/Ni-cub.json b/tests/fixtures/parsers/espresso/Ni-cub.json new file mode 100644 index 00000000..42fb72fc --- /dev/null +++ b/tests/fixtures/parsers/espresso/Ni-cub.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:168290b2ea69a7144b4b4acc4c76105ab16fa520970b054027711d433d7945aa +size 1542 diff --git a/tests/fixtures/parsers/utils/fortran-file-1.in b/tests/fixtures/parsers/utils/fortran-file-1.in new file mode 100644 index 00000000..6e59689f --- /dev/null +++ b/tests/fixtures/parsers/utils/fortran-file-1.in @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79d09c599dc6f4799cf2f374b5cfec47aa5daf3a3c2a3673862ce39b14b5db7c +size 845 diff --git a/tests/fixtures/parsers/utils/fortran-file-1.json b/tests/fixtures/parsers/utils/fortran-file-1.json new file mode 100644 index 00000000..840be60f --- /dev/null +++ b/tests/fixtures/parsers/utils/fortran-file-1.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bccc065c15bced137d8acfd91ff68c882de9c93b294d6081b9969af24e289af +size 1015 diff --git a/tests/parsers/espresso.js b/tests/parsers/espresso.js index d130512b..4e3c9f96 100644 --- a/tests/parsers/espresso.js +++ b/tests/parsers/espresso.js @@ -1,12 +1,41 @@ import { expect } from "chai"; import { Material } from "../../src/material"; +import { ESPRESSOMaterialParser } from "../../src/parsers/espresso/parser"; import parsers from "../../src/parsers/parsers"; -import { Si, SiPWSCFInput } from "../enums"; +import { + BNHex, + BNHexIbravPWSCFInput, + BNHexPWSCF, + NiCub, + NiCubIbravAPWSCFInput, + Si, + SiPWSCFInput, +} from "../enums"; +import { assertDeepAlmostEqual } from "../utils"; describe("Parsers:Espresso", () => { it("should return textual representation of a material according to QE pw.x input format", () => { const material = new Material(Si); expect(parsers.espresso.toEspressoFormat(material)).to.be.equal(SiPWSCFInput); }); + + it("should return a material config from QE input file for BN Hex with specified ibrav and celldm parameter", () => { + const parser = new ESPRESSOMaterialParser(); + const materialConfig = parser.parse(BNHexIbravPWSCFInput, "material"); + assertDeepAlmostEqual(materialConfig, BNHex, ["name"]); + }); + + it("should return a material config from QE input file for BN Hex with cell parameters given", () => { + const parser = new ESPRESSOMaterialParser(); + const materialConfig = parser.parse(BNHexPWSCF, "material"); + assertDeepAlmostEqual(materialConfig, BNHex, ["name", "lattice"]); // lattice.type is not detected, defaults to TRI, skipping it in tests + assertDeepAlmostEqual(materialConfig.lattice, BNHex.lattice, ["type"]); + }); + + it("should return a material config from QE input file for Ni Cub with specified ibrav and A parameter", () => { + const parser = new ESPRESSOMaterialParser(); + const materialConfig = parser.parse(NiCubIbravAPWSCFInput, "material"); + assertDeepAlmostEqual(materialConfig, NiCub, ["name"]); + }); }); diff --git a/tests/parsers/native_formats.js b/tests/parsers/native_formats.js index 7ab2ee05..94a64261 100644 --- a/tests/parsers/native_formats.js +++ b/tests/parsers/native_formats.js @@ -1,7 +1,14 @@ import { expect } from "chai"; import nativeFormatParsers from "../../src/parsers/native_format_parsers"; -import { Graphene, GraphenePoscar, NiHex, NiHexPoscar } from "../enums"; +import { + BNHex, + BNHexIbravPWSCFInput, + Graphene, + GraphenePoscar, + NiHex, + NiHexPoscar, +} from "../enums"; import { assertDeepAlmostEqual } from "../utils"; describe("Parsers.NativeFormat", () => { @@ -23,6 +30,11 @@ describe("Parsers.NativeFormat", () => { assertDeepAlmostEqual(config.lattice, NiHex.lattice, ["type"]); // to omit "lattice.type" property }); + it("should return a material config from QE input file for BN", () => { + const materialConfig = nativeFormatParsers.convertFromNativeFormat(BNHexIbravPWSCFInput); + assertDeepAlmostEqual(materialConfig, BNHex, ["name"]); + }); + it("should throw an error for unknown format", () => { const text = "A\n snippet from an unknown format"; expect(() => nativeFormatParsers.convertFromNativeFormat(text)).to.throw("Unknown format"); diff --git a/tests/parsers/utils/fortran.js b/tests/parsers/utils/fortran.js new file mode 100644 index 00000000..fdb95421 --- /dev/null +++ b/tests/parsers/utils/fortran.js @@ -0,0 +1,15 @@ +import { expect } from "chai"; +import { mix } from "mixwith"; + +import { BaseParser } from "../../../src/parsers/init"; +import { FortranParserMixin } from "../../../src/parsers/utils/fortran"; +import { FortranFile1, FortranFile1JSON } from "../../enums"; + +describe("Parsers:Fortran", () => { + class TestParser extends mix(BaseParser).with(FortranParserMixin) {} // Test class + it("should return intermediate format of parsed input file", () => { + const parser = new TestParser({}); + const data = parser.fortranParseNamelists(FortranFile1); + expect(data).to.be.deep.equal(FortranFile1JSON); + }); +});