Functional Weave
Code in Python

fleet.emissions

Vehicle greenhouse gas emissions (grams CO2e) from fuel used or distance, using UK government DESNZ conversion factors.

1.0.1 · published 2026-10-03 by charlie · Anterra

Pinned by 23 tests, run in TypeScript, Python and Rust.

What it does

Greenhouse gas emissions, in grams of CO2 equivalent, for fuel a fleet burned or distance it drove, using the UK government's conversion factors for company reporting (published by DESNZ, the Department for Energy Security and Net Zero). `fleetEmissions("fuel-diesel", "100", "litres", 2025)` is 257,082 g.

## Which factor

For example

  • fleet_emissions(fuel-diesel, 100, litres, 2,025) → co2e grams 257,082, factor 2.57082, factor year 2,025, activity fuel-diesel, unit litres 100 litres of pump diesel in 2025 is 257.082 kg
  • fleet_emissions(fuel-petrol, 40.5, litres, 2,025) → co2e grams 83,801, factor 2.06916, factor year 2,025, activity fuel-petrol, unit litres 40.5 litres of petrol in 2025: 83,800.98 g rounds to 83,801
  • fleet_emissions(fuel-diesel, 1, litres, 2,023) → co2e grams 2,512, factor 2.51206, factor year 2,023, activity fuel-diesel, unit litres one litre of diesel in the 2023 set

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 fleet_emissions(activity: str, quantity: str, unit: EmissionsUnit, factor_year: int) -> FleetEmissions
activitystringa fuel (fuel-diesel, fuel-petrol, ...) or a vehicle class (car-medium-petrol, van-average-diesel, hgv-all-hgvs-laden-average, ...)
quantitystringplain decimal text, not negative: at most 15 significant digits and 9 decimal places
unitEmissionsUnitlitres for a fuel; km or miles for a vehicle class
factor_yearintthe DESNZ factor set, 2023 to 2026: normally the year you are reporting
returnsFleetEmissionsscope 1 (direct, tailpipe) emissions, rounded half up to a whole gram

The types it declares, generated into your project

EmissionsUnit = Literal["litres", "km", "miles"]

@dataclass(frozen=True)
class FleetEmissions:
    """The emissions and the factor that produced them, for the audit trail."""

    #: quantity times factor, rounded half up to the gram
    co2e_grams: int
    #: the factor as published, kg CO2e per unit, to 5 decimal places
    factor: str
    factor_year: int
    activity: str
    unit: str

Your code names it in one line, in the file that uses it

from fune.fleet.emissions import fleet_emissions  # fleet.emissions@^1
impl/python.py · 64 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import re
from typing import Optional

from .fleet_emissions_data import GHG_FACTORS  ← this capability’s own data, compiled from data/ghg-factors.json into the same file by fune build
from .fleet_emissions_types import EmissionsUnit, FleetEmissions

DECIMAL = re.compile(r"([0-9]+)(?:\.([0-9]+))?")
MAX_SAFE = 9007199254740991


def _format_factor(factor: int) -> str:
    return "%d.%05d" % (factor // 100000, factor % 100000)


def fleet_emissions(activity: str, quantity: str, unit: EmissionsUnit, factor_year: int) -> FleetEmissions:
    """Scope 1 emissions for fuel burned or distance driven, from the UK
    government's conversion factors for company reporting.

    The quantity is decimal text and the factor an integer in 1e-5 kg, so the
    product is exact and rounded once, to the gram. Multiplying floats instead
    drifts in the last place and can round a half-gram the wrong way.
    """
    if unit not in ("litres", "km", "miles"):
        raise ValueError('unknown unit "%s": use litres, km or miles' % (unit,))
    if isinstance(factor_year, bool) or not isinstance(factor_year, int):
        raise ValueError("factorYear must be a whole year, received %s" % (factor_year,))
    match = DECIMAL.fullmatch(quantity) if isinstance(quantity, str) else None
    if match is None:
        raise ValueError('quantity must be plain decimal text, not negative, received "%s"' % (quantity,))
    fraction = match.group(2) or ""
    digits = (match.group(1) + fraction).lstrip("0")
    if len(digits) > 15 or len(fraction) > 9:
        raise ValueError(
            'quantity must have at most 15 significant digits and 9 decimal places, received "%s"' % (quantity,)
        )

    found: Optional[int] = None
    year_known = False
    activity_known = False
    for row in GHG_FACTORS:
        if row.factor_year != factor_year:
            continue
        year_known = True
        if row.activity != activity:
            continue
        activity_known = True
        if row.unit == unit:
            found = row.factor
    if not year_known:
        raise ValueError("no conversion factors for %d: this package carries 2023 to 2026" % (factor_year,))
    if not activity_known:
        raise ValueError('unknown activity "%s" in the %d factors' % (activity, factor_year))
    if found is None:
        raise ValueError("no conversion factor for %s per %s in %d" % (activity, unit, factor_year))

    # grams = quantity x factor x 1e-5 kg x 1000 g/kg = quantity x factor / 100
    numerator = int(match.group(1) + fraction) * found
    denominator = 10 ** len(fraction) * 100
    grams = (2 * numerator + denominator) // (2 * denominator)
    if grams > MAX_SAFE:
        raise ValueError("the result exceeds 2^53 - 1 grams")
    return FleetEmissions(
        co2e_grams=grams, factor=_format_factor(found), factor_year=factor_year, activity=activity, unit=unit
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and nothing else, 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 fleet.emissions
Download for Python fleet.emissions-1.0.1-python.fune · 135,230 bytes sha256 c5a2ba9c0d815e5ad33cbb7bda1c8ef355bb721db126008f7e97c511dc7b99fb

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./fleet.emissions-1.0.1-python.fune, or fetch it from a terminal with fune pull fleet.emissions@1.0.1:python.

The whole function, every language, is one file too: fleet.emissions-1.0.1.fune, 142,757 bytes, sha256 889b6473407bbb10b8b415ea8a5d9fa9ddc9d09a15beacbd289c885db32cce5c. 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 fleet.emissions

after — your function gets the result and the arguments, and returns the final result.

# fune: after fleet.emissions

replace — it requires no other capability, so there is no dependency to replace.

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 fleet.emissions --steps.

# fune: step fleet.emissions 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.

CaseArgumentsExpected
100 litres of pump diesel in 2025 is 257.082 kg fuel-diesel, 100, litres, 2,025 → co2e grams 257,082, factor 2.57082, factor year 2,025, activity fuel-diesel, unit litres
40.5 litres of petrol in 2025: 83,800.98 g rounds to 83,801 fuel-petrol, 40.5, litres, 2,025 → co2e grams 83,801, factor 2.06916, factor year 2,025, activity fuel-petrol, unit litres
one litre of diesel in the 2023 set fuel-diesel, 1, litres, 2,023 → co2e grams 2,512, factor 2.51206, factor year 2,023, activity fuel-diesel, unit litres
one litre of diesel in the 2024 set fuel-diesel, 1, litres, 2,024 → co2e grams 2,513, factor 2.51279, factor year 2,024, activity fuel-diesel, unit litres
one litre of diesel in the 2026 set fuel-diesel, 1, litres, 2,026 → co2e grams 2,584, factor 2.58354, factor year 2,026, activity fuel-diesel, unit litres
an exact half gram rounds up: 0.1 litre of 2026 petrol is 207.5 g fuel-petrol, 0.1, litres, 2,026 → co2e grams 208, factor 2.07500, factor year 2,026, activity fuel-petrol, unit litres
1,000 km in an average petrol car, 2025 car-average-petrol, 1000, km, 2,025 → co2e grams 162,720, factor 0.16272, factor year 2,025, activity car-average-petrol, unit km
100 miles uses DESNZ's own per-mile factor car-average-petrol, 100, miles, 2,025 → co2e grams 26,187, factor 0.26187, factor year 2,025, activity car-average-petrol, unit miles
the same trip in km (160.9344) agrees to the gram car-average-petrol, 160.9344, km, 2,025 → co2e grams 26,187, factor 0.16272, factor year 2,025, activity car-average-petrol, unit km
250 km in an average laden HGV in 2026: 224,357.5 g rounds up hgv-all-hgvs-laden-average, 250, km, 2,026 → co2e grams 224,358, factor 0.89743, factor year 2,026, activity hgv-all-hgvs-laden-average, unit km
Show the other 13 tests
CaseArgumentsExpected
zero distance is zero car-average-petrol, 0, km, 2,025 → co2e grams 0, factor 0.16272, factor year 2,025, activity car-average-petrol, unit km
a nine-decimal quantity is accepted and rounds to nothing fuel-diesel, 0.000000001, litres, 2,025 → co2e grams 0, factor 2.57082, factor year 2,025, activity fuel-diesel, unit litres
a fuel has no per-km factor fuel-diesel, 10, km, 2,025 → error: no conversion factor for fuel-diesel per km in 2025
a vehicle class has no per-litre factor car-average-petrol, 10, litres, 2,025 → error: no conversion factor for car-average-petrol per litres in 2025
battery electric cars are not carried: their emissions are electricity, not scope 1 car-average-bev, 10, km, 2,025 → error: unknown activity "car-average-bev" in the 2025 factors
van PHEV factors start in 2024 van-average-phev, 10, km, 2,023 → error: unknown activity "van-average-phev" in the 2023 factors
2022 factors are not carried fuel-diesel, 10, litres, 2,022 → error: no conversion factors for 2022
a negative quantity is refused fuel-diesel, -5, litres, 2,025 → error: quantity must be plain decimal text, not negative
exponent notation is refused fuel-diesel, 1e3, litres, 2,025 → error: quantity must be plain decimal text, not negative
a number instead of text is refused fuel-diesel, 12, litres, 2,025 → error: quantity must be plain decimal text, not negative
ten decimal places are too many fuel-diesel, 0.0000000001, litres, 2,025 → error: at most 15 significant digits and 9 decimal places
an unknown unit is refused fuel-diesel, 10, gallons, 2,025 → error: unknown unit "gallons"
a result beyond 2^53 - 1 grams is refused fuel-diesel, 123456789012345, litres, 2,025 → error: the result exceeds 2^53 - 1 grams

More from the author

Fuel is the better measure when you have it (fuel cards, bunkered fuel): it already reflects how the vehicles were driven. Distance by vehicle class is the fallback. The factors are the **Scope 1** figures, direct tailpipe emissions in kg CO2e (CO2, CH4 and N2O together):

- fuels, per litre: `fuel-diesel` and `fuel-petrol` (UK forecourt fuel with its average biofuel blend), `fuel-diesel-mineral` and `fuel-petrol-mineral` (100% mineral), `fuel-lpg`; - cars by size, per km or mile: `car-{small|medium|large|average}-{diesel|petrol|hybrid|phev|cng|lpg|unknown}` where DESNZ publishes one (there is no small CNG or LPG car); - `motorbike-{small|medium|large|average}`; - vans: `van-{class-1|class-2|class-3|average}-{diesel|petrol}`, and `van-average-{cng|lpg|unknown|phev}` and `van-class-3-phev` (the van PHEV factors appear from 2024); - HGVs (all diesel): `hgv-{rigid-3.5-7.5t|rigid-7.5-17t|rigid-over-17t|all-rigids|artic-3.5-33t|artic-over-33t|all-artics|all-hgvs}-laden-{0|50|100|average}`.

Battery electric vehicles are deliberately absent: their Scope 1 factor is zero, and their emissions belong to the electricity used (Scope 2, DESNZ's "UK electricity for EVs" table), which this package does not carry. A plug-in hybrid's factor likewise covers only the fuel it burns.

Per-mile factors are DESNZ's own, not the per-km factor times 1.609344, so a report matches the published spreadsheet exactly.

## Which year

`factorYear` picks the DESNZ set: 2023, 2024, 2025 or 2026. DESNZ publishes each set in June for reporting periods in that year; use the set for the year you are reporting, and keep it for restatements. The sets are publications, not rules that expire, so the table has no effective columns and ships whole.

In the 2026 set DESNZ split HGVs into refrigerated and non-refrigerated and renamed the averages; the `hgv-*` codes read the non-refrigerated rows for 2026 (earlier years' "HGV (all diesel)" table is the non-refrigerated fleet, refrigerated HGVs having had their own table), so the codes mean the same vehicle in every year.

## Exactness

Each factor is stored as an integer in units of 0.00001 kg (10 mg): DESNZ publishes five decimal places. The 2023 flat file holds the unrounded values behind them (2.5120638845637586 for diesel); they are rounded half up to the five places shown in the published tables (2.51206). The quantity is decimal text, so quantity times factor is exact, and the only rounding is the final one, half up to a whole gram. Quantities are limited to 15 significant digits and 9 decimal places, and a result above 2^53 - 1 grams is an error, so all three languages agree.

## Relationship to energy.*

This package is self-contained. If an `energy.carbon-emissions` capability with the same DESNZ tables appears in the registry, a later version of this one should read its data instead of carrying a second copy.

## Sources

- DESNZ, "Greenhouse gas reporting: conversion factors 2023", flat file (ghg-conversion-factors-2023-flat-file-update.xlsx): https://www.gov.uk/government/publications/greenhouse-gas-reporting-conversion-factors-2023 - DESNZ, "Greenhouse gas reporting: conversion factors 2024", flat file v1.1 (ghg-conversion-factors-2024-FlatFormat_v1_1.xlsx): https://www.gov.uk/government/publications/greenhouse-gas-reporting-conversion-factors-2024 - DESNZ, "Greenhouse gas reporting: conversion factors 2025", flat file (ghg-conversion-factors-2025-flat-format.xlsx): https://www.gov.uk/government/publications/greenhouse-gas-reporting-conversion-factors-2025 - DESNZ, "Greenhouse gas reporting: conversion factors 2026", flat file, revised July 2026 (ghg-conversion-factors-2026-flat-format-revised.xlsx): https://www.gov.uk/government/publications/greenhouse-gas-reporting-conversion-factors-2026

Every row of `data/ghg-factors.json` names the spreadsheet path it came from (Level 1 / Level 2 / Level 3 / column), always the "kg CO2e" row of Scope 1.

## Notices

Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).

1.0.1 adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE178
README.md4,562
data/ghg-factors.json106,856
impl/python.py2,788
impl/rust.rs4,419
impl/typescript.ts2,804
vectors.json6,107