Skip to content

The specific heat of a gas mixture, live

A gas mixture's specific heat, the mass-weighted mean of its gases', c_p = Σ wᵢ c_p,ᵢ with wᵢ = xᵢ Mᵢ / M the mass fractions: an ideal mixture, each gas holding its heat as it would alone. The library's kernel (lusine.properties.mixture) takes a game's whole table of parts and the mixture's molar mass (lusine.properties.molar_mass's), and is bound over the game's mixtures. Below, a game's tables are written out: five gases, dry air, the same air in thousands of moles, nitrogen alone, a mole each of nitrogen and steam, and humid air, whose water is swept from none to 5% of its moles, 0.05% a step, twenty steps a second, the two kernels giving every mixture's molar mass and specific heat at each step on your GPU as you read.

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 specific heat 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, each's specific heat weighed by its share of the mass: the two lie one over the other, and humid air holds more heat per kilogram as its water rises. Below it, the share and the specific heat there.
  • The mixtures: dry air, the same air in thousands of moles, nitrogen alone, and a mole each of nitrogen and steam, beside the two gases weighed by mass, (M₁ c₁ + M₂ c₂) / (M₁ + M₂), and by amount, the plain mean of the two, which the law is not.
  • The checks, each held at every step: dry air, within 10 J/(kg K) of the standard atmosphere's 1005; a gas alone, nitrogen alone its own specific heat to a rounding; by mass, the mole each of nitrogen and steam within 0.00005% of the two weighed by mass; proportions, the same air in thousands of moles the same within 0.00005%; more heat when humid, humid air's specific heat higher at every step as its water rises; two mixtures mixed, humid air within 0.0002% 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 kernels' 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 molar mass and then the specific heat are bound over the mixtures, and the checks are taken.

// The gas mixture's specific heat's live page: a game's tables of gases, mixtures and parts, the library's molar mass
// and specific heat bound over the mixtures, humid air's water swept from none to 5% of its moles, against the
// standard atmosphere's dry air, a gas alone, equal amounts weighed by mass, 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 mixture;

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) and a specific heat near 300 K (the CRC Handbook's and
// Engineering ToolBox's).
struct gas { text name; float<kg/mol> molar_mass; float<J/(kg K)> specific_heat; };
table<gas> gases(capacity = 8, key = name) = {
  { name = "N2", molar_mass = 0.0280134 kg/mol, specific_heat = 1040.0 J/(kg K) },
  { name = "O2", molar_mass = 0.0319988 kg/mol, specific_heat = 918.0 J/(kg K) },
  { name = "Ar", molar_mass = 0.039948 kg/mol, specific_heat = 520.3 J/(kg K) },
  { name = "CO2", molar_mass = 0.0440095 kg/mol, specific_heat = 844.0 J/(kg K) },
  { name = "H2O", molar_mass = 0.01801528 kg/mol, specific_heat = 1864.0 J/(kg K) },
};

// The game's mixtures, each numbered, its molar mass and specific heat the kernels': dry air, the same air in
// thousands of moles, nitrogen alone, equal amounts of nitrogen and steam, and humid air, whose water the sweep moves.
struct mixture { text name; integer number; float<kg/mol> molar_mass; float<J/(kg K)> specific_heat; };
table<mixture> mixtures(capacity = 8, key = name) = {
  { name = "air", number = 0 }, { name = "kiloair", number = 1 }, { name = "nitrogen", number = 2 },
  { name = "even", 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 in two moles, a mole each of nitrogen and steam, 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 = 2.0, gas = gases["N2"] },
  { mixture = mixtures["even"], fraction = 1.0, gas = gases["N2"] },
  { mixture = mixtures["even"], fraction = 1.0, gas = gases["H2O"] },
  { 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 },
};

// 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 specific heat, read from its table; humid air's at the last step; dry air's molar mass; equal
// amounts of nitrogen and steam weighed by mass, (M_1 c_1 + M_2 c_2) / (M_1 + M_2), and by amount, the mean of the
// two; and humid air as dry air and steam mixed, each's specific heat weighed by its mass in the mixture.
struct {
  float<J/(kg K)> air; float<J/(kg K)> kiloair; float<J/(kg K)> nitrogen; float<J/(kg K)> even;
  float<J/(kg K)> humid; float<J/(kg K)> before; float<kg/mol> air_mass; float<J/(kg K)> by_mass;
  float<J/(kg K)> by_amount; float<J/(kg K)> mixed;
} law;

// The checks: dry air within 10 J/(kg K) of 1005 (the standard atmosphere's); a gas alone its own specific heat to a
// rounding; equal amounts of nitrogen and steam weighed by mass, within 0.00005%, not by amount; the same air in
// thousands of moles the same within 0.00005%; humid air holding more heat per kilogram at every step as its water
// rises; and humid air dry air and steam mixed within 0.0002%.
struct { bool air = true; bool alone = true; bool by_mass = true; bool proportions = true; bool humid = 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<J/(kg K)> heat_of(table<float<J/(kg K)>> specific_heat, table<integer> number, integer which)
{
  return sum(specific_heat) where number == which;
}

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> molar_mass(relation<gases> row) { return row.molar_mass; }
float<J/(kg K)> specific_heat(relation<gases> row) { return row.specific_heat; }

float<J/(kg K)> by_mass(relation<gases> first, relation<gases> second)
{
  return (molar_mass(row = first) * specific_heat(row = first) + molar_mass(row = second) * specific_heat(row = second))
         / (molar_mass(row = first) + molar_mass(row = second));
}

// Dry air and steam mixed, x of the moles steam: each's specific heat weighed by its share of the mass.
float<J/(kg K)> mixed(float<J/(kg K)> air, float<kg/mol> air_mass, relation<gases> steam, float water)
{
  let air_part = (1.0 - water) * air_mass;
  let steam_part = water * molar_mass(row = steam);
  return (air_part * air + steam_part * specific_heat(row = steam)) / (air_part + steam_part);
}

output table<lusine.widget> panels = {
  lusine.series { { panel = "humid air", chart = "specific heat", quantity = law.humid, steps = 101,
                    against = sweep.water, label = "the kernel" } },
  lusine.series { { panel = "humid air", chart = "specific heat", quantity = law.mixed, steps = 101,
                    against = sweep.water, label = "dry air and steam mixed" } },
  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.nitrogen, label = "nitrogen alone" } },
  lusine.value { { panel = "the mixtures", quantity = law.even, label = "nitrogen and steam, a mole each" } },
  lusine.value { { panel = "the mixtures", quantity = law.by_mass, label = "weighed by mass" } },
  lusine.value { { panel = "the mixtures", quantity = law.by_amount, label = "by amount, which it is not" } },
  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.by_mass, label = "by mass" } },
  lusine.value { { panel = "the checks", quantity = checks.proportions, label = "proportions" } },
  lusine.value { { panel = "the checks", quantity = checks.humid, label = "more heat 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);
  mixtures.specific_heat = lusine.properties.mixture(parts, mixtures.molar_mass);
  law.before = law.humid;
  law.air = heat_of(mixtures.specific_heat, mixtures.number, which = 0);
  law.kiloair = heat_of(mixtures.specific_heat, mixtures.number, which = 1);
  law.nitrogen = heat_of(mixtures.specific_heat, mixtures.number, which = 2);
  law.even = heat_of(mixtures.specific_heat, mixtures.number, which = 3);
  law.humid = heat_of(mixtures.specific_heat, mixtures.number, which = 4);
  law.air_mass = mass_of(mixtures.molar_mass, mixtures.number, which = 0);
  law.by_mass = by_mass(first = gases["N2"], second = gases["H2O"]);
  law.by_amount = 0.5 * (specific_heat(row = gases["N2"]) + specific_heat(row = gases["H2O"]));
  law.mixed = mixed(law.air, law.air_mass, steam = gases["H2O"], sweep.water);
  checks.air = checks.air && abs(law.air - 1005.0 J/(kg K)) < 10.0 J/(kg K);
  checks.alone = checks.alone
                 && abs(law.nitrogen - specific_heat(row = gases["N2"]))
                    <= 0.00000012 * specific_heat(row = gases["N2"]);
  checks.by_mass = checks.by_mass && abs(law.even - law.by_mass) <= 0.0000005 * law.by_mass;
  checks.proportions = checks.proportions && abs(law.kiloair - law.air) <= 0.0000005 * law.air;
  checks.humid = checks.humid && (sweep.step == 0 || law.humid > law.before);
  checks.mixed = checks.mixed && abs(law.humid - law.mixed) <= 0.000002 * law.mixed;
  lamps.colour = lamp_colour(holds = checks.air && checks.alone && checks.by_mass && checks.proportions
                                     && checks.humid && checks.mixed);
}

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