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.
pub fn solar_generation(arrays: &[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
/// One PV array, as the MCS performance estimate describes it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PvArray {
/// sum of the module data-plate ratings at STC, in watts: 4.32 kWp is 4320
pub watts_peak: i64,
/// kWh/kWp from the MCS table for the postcode zone, pitch and orientation
pub kk: i64,
/// SF in hundredths, 1.00 (no shading) is 100, 0.89 is 89
pub shade_factor: i64,
}
/// Annual AC generation, by array and in total.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SolarEstimate {
/// kWh a year for each array, in the order given
pub arrays: Vec<i64>,
/// the sum of the arrays
pub total: i64,
}
Your code names it in one line, in the file that uses it
fune!(energy.solar-generation@^1); // then call solar_generation(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_round_div::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.
const DIVISOR: i64 = 100_000;
fn check(what: &str, value: i64, max: i64) {
if value < 0 || value > max {
panic!("{} must be a whole number from 0 to {}, received {}", what, max, value);
}
}
/// 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.
///
/// # Panics
/// Panics on no arrays, or a rating, Kk or shade factor out of range.
pub fn solar_generation(arrays: &[PvArray]) -> SolarEstimate {
if arrays.is_empty() {
panic!("a solar estimate needs at least one array");
}
let kwh: Vec<i64> = arrays
.iter()
.map(|a| {
check("wattsPeak", a.watts_peak, 100_000_000);
check("kk", a.kk, 5000);
check("shadeFactor", a.shade_factor, 100);
round_div(a.watts_peak * a.kk * a.shade_factor, DIVISOR, "half-up")
})
.collect();
let total = kwh.iter().sum();
SolarEstimate { arrays: kwh, total }
}
fn whole(what: &str, max: i64, v: &Value) -> i64 {
if let Value::Float(f) = v {
panic!("{} must be a whole number from 0 to {}, received {}", what, max, f);
}
v.as_i64()
}
pub fn pv_array_from_value(v: &Value) -> PvArray {
PvArray {
watts_peak: whole("wattsPeak", 100_000_000, v.get("wattsPeak")),
kk: whole("kk", 5000, v.get("kk")),
shade_factor: whole("shadeFactor", 100, v.get("shadeFactor")),
}
}
pub fn solar_estimate_to_value(e: &SolarEstimate) -> Value {
Value::obj(vec![
("arrays", Value::Arr(e.arrays.iter().map(|k| Value::Int(*k)).collect())),
("total", Value::Int(e.total)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let arrays: Vec<PvArray> = args[0].as_arr().iter().map(pv_array_from_value).collect();
solar_estimate_to_value(&solar_generation(&arrays))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. 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 Rust implementation. Install it without the registry with fune add ./energy.solar-generation-1.0.0-rust.fune, or fetch it from a terminal with fune pull energy.solar-generation@1.0.0:rust.
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 |