energy.solar-generation
Estimated annual solar PV generation by the MCS method: kWp x Kk x shade factor, per array and in total.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 14 tests, run in TypeScript, Python and Rust.
What it does
The estimated annual electricity (AC) a solar PV system generates, by the standard estimation method of the MCS Solar PV Standard (MIS 3002), the one every MCS-certified installer quotes to customers:
Annual AC output (kWh) = kWp x Kk x SF
For example
solar_generation(arrays ×1)→ arrays 3,800, total 3,800 a 4 kWp south-facing array, no shading, Kk 950solar_generation(arrays ×1)→ arrays 3,368, total 3,368 4.32 kWp with 11% shading loss: 3368.04 kWh rounds to 3368solar_generation(arrays ×1)→ arrays 1,463, total 1,463 an exact half kWh rounds up: 3.25 kWp x 1000 x 0.45 is 1462.5
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
def solar_generation(arrays: Sequence[PvArray]) -> SolarEstimate
| arrays | PvArray[] | one entry per array with its own orientation, pitch or shading; at least one |
| returns | SolarEstimate | kWh a year for each array, rounded half-up, and their sum |
The types it declares, generated into your project
@dataclass(frozen=True)
class PvArray:
"""One PV array, as the MCS performance estimate describes it."""
#: sum of the module data-plate ratings at STC, in watts: 4.32 kWp is 4320
watts_peak: int
#: kWh/kWp from the MCS table for the postcode zone, pitch and orientation
kk: int
#: SF in hundredths, 1.00 (no shading) is 100, 0.89 is 89
shade_factor: int
@dataclass(frozen=True)
class SolarEstimate:
"""Annual AC generation, by array and in total."""
#: kWh a year for each array, in the order given
arrays: List[int]
#: the sum of the arrays
total: int
Your code names it in one line, in the file that uses it
from fune.energy.solar_generation import solar_generation # energy.solar-generation@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import Sequence
from .energy_solar_generation_types import PvArray, SolarEstimate
from .math_round_div import round_div ← from math.round-div ^1.0.0 · built alongside by fune
# watts x Kk x SF (hundredths) / (1000 W/kW x 100) is kWh.
DIVISOR = 100_000
def _check(what: str, value: object, maximum: int) -> None:
if not isinstance(value, int) or isinstance(value, bool) or value < 0 or value > maximum:
raise ValueError("%s must be a whole number from 0 to %d, received %s" % (what, maximum, value))
def solar_generation(arrays: Sequence[PvArray]) -> SolarEstimate:
"""Annual AC generation by the MCS method, kWp x Kk x SF, rounded half-up
to a whole kWh for each array; the total is the sum of the arrays.
"""
if len(arrays) == 0:
raise ValueError("a solar estimate needs at least one array")
kwh = []
for a in arrays:
_check("wattsPeak", a.watts_peak, 100_000_000)
_check("kk", a.kk, 5000)
_check("shadeFactor", a.shade_factor, 100)
kwh.append(round_div(a.watts_peak * a.kk * a.shade_factor, DIVISOR, "half-up"))
return SolarEstimate(arrays=kwh, total=sum(kwh))Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add energy.solar-generation
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./energy.solar-generation-1.0.0-python.fune, or fetch it from a terminal with fune pull energy.solar-generation@1.0.0:python.
The whole function, every language, is one file too: energy.solar-generation-1.0.0.fune, 11,670 bytes, sha256 a6eb49277bb295c09e59a36e09900ec037b8cf1787bf39d098595bf24e77cbd0. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
# fune: before energy.solar-generation
after — your function gets the result and the arguments, and returns the final result.
# fune: after energy.solar-generation
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
# fune: replace math.round-div in energy.solar-generation
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show energy.solar-generation --steps.
# fune: step energy.solar-generation after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| a 4 kWp south-facing array, no shading, Kk 950 | arrays ×1 | → | arrays 3,800, total 3,800 |
| 4.32 kWp with 11% shading loss: 3368.04 kWh rounds to 3368 | arrays ×1 | → | arrays 3,368, total 3,368 |
| an exact half kWh rounds up: 3.25 kWp x 1000 x 0.45 is 1462.5 | arrays ×1 | → | arrays 1,463, total 1,463 |
| east/west arrays are estimated separately and summed | arrays ×2 | → | arrays 2,100, 2,052, total 4,152 |
| two half-kWh arrays total 2926, not the 2925 rounding the sum would give | arrays ×2 | → | arrays 1,463, 1,463, total 2,926 |
| a fully shaded array generates nothing | arrays ×1 | → | arrays 0, total 0 |
| a single 400 W panel | arrays ×1 | → | arrays 404, total 404 |
| a 100 kWp commercial roof | arrays ×1 | → | arrays 86,330, total 86,330 |
| a vertical north-facing array with a tiny Kk | arrays ×1 | → | arrays 430, total 430 |
| no arrays is an error | → | error: a solar estimate needs at least one array |
Show the other 4 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a shade factor above 1.00 is refused | arrays ×1 | → | error: shadeFactor must be a whole number from 0 to 100 |
| a shade factor given as a fraction is refused | arrays ×1 | → | error: shadeFactor must be a whole number from 0 to 100 |
| a negative rating is refused | arrays ×1 | → | error: wattsPeak must be a whole number |
| an implausible Kk is refused | arrays ×1 | → | error: kk must be a whole number from 0 to 5000 |
More from the author
- **kWp**: the sum of the module data-plate ratings (Wp at STC), passed in watts. - **Kk**: kWh per kWp, looked up in MCS's table for the site's postcode zone (the SAP zones), the array pitch (to the nearest 1°) and its orientation from due south (to the nearest 5°). It carries the irradiance and the orientation and pitch factor together. - **SF**: the shade factor, 1.00 with a clear horizon, otherwise 1 - estimated shading loss (for example 0.89), to two places.
## Why the Kk table is an argument
MCS publishes Kk as downloadable tables: 21 postcode zones, each a grid of pitch 0-90° by orientation 0-180°, drawn from the European Commission's PVGIS dataset (multiplied by 0.8). That is tens of thousands of values, and MCS updates them with the standard. Shipping a partial copy would invite silent use of the wrong zone, so the caller looks Kk up (from the MCS tables or their design software) and this capability does the arithmetic and the rounding the same way everywhere.
## Several arrays
MIS 3002 estimates an east/west or multi-roof system array by array, each with its own Kk and SF. Each array is rounded to a whole kWh, **half-up**, and the total is the sum of those rounded figures, so the total always equals the lines on the estimate. (Rounding the unrounded sum instead can differ by a kWh or two.)
MIS 3002 does not state a rounding rule; whole kWh, half-up, matches the estimates installers print.
## Sources
MCS, "MIS 3002: The Solar PV Standard (Installation)", issue 5.0, 10 May 2023, Appendix B "Standard estimation method" and section 4.1.6, https://mcscertified.com/wp-content/uploads/2025/02/MIS-3002_Solar-PV-Systems-V5.0-Final-for-publication.pdf
Files
| Path | Bytes |
|---|---|
| README.md | 1,978 |
| impl/python.py | 1,106 |
| impl/rust.rs | 1,997 |
| impl/typescript.ts | 1,098 |
| vectors.json | 2,464 |