Skip to content

The molar mass of a mixture, live

A mixture's molar mass, its gases' weighted by amount, M = Σ xᵢ Mᵢ with xᵢ = nᵢ / Σ n: the mass of a mole of it. The library's kernel (lusine.properties.molar_mass) takes a game's whole table of parts, each a mixture's amount of a gas, and is bound over the game's mixtures, folding each one's own parts. Below, a game's tables are written out: five gases, dry air, the same air in thousands of moles, nitrogen alone, and humid air, whose water is swept from none to 5% of its moles, 0.05% a step, twenty steps a second, the kernel giving every mixture's molar mass at each step on your GPU as you read. A second table of parts relates to the library's own chemistry, and the same kernel binds it.

This page runs the program on your GPU through WebGPU, so it needs a browser with WebGPU (a recent Chrome, Edge, Safari or Firefox). Without it the program does not run and nothing is drawn below.

What it shows

  • The strip above the controls is green while every check holds, and turns red for good once one fails.
  • Humid air: the chart draws humid air's molar mass, in g/mol, against its water's share of the moles, a point a step over one whole sweep, and beside it the same air worked out as dry air and steam mixed, (1 − x) M_air + x M_water, from the dry air's molar mass and water's alone: the two lie one over the other, and air grows lighter by the mole as its water rises, which is why humid air rises. Below it, the share and the molar mass there.
  • The mixtures: dry air, the same air in thousands of moles, nitrogen alone in three moles, and steam, the library's water alone through the second table.
  • The checks, each held at every step: dry air, at 28.965 g/mol within 0.002 g/mol (the US Standard Atmosphere's 28.9645, from a composition whose carbon dioxide is today's mean); a gas alone, nitrogen in one mole exactly its own molar mass and in three to a rounding, and the library's water alone exactly the library's water's; proportions, the same air in thousands of moles the same within 0.00005%; lighter when humid, humid air lighter at every step as its water rises; two mixtures mixed, humid air within 0.0001% of dry air and steam mixed.

Every number here is computed by the program on the GPU; the page only reads the values back and draws them. The gases, the mixtures and the sweep are the program's tables and settings, which the host may write; this page offers no control to change them yet.

The program

The game's tables, the sweep, the kernel's answers and the checks, the panels the page draws, and the one procedure, main, run each step: the sweep writes humid air's water, the kernel is bound over both tables of mixtures, and the checks are taken.

// The molar mass's live page: a game's tables of gases, mixtures and parts, the library's molar mass bound over the
// mixtures, humid air's water swept from none to 5% of its moles, against the US Standard Atmosphere's dry air, a gas
// alone, the same air in thousands of moles and the mixing of two mixtures, with the page's checks computed here on
// the GPU and shown by the panels the program declares.

namespace molar_mass;

lusine.version version = { { number = 1 } };

// The reader's settings: the sweep, how far each step moves humid air's share of water by amount, from none.
host struct { float stride = 0.0005; } settings;

// The game's gases, each a molar mass (the CRC Handbook's).
struct gas { text name; float<kg/mol> molar_mass; };
table<gas> gases(capacity = 8, key = name) = {
  { name = "N2", molar_mass = 0.0280134 kg/mol }, { name = "O2", molar_mass = 0.0319988 kg/mol },
  { name = "Ar", molar_mass = 0.039948 kg/mol }, { name = "CO2", molar_mass = 0.0440095 kg/mol },
  { name = "H2O", molar_mass = 0.01801528 kg/mol },
};

// The game's mixtures, each numbered, its molar mass the kernel's: dry air, the same air in thousands of moles,
// nitrogen alone in one mole and in three, and humid air, whose water the sweep moves.
struct mixture { text name; integer number; float<kg/mol> molar_mass; };
table<mixture> mixtures(capacity = 8, key = name) = {
  { name = "air", number = 0 }, { name = "kiloair", number = 1 }, { name = "nitrogen", number = 2 },
  { name = "nitrogen3", number = 3 }, { name = "humid", number = 4 },
};

// The parts, each a mixture's amount of a gas: dry air by amount (the US Standard Atmosphere's, its carbon dioxide at
// today's mean), the same in thousands of moles, nitrogen alone, and humid air, dry air with water, the one part the
// sweep writes (its role 1).
struct part { relation<mixtures> mixture; float fraction; relation<gases> gas; integer role; };
table<part> parts(capacity = 32) = {
  { mixture = mixtures["air"], fraction = 0.78084, gas = gases["N2"] },
  { mixture = mixtures["air"], fraction = 0.20946, gas = gases["O2"] },
  { mixture = mixtures["air"], fraction = 0.00934, gas = gases["Ar"] },
  { mixture = mixtures["air"], fraction = 0.000412, gas = gases["CO2"] },
  { mixture = mixtures["kiloair"], fraction = 780.84, gas = gases["N2"] },
  { mixture = mixtures["kiloair"], fraction = 209.46, gas = gases["O2"] },
  { mixture = mixtures["kiloair"], fraction = 9.34, gas = gases["Ar"] },
  { mixture = mixtures["kiloair"], fraction = 0.412, gas = gases["CO2"] },
  { mixture = mixtures["nitrogen"], fraction = 1.0, gas = gases["N2"] },
  { mixture = mixtures["nitrogen3"], fraction = 3.0, gas = gases["N2"] },
  { mixture = mixtures["humid"], fraction = 0.78084, gas = gases["N2"] },
  { mixture = mixtures["humid"], fraction = 0.20946, gas = gases["O2"] },
  { mixture = mixtures["humid"], fraction = 0.00934, gas = gases["Ar"] },
  { mixture = mixtures["humid"], fraction = 0.000412, gas = gases["CO2"] },
  { mixture = mixtures["humid"], fraction = 0.0, gas = gases["H2O"], role = 1 },
};

// A second table of mixtures whose parts relate to another table of gases, the library's own chemistry: steam, the
// library's water alone.
table<mixture> blends(capacity = 2, key = name) = { { name = "steam", number = 0 } };
struct share { relation<blends> mixture; float fraction; relation<lusine.materials.chemical> gas; };
table<share> shares(capacity = 2) = {
  { mixture = blends["steam"], fraction = 1.0, gas = lusine.materials.chemical[lusine.materials.substances["water"]] },
};

// The sweep: the step it is at, from 0 to 100 and round again, and humid air's water's share of its moles there.
struct { integer step = -1; float water; } sweep;

// Each mixture's molar mass, read from its table; humid air's at the last step; the library's water; and humid air
// as dry air and steam mixed, (n_air M_air + n_water M_water) / (n_air + n_water), from the dry air's molar mass;
// and humid air's two as numbers of grams a mole, for the chart.
struct {
  float<kg/mol> air; float<kg/mol> kiloair; float<kg/mol> nitrogen; float<kg/mol> nitrogen3; float<kg/mol> humid;
  float<kg/mol> before; float<kg/mol> nitrogen_gas; float<kg/mol> steam; float<kg/mol> water; float<kg/mol> mixed;
  float humid_grams; float mixed_grams;
} law;

// The checks: dry air at 28.965 g/mol within 0.002 g/mol (the standard atmosphere's 28.9645, the composition's carbon
// dioxide today's); a gas alone its own molar mass, exactly in one mole and to a rounding in three, and the library's
// water alone the library's water's, exactly; the same air in thousands of moles the same within 0.00005%; humid air
// lighter at every step as its water rises; and humid air dry air and steam mixed within 0.0001%.
struct { bool air = true; bool alone = true; bool proportions = true; bool lighter = true; bool mixed = true; } checks;

// A strip of lamps the page draws, lit green while every check holds and red once one fails.
struct lamp { vector<float, 4> colour; };
lattice<lamp, 2> lamps(spacing = 1 m, boundary = wall);

vector<float, 4> lamp_colour(bool holds)
{
  return holds ? vector<float, 4>(0.15, 0.6, 0.3, 1.0) : vector<float, 4>(0.75, 0.15, 0.15, 1.0);
}

// The water's amount in humid air for a share of its moles, against dry air's 1.000052 moles.
float watered(float fraction, integer role, float water)
{
  return role == 1 ? 1.000052 * water / (1.0 - water) : fraction;
}

float<kg/mol> mass_of(table<float<kg/mol>> molar_mass, table<integer> number, integer which)
{
  return sum(molar_mass) where number == which;
}

float<kg/mol> gas_mass(relation<gases> row) { return row.molar_mass; }

float<kg/mol> library_water(relation<lusine.materials.chemical> row) { return row.molar_mass; }

float<kg/mol> mixed(float<kg/mol> air, float<kg/mol> steam, float water)
{
  return (1.0 - water) * air + water * steam;
}

output table<lusine.widget> panels = {
  lusine.series { { panel = "humid air", chart = "molar mass", quantity = law.humid_grams, steps = 101,
                    against = sweep.water, label = "the kernel, g/mol" } },
  lusine.series { { panel = "humid air", chart = "molar mass", quantity = law.mixed_grams, steps = 101,
                    against = sweep.water, label = "dry air and steam mixed, g/mol" } },
  lusine.value { { panel = "humid air", quantity = sweep.water, label = "water, by amount" } },
  lusine.value { { panel = "humid air", quantity = law.humid, label = "humid air" } },
  lusine.value { { panel = "the mixtures", quantity = law.air, label = "dry air" } },
  lusine.value { { panel = "the mixtures", quantity = law.kiloair, label = "in thousands of moles" } },
  lusine.value { { panel = "the mixtures", quantity = law.nitrogen3, label = "nitrogen, 3 mol" } },
  lusine.value { { panel = "the mixtures", quantity = law.steam, label = "the library's water alone" } },
  lusine.value { { panel = "the checks", quantity = checks.air, label = "dry air" } },
  lusine.value { { panel = "the checks", quantity = checks.alone, label = "a gas alone" } },
  lusine.value { { panel = "the checks", quantity = checks.proportions, label = "proportions" } },
  lusine.value { { panel = "the checks", quantity = checks.lighter, label = "lighter when humid" } },
  lusine.value { { panel = "the checks", quantity = checks.mixed, label = "two mixtures mixed" } },
};

void main()
{
  sweep.step = sweep.step >= 100 ? 0 : sweep.step + 1;
  sweep.water = float(sweep.step) * settings.stride;
  parts.fraction = watered(parts.fraction, parts.role, sweep.water);
  mixtures.molar_mass = lusine.properties.molar_mass(parts);
  blends.molar_mass = lusine.properties.molar_mass(parts = shares);
  law.before = law.humid;
  law.air = mass_of(mixtures.molar_mass, mixtures.number, which = 0);
  law.kiloair = mass_of(mixtures.molar_mass, mixtures.number, which = 1);
  law.nitrogen = mass_of(mixtures.molar_mass, mixtures.number, which = 2);
  law.nitrogen3 = mass_of(mixtures.molar_mass, mixtures.number, which = 3);
  law.humid = mass_of(mixtures.molar_mass, mixtures.number, which = 4);
  law.nitrogen_gas = gas_mass(row = gases["N2"]);
  law.steam = mass_of(molar_mass = blends.molar_mass, number = blends.number, which = 0);
  law.water = library_water(row = lusine.materials.chemical[lusine.materials.substances["water"]]);
  law.mixed = mixed(law.air, steam = gas_mass(row = gases["H2O"]), sweep.water);
  law.humid_grams = law.humid / 0.001 kg/mol;
  law.mixed_grams = law.mixed / 0.001 kg/mol;
  checks.air = checks.air && abs(law.air - 0.028965 kg/mol) < 0.000002 kg/mol;
  checks.alone = checks.alone && law.nitrogen == law.nitrogen_gas
                 && abs(law.nitrogen3 - law.nitrogen_gas) <= 0.00000012 * law.nitrogen_gas && law.steam == law.water;
  checks.proportions = checks.proportions && abs(law.kiloair - law.air) <= 0.0000005 * law.air;
  checks.lighter = checks.lighter && (sweep.step == 0 || law.humid < law.before);
  checks.mixed = checks.mixed && abs(law.humid - law.mixed) <= 0.000001 * law.mixed;
  lamps.colour = lamp_colour(holds = checks.air && checks.alone && checks.proportions && checks.lighter
                                     && checks.mixed);
}

The law's other proof (every mixture bit for bit against a binary32 model of the kernel's folds, and planted faults caught) is on its proof page.