Skip to content
Edit on GitHub

Add a scientific route

verifiedAgainst 9a2d070 · verifiedOn 2026-09-10 · the automated staleness banner is planned, not built.

This page authors one route — a substance-and-reaction pair — end to end, then strips the steps into a checklist. The reference edits to read alongside it are the two hardest authored substances (GraphiteSolid.asset, a self-derived Shomate fit; MalachiteSolid.asset, a flat-Cp approximation with a deliberately short validity range) and the ReactionCatalogTests.cs fixtures — the only worked reactions in the repo today.

The malachite roasting route:

Cu2CO3(OH)2(s) -> 2 CuO(s) + CO2(g) + H2O(g) ΔrH°(298 K) = +104 kJ/mol

Concretely:

  1. A new CopperOxideSolid (CuO, tenorite) SubstanceDefinition, stable id 11, with sourced ΔfH° / S° and a Shomate fit.
  2. A new WaterVaporGas SubstanceDefinition — stable id 4 (the same substance as liquid water) in the Gas phase, from NIST’s published gas-phase Shomate set. The roast evolves steam, not liquid.
  3. The project’s first ReactionDefinition (MalachiteRoasting.asset) and first ReactionCatalog.asset — atom- and charge-balanced, thermodynamically real, with the Arrhenius A / Ea explicitly deferred the same way Docs/CHEMISTRY_SOURCES.md defers the Boudouard and combustion cluster.
  4. EditMode tests mirroring SubstanceCatalogTests and ReactionCatalogTests.

You will also find that the route’s honest usable temperature window today is only 500–570 K — narrower than the reaction’s real onset — and the page explains why that is the correct state to ship.

The natural-deposits rule does not constrain this page (no new block generates); the binding rule here is Docs/MASTERPLAN.md §28.5 — A and Ea are never invented to hit a play time — and §36.3’s “every thermodynamic value is sourced”.

  • VoxelSandbox.Data.Chemistry owns the authoring layer: SubstanceDefinition / SubstanceCatalog and ReactionDefinition / ReactionTermAuthoring / ReactionCatalog. These are ScriptableObjects a designer edits.
  • VoxelSandbox.Chemistry owns the immutable baked targets — ThermodynamicPhaseDefinition, SubstanceTable, ChemicalReaction — and the math: ShomateThermodynamics, ReactionThermodynamics, ReactionKineticsMath, AtomBalance / ChargeBalance. It has no UnityEngine reference.
  • The bake direction is one way: Data.Chemistry → Chemistry. Chemistry never sees a ScriptableObject. See Assemblies & boundaries.
flowchart LR
  SD["SubstanceDefinition\nCuO, H2O(g) — authored"]
  SC["SubstanceCatalog\nvalidated list"]
  ST["SubstanceTable\nbaked, immutable"]
  RT["ReactionTermAuthoring\nsubstance + coefficient + order"]
  RD["ReactionDefinition\nterms + kinetics + citation"]
  RC["ReactionCatalog\nvalidated list"]
  CR["ChemicalReaction\nbaked, atom/charge-balanced"]
  TH["ReactionThermodynamics\nΔrH° ΔrS° ΔrG° K"]
  KI["ReactionKineticsMath\nk(T)=A·exp(-Ea/RT)"]

  SD --> SC --> ST
  SD --> RT --> RD --> RC --> CR
  ST --> TH
  CR --> TH
  CR --> KI
  KI -. blocked: no sourced A/Ea .-> CR

Read these Docs/ sections:

  • Docs/CHEMISTRY_FOUNDATIONS.md — R1 (deterministic transcendentals), D1 (the 60× process-rate factor; thermodynamic state is never scaled), C0 sub-stepping.
  • Docs/CHEMISTRY_SOURCES.md — “Kinetics and reaction affinity”, “Reactions — none authored yet” (read this in full — it is the template for your deferral paragraph), and “Known gaps”.
  • Docs/MASTERPLAN.md §28.5 (kinetics sourcing bar), §28.6 (redox, if your route is electrochemical), §36.1–36.3 (the authoring layer and the copper chain).
  • The existing citations in Assets/_Game/Data/Chemistry/Substances/GraphiteSolid.asset and MalachiteSolid.asset — the standard this project holds a sourceCitation to.

Intake decisions for this route:

Decision Choice Why
New substances needed? CuO(s) (new id 11); H2O(g) (new phase of id 4) neither is in the catalog; the liquid-water Shomate is capped at 500 K and is the wrong phase for a roast
Thermodynamics sourced ΔfH° / S° from NIST-JANAF / CRC; Shomate from a published set or a documented fit
Kinetics (A, Ea) deferred no single citable rate constant for malachite decomposition — TGA studies span a wide Ea; §28.5 forbids picking one
Redox? no not an electron-transfer reaction
Surrogate? yes a deferred-kinetics reaction is excluded from accuracy claims (isSurrogate = true)

Validation: the Chemistry Lab module (Tools ▸ Voxel Sandbox ▸ Voxel Workshop ▸ Chemistry Lab) validates the SubstanceCatalog and evaluates one substance’s Cp / S / ΔfH°(T). It has no reaction preview yet — the reaction is validated by an EditMode test.


SubstanceDefinition (SubstanceDefinition.cs:16) carries the fields below, and TryBuildDefinition (:97) rejects it if the sourceCitation (:48) is blank, if the formula-derived molar mass disagrees with the authored value by more than 1 %, or if the Shomate range is unordered.

Create Assets/_Game/Data/Chemistry/Substances/CopperOxideSolid.asset (menu: Voxel Sandbox/Chemistry/Substance Definition), matching the field shape of MalachiteSolid.asset:

# Assets/_Game/Data/Chemistry/Substances/CopperOxideSolid.asset — the authored fields
stableId: 11
displayName: Copper(II) Oxide
formula: CuO
phase: 0 # Solid
molarMassGramsPerMol: 79.545
standardEnthalpyOfFormationKilojoulesPerMol: -157.3 # measured — NIST-JANAF / CRC 97th ed.
standardEntropyJoulesPerMolKelvin: 42.6 # measured — same
densityKilogramsPerCubicMeter: 6315 # tenorite, calculated from the unit cell
shomate:
minimumTemperatureKelvin: 298.15
maximumTemperatureKelvin: 1300
a: 44.0 # illustrative — fit these from the NIST-JANAF CuO Cp(T) rows
b: 12.0 # the same least-squares way GraphiteSolid.asset was fit;
c: 0.0 # do NOT ship these placeholder numbers.
d: 0.0
e: -0.4
f: -170.6
g: 90.0
h: -157.3 # = ΔfH°, by the Shomate convention

The shomate block above is illustrative. Follow GraphiteSolid.asset’s citation: NIST-JANAF publishes CuO as a row-by-row Cp / S / H−H298 table, not pre-fit Shomate coefficients — fit the Shomate Cp polynomial to those rows by unweighted least squares over 298–1300 K, record the max relative error, and calibrate F and G so H(T)−H298 and S match the table at 298.15 K. H is the standard enthalpy of formation.

The sourceCitation is required and is prose, not a URL list. Name every number’s source, its uncertainty, and — as MalachiteSolid.asset does — say plainly where the data is an approximation and what a future pass should replace.

Steam has a published Shomate set (NIST-JANAF via the NIST WebBook, Chase 1998), so no fitting is needed. It is the same substance as WaterLiquid.asset — reuse stableId: 4 and change only the phase. SubstanceCatalog.ValidateCatalog (SubstanceCatalog.cs:46) keys uniqueness on (SubstanceId, Phase) — (4, Gas) and (4, Liquid) coexist cleanly.

# Assets/_Game/Data/Chemistry/Substances/WaterVaporGas.asset
stableId: 4
displayName: Water Vapour
formula: H2O
phase: 2 # Gas
molarMassGramsPerMol: 18.0153
standardEnthalpyOfFormationKilojoulesPerMol: -241.8264 # measured — NIST-JANAF (Chase 1998)
standardEntropyJoulesPerMolKelvin: 188.835 # measured — same
densityKilogramsPerCubicMeter: 0.804
shomate:
minimumTemperatureKelvin: 500 # NIST publishes the 500–1700 K set
maximumTemperatureKelvin: 1700
a: 30.09200
b: 6.832514
c: 6.793435
d: -2.534480
e: 0.082139
f: -250.8810
g: 223.3967
h: -241.8264

Those nine numbers are copied verbatim from the NIST WebBook water (gas) page — cite it as such, with the CAS number, in the sourceCitation. (evidence: measured)

3. Register both in the catalog — SubstanceCatalog.asset

Section titled “3. Register both in the catalog — SubstanceCatalog.asset”

Add both .asset GUIDs to the definitions: list in Assets/_Game/Data/Chemistry/SubstanceCatalog.asset (the Inspector’s list, or the YAML directly):

definitions:
- {fileID: 11400000, guid: <existing…>, type: 2}
+ - {fileID: 11400000, guid: <CopperOxideSolid guid>, type: 2}
+ - {fileID: 11400000, guid: <WaterVaporGas guid>, type: 2}

TryBuildTable (SubstanceCatalog.cs:87) refuses to bake if any entry has a validation issue, so a half-authored citation fails the whole table rather than silently dropping one row.

4. Author the reaction — MalachiteRoasting.asset

Section titled “4. Author the reaction — MalachiteRoasting.asset”

ReactionDefinition (ReactionDefinition.cs:16) holds a List<ReactionTermAuthoring> plus the kinetics block (:27). Each ReactionTermAuthoring (ReactionTermAuthoring.cs:13) is a SubstanceDefinition reference, a signed stoichiometric coefficient (negative = reactant, positive = product, never zero), and a rate-law order used only for reactants.

The four terms:

Substance Coefficient Formula contribution
MalachiteSolid (id 9) -1 Cu₂ C₁ O₅ H₂
CopperOxideSolid (id 11) +2 Cu₂ O₂
CarbonDioxideGas (id 2) +1 C₁ O₂
WaterVaporGas (id 4, Gas) +1 H₂ O₁

Reactant and product atom sums are both Cu₂ C₁ O₅ H₂; every formal charge is 0. The build in tests looks like the ReactionCatalogTests fixture (ReactionCatalogTests.cs:33):

// illustrative — the authoring call, mirroring CreateHydrogenCombustion()
var terms = new[]
{
ReactionTermAuthoring.CreateDefault(malachite, -1),
ReactionTermAuthoring.CreateDefault(copperOxide, +2),
ReactionTermAuthoring.CreateDefault(carbonDioxide, +1),
ReactionTermAuthoring.CreateDefault(waterVapour, +1),
};
var roasting = ReactionDefinition.CreateDefault(
stableId: 1, "Malachite roasting", terms,
preExponentialFactor: 1d, // deferred — see the citation
activationEnergyKilojoulesPerMol: 0d,
isHeterogeneous: true, // a solid decomposing with gas products
isSurrogate: true, // excluded from accuracy claims until Ea is sourced
sourceCitation: "<the deferral paragraph — see below>");

TryBuildReaction (ReactionDefinition.cs:90) forwards to ChemicalReaction.TryCreate (ChemicalReaction.cs:193), which runs AtomBalance.Check and ChargeBalance.Check on the supplied formulae and fails with "Reaction does not conserve atoms" (:243) or "…charge" (:250) otherwise. The balance is proved at construction — you cannot get an unbalanced ChemicalReaction.

5. The kinetics deferral — the crux of this page

Section titled “5. The kinetics deferral — the crux of this page”

ReactionDefinition requires a non-empty sourceCitation (:53) for its kinetics — the field docstring quotes §28.5: “A and Ea are never invented merely to hit a desired play time.” There is no single citable Arrhenius pair for malachite (basic copper carbonate) thermal decomposition: reported activation energies from TGA / DSC studies span roughly 80–170 kJ/mol depending on sample, atmosphere, and the model fitted, and the reaction proceeds through hydroxide loss then carbonate loss rather than one elementary step.

So the honest state — matching how CHEMISTRY_SOURCES.md treats Boudouard, combustion, slaking and calcination — is:

  • author the reaction with isSurrogate: true and placeholder A = 1, Ea = 0;
  • write a sourceCitation that states, in prose: the balanced equation and its thermodynamic sources; that the kinetics are deferred; the literature situation (the Ea spread, the multi-step mechanism); and what a future pass must find (a primary TGA study for one well-characterised malachite, or a defensible multi-step scheme);
  • keep the ReactionCatalog.asset out of any solver-loaded path until real kinetics land. VesselRuntime takes optional catalog references — do not wire this one in.

A reader who wants the number later has the balanced, thermodynamically-anchored reaction ready; only the rate law is missing, and its absence is documented at the point of use.

Create Assets/_Game/Data/Chemistry/ReactionCatalog.asset (menu: Voxel Sandbox/Chemistry/Reaction Catalog) and add MalachiteRoasting.asset to its definitions: list. TryBuildReactions (ReactionCatalog.cs:69) validates every entry, then enforces reaction-id uniqueness at bake time (matching VesselSolver’s own duplicate check). This asset exists so the authoring path is exercised and the reaction is diffable — not so the solver runs it.

7. What the thermodynamics say — and their ceiling

Section titled “7. What the thermodynamics say — and their ceiling”

ReactionThermodynamics.TryEvaluate (ReactionThermodynamics.cs:72) sums Σνᵢ(ΔfH°298 + [H°(T)−H°298]) and ΣνᵢS°(T) in the supplied term order, then derives ΔrG° = ΔrH° − TΔrS° and K = exp(−ΔrG°/RT). From the catalogued ΔfH° values:

ΔrH°(298 K) = 2(-157.3) + (-393.5224) + (-241.8264) - (-1053.9) = +103.95 kJ/mol

(evidence: calculated from the catalogued standard enthalpies of formation) — endothermic, as a roast must be.

The ceiling. ReactionThermodynamics.TryEvaluate calls the strict ShomateThermodynamics.TryEvaluate (ShomateThermodynamics.cs:129) for every term and returns false if any one is out of range. The per-species valid ranges here are:

Species Shomate range Set by
Cu2CO3(OH)2(s) 298.15 – 570 K MalachiteSolid.asset — a flat-Cp approximation, capped below the roast onset on purpose
H2O(g) 500 K – 1700 K NIST’s published set starts at 500 K
CuO(s) 298.15 – 1300 K your fit
CO2(g) 298 – 1200 K CarbonDioxideGas.asset

The intersection is 500–570 K — a 70 K window, and the reaction physically runs above ~600 K. This is the correct thing to ship: the malachite entry’s own citation says its high-temperature Cp is unsourced and “a future pass should replace [it] with a sourced polynomial before trusting this substance above room temperature.” Your route inherits that gap; document it, don’t paper over it.

Add MalachiteRouteTests.cs (or extend ReactionCatalogTests), following the fixture pattern — build the definitions with CreateDefault, assert, and DestroyImmediate in [TearDown] (ReactionCatalogTests.cs:13):

// illustrative
[Test]
public void MalachiteRoasting_BalancesAndBuilds()
{
ReactionDefinition roasting = CreateMalachiteRoasting();
Assert.That(roasting.TryBuildReaction(out ChemicalReaction reaction, out string error), Is.True, error);
Assert.That(reaction.SpeciesCount, Is.EqualTo(4));
}
[Test]
public void MalachiteRoasting_ThermodynamicsEvaluateInsideTheSharedWindow()
{
// 540 K is inside 500–570 K, the only band where every species has valid Shomate data.
var terms = BuildThermodynamicTerms();
Assert.That(ReactionThermodynamics.TryEvaluate(terms, 540d, out ReactionThermodynamicState state), Is.True);
Assert.That(state.EnthalpyKilojoulesPerMol, Is.GreaterThan(0d)); // endothermic
}
[Test]
public void MalachiteRoasting_ThermodynamicsFailAboveTheMalachiteCeiling()
{
var terms = BuildThermodynamicTerms();
Assert.That(ReactionThermodynamics.TryEvaluate(terms, 650d, out _), Is.False);
}

Also extend SubstanceCatalogTests with a build assertion for each new .asset, mirroring ValidDefinition_BuildsIntoASubstanceTable (SubstanceCatalogTests.cs:36).


Run the Data tests headlessly (path from CLAUDE.md):

cd /home/soulwax/workspace/engines/unity/minecraft/Minecraft-HD
"/home/soulwax/Unity/Hub/Editor/6000.6.0f1/Editor/Unity" -runTests -batchmode \
-projectPath . -testPlatform EditMode \
-testFilter "VoxelSandbox.Tests.ReactionCatalogTests|VoxelSandbox.Tests.SubstanceCatalogTests|VoxelSandbox.Tests.MalachiteRouteTests" \
-testResults ./Logs/route.xml -logFile ./Logs/route.log

Then, in the open Editor — Chemistry Lab:

  1. The validation summary reads “12 substance(s), all valid.” (10 existing + CuO + steam), no error rows.
  2. Select Copper(II) Oxide (CuO, Solid) — Cp / S / ΔfH°(T) evaluate at 298.15 K and at 1000 K.
  3. Select Water Vapour (H2O, Gas) — evaluates at 1000 K; at 298.15 K it reports “outside the Shomate range” (the set starts at 500 K). That message is expected, not a bug.

“Done” = a green EditMode run, both substances valid in Chemistry Lab, and the reaction building + balancing + evaluating inside 500–570 K in the test.

  1. Name the route and write the balanced equation first. State symbols on every species. Confirm the atom and charge sums by hand before you touch code — ChemicalReaction.TryCreate will reject an unbalanced set, but knowing why it balances is the point.
  2. List the substances the route needs and check the catalog. For each missing one: a SubstanceDefinition with stableId (a genuinely new substance takes the next free id; a new phase of an existing substance reuses its id), formula, phase, molar mass (within 1 % of the formula), ΔfH°, S°, density, a Shomate set, and a prose sourceCitation.
  3. Source, don’t fit, when you can. A published Shomate set (NIST WebBook gas-phase species) is copied verbatim. A row-by-row Cp(T) table (JANAF solids, minerals) is least-squares fit — record the max error and calibrate F, G at 298.15 K, as GraphiteSolid.asset does. A single 298 K Cp point is a flat-Cp approximation with a short, honest validity cap, as MalachiteSolid.asset does.
  4. Register every new .asset in SubstanceCatalog.asset. TryBuildTable is all-or-nothing.
  5. Author the ReactionDefinition: signed integer coefficients, one ReactionTermAuthoring per species, rate-law orders on reactants.
  6. Kinetics — the decision that varies most:
    • sourced — a primary-source Arrhenius A / Ea in the right units (check cal vs J on Ea), converted into this codebase’s dimensionless-activity, whole-vessel-rate contract (r = k·Πaᵢ^νᵢ·S·(1−Q/K)) and checked against the C0 acceptance values. Real work; see the CO-oxidation note in CHEMISTRY_SOURCES.md.
    • deferred — isSurrogate: true, placeholder A/Ea, a sourceCitation that says what is missing and why, and the .asset kept out of any solver-loaded catalog.
  7. Redox routes additionally set isRedox, electronsTransferred, and a sourced standardCellPotentialVolts (§28.6; see the Electrochemistry Lab module and ElectrochemicalSeries).
  8. Check the shared Shomate window. ReactionThermodynamics needs every species valid at the evaluation temperature. Intersect the ranges; if the window doesn’t cover where the reaction runs, that is a finding to document.
  9. Tests mirroring SubstanceCatalogTests / ReactionCatalogTests — build, balance, evaluate in-window, fail out-of-window.
  10. CHANGELOG.md + VERSION bump; update the “Reactions — none authored yet” section of CHEMISTRY_SOURCES.md if you authored a real one.

ΔfH° and the Shomate H from different sources. Symptom: ΔfH°(T) drifts from the literature as T leaves 298 K; a reaction’s ΔrH°(T) is wrong only away from standard temperature. Cause: the Shomate H term and the standalone standardEnthalpyOfFormation were taken from datasets with different reference conventions. Fix: by the Shomate convention H is the standard enthalpy of formation — set them from the same source, equal.

Inventing A / Ea to make the route “work in-game”. Symptom: code review or a science audit flags the reaction; the citation can’t be defended. Cause: a plausible-looking Arrhenius pair was chosen to hit a play time — exactly what §28.5 forbids. Fix: defer it (isSurrogate, documented, out of the solver path). A missing rate law that is honest beats a present one that is fiction.

An unbalanced equation “almost” reaching the solver. Symptom: TryBuildReaction returns false with "does not conserve atoms". Cause: a coefficient sign or magnitude is wrong, or a formula string doesn’t parse the way ChemicalFormula.Parse reads it (check parentheses like Cu2CO3(OH)2). Fix: the balance proof is the feature. Fix the stoichiometry; the reaction cannot exist unbalanced.

A Shomate fit used outside its validity range. Symptom: ReactionThermodynamics.TryEvaluate returns false at the temperature the reaction actually runs. Cause: one species’ Shomate range (often a flat-Cp approximation deliberately capped, like malachite’s 570 K) doesn’t reach the reaction temperature. Fix: intersect all species’ ranges; either source a wider fit for the limiting species or ship the route with its window documented (as this page does).

Reusing a substance id for a genuinely different substance. Symptom: the catalog reports a duplicate (SubstanceId, Phase), or worse, silently maps two materials to one table row. Cause: stableId is the substance’s identity. A new phase of water is still id 4; copper oxide is a new substance and takes a new id. Fix: next free id for a new substance; the same id + a different phase for a new phase.

Skipping the sourceCitation. Symptom: TryBuildDefinition / TryBuildReaction returns false with "no source citation"; Chemistry Lab shows an error row. Cause: the field is required by design — there is no valid unsourced entry. Fix: write the prose citation. If you can’t source a number, you can’t author the entry yet.

  • Assemblies & boundaries — why VoxelSandbox.Chemistry carries no UnityEngine reference and never sees a ScriptableObject.
  • Add a voxel block type — its deferred BlockDefinition.composition step is completed by authoring the substance here.
  • Docs/CHEMISTRY_SOURCES.md “Reactions — none authored yet” and “Known gaps”; Docs/CHEMISTRY_FOUNDATIONS.md R1 / D1 / C0; Docs/MASTERPLAN.md §28.5, §36.1–36.3.

User-contributed notes

Corrections, clarifications, and practical tips for this page. Anonymous is fine — a name is optional. Basic Markdown works: **bold**, *italic*, `code`, and links.

Notes policy

Notes are lightly filtered for spam and may be edited or removed. Keep them about this page — no support requests, no personal data, nothing you would not publish. Links are limited and marked nofollow.

  1. Loading notes…