Resources Numerical accuracy Calculation policy

Solar string sizing

Works out how many modules go in a string and how many strings on an inverter from the module and inverter ratings and the temperatures given.

How the precision is kept

Cold and hot voltages are linear in temperature and computed in 64-bit. The longest and shortest string come from the unrounded voltages, with a 1e-9 guard before floor and ceil. The string voltages that are checked against the limits, however, are built from per-module voltages already rounded to 0.01 V and are rounded again, so a check can disagree with the string length just computed.

Open item. Compute the string voltages and their checks from the unrounded cold and hot voltages, and round only what is shown.

Calculations under this policy

6 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-string.js BimCraft 0.8.1 64-bit 6 calculations Reviewed

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

Module voltage at a temperature

64-bit

The open-circuit voltage of a module at a cell temperature.

V(T) = V × (1 + β × (T − 25))

One 64-bit expression; β is a fraction per kelvin as typed.

  • pv-string.js
  • line 15
  • BimCraft 0.8.1
export const vocAt = (m, T) => m.voc * (1 + m.betaVoc * (T - 25));

Longest and shortest string

64-bit

The most and the fewest modules a string may have.

nMax = min(⌊V_limit ÷ (Voc,cold × f) + 1e-9⌋, ⌊MPPT max ÷ Vmp,cold + 1e-9⌋); nMin = ⌈MPPT min ÷ Vmp,hot − 1e-9⌉

The counts use the unrounded voltages. The voltages returned in the result (vocCold, vmpCold, vmpHot) are rounded to two decimals, and designStrings reads those rounded values.

  • pv-string.js
  • lines 20 to 27
  • BimCraft 0.8.1
export function stringWindow(module, inv, o = {}) {
  const O = { ...DEFAULT_STRING, ...o };
  const vocCold = vocAt(module, O.tMin), vmpCold = vmpAt(module, O.tMin), vmpHot = vmpAt(module, O.tHot);
  const vLimit = Math.min(inv.vdcMax, O.vSys);
  const byVoc = Math.floor(vLimit / (vocCold * O.vocFactor) + 1e-9);
  const byMppt = inv.mpptMax > 0 ? Math.floor(inv.mpptMax / vmpCold + 1e-9) : Infinity;
  return { vocCold: r2(vocCold), vmpCold: r2(vmpCold), vmpHot: r2(vmpHot), vLimit, nMax: Math.min(byVoc, byMppt), byVoc, byMppt, nMin: Math.ceil(inv.mpptMin / vmpHot - 1e-9), O };
}

String length and number of strings

Exact

The string length inside the window that leaves the fewest modules over.

count = ⌊modules ÷ n⌋; left = modules − count × n

Integer arithmetic, from the longest string down; the search stops at the first length that divides the modules evenly.

  • pv-string.js
  • lines 36 to 41
  • BimCraft 0.8.1
for (let n = Math.min(W.nMax, modules); n >= Math.max(1, W.nMin); n--) {
  const count = Math.floor(modules / n), left = modules - count * n;
  if (count < 1) continue;
  if (!best || left < best.left) best = { n, count, left };
  if (left === 0) break;
}

String voltage checks

64-bit

The string's cold and hot voltages against the inverter's and the system's limits.

V_string = round(n × round(V_module, 2 decimals), 2 decimals)

W.vocCold, W.vmpHot and W.vmpCold are already rounded to 0.01 V; the product is rounded again and that value is compared with the limit. The rounding of the module voltage, up to 0.005 V, is multiplied by n.

  • pv-string.js
  • lines 44 to 47
  • BimCraft 0.8.1
const vocString = r2(n * W.vocCold * O.vocFactor), vmpHotString = r2(n * W.vmpHot), vmpColdString = r2(n * W.vmpCold);
checks.push({ key: 'voc', value: vocString, limit: W.vLimit, op: '<=', unit: 'V', ok: vocString <= W.vLimit });
checks.push({ key: 'mpptMax', value: vmpColdString, limit: inverter.mpptMax, op: '<=', unit: 'V', ok: !(inverter.mpptMax > 0) || vmpColdString <= inverter.mpptMax });
checks.push({ key: 'mpptMin', value: vmpHotString, limit: inverter.mpptMin, op: '>=', unit: 'V', ok: vmpHotString >= inverter.mpptMin });

Strings per inverter input

64-bit

How many strings one input can carry by its current limits, and how many inputs are needed.

per input = min(⌊I_dc max ÷ Imp + 1e-9⌋, ⌊I_sc max ÷ Isc + 1e-9⌋); inputs needed = ⌈count ÷ per input⌉

64-bit quotients with an absolute 1e-9 guard before the floor. A limit that is not given counts as no limit.

  • pv-string.js
  • lines 52 to 56
  • BimCraft 0.8.1
const mppts = Math.max(1, (inverter.mppts || 1) * inverters);
const perMpptImp = inverter.idcMax > 0 ? Math.floor(inverter.idcMax / module.imp + 1e-9) : Infinity;
const perMpptIsc = inverter.iscMax > 0 ? Math.floor(inverter.iscMax / module.isc + 1e-9) : Infinity;
const perMppt = Math.min(perMpptImp, perMpptIsc);
const needMppts = perMppt >= 1 ? Math.ceil(count / perMppt) : Infinity;

DC to AC ratio

64-bit

The array's peak power against the inverter's AC rating.

kWp = n × count × Wp ÷ 1000; DC ÷ AC = kWp ÷ (P_ac × inverters)

The pass or fail uses the unrounded ratio; the value shown is rounded to two decimals. A zero AC rating gives a ratio of 0.

  • pv-string.js
  • lines 63 to 64
  • BimCraft 0.8.1
const kwp = n * count * module.wp / 1000, ac = inverter.pacKw * inverters, dcac = ac > 0 ? kwp / ac : 0;
checks.push({ key: 'dcac', value: r2(dcac), limit: O.dcacMax, op: '<=', unit: '', ok: dcac >= O.dcacMin && dcac <= O.dcacMax });

In detail

Method
V(T) = V × (1 + β × (T − 25)); nMax = ⌊min(V_dc max, system limit) ÷ (Voc,cold × factor)⌋ and ⌊MPPT max ÷ Vmp,cold⌋; nMin = ⌈MPPT min ÷ Vmp,hot⌉; strings per input = ⌊I max ÷ I module⌋.
Number types
64-bit numbers; counts are integers.
Tolerances
1e-9 added before Math.floor and subtracted before Math.ceil (absolute, on a ratio).
Limits
Example: a cold Voc of 47.617 V and a 1000 V limit allow 21 modules (999.957 V), but for a string of 21 the check uses 21 × 47.62 = 1000.02 V and reports the limit as exceeded. The starter temperatures and factors are labelled as placeholders in the file.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach