Proposed composition from lab results
64-bitThe mole fractions a sample's results give for a stream, for a person to accept or reject.
w = % ÷ 100; w = ppm × 1e-6; w = (mg/L × 1e-3) ÷ ρ; nᵢ = wᵢ ÷ MWᵢ; zᵢ = nᵢ ÷ Σ n
64-bit throughout; the mole amounts are summed in component order and each is divided by the total. The balance component gets max(0, 1 − Σ w), so a sum slightly above 1 gives it zero. The percentage in the warning text is rounded to one decimal; the composition is not rounded.
/** map { analytes: { 'Methanol': { component: 'methanol', basis: 'mass_frac' | 'mole_frac' | 'mass_pct' | 'mole_pct' | 'ppm_mass' | 'mg_per_l' (needs density) } }, balance?: 'water', density?: kg/m3 }.
* → { stream: { z (mole fractions in th order), T, P }, rows, problems, provenance, review } — a PROPOSAL. */
export function proposeStream(sample, results, map, th, { maxAgeDays = null, now = Date.now(), system = '' } = {}) {
const problems = [], rows = [], about = [{ kind: 'sample', id: sample.id }], mass = new Array(th.n).fill(null), mole = new Array(th.n).fill(null);
for (const r of results) {
const m = map.analytes?.[r.analyte];
if (!m) { problems.push(problem('unmapped', 'note', `Analyte "${r.analyte}" has no component in the map; it was left out.`, about)); continue; }
const i = th.ids.indexOf(m.component); if (i < 0) { problems.push(problem('nocomponent', 'warning', `Component "${m.component}" is not in this flowsheet.`, about)); continue; }
if (r.value == null) { problems.push(problem('novalue', 'warning', `${r.analyte} has no numeric value.`, about)); continue; }
if (r.flag && /^(R|F|INV|REJ)/i.test(r.flag)) { problems.push(problem('flagged', 'warning', `${r.analyte} is flagged "${r.flag}" in the source and was left out.`, about)); continue; }
const mw = th.c[i].MW; let v = r.value;
switch (m.basis) {
case 'mass_frac': mass[i] = v; break; case 'mass_pct': mass[i] = v / 100; break; case 'ppm_mass': mass[i] = v * 1e-6; break;
case 'mole_frac': mole[i] = v; break; case 'mole_pct': mole[i] = v / 100; break;
case 'mg_per_l': if (!(map.density > 0)) { problems.push(problem('density', 'warning', `${r.analyte} is in mg/L; the map needs a density (kg/m3) to turn that into a fraction.`, about)); continue; } mass[i] = v * 1e-3 / map.density; break;
default: problems.push(problem('basis', 'warning', `${r.analyte}: unknown basis "${m.basis}".`, about)); continue;
}
rows.push({ analyte: r.analyte, component: m.component, value: r.value, unit: r.unit, basis: m.basis, resultId: r.id });
}
// everything to mole amounts
let n = new Array(th.n).fill(0), used = 0;
for (let i = 0; i < th.n; i++) { if (mass[i] != null) { n[i] = mass[i] / th.c[i].MW; used += mass[i]; } else if (mole[i] != null) n[i] = mole[i]; }
const anyMass = mass.some((v) => v != null), anyMole = mole.some((v) => v != null);
if (anyMass && anyMole) problems.push(problem('mixed', 'warning', 'Some analytes are on a mass basis and some on a mole basis; the proposal mixes them as moles per kilogram and per mole, which is not meaningful. Use one basis.', about));
if (anyMass && used > 1.005) problems.push(problem('over', 'error', `The mass fractions add up to ${(used * 100).toFixed(1)} %.`, about));
if (map.balance && anyMass) { const i = th.ids.indexOf(map.balance); if (i >= 0 && mass[i] == null) n[i] = Math.max(0, 1 - used) / th.c[i].MW; else problems.push(problem('balance', 'warning', 'The balance component is missing or already measured.', about)); }
else if (anyMass && used < 0.995) problems.push(problem('under', 'warning', `The mass fractions add up to only ${(used * 100).toFixed(1)} % and no balance component is named; they were normalised.`, about));
const tot = n.reduce((a, b) => a + b, 0); let z = null;
if (tot > 0) z = n.map((v) => v / tot); else problems.push(problem('empty', 'error', 'No usable result: the proposal has no composition.', about));
if (maxAgeDays != null && sample.collected) { const t = Date.parse(sample.collected); if (Number.isFinite(t) && (now - t) / 864e5 > maxAgeDays) problems.push(problem('stale', 'warning', `The sample was collected ${Math.round((now - t) / 864e5)} days ago (limit ${maxAgeDays}).`, about)); }
return { kind: 'cadcraft-lims-proposal', stream: { z, T: sample.temperature, P: sample.pressure }, rows, problems, provenance: { system: str(system), sample: sample.id, name: sample.name, results: rows.map((r) => r.resultId), collected: sample.collected }, review: { state: 'proposed' }, disclaimer: DISCLAIMER };
}