Skip to content

Commit

Permalink
Export Molecule and update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
NotMyWing committed Dec 14, 2022
1 parent a727be2 commit 4f009e2
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 33 deletions.
96 changes: 66 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,67 +13,103 @@ npm install stoik
To use Stoik in your project, import the `evaluate` function as well as any other necessary functions for your project:

```js
import { tokenize, evaluate, toRPN, getConstituents, addOrSubtract } from "stoik";
import { tokenize, evaluate, toRPN, Molecule } from "stoik";
```
You can then use the `evaluate` function to evaluate a chemical formula and obtain the result:

To evaluate a chemical formula using Stoik, you can use the `evaluate` function. This function takes a chemical formula as input and returns a `Molecule` instance, which is essentially an extension of Map and contains the atoms and their frequencies in the molecule. For example:

```js
const formula = "H2O";

// Use the evaluate function to evaluate the formula. This results in an internal token.
const result = evaluate("H2O");
// [TokenType.Molecule, TreeMap([["H": 2], ["O": 1]])]

// Use the getConstituents function to extract the resulting constituents from a molecule/atom token.
const constituents = getConstituents(result);
// TreeMap([["H": 2], ["O": 1]])
evaluate(formula); // Map {"H" => 2, "O" => 1}

// Formulas can consist of fairly complicated operations as well.
evaluate("5(H2O)3((FeW)5CrMo2V)6CoMnSi");

// The result of the expression above is equivalent to:
new Molecule([
["H", 30],
["Co", 5],
["Cr", 30],
["Fe", 150],
["Mn", 5],
["Mo", 60],
["O", 15],
["Si", 5],
["V", 30],
["W", 150],
]);
```

Alternatively, it's possible to `evaluate` formulas step-by-step, if you need to alter any.
### Tokenization and Parsing

Alternatively, it's possible to `evaluate` formulas step-by-step, if you need to alter any of the steps.

The `tokenize` function combines the tokenization and parsing functionality. It is capable of detecting malformed formulas to some extent.
However, if the input is not a valid formula, the function is not guaranteed to return a sequence that will correctly evaluate into a Molecule.

```js
// First, tokenize the formula to get a FIFO object of tokens.
// First, tokenize the formula to get a Denque sequence of tokens.
const tokens = tokenize(formula);
// [
// new Denque([
// [TokenType.Atom, "H"],
// [TokenType.Subscript],
// [TokenType.Number, 2],
// [TokenType.Add]
// [TokenType.Atom, "O"]
// ]
// ]);
```

Before supplying the tokens to `evaluate`, they first have to be converted to the Reverse Polish Notation using `toRPN`.
Like `tokenize`, this function is not guaranteed to return a valid sequence of tokens if the input is inherently incorrect.

```js
// Next, convert the tokens to Reverse Polish Notation (RPN) using the toRPN function. Note the different order.
const RPN = toRPN(tokens);
// [
// new Denque([
// [TokenType.Atom, "H"],
// [TokenType.Number, 2],
// [TokenType.Subscript],
// [TokenType.Atom, "O"],
// [TokenType.Add]
// ]

// Finally, use the evaluate function to evaluate the formula in RPN.
const result = evaluate(RPN);
// [TokenType.Molecule, TreeMap([["H": 2], ["O": 1]])]
// ]);
```

You can also use the `addOrSubtract` function to add or subtract two atoms or molecules:
Finally, the RPN token sequence can be supplied to `evaluate` to evaluate the formula.
This will throw a concise error if the input is incorrect.

```js
// Add CaCO3 to H2O.
const newMolecule = addOrSubtract(evaluate("H2O"),
evaluate("CaCO3"));
// [TokenType.Molecule, TreeMap([["H": 2], ["O": 4], ["C": 1], ["Ca": 1]])]

// Subtract CaCO3 from H2O.
const newMolecule = addOrSubtract(evaluate("H2O"),
evaluate("CaCO3"), true);
// [TokenType.Molecule, TreeMap([["H": 2], ["O": -2], ["C": -1], ["Ca": -1]])]
evaluate(RPN); // Map {"H" => 2, "O" => 1}
```

## Molecule Class

The `Molecule` class includes methods for performing basic arithmetic operations on molecules.
By default, these methods return new `Molecule` instances, which means that they do not mutate the original molecule.
However, each method also has a mutable counterpart (e.g. `add` and `addMut`) that can be used to modify the original molecule instead.

An `AtomLiteral` is a type that represents a valid atom in a chemical molecule. It must be either a single uppercase letter (e.g. "H" for hydrogen), or a combination of an uppercase letter followed by a lowercase letter (e.g. "Cl" for chlorine).

Molecule objects can be constructed in several ways:
* With no arguments, to create an empty molecule
* With a single `AtomLiteral` argument, to create a molecule containing a single atom at a frequency of 1
* With a single `AtomLiteral` and a `number` argument, to create a molecule containing a single atom at a specified frequency
* With a `Molecule` argument, to create a new molecule with the same atoms and frequencies as the input molecule
* With an array of tuples, where each tuple contains an `AtomLiteral` and an optional `number`, to create a molecule containing the atoms and frequencies specified in the input array


The Molecule class also contains several methods for manipulating molecules:
* The `set` method can be used to add or update the frequency of an atom in the molecule
* The `add` method can be used to add a molecule to this molecule
* The `subtract` method can be used to subtract a molecule from this molecule
* The `multiply` method can be used to multiply this molecule by a number
* All other methods provided by the standard `Map` class

In addition, the `Molecule` class has a `fromAtom` static method that can be used to quickly create a molecule containing a single atom at a specified frequency.

## Design and Implementation

Stoik uses a combination of recursive descent parsing and the Shunting-yard algorithm to parse and evaluate chemical formulas. The `tokenize` function uses a simple state machine to split the input string into tokens, and the `evaluate` function uses a stack to evaluate the formula in RPN. The `addOrSubtract` function uses the `TreeMap` class to add or subtract the atoms and molecules in two molecules.
Stoik uses a combination of recursive descent parsing and the Shunting-yard algorithm to parse and evaluate chemical formulas. The `tokenize` function uses a simple state machine to split the input string into tokens, and the `evaluate` function uses a stack to evaluate the formula in RPN.

## License

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"denque": "^2.1.0"
},
"license": "LGPLv3",
"version": "0.1.0",
"version": "0.1.1",
"name": "stoik",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
92 changes: 90 additions & 2 deletions src/impl/Molecule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,15 @@ export class Molecule extends Map<AtomLiteral, number> {
this.set(rhs, rhsFreq);
} else if (!isString) {
if (rhs instanceof Molecule) super(rhs);
else if (rhs) super(rhs.map(([atom, freq]) => [atom, freq ?? 1]));
else super();
else if (rhs) {
super(
rhs.map(([atom, freq]) => {
if (!ATOM_LITERAL_TEST.test(atom)) throw new Error(`Invalid atom format: ${atom}`);

return [atom, freq ?? 1];
}),
);
} else super();
}
}

Expand Down Expand Up @@ -95,30 +102,89 @@ export class Molecule extends Map<AtomLiteral, number> {
return lhs;
}

/**
* Adds a molecule to this molecule, modifying the molecule in-place.
*
* @param molecule - The molecule to add.
* @returns The modified molecule.
*/
addMut(molecule: Molecule): this;

/**
* Adds an atom to this molecule, modifying the molecule in-place.
*
* @param atom - The atom to add.
* @param freq - The frequency of the atom to add.
* @returns The modified molecule.
*/
addMut(atom: AtomLiteral, freq?: number): this;

addMut(...args: [AtomLiteral, number?] | [Molecule]): this {
Molecule.addOrSubtractStatic(this, false, ...args);
return this;
}

/**
* Adds a molecule to this molecule. Returns a new molecule.
*
* @param molecule - The molecule to add.
* @returns The modified molecule.
*/
add(molecule: Molecule): Molecule;

/**
* Adds an atom to this molecule. Returns a new molecule.
*
* @param atom - The atom to add.
* @param freq - The frequency of the atom to add.
* @returns A new molecule.
*/
add(atom: AtomLiteral, freq?: number): Molecule;

add(...args: [AtomLiteral, number?] | [Molecule]): Molecule {
const molecule = new Molecule(this);
Molecule.addOrSubtractStatic(molecule, false, ...args);
return molecule;
}

/**
* Subtracts a molecule from this molecule, modifying the molecule in-place.
*
* @param molecule - The molecule to subtract.
* @returns A new molecule.
*/
subtractMut(molecule: Molecule): this;

/**
* Subtracts an atom from this molecule, modifying the molecule in-place.
*
* @param atom - The atom to subtract.
* @param freq - The frequency of the atom to subtract.
* @returns A new molecule.
*/
subtractMut(atom: AtomLiteral, freq?: number): this;

subtractMut(...args: [AtomLiteral, number?] | [Molecule]): this {
Molecule.addOrSubtractStatic(this, true, ...args);
return this;
}

/**
* Subtracts a molecule from this molecule. Returns a new molecule.
*
* @param molecule - The molecule to subtract.
* @returns A new molecule.
*/
subtract(molecule: Molecule): Molecule;

/**
* Subtracts a molecule from this molecule. Returns a new molecule.
*
* @param molecule - The molecule to subtract.
* @returns A new molecule.
*/
subtract(atom: AtomLiteral, freq?: number): Molecule;

subtract(...args: [AtomLiteral, number?] | [Molecule]): Molecule {
const molecule = new Molecule(this);
Molecule.addOrSubtractStatic(molecule, true, ...args);
Expand All @@ -130,18 +196,40 @@ export class Molecule extends Map<AtomLiteral, number> {
return molecule;
}

/**
* Multiplies all atom frequencies of the molecule by -1, modifying the molecule in-place.
*
* @returns The modified molecule.
*/
negateMut(): this {
return Molecule.multiplyStatic(this, -1);
}

/**
* Multiplies all atom frequencies of the molecule by -1. Returns a new molecule.
*
* @returns The new molecule.
*/
negate(): Molecule {
return Molecule.multiplyStatic(new Molecule(this), -1);
}

/**
* Multiplies all atom frequencies of the molecule by the given number, modifying the molecule in-place.
*
* @param multiplier The multiplier.
* @returns The modified molecule.
*/
multiplyMut(multiplier: number): this {
return Molecule.multiplyStatic(this, multiplier);
}

/**
* Multiplies all atom frequencies of the molecule by the given number. Returns a new molecule.
*
* @param multiplier The multiplier.
* @returns The new molecule.
*/
multiply(multiplier: number): Molecule {
return Molecule.multiplyStatic(new Molecule(this), multiplier);
}
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { AtomLiteral, Molecule } from "./impl/Molecule";
import Denque from "denque";

export { Molecule } from "./impl/Molecule";

/**
* The type of a token.
*/
Expand Down

0 comments on commit 4f009e2

Please sign in to comment.