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.
formula_from_composition — elemental analysis → empirical (and molecular) formula
Give the mass percent (or the grams) of each element and get the empirical formula, the integer
multiplier the analysis needed, and — when molarMass is also given — the molecular formula.
{ "mode": "percent",
"parts": [{ "symbol": "C", "value": 40.0 }, { "symbol": "H", "value": 6.71 }, { "symbol": "O", "value": 53.29 }],
"molarMass": 180.156 }
{
"valid": true,
"mode": "percent",
"empiricalFormula": "CH2O",
"empiricalMolarMass": 30.026,
"multiplier": 1,
"maxDeviation": 0.00114558531746,
"exact": true,
"elements": [
{ "symbol": "C", "massG": 40, "atomicMass": 12.011, "moles": 3.33028057614, "ratio": 1, "subscript": 1, "massFraction": 40.0019982682 },
{ "symbol": "H", "massG": 6.71, "atomicMass": 1.008, "moles": 6.65674603175, "ratio": 1.99885441468, "subscript": 2, "massFraction": 6.7141810431 },
{ "symbol": "O", "massG": 53.29, "atomicMass": 15.999, "moles": 3.33083317707, "ratio": 1.00016593225, "subscript": 1, "massFraction": 53.2838206887 }
],
"molecularFormula": "C6H12O6",
"molecularMultiplier": 6,
"molecularExact": true,
"measuredMolarMass": 180.156,
"impliedMolarMass": 180.156,
"molarMassErrorPercent": 0
}
What it does deliberately:
- The smallest ratio is not automatically 1 —
Fe111.69 g +O47.997 g gives ratios 1 : 1.5, so subscripts come from the smallest multiplierk ∈ [1, 8]whose scaled ratios are all within ±0.05 of integers (herek = 2→Fe2O3), andmultiplieris reported so the step is visible. - A molecular formula needs a whole multiple —
molarMassis only accepted as a molecular formula when it is within 2 % of an integer multiple of the empirical mass; otherwisemolecularExact: falseand anotesay how far off it is. - No silent rounding — when no multiplier fits, the closest fit is returned with
exact: falseand a note, because an analysis that does not support integer subscripts is a measurement problem, not a formula. - Percent data must add up —
mode: "percent"requires the parts to sum to 100 ± 1; a 90 % total is an error naming the total, not something to normalise away. - Hill order — C, then H, then alphabetical (alphabetical without carbon), so the same analysis always prints the same string regardless of input order.
- Errors instead of guesses — unknown symbol, a duplicate element (
Handh), a non-positive value, more than 12 elements, or a non-positivemolarMassall returnvalid: falsewith the reason.
equilibrium — weak acid / weak base ICE arithmetic
The calculation everyone skips: a weak acid does not dissociate completely, so x is the positive root
of x² + K·x − K·C = 0, not √(K·C). This tool solves the quadratic exactly and also tells you what the
shortcut would have said.
{ "mode": "weak_acid", "k": 1.8e-5, "concentration": 0.1 }
{
"valid": true,
"mode": "weak_acid",
"constantK": 1.8e-5,
"pK": 4.7447274949,
"concentration": 0.1,
"x": 0.00133267097308,
"hConc": 0.00133267097308,
"ohConc": 7.50372762821e-12,
"pH": 2.87527706155,
"pOH": 11.1247229384,
"degreeOfDissociation": 0.0133267097308,
"percentDissociation": 1.33267097308,
"approximation": { "x": 0.0013416407865, "errorPercent": 0.673070367938 },
"kFromResult": 1.8e-5
}
- The shortcut is reported, not assumed valid —
approximation.errorPercentis how much√(K·C)overstatesx. It grows as the solution gets more dilute (0.21 % at 1 M acetic acid, 2.14 % at 0.01 M, 6.93 % at 0.001 M ammonia with Kb = 1.8e-5); past 5 % anotesays to use the quadratic answer. - Cancellation-free root —
x = 2KC/(K + √(K²+4KC))instead of(−K + √(K²+4KC))/2: algebraically the same root, but the second form loses precision exactly in the weak-acid regime (K ≪ C). - A back-check is part of the answer —
kFromResultrecomputesx²/(C − x), which must equal the K you supplied; if it does not, the numbers are wrong. weak_basemirrors it — Kb solves for[OH-], thenpHcomes from Kw = 1e-14 at 25 °C.- Scope, stated plainly — monoprotic acids/bases, no activity coefficients, no ionic strength, no polyprotic steps, no temperature dependence.
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
No comments yet. Be the first to write one.