Skip to content

Declining energy return on investment

The extraction of a finite energy reserve whose energy return on investment declines as it depletes.

  • Name: energy_return
  • Field: physics/climate
  • Formalism: ordinary differential equations
  • References: Mobus 2022

Factory

@register_model(
    field='physics/climate',
    tags=('energy', 'net energy', 'stocks and flows', 'system dynamics', 'ode'),
    references=('Mobus 2022',),
    defaults={'until': 250.0, 'dt': 1.0},
    observables=lambda system: [
        ObservableSpec('eroi', _eroi, element='energy'),
        ObservableSpec('gross_energy', _gross_energy, element='energy'),
        ObservableSpec('extraction_cost', _extraction_cost, element='energy'),
        ObservableSpec('net_per_capita', _net_per_capita, element='energy'),
    ],
)
def energy_return(
    reserve: float = 1000.0,
    capacity: float = 0.1,
    *,
    initial_eroi: float = 20.0,
    reinvestment: float = 0.1,
    birth_rate: float = 0.03,
) -> System:
    """Return the extraction of a finite energy reserve whose energy return on investment declines as it depletes.

    The extraction capacity draws the gross energy from the reserve at a rate that falls with the remaining fraction,
    since the easy sources go first. Part of the gross energy pays for the extraction work, the gross energy divided
    by the energy return on investment (EROI), which falls from `initial_eroi` to one with the cube of the remaining
    fraction as the reserve empties. The rest is the net energy that society receives. Society reinvests a share of
    the net energy in extraction capacity, which depreciates, and the population grows exponentially with no feedback
    from the energy it receives. Energy is conserved, every unit leaving the reserve is either cost or net energy, so
    the gross energy rises exponentially and peaks, the net energy peaks before it, and the net energy per person
    peaks earlier still. The structure follows Mobus (2022, Section 9.6) and its figures of peak net energy, with
    illustrative parameters in energy units, persons, and years.

    Args:
        reserve:
            The initial energy reserve, also its full size.

        capacity:
            The initial extraction capacity, the gross energy per year from a full reserve.

        initial_eroi:
            The energy return on investment of a full reserve.

        reinvestment:
            The share of the net energy reinvested in extraction capacity.

        birth_rate:
            The relative birth rate of the population per year.

    Returns:
        system:
            The model, with the reserve `energy.reserve`, the capacity `energy.capacity`, the population
            `energy.population`, and the flows `energy.energy_cost`, `energy.net_energy`, `energy.births`, and
            `energy.deaths` per year, over years. The observables of the model are `energy.eroi`,
            `energy.gross_energy`, `energy.extraction_cost`, and `energy.net_per_capita`.

    Examples:
        >>> from skcomplex.models.physics.climate import energy_return
        >>> energy_return(initial_eroi=20.0).elements[0].initial_eroi
        20.0
    """
    sector = EnergySectorElement(
        'energy',
        reserve=reserve,
        capacity=capacity,
        reserve_size=reserve,
        initial_eroi=initial_eroi,
        reinvestment=reinvestment,
        birth_rate=birth_rate,
    )
    return System(elements=[sector])

Components

class EnergySectorElement(Element):
    """Energy sector of a society, a finite reserve, the capacity that extracts it, and the population it supplies.

    The capacity draws the gross energy `capacity reserve / reserve_size` from the reserve. Part of it pays for the
    extraction work, the gross energy divided by the energy return on investment
    `1 + (initial_eroi - 1) (reserve / reserve_size)^decline`, and the rest is the net energy society receives. A share
    `reinvestment` of the net energy builds capacity, which depreciates over `lifetime`, and the population grows at
    the rate `birth_rate - death_rate`.

    Args:
        label:
            The label of the sector.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        reserve:
            The initial energy reserve.

        capacity:
            The initial extraction capacity, the gross energy per year from a full reserve.

        population:
            The initial population.

        reserve_size:
            The full size of the reserve.

        initial_eroi:
            The energy return on investment of a full reserve.

        decline:
            The power of the remaining fraction of the reserve in the energy return on investment.

        reinvestment:
            The share of the net energy reinvested in extraction capacity.

        lifetime:
            The lifetime of the extraction capacity in years.

        birth_rate:
            The relative birth rate of the population per year.

        death_rate:
            The relative death rate of the population per year.

    Examples:
        >>> from skcomplex.models.physics.climate import EnergySectorElement
        >>> EnergySectorElement('energy').initial_eroi
        20.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        reserve: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo), quantity='energy')] = 1000.0,
        capacity: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
        population: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo), quantity='people')] = 1.0,
        reserve_size: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1000.0,
        initial_eroi: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 20.0,
        decline: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 3.0,
        reinvestment: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
        lifetime: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 20.0,
        birth_rate: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.03,
        death_rate: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.015,
    ) -> None:
        super().__init__(label, visual=visual)
        self.reserve = reserve
        self.capacity = capacity
        self.population = population
        self.reserve_size = reserve_size
        self.initial_eroi = initial_eroi
        self.decline = decline
        self.reinvestment = reinvestment
        self.lifetime = lifetime
        self.birth_rate = birth_rate
        self.death_rate = death_rate

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the extraction of the reserve, its energy cost, the growth of the capacity, and the population.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The transfers out of the reserve, the rate law of the capacity, and the births and deaths.
        """
        cost = _extraction_cost(s)
        net = _gross_energy(s) - cost
        return [
            TransferLaw(s.reserve, Sink('extraction_work'), cost, label='energy_cost'),
            TransferLaw(s.reserve, Sink('society'), net, label='net_energy'),
            RateLaw(s.capacity, s.reinvestment * net - s.capacity / s.lifetime),
            TransferLaw(Source('births'), s.population, s.birth_rate * s.population, label='births'),
            TransferLaw(s.population, Sink('deaths'), s.death_rate * s.population, label='deaths'),
        ]

Equations

energy.reserve: diff(reserve_energy(t), t) == -capacity_energy(t)*reserve_energy(t)/reserve_size_energy
energy.capacity: diff(capacity_energy(t), t) == reinvestment_energy*(capacity_energy(t)*reserve_energy(t)/reserve_size_energy - capacity_energy(t)*reserve_energy(t)/(((initial_eroi_energy - 1)*(reserve_energy(t)/reserve_size_energy)^decline_energy + 1)*reserve_size_energy)) - capacity_energy(t)/lifetime_energy
energy.population: diff(population_energy(t), t) == birth_rate_energy*population_energy(t) - death_rate_energy*population_energy(t)