Solar constant
64-bitThe 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.
export const GSC = 1367;
Resources Numerical accuracy Calculation policy
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.
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.
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.
Tests tools/bimcraft-test/ops-solar.mjs
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.
export const GSC = 1367;
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.
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 };
}
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.
/** 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');
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.
/**
* 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 };
}
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.