npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

chem-equilibrium

v3.2.0

Published

Compute chemical equilibrium

Readme

chem-equilibrium

NPM version npm download test coverage license

Compute chemical equilibrium.

Given the total amount of each component and the formation constants of the species they form, the solver returns the concentration of every specie at equilibrium. Acid/base, complexation and precipitation reactions are supported, and a component can either be given as a total amount or pinned to a fixed concentration at equilibrium (typically H+, to work at an imposed pH).

Installation

npm install chem-equilibrium

This package is ESM-only. CommonJS consumers can still require() it on Node.js 22.12 or later (any 24.x and above also works), or switch to import.

Usage

Solving an explicit model

Describe the components and the species they form, then solve:

import { Equilibrium } from 'chem-equilibrium';

const model = {
  components: [
    // the pH is imposed, so H+ is fixed at equilibrium
    { label: 'H+', atEquilibrium: 10 ** -4.75 },
    // 1 mol of acetate is introduced
    { label: 'CH3COO-', total: 1 },
  ],
  formedSpecies: [
    // coefficients follow the order of `components`
    { label: 'OH-', beta: 10 ** -14, components: [-1, 0] },
    { label: 'CH3COOH', beta: 10 ** 4.75, components: [1, 1] },
  ],
};

const solution = new Equilibrium(model).solveRobust();
// at pH = pKa the acid and its conjugated base are equimolar:
// {
//   'CH3COO-': 0.5,
//   'OH-': 5.62341325190349e-10,
//   CH3COOH: 0.5,
//   'H+': 0.00001778279410038923
// }

solveRobust() retries with random starting points until the Newton-Raphson algorithm converges, and returns null if it never does. solve() runs a single pass from the concentrations given to setInitial(), which is much faster when solving a series of closely related systems.

Using the bundled database

Helper builds the model for you from the bundled database of formation constants — you only declare which species you put in the solution:

import { Helper } from 'chem-equilibrium';

const helper = new Helper();
helper.addSpecie('CO3--', 0.1);
helper.setAtEquilibrium('H+', 10 ** -7); // work at pH 7

const solution = helper.getEquilibrium().solveRobust();
// {
//   'CO3--': 0.000038978124414873076,
//   'OH-': 1e-7,
//   'HCO3-': 0.08333375231890001,
//   H2CO3: 0.01662726955668513,
//   'H+': 1e-7
// }

Helper walks the database to pull in every specie reachable from the ones you added, so HCO3- and H2CO3 appear without being declared. Individual equations can be turned off with disableEquation(formedSpecie) and back on with enableEquation(formedSpecie) / enableAllEquations().

Titrations and speciation curves

Serie solves a whole series of equilibria, reusing each solution as the starting point of the next one:

import { Helper, Serie } from 'chem-equilibrium';

const serie = new Serie(new Helper());
const titration = serie.getTitration({
  solution: { type: 'CH3COO-', concentration: 0.1, volume: 0.05 },
  titrationSolution: { type: 'H+', concentration: 0.1, volume: 0.1 },
  chunks: 5,
});

titration.volumes;
// [0, 0.02, 0.04, 0.06, 0.08, 0.1]
titration.species;
// ['H+', 'CH3COO-', 'OH-', 'CH3CO2H']
titration.xy;
// volume / pH pairs, ready to plot:
// [0, 8.85, 0.02, 4.876, 0.04, 4.102, 0.06, 2.037, 0.08, 1.636, 0.1, 1.477]

solution also takes a list, for a mixture titrated in one flask. The flask holds the sum of the volumes, and each acid is neutralised in turn — here the strong one at 10 mL and the acetic acid at 20 mL:

serie.getTitration({
  solution: [
    { type: 'HCl', concentration: 0.1, volume: 0.01 },
    { type: 'CH3CO2H', concentration: 0.1, volume: 0.01 },
  ],
  titrationSolution: { type: 'OH-', concentration: 0.1, volume: 0.03 },
});

getSolutions() sweeps one specie instead of adding a titrant — here the pH, from 0 to 14:

const helper = new Helper();
helper.addSpecie('CH3COO-', 1);

const sweep = new Serie(helper).getSolutions({
  varying: 'H+',
  isFixed: true, // pin H+ at equilibrium rather than as a total amount
  log: true, // `from` and `to` are -log10 values, i.e. pH units
  from: 0,
  to: 14,
  chunks: 14,
});

sweep.x; // [0, 1, 2, ..., 14]
sweep.solutions[5];
// {
//   'CH3COO-': 0.6661394245831219,
//   'OH-': 9.999999999999999e-10,
//   CH3CO2H: 0.333860575416878,
//   'H+': 0.00001
// }

API

Everything is a named export; this package has no default export. The classes are Equilibrium, Helper, Serie, Equation and EquationSet, plus the low-level newtonRaphton solver and the bundled data, database and speciesNames. The package is written in TypeScript and ships its own types.

new Equilibrium(model[, options])

| option | default | description | | ---------------- | ------------- | ---------------------------------------------------------------------- | | volume | 1 | Volume of the solution; totals are divided by it to get concentrations | | robustMaxTries | 10 | Number of random restarts attempted by solveRobust() | | random | Math.random | Random number generator used to initialize concentrations | | autoInitial | true | Reuse the result of solve() as the next starting point | | tolerance | 1e-15 | Convergence tolerance on the dissolved species | | solidTolerance | 1e-5 | Convergence tolerance on the solid species | | maxIterations | 99 | Iterations before Newton-Raphson gives up |

Methods: solve(), solveRobust(), setInitial(concentrations).

A model has a components array ({ label, total } or { label, atEquilibrium }) and a formedSpecies array ({ label, beta, components, solid }), where components is the array of stoechiometric coefficients in the order the components were declared, and solid: true marks a precipitate.

new Helper([options])

| option | default | description | | ---------- | --------- | ------------------------------------------------------------ | | solvent | 'H2O' | Solvent; equations without a pK for it are dropped | | database | (bundled) | Replace the bundled database with your own list of equations | | extend | false | Append database to the bundled one instead of replacing it |

Methods: addSpecie(label[, total]), setTotal(label, total), setAtEquilibrium(label, value), resetSpecies(), setOptions(options), disableEquation(formedSpecie), enableEquation(formedSpecie), enableAllEquations(), getSpecies(options), getComponents(options), getEquations(options), getModel(), getEquilibrium(), clone().

new Serie(helper)

Methods: getTitration(options) and getSolutions(options). Both accept chunks (number of intervals, default 200, so chunks + 1 points are computed) and forward any other option to the underlying Helper and Equilibrium.

Database

The formation constants live in src/data/database.ts, which is the source of truth — edit it there. Each entry carries its pK, the temperature and the literature source when they are documented, and a warning when the value is known to be doubtful:

import { database, speciesNames } from 'chem-equilibrium';

database.length; // 129
speciesNames['CO3--'].name; // 'carbonate ion'

pK is always the base-10 logarithm of the formation constant of formed from its components, so beta = 10 ** pK. For an acid/base couple that number is the pKa; for a precipitation equilibrium it is -log10(Ksp); for a complex it is +log β.

An entry marked active: false is kept for the record — it documents a constant that was considered and deliberately left out — and never enters a model, so Helper builds its systems from the 127 active ones:

database.filter((entry) => entry.active !== false).length; // 127

Browse the whole table, sources included, on equilibrium.cheminfo.org/#/data.

API Documentation

License

MIT