Resources Numerical accuracy Calculation policy

Imported carbon factors, prices and climate data

Reads a person's own carbon-factor file, cost book or climate answer into the tables the carbon, cost and solar panes calculate from.

How the precision is kept

The numbers are the source's; this code converts units and cleans them. Factor and money cells have currency signs, spaces and every comma removed before they become 64-bit numbers. Monthly climate values are rounded to two decimals when stored. A request to a climate provider carries the location to three decimals of a degree, and answers are cached on a 0.1 degree grid.

Open item. Read a decimal comma in imported files, or refuse a cell whose only comma is followed by one or two digits, so that 1,5 is never read as 15.

Calculations under this policy

5 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

data-packs.js BimCraft 0.8.1 64-bit 5 calculations Reviewed

Tests tools/bimcraft-test/datapacks-test.mjs

Number from a factor or cost cell

64-bit

A cell such as '$1,250.00' as a number.

Currency signs, white space and every comma are removed, then Number() converts the rest. A thousands comma is handled; a decimal comma is not: '1,5' gives 15 and '0,25' gives 25.

  • data-packs.js
  • line 28
  • BimCraft 0.8.1
const num = (v) => { const s = String(v ?? '').replace(/[$€£,\s]/g, ''); if (s === '') return null; const n = Number(s); return Number.isFinite(n) ? n : null; };

Carbon factor from a file row

64-bit

A material's embodied-carbon factor in the unit the carbon pane works in.

A1-A3 factor = value × unit factor (1 for kg, m, m², m³ and items; 0.001 for tonnes)

One 64-bit multiplication; 0.001 is not exact in binary, so a per-tonne value can change in its last digit. A per-kilogram factor with no density takes the density of the matching material in the starter table, and the row's note says so.

  • data-packs.js
  • lines 83 to 106
  • BimCraft 0.8.1
export function carbonFromFile(text, opts = {}) {
  const all = parseCsv(text);
  if (all.length < 2) return { rows: [], errors: [{ line: 0, code: 'empty' }], columns: {} };
  const head = all[0].cells.map((x) => x.trim());
  const c = pickCols(head, opts.map, ['name', 'unit', 'gwp', 'density'], SYN);
  if (c.name < 0 || c.gwp < 0) return { rows: [], errors: [{ line: all[0].line, code: 'header' }], columns: c };
  // no unit column: read it from a header like "kgCO2e/kg" or "kgCO2e per m3"
  const hu = /(?:\/|per)\s*(kg|t|tonne|m3|m³|m2|m²|m)\b/i.exec(head[c.gwp] || '');
  const rows = [], errors = [], seen = new Set();
  for (const r of all.slice(1, 5001)) {
    const g = (i) => (i >= 0 ? unguard(String(r.cells[i] ?? '')) : '');
    const u = unitOf(g(c.unit)) || (hu ? unitOf(hu[1]) : null) || (opts.unit ? unitOf(opts.unit) : null);
    const gwp = num(g(c.gwp));
    if (!g(c.name).trim() || gwp === null || !u) { errors.push({ line: r.line, code: !u ? 'unit' : 'value' }); continue; }
    let id = slug(g(c.name)); for (let n = 2; seen.has(id); n++) id = slug(g(c.name)).slice(0, 44) + '-' + n; seen.add(id);
    // a per-kg figure needs a density to meet the model's volumes: the file's, the caller's, else the starter table's for that name (said in the note)
    let dens = num(g(c.density)) ?? opts.density ?? null, dnote = '';
    if (u[0] === 'kg' && dens === null) { const m = matchRow(STARTER, { material: g(c.name), name: g(c.name) }); if (m) { dens = m.density; dnote = ' Density ' + m.density + ' kg/m³ from the starter table.'; } }
    const cl = cleanRow({ id, match: [g(c.name)], classes: [], basis: u[0], density: dens, a1a3: gwp * u[1], source: opts.source || 'your file', checked: false, note: IMPORT_LABEL + dnote });
    if (!cl.row) { errors.push({ line: r.line, code: cl.errors[0] }); continue; }
    rows.push({ ...cl.row, origin: 'yours' });
  }
  return { rows, errors, columns: c };
}

Monthly climate from NASA POWER

64-bit

Twelve monthly values of daily sunlight on a horizontal surface and of air temperature.

ghi = value rounded to 0.01 kWh/m²/day; T = value rounded to 0.01 °C

A value outside 0 to 12 kWh/m²/day or −60 to 60 °C rejects the whole answer. What is kept is rounded to two decimals.

  • data-packs.js
  • lines 140 to 146
  • BimCraft 0.8.1
export function parseNasaPower(j) {
  const p = j?.properties?.parameter, a = p?.ALLSKY_SFC_SW_DWN, t = p?.T2M;
  if (!a || !t) return null;
  const ghi = MONTHS.map((m) => Number(a[m])), tAmb = MONTHS.map((m) => Number(t[m]));
  if (ghi.some((x) => !Number.isFinite(x) || x < 0 || x > 12) || tAmb.some((x) => !Number.isFinite(x) || x < -60 || x > 60)) return null;   // NASA marks missing as -999
  return { ghi: ghi.map(r2), tAmb: tAmb.map(r2), source: 'NASA POWER climatology (ALLSKY_SFC_SW_DWN kWh/m²/day, T2M daily-mean °C)' };
}

Monthly climate from PVGIS

64-bit

Twelve monthly values of daily sunlight and air temperature, averaged over the years in the answer.

ghi = Σ H(h)_m ÷ years ÷ days in month; T = Σ T2m ÷ years; both rounded to 0.01

Monthly sums are averaged over the years in the answer and divided by the days in the month, with February as 28.25 days. A missing temperature makes that month NaN and the answer is rejected. The results are rounded to two decimals.

  • data-packs.js
  • lines 147 to 160
  • BimCraft 0.8.1
export function parsePvgis(j) {
  const list = j?.outputs?.monthly;
  if (!Array.isArray(list) || !list.length) return null;
  const sum = new Array(12).fill(0), ts = new Array(12).fill(0), n = new Array(12).fill(0);
  for (const e of list) {
    const m = Number(e.month) - 1, h = Number(e['H(h)_m']), t = Number(e.T2m ?? e['T2m']);
    if (!(m >= 0 && m < 12) || !Number.isFinite(h)) return null;
    sum[m] += h; ts[m] += Number.isFinite(t) ? t : NaN; n[m]++;
  }
  if (n.some((x) => !x)) return null;
  const ghi = sum.map((s, m) => r2(s / n[m] / DIM[m])), tAmb = ts.map((s, m) => r2(s / n[m]));
  if (ghi.some((x) => x < 0 || x > 12) || tAmb.some((x) => !Number.isFinite(x))) return null;
  return { ghi, tAmb, source: 'PVGIS MRcalc monthly (H(h)_m kWh/m²/month ÷ days, T2m °C)' };
}

Location sent to the climate provider

64-bit

The latitude and longitude put into the request address.

latitude.toFixed(3), longitude.toFixed(3)

The request carries the position rounded to three decimals of a degree. A position outside ±90 and ±180 degrees gives no address.

  • data-packs.js
  • line 131
  • BimCraft 0.8.1
const coord = (lat, lon) => (Number.isFinite(lat) && Number.isFinite(lon) && Math.abs(lat) <= 90 && Math.abs(lon) <= 180 ? [lat.toFixed(3), lon.toFixed(3)] : null);

In detail

Method
Carbon factor per kilogram = the file's value × 0.001 when its unit is the tonne. PVGIS monthly irradiation ÷ years ÷ days in the month gives kWh/m² per day; NASA POWER values are taken per day as given.
Number types
64-bit numbers.
Limits
A decimal comma is not understood: '1,5' is read as 15. The units t, tonne, tonnes and ton all use the factor 0.001. February counts 28.25 days. Stored climate values differ from the source by up to 0.005.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach