Resources Numerical accuracy Calculation policy

Solar yield estimate

Estimates the DC and AC energy a solar array gives in a year, month by month, with shading, temperature, losses and inverter clipping, and its performance ratio.

How the precision is kept

Each month is one representative day of hourly values times the days in the month, so the method sets the accuracy; the file calls it an early-design estimate. Hourly power and the DC sums are 64-bit. The monthly AC energy is rounded to 0.1 kWh when stored, and the annual AC energy, the specific yield and the performance ratio are then computed from those rounded months and from a kWp rounded to 0.001.

Open item. Keep the monthly AC energy unrounded for the annual sum, the specific yield and the performance ratio, and round only the values that are shown.

Calculations under this policy

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

pv-yield.js BimCraft 0.8.1 64-bit 8 calculations Reviewed

Tests tools/bimcraft-test/eg-pv-test.mjs

Cell temperature

64-bit

The temperature of the cells from the air temperature and the light on the module.

T_cell = T_amb + (NOCT − 20) ÷ 800 × G

One 64-bit expression: a division by 800, a multiplication by the irradiance in W/m² and an addition.

  • pv-yield.js
  • line 25
  • BimCraft 0.8.1
export const cellTemperature = (tAmb, g, noct) => tAmb + (noct - 20) / 800 * g;

Module DC power

64-bit

The power of one module at a given irradiance and cell temperature.

P = Wp × G ÷ 1000 × (1 + γ × (T_cell − 25))

One 64-bit expression; the caller sets a negative result to 0.

  • pv-yield.js
  • line 26
  • BimCraft 0.8.1
export const moduleDc = (g, tCell, m) => m.wp * (g / 1000) * (1 + m.gammaPmax * (tCell - 25));

Loss chain

64-bit

The share of the DC energy left after soiling, mismatch, wiring, downtime and the inverter.

chain = (1 − soiling) × (1 − mismatch) × (1 − wiring) × (1 − availability) × inverter efficiency

Five 64-bit factors multiplied in order. The value listed in the result's inputs is rounded to three decimals; the calculation uses the unrounded product.

  • pv-yield.js
  • line 42
  • BimCraft 0.8.1
const chain = (1 - L.soiling) * (1 - L.mismatch) * (1 - L.wiring) * (1 - L.availability) * (L.inverter ?? inverter?.eta ?? 0.97);

Power in one hour

64-bit

The light on the array, the cell temperature and the DC and AC power for one hour of a month's representative day.

G = beam × (1 − shade) + sky + ground; P = n × max(0, module power); AC before clipping = P ÷ 1000 × chain

64-bit; the day's sums are added hour by hour in order. The shade share for that month and hour comes from pv-shade.js and is 0 when there is none.

  • pv-yield.js
  • lines 53 to 63
  • BimCraft 0.8.1
for (const x of d.hours) {
  if (!x.sun) continue;
  const p = poa(x, a.normal, { albedo, b0: a.module.iamB0 });
  const f = sh?.[mi]?.[x.h] ?? 0;
  const g = p.beam > 0 ? p.beamIam * (1 - f) + p.sky + p.ground : p.sky + p.ground;
  const tc = cellTemperature(T, g, a.module.noct);
  const w = n * Math.max(0, moduleDc(g, tc, a.module));
  dcDay += w / 1000;
  hourly[ai][mi][x.h] = w / 1000 * chain;
  effDay += g / 1000; effFreeDay += p.total / 1000; irrDay += p.totalNoIam / 1000; beamDay += p.beamIam / 1000; lostDay += p.beamIam * f / 1000;
}

Monthly figures

64-bit

A month's light, shade loss and DC energy from its representative day.

month = day × days in month; shade loss % = (unshaded − shaded) ÷ unshaded × 100

The monthly figures kept for display are rounded to one decimal; the yearly accumulators add the unrounded products.

  • pv-yield.js
  • lines 64 to 66
  • BimCraft 0.8.1
const k = DAYS_IN_MONTH[mi];
mo.push({ month: mi + 1, poaKwhM2: r1(irrDay * k), shadeLossPct: r1(effFreeDay > 0 ? (effFreeDay - effDay) / effFreeDay * 100 : 0), beamLossPct: r1(beamDay > 0 ? lostDay / beamDay * 100 : 0), dcKwh: r1(dcDay * k), acKwh: 0 });
dcY += dcDay * k; poaY += effDay * k; poaFree += irrDay * k; effFree += effFreeDay * k; beamY += beamDay * k; beamLost += lostDay * k;

Inverter clipping and monthly AC energy

64-bit

The AC energy of each month after the inverter's rating limits the hourly power.

scale = P_ac ÷ Σ P when Σ P > P_ac; month AC = Σₕ P × scale × days, rounded to 0.1 kWh

Clipping scales every array's hour by the same factor. Each array's monthly AC energy is stored rounded to 0.1 kWh, and that rounded number is what the annual sum reads.

  • pv-yield.js
  • lines 71 to 82
  • BimCraft 0.8.1
const pac = inverter?.pacKw > 0 ? inverter.pacKw : Infinity;
let clippedKwh = 0, unclippedKwh = 0;
const monthTot = days.map(() => ({ dc: 0, ac: 0, poa: 0 }));
days.forEach((d, mi) => {
  const k = DAYS_IN_MONTH[mi], scale = new Array(24).fill(1);
  for (let h = 0; h < 24; h++) {
    const tot = arrays.reduce((s, _, ai) => s + hourly[ai][mi][h], 0);
    if (tot > pac) { scale[h] = pac / tot; clippedKwh += (tot - pac) * k; }
    unclippedKwh += tot * k;
  }
  arrays.forEach((_, ai) => { out[ai].monthly[mi].acKwh = r1(hourly[ai][mi].reduce((s, v, h) => s + v * scale[h], 0) * k); });
});

Annual energy, specific yield and performance ratio of an array

64-bit

An array's yearly DC and AC energy, kWh per kWp and performance ratio.

AC = Σ rounded months; specific yield = AC ÷ kWp; PR = AC ÷ (kWp × POA)

AC is the sum of twelve values already rounded to 0.1 kWh, and kWp was rounded to 0.001 when the array record was made; DC uses the unrounded sum. The annual energies are then rounded to whole kWh and PR to three decimals.

  • pv-yield.js
  • lines 84 to 87
  • BimCraft 0.8.1
const ac = o.monthly.reduce((s, m) => s + m.acKwh, 0), dc = o._dcY;
o.annual = { poaKwhM2: r1(o._poaFree), poaFreeKwhM2: r1(o._poaFree), dcKwh: Math.round(dc), acKwh: Math.round(ac), specificKwhKwp: o.kwp > 0 ? Math.round(ac / o.kwp) : 0,
  pr: o.kwp > 0 && o._poaFree > 0 ? r3(ac / (o.kwp * o._poaFree)) : 0, shadeLossPct: r1(o._effFree > 0 ? (o._effFree - o._poaY) / o._effFree * 100 : 0), beamLossPct: r1(o._beamY > 0 ? o._beamLost / o._beamY * 100 : 0) };
sum.dcKwh += dc; sum.acKwh += ac; sum.kwp += o.kwp; sum.poa += o._poaFree * o.kwp;

Plant totals

64-bit

The energy, specific yield, performance ratio, clipping and shade loss of all arrays together.

PR = Σ AC ÷ Σ(kWp × POA); clipped % = clipped ÷ unclipped × 100

The totals add the per-array values above, so they carry the same rounded months and rounded kWp. The plant's shade loss averages percentages already rounded to 0.1, weighted by the rounded kWp and the rounded irradiation.

  • pv-yield.js
  • lines 91 to 94
  • BimCraft 0.8.1
const total = { kwp: r3(sum.kwp), dcKwh: Math.round(sum.dcKwh), acKwh: Math.round(sum.acKwh), specificKwhKwp: sum.kwp > 0 ? Math.round(sum.acKwh / sum.kwp) : 0,
  pr: sum.poa > 0 ? r3(sum.acKwh / sum.poa) : 0, clippedKwh: Math.round(clippedKwh), clippedPct: unclippedKwh > 0 ? r1(clippedKwh / unclippedKwh * 100) : 0,
  monthly: days.map((_, mi) => ({ month: mi + 1, acKwh: r1(out.reduce((s, o) => s + o.monthly[mi].acKwh, 0)) })) };
total.shadeLossPct = sum.poa > 0 ? r1(out.reduce((s, o) => s + o.annual.shadeLossPct * o.kwp * o.annual.poaFreeKwhM2, 0) / sum.poa) : 0;

In detail

Method
Cell temperature by the NOCT model (the file cites Duffie and Beckman §23.3); module power linear in irradiance and temperature; the losses multiplied as one chain; the summed AC power of each hour clipped at the inverter rating; performance ratio = AC energy ÷ (kWp × unshaded plane-of-array kWh/m²).
Number types
64-bit numbers.
Limits
One day per month and one value per hour. The annual AC sum can differ from the unrounded sum by up to 0.6 kWh per array (twelve months each rounded to 0.1 kWh). The default losses, a 20 °C air temperature and an albedo of 0.2 are starter values.

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