Last active
January 27, 2022 02:57
-
-
Save quantum9Innovation/fa9597603bf731f62ccc9fd228a38333 to your computer and use it in GitHub Desktop.
Calculate empirical formulas of chemical compounds given their stoichiometric mass composition data (assuming a ~1% error rate)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Calculate empirical formulas of chemical compounds given their stochiometric | |
// composition data | |
// USAGE: | |
// `node index.js {percentage}%{symbol} ...` | |
// Imports | |
const MolarMass = require('molarmass.js') | |
const molar = new MolarMass() | |
const lcm = require('lcm') | |
// Argument parsing | |
const args = process.argv.slice(2) | |
switch ( args.length ) { | |
case 0: throw new Error('No arguments given') | |
case 1: console.log(args[0].split('%')[1] + '1'); process.exit(0) | |
} | |
// Embedded utils | |
let simplify = (continued) => { | |
// Simplify a continued fraction | |
// into a fraction with numerator `p` | |
// and denominator `q`: | |
// return [p, q] | |
let p = continued[continued.length - 1] | |
let q = 1 | |
for ( let i = continued.length - 2; i >= 0; i-- ) { | |
// Cache `p` and `q` | |
let p_cache = p | |
let q_cache = q | |
// Update `p` and `q` | |
// Reciprocal | |
p = q_cache | |
q = p_cache | |
// Add | |
p += continued[i] * q | |
} | |
return [p, q] | |
} | |
let rational = (decimal, error=0.01) => { | |
// Express `decimal` as a continued fraction, | |
// then simplify the result and | |
// return the numerator (p) and denominator (q) | |
// in an array: [p, q] | |
// while guaranteeing that |p/(q*d) - 1| <= e | |
// where e is the error and d is `decimal` | |
// Express as continued fraction | |
let expansion = [] | |
let latest = decimal | |
let p = Math.floor(decimal); let q = 1; | |
while ( Math.abs(p / (q * decimal) - 1) > error ) { | |
// Append to expansion | |
expansion.push( | |
Math.floor(latest) | |
) | |
// Update latest | |
latest = 1 / (latest - Math.floor(latest)) | |
// Update p and q | |
fraction = simplify(expansion) | |
p = fraction[0]; q = fraction[1] | |
} | |
return [p, q] | |
} | |
// Main | |
let map = {} | |
// Parse arguments as raw percentages (masses) into `map` | |
for ( let arg of args ) { | |
let [percentage, symbol] = arg.split('%') | |
map[symbol] = parseFloat(percentage) | |
} | |
// Convert masses to moles | |
for ( let [symbol, mass] of Object.entries(map) ) { | |
map[symbol] = mass / molar.getMolarMass(symbol) | |
} | |
// Convert moles to ratios (in terms of first element) | |
const first = map[Object.keys(map)[0]] | |
for ( let [symbol, moles] of Object.entries(map) ) { | |
map[symbol] = moles / first | |
} | |
// Convert ratios to rational approximations ([p, q]) | |
for ( let [symbol, ratio] of Object.entries(map) ) { | |
map[symbol] = rational(ratio) | |
} | |
// Get rid of all the denominators (`q`) | |
let denominators = Object.values(map).map(fraction => fraction[1]) | |
let k = 1 | |
for ( let denominator of denominators ) k = lcm(k, denominator) | |
for ( let [symbol, [p, q]] of Object.entries(map) ) { | |
map[symbol] = Math.floor(k * p / q) | |
} | |
let str = '' | |
for ( let [symbol, subscript] of Object.entries(map) ) { | |
str += symbol + subscript | |
} | |
console.log(str) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment