dsh-chem
Deterministic chemistry math for DeepSeek Harness: formula parsing + molar mass, mass/mole/particle conversions, solution concentration, pH math, exact integer equation balancing (charge-conserving) and ideal-gas arithmetic — zero runtime dependencies
- Stars
- 1
- Language
- TypeScript
- Created
- Sep 5, 2026
- Updated
- Oct 18, 2026
Introduction
dsh-chem
Deterministic chemistry math for DeepSeek Harness (dsh) — zero runtime dependencies, pure arithmetic over IUPAC data.
中文简介:化学计量数学工具箱。七个零依赖确定性工具——化学式解析与摩尔质量(支持嵌套括号、结晶水、离子电荷)、质量/摩尔/粒子数换算、溶液浓度(摩尔浓度、稀释 C1V1=C2V2、百分比浓度)、pH/pOH 与缓冲溶液、配平化学方程式(精确有理数运算,含电荷守恒)、理想气体定律(PV=nRT 求解、摩尔体积、气体密度与由密度反推摩尔质量)。专门替代 LLM 高频出错的化学心算。
Why this plugin
Agents get chemistry arithmetic wrong in predictable ways:
- Molar-mass sums — miscounted subscripts, dropped hydrates (CuSO4·5H2O is 249.68 g/mol, not 159.61).
- Element case sensitivity — "Co" is cobalt (58.93), "CO" is carbon monoxide (28.01).
- Dilution math — C1V1 = C2V2 with a wrong or missing factor.
- pH log/antilog arithmetic — pH of 0.01 M HCl is 2, not 12.
- Limiting-reagent reasoning — dividing two amounts by the coefficients and comparing, then carrying the extent of reaction through every product.
These tools replace all of it with deterministic tables and formulas. No network, no state, no dependencies — install and call.
Tools
formula_mass — parse a formula and compute molar mass
Accepts nested parentheses, hydrate dots (CuSO4·5H2O, * also works), leading coefficients (2H2O) and ionic charges. Uses IUPAC standard atomic weights; elements without stable isotopes use the most-stable-isotope mass number (flagged with usedMassNumber).
{ "formula": "H2SO4" }
{
"valid": true,
"formula": "H2SO4",
"molarMass": 98.072,
"elements": [
{ "symbol": "H", "name": "Hydrogen", "count": 2, "atomicMass": 1.008, "massContribution": 2.016, "massFraction": 0.02056 },
{ "symbol": "O", "name": "Oxygen", "count": 4, "atomicMass": 15.999, "massContribution": 63.996, "massFraction": 0.65254 },
{ "symbol": "S", "name": "Sulfur", "count": 1, "atomicMass": 32.06, "massContribution": 32.06, "massFraction": 0.32690 }
],
"usedMassNumber": false
}
Charge disambiguation rule (documented, deterministic): digits immediately before a trailing +/- belong to the ionic charge only when every element parsed so far is the same single element — Fe3+ is Fe with 3+, S2- is S with 2-, Ca2+ is Ca with 2+. Otherwise they are a subscript and the bare sign is a 1-unit charge — NH4+ is N + H4 with +, NO3- is N + O3 with -. The explicit form SO4^2- is always the charge, and O2^+ escapes the rule for subscript-before-charge. Single lowercase symbols are auto-corrected (h2o → H2O); multi-letter lowercase runs are rejected with a case-sensitivity hint.
mass_moles — mass / amount / particle conversions
Six modes (mass_to_moles, moles_to_mass, mass_to_particles, particles_to_mass, moles_to_particles, particles_to_moles), molar mass from a formula or given directly, Avogadro constant 6.02214076e23.
{ "mode": "mass_to_moles", "value": 98, "formula": "H2SO4" }
{ "valid": true, "mode": "mass_to_moles", "molarMass": 98.072, "avogadro": 6.02214076e+23, "formula": "H2SO4", "moles": 0.9992658455012644 }
solution — concentration math
Four operations:
molarity— mass of solute + volume → mol/L:{ "operation": "molarity", "massG": 58.44, "volumeMl": 250, "formula": "NaCl" }→molarity: 4prepare— target molarity + volume → solute mass:{ "operation": "prepare", "molarity": 0.1, "volumeMl": 500, "formula": "NaOH" }→massG: 1.99985dilute— C1V1 = C2V2, give any three ofc1/v1/c2/v2(M, mL) and the missing one is computed:{ "operation": "dilute", "c1": 2, "c2": 0.5, "v2": 250 }→v1: 62.5,solved: "v1"percent—wv(g per 100 mL),vv(mL per 100 mL) ormasspercent:{ "operation": "percent", "percentType": "wv", "soluteG": 5, "solutionMl": 200 }→percent: 2.5
ph — pH chemistry at 25 °C (Kw = 1e-14)
Five operations:
from_h—{ "operation": "from_h", "hConc": 0.001 }→pH: 3, pOH: 11, ohConc: 1e-11from_oh— [OH-] → pH/pOHfrom_ph— pH → [H+]from_poh— pOH → [OH-]buffer— Henderson–Hasselbalch:{ "operation": "buffer", "pKa": 4.76, "acidConc": 0.2, "baseConc": 0.1 }→pH: 4.4589700043360185, bufferRatio: 0.5
Negative pH for >1 M acids is supported ([H+] = 2 → pH −0.301).
balance_equation — balance an equation (exact integer coefficients)
The solver runs entirely over BigInt-backed rationals, so coefficients are exact integers — never 2.9999997. Write species separated by + and one arrow (->, =, →); input coefficients like 2H2 are accepted and ignored (scaling a column cannot change the solution).
{ "equation": "C3H8 + O2 -> CO2 + H2O" }
{
"valid": true,
"balanced": true,
"unique": true,
"equation": "C3H8 + 5 O2 -> 3 CO2 + 4 H2O",
"reactants": [{ "formula": "C3H8", "coefficient": 1 }, { "formula": "O2", "coefficient": 5 }],
"products": [{ "formula": "CO2", "coefficient": 3 }, { "formula": "H2O", "coefficient": 4 }],
"elements": [
{ "symbol": "C", "left": 3, "right": 3 },
{ "symbol": "H", "left": 8, "right": 8 },
{ "symbol": "O", "left": 10, "right": 10 }
],
"chargeBalanced": false
}
Deterministic behaviour worth knowing:
- Charged species force charge conservation too —
MnO4- + Fe2+ + H+ -> Mn2+ + Fe3+ + H2O→ 1/5/8/1/5/4 withchargeBalanced: true. - Hydrate water counts as water —
CuSO4·5H2O -> CuSO4 + H2O→ 1/1/5. - Separation rule: a bare
+next to a symbol is a charge, soNa+ + Cl-keeps its ions while the compact formFe+O2->Fe2O3splits. Charged species must use the spaced form (the compact form is ambiguous by construction). - Honest failures:
H2 + O2 -> H2O + N2returnsvalid: true, balanced: falsewith the reason (“at least one species would need coefficient 0”) instead of silently dropping N2. Equations with several independent balances (Fe + O2 -> Fe2O3 + Fe3O4) returnunique: falseplus the smallest solution by total coefficient sum. Malformed input (no arrow, unknown element) returnsvalid: false.
gas_law — ideal gas arithmetic with unit conversions
Fixed units, no unit guessing: pressure atm (also reported in kPa/mmHg), volume L (also mL), amount mol, temperature K (also °C). Four operations:
ideal— give exactly three ofpressureAtm,volumeL,moles,temperatureKand the fourth is solved from PV = nRT:{ "operation": "ideal", "moles": 1, "temperatureK": 273.15, "volumeL": 22.4 }→pressureAtm: 1.0006236404024167.molar_volume— V/n = RT/P, defaults 0 °C / 1 atm:{ "operation": "molar_volume" }→molarVolumeLPerMol: 22.413969545014133(textbook 22.414).density— ρ = P·M/(R·T) fromformulaormolarMass:{ "operation": "density", "formula": "CO2", "pressureAtm": 1, "temperatureK": 273.15 }→densityGPerL: 1.9634630051413435.molar_mass— M = ρ·R·T/P from a measured density:{ "operation": "molar_mass", "densityGPerL": 1.96, "pressureAtm": 1, "temperatureK": 273.15 }→molarMass: 43.9313803082277.
R = 8.31446261815324 / 101.325 L·atm/(mol·K). Missing operands and non-positive values are reported as errors rather than guessed.
stoichiometry — limiting reagent, yield and leftovers
Give the equation plus how much of each reactant you actually have (moles or grams) and the tool
answers the questions that follow: the extent of reaction, which reactant runs out first, how much of
every product forms, what is left over — and, with product + actualYieldG, the theoretical and
percent yield.
{ "equation": "C3H8 + O2 -> CO2 + H2O",
"reactants": [{ "formula": "C3H8", "moles": 0.5 }, { "formula": "O2", "moles": 2 }],
"product": "CO2", "actualYieldG": 44 }
{
"valid": true,
"equation": "C3H8 + 5O2 -> 3CO2 + 4H2O",
"extent": 0.4,
"limiting": ["O2"],
"reactants": [
{ "formula": "C3H8", "coefficient": 1, "molarMass": 44.097, "availableMoles": 0.5,
"availableGrams": 22.0485, "consumedMoles": 0.4, "leftoverMoles": 0.1,
"leftoverGrams": 4.4097, "limiting": false },
{ "formula": "O2", "coefficient": 5, "molarMass": 31.998, "availableMoles": 2,
"availableGrams": 63.996, "consumedMoles": 2, "leftoverMoles": 0,
"leftoverGrams": 0, "limiting": true }
],
"products": [
{ "formula": "CO2", "coefficient": 3, "molarMass": 44.009, "moles": 1.2, "grams": 52.8108 },
{ "formula": "H2O", "coefficient": 4, "molarMass": 18.015, "moles": 1.6, "grams": 28.824 }
],
"yieldProduct": "CO2",
"theoreticalYieldMol": 1.2,
"theoreticalYieldG": 52.8108,
"actualYieldG": 44,
"percentYield": 83.3162913646
}
Deterministic behaviour worth knowing:
- Every reactant needs an amount — an unstated amount would make the limiting reagent undefined, so it is an error naming the missing species instead of an assumption about “excess”.
- Co-limiting is reported, not broken by a tie-break: at exactly 2 mol H2 : 1 mol O2 both are returned in
limiting, and nothing is left over. - Grams are converted with the same IUPAC molar masses
formula_massreports, so6.54 g Znis the same number everywhere. - Coefficients come from the exact rational balancer — this tool balances first (
C3H8 + O2 -> …works as written), charge and hydrate included. - Honest failures: a species that is not a reactant, both/neither of
moles/grams, a duplicate entry, a non-positive amount, a product that is not in the equation, oractualYieldGwithoutproductall returnvalid: falsewith the reason.
Install
dsh plugin --profile <profile> add github:TYEclipse/dsh-chem
(Requires pnpm on PATH — dsh plugin forwards installs to it.)
Verify
dsh plugin --profile <profile> list # dsh-chem present
dsh --dump-config --profile <profile> # == dsh-chem layer present
Development
pnpm install
pnpm build # tsc -> dist/ (committed; git installs do not build)
pnpm test # vitest, 136 tests
pnpm lint # oxlint src test
All numeric anchors are generated by the independent Python oracle test/anchors-chem.py (own periodic table + own tokenizer + own Fraction-based equation balancer) and cross-validated against published textbook values (H2O 18.015, H2SO4 98.08, NaCl 58.44, Ca(OH)2 74.09, CuSO4·5H2O 249.68, C6H12O6 180.16, KMnO4 158.03, NH3 17.03; propane 1/5/3/4; permanganate/HCl 2/16/2/2/5/8; 22.414 L/mol at STP). Run it with python3 test/anchors-chem.py.
License
MIT