Resources Numerical accuracy Calculation policy

Sun gain through windows

Works out the heat the sun brings through each window over one clear day: the gain at each time step, the peak and the daily total.

How the precision is kept

64-bit arithmetic on an empirical clear-sky model whose coefficients are literals of three or four digits. The day is stepped at a fixed interval, a quarter hour by default, and the energy is the sum of the steps, so the peak and the daily total are resolved to that step.

Calculations under this policy

4 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.

daylight-bim-sky.js BimCraft 0.8.1 64-bit 4 calculations Reviewed

Tests tools/bimcraft-test/ops-solar.mjs

Solar constant

64-bit

The sun's irradiance outside the atmosphere at the mean earth–sun distance.

G_sc = 1367 W/m²

A four-digit whole-number literal in W/m², used as written.

  • daylight-bim-sky.js
  • line 130
  • BimCraft 0.8.1
export const GSC = 1367;

Clear-sky irradiance

64-bit

Beam and diffuse irradiance under a clear sky for a day of the year and a sun height.

τ_b = a₀ + a₁ e^(−k ÷ cos θz); G_on = 1367 (1 + 0.033 cos(2π n ÷ 365)); τ_d = 0.271 − 0.294 τ_b

One 64-bit expression per quantity; the model's coefficients are decimal literals of three or four digits (0.4237, 0.00821, 0.5055, 0.00595, 0.2711, 0.01858). The altitude is clamped to 0…2.5 km. With the sun at or below the horizon everything but G_on is returned as zero before −k ÷ cos θz is formed.

  • daylight-bim-sky.js
  • lines 131 to 139
  • BimCraft 0.8.1
export function clearSky(n, cosZ, { altitudeKm = 0, climate = 'midlat-summer' } = {}) {
  const A = Math.max(0, Math.min(2.5, altitudeKm));
  const [r0, r1, rk] = CLIMATES[climate] || CLIMATES['midlat-summer'];
  const a0 = r0 * (0.4237 - 0.00821 * (6 - A) ** 2), a1 = r1 * (0.5055 + 0.00595 * (6.5 - A) ** 2), k = rk * (0.2711 + 0.01858 * (2.5 - A) ** 2);
  const Gon = GSC * (1 + 0.033 * Math.cos(2 * Math.PI * n / 365));
  if (!(cosZ > 0)) return { Gon, tauB: 0, Gbn: 0, Gbh: 0, Gdh: 0 };
  const tauB = a0 + a1 * Math.exp(-k / cosZ), tauD = 0.271 - 0.294 * tauB;
  return { Gon, tauB, Gbn: Gon * tauB, Gbh: Gon * tauB * cosZ, Gdh: Gon * cosZ * tauD };
}

Climate factors for the date

64-bit

Picks the row of climate factors that fits the latitude and the month.

Tropical within 23.5° of the equator; otherwise summer for April to September in the northern hemisphere and for the other months in the southern. It chooses a table row; it does not change a value.

  • daylight-bim-sky.js
  • lines 158 to 159
  • BimCraft 0.8.1
/** The climate factors that fit the date and hemisphere (Hottel's mid-latitude summer / winter; tropics within 23.5°). */
export const climateFor = (lat, month) => (Math.abs(lat) < 23.5 ? 'tropical' : (lat >= 0) === (month >= 4 && month <= 9) ? 'midlat-summer' : 'midlat-winter');

Sun gain through one day

64-bit

The gain through each window at every time step, each room's peak and when it falls, and the day's energy.

gain = g × A × (G_bn cos i unless shaded + G_dh × 0.5 × sky fraction + (G_bh + G_dh) × ρ × 0.5); kWh = Σ gain × step ÷ 1000

Clock time advances by repeated addition of the step; the default 0.25 h is exact in binary, other steps accumulate rounding. The daily energy is a rectangle sum of the steps and the peak is the largest step value. The sun counts as hidden when its altitude lies inside a hidden interval of the section toward it.

  • daylight-bim-sky.js
  • lines 161 to 204
  • BimCraft 0.8.1
/**
 * The sun on each window through one day. site = { lat, lon, north } (north in the model's plan); times are clock hours in
 * `tz` (UTC offset, standard time). Returns the gain through the glass, W, per step.
 */
export function solarDay(rooms, prods, site, { month = 7, day = 21, year = 2026, tz = null, step = 0.25, g = 0.6, glazedShare = DEFAULTS.glazedShare, ground = 0.2, climate = null, altitudeKm = 0 } = {}) {
  const zone = Number.isFinite(tz) ? tz : Math.round(site.lon / 15);
  const n0 = dayOfYear(month, day, year), cl = climate || climateFor(site.lat, month);
  const north = site.north || [0, 1], east = [north[1], -north[0]];
  const times = [];
  for (let h = 0; h < 24 - 1e-9; h += step) times.push(h);
  const suns = times.map((h) => {
    const s = sunPosition({ month, day, hour: h, lat: site.lat, lon: site.lon, tz: zone, year });
    if (!s.up) return null;
    const d = [east[0] * s.dir[0] + north[0] * s.dir[1], east[1] * s.dir[0] + north[1] * s.dir[1], s.dir[2]];
    return { d, alt: s.altitude, sky: clearSky(n0, s.dir[2], { altitudeKm, climate: cl }) };
  });
  const out = rooms.map((r) => {
    const hourly = times.map(() => 0), wins = [];
    for (const w of r.windows) {
      const tris = frontTris(prods, w), prof = w.prof || profile(w, tris);
      const sf = skyFraction(prof), A = w.area * glazedShare;
      const wh = times.map(() => 0);
      let shaded = 0;
      suns.forEach((s, i) => {
        if (!s) return;
        const cosI = s.d[0] * w.n[0] + s.d[1] * w.n[1];
        let beam = 0;
        if (cosI > 0) {
          const hz = Math.hypot(s.d[0], s.d[1]) || 1, dh = [s.d[0] / hz, s.d[1] / hz, 0];
          const hidden = cut(w.origin, dh, tris).some(([a, b]) => s.alt >= a && s.alt <= b);
          if (hidden) shaded++; else beam = s.sky.Gbn * cosI;
        }
        const diffuse = s.sky.Gdh * 0.5 * sf, refl = (s.sky.Gbh + s.sky.Gdh) * ground * 0.5;
        wh[i] = g * A * (beam + diffuse + refl);
      });
      const peak = Math.max(0, ...wh), kWh = wh.reduce((t, x) => t + x, 0) * step / 1000;
      wins.push({ id: w.id, guid: w.guid, name: w.name, peak, peakAt: times[wh.indexOf(peak)] ?? null, kWh, shaded, skyFraction: sf, hourly: wh });
      wh.forEach((x, i) => { hourly[i] += x; });
    }
    const peak = Math.max(0, ...hourly);
    return { id: r.id, guid: r.guid, name: r.name, longName: r.longName, storey: r.storey, peak, peakAt: peak > 0 ? times[hourly.indexOf(peak)] : null, kWh: hourly.reduce((t, x) => t + x, 0) * step / 1000, hourly, windows: wins };
  });
  return { times, tz: zone, climate: cl, day: n0, rooms: out };
}

In detail

Method
Hottel's clear-sky beam transmittance with climate factors and Liu and Jordan's diffuse relation, as the file's header cites them from Duffie and Beckman. Sun position from render/sun.js. Gain = g × glazed area × (beam on the glass unless shaded + half the diffuse × sky fraction + half the ground-reflected).
Number types
64-bit numbers.
Tolerances
Site altitude clamped to 0…2.5 km. A sun at or below the horizon gives zero. The time loop ends at 24 h less 1e-9.
Limits
Solar constant 1367 W/m². Shading is all or nothing per step, tested from the window's origin. The daily energy is a rectangle sum over the steps. Without a time zone, longitude ÷ 15 rounded to a whole hour is used.

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