Module nemo.penalties

Penalty functions for the optimisation.

Expand source code
# Copyright (C) 2021 Ben Elliston
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.

"""Penalty functions for the optimisation."""

from nemo import generators

_reason_labels = ['unserved', 'emissions', 'fossil', 'bioenergy',
                  'hydro', 'reserves', 'min-regional-gen']

reasons = {}
for i, label in enumerate(_reason_labels):
    reasons[label] = 1 << i

# Conversion factor between MWh and TWh.
_twh = pow(10., 6)


def unserved(ctx, _):
    """Penalty: unserved energy."""
    minuse = ctx.total_demand() * (ctx.relstd / 100)
    use = max(0, ctx.unserved_energy() - minuse)
    reason = reasons['unserved'] if use > 0 else 0
    return pow(use, 3), reason


def _calculate_reserve(gen, time):
    """Calculate headroom for each generator.

    Note: except pumped hydro and CST -- tricky to calculate capacity.
    """
    if isinstance(gen, generators.Fuelled) and not \
       isinstance(gen, generators.PumpedHydroTurbine) and not \
       isinstance(gen, generators.CST):
        return gen.capacity - gen.series_power[time]
    return 0


def reserves(ctx, args):
    """Penalty: minimum reserves."""
    pen, reas = 0, 0
    for time in range(ctx.timesteps()):
        reserve, spilled = 0, 0
        for gen in ctx.generators:
            try:
                spilled += gen.series_spilled[time]
            except KeyError:
                # non-variable generators may not have spill data
                pass
            reserve += _calculate_reserve(gen, time)

        if reserve + spilled < args.reserves:
            reas |= reasons['reserves']
            pen += pow(args.reserves - reserve + spilled, 3)
    return pen, reas


def _regional_generation(region, gens):
    """Sum generation in a given region."""
    regional_generation = 0
    for gen in gens:
        if gen.region() is region:
            regional_generation += sum(gen.series_power.values())
    return regional_generation


def _regional_demand(region, demand):
    """Sum demand in a given region."""
    regional_demand = 0
    for poly in region.polygons:
        regional_demand += demand[poly - 1].sum()
    return regional_demand


def min_regional(ctx, _):
    """Penalty: minimum share of regional generation."""
    shortfall = 0
    for rgn in ctx.regions:
        regional_demand = _regional_demand(rgn, ctx.demand)
        regional_generation = _regional_generation(rgn, ctx.generators)
        min_regional_generation = regional_demand * ctx.min_regional_generation
        shortfall += max(0, min_regional_generation - regional_generation)

    if shortfall > 0:
        reason = reasons['min-regional-gen']
    else:
        reason = 0
    return pow(shortfall, 3), reason


def emissions(ctx, args):
    """Penalty: total emissions."""
    total_emissions = 0
    for gen in ctx.generators:
        if hasattr(gen, 'intensity'):
            total_emissions += sum(gen.series_power.values()) * gen.intensity
    emissions_limit = args.emissions_limit * pow(10, 6) * ctx.years()
    # exceedance in tonnes CO2-e
    emissions_exceedance = max(0, total_emissions - emissions_limit)
    reason = reasons['emissions'] if emissions_exceedance > 0 else 0
    return pow(emissions_exceedance, 3), reason


def fossil(ctx, args):
    """Penalty: limit fossil to fraction of annual demand."""
    fossil_energy = 0
    for gen in ctx.generators:
        if isinstance(gen, generators.Fossil):
            fossil_energy += sum(gen.series_power.values())
    fossil_limit = ctx.total_demand() * args.fossil_limit * ctx.years()
    fossil_exceedance = max(0, fossil_energy - fossil_limit)
    reason = reasons['fossil'] if fossil_exceedance > 0 else 0
    return pow(fossil_exceedance, 3), reason


def bioenergy(ctx, args):
    """Penalty: limit biofuel use."""
    biofuel_energy = 0
    for gen in ctx.generators:
        if isinstance(gen, generators.Biofuel):
            biofuel_energy += sum(gen.series_power.values())
    biofuel_limit = args.bioenergy_limit * _twh * ctx.years()
    biofuel_exceedance = max(0, biofuel_energy - biofuel_limit)
    reason = reasons['bioenergy'] if biofuel_exceedance > 0 else 0
    return pow(biofuel_exceedance, 3), reason


def hydro(ctx, args):
    """Penalty: limit hydro use."""
    hydro_energy = 0
    for gen in ctx.generators:
        if isinstance(gen, generators.Hydro) and \
           not isinstance(gen, generators.PumpedHydroTurbine):
            hydro_energy += sum(gen.series_power.values())
    hydro_limit = args.hydro_limit * _twh * ctx.years()
    hydro_exceedance = max(0, hydro_energy - hydro_limit)
    reason = reasons['hydro'] if hydro_exceedance > 0 else 0
    return pow(hydro_exceedance, 3), reason

Functions

def bioenergy(ctx, args)

Penalty: limit biofuel use.

Expand source code
def bioenergy(ctx, args):
    """Penalty: limit biofuel use."""
    biofuel_energy = 0
    for gen in ctx.generators:
        if isinstance(gen, generators.Biofuel):
            biofuel_energy += sum(gen.series_power.values())
    biofuel_limit = args.bioenergy_limit * _twh * ctx.years()
    biofuel_exceedance = max(0, biofuel_energy - biofuel_limit)
    reason = reasons['bioenergy'] if biofuel_exceedance > 0 else 0
    return pow(biofuel_exceedance, 3), reason
def emissions(ctx, args)

Penalty: total emissions.

Expand source code
def emissions(ctx, args):
    """Penalty: total emissions."""
    total_emissions = 0
    for gen in ctx.generators:
        if hasattr(gen, 'intensity'):
            total_emissions += sum(gen.series_power.values()) * gen.intensity
    emissions_limit = args.emissions_limit * pow(10, 6) * ctx.years()
    # exceedance in tonnes CO2-e
    emissions_exceedance = max(0, total_emissions - emissions_limit)
    reason = reasons['emissions'] if emissions_exceedance > 0 else 0
    return pow(emissions_exceedance, 3), reason
def fossil(ctx, args)

Penalty: limit fossil to fraction of annual demand.

Expand source code
def fossil(ctx, args):
    """Penalty: limit fossil to fraction of annual demand."""
    fossil_energy = 0
    for gen in ctx.generators:
        if isinstance(gen, generators.Fossil):
            fossil_energy += sum(gen.series_power.values())
    fossil_limit = ctx.total_demand() * args.fossil_limit * ctx.years()
    fossil_exceedance = max(0, fossil_energy - fossil_limit)
    reason = reasons['fossil'] if fossil_exceedance > 0 else 0
    return pow(fossil_exceedance, 3), reason
def hydro(ctx, args)

Penalty: limit hydro use.

Expand source code
def hydro(ctx, args):
    """Penalty: limit hydro use."""
    hydro_energy = 0
    for gen in ctx.generators:
        if isinstance(gen, generators.Hydro) and \
           not isinstance(gen, generators.PumpedHydroTurbine):
            hydro_energy += sum(gen.series_power.values())
    hydro_limit = args.hydro_limit * _twh * ctx.years()
    hydro_exceedance = max(0, hydro_energy - hydro_limit)
    reason = reasons['hydro'] if hydro_exceedance > 0 else 0
    return pow(hydro_exceedance, 3), reason
def min_regional(ctx, _)

Penalty: minimum share of regional generation.

Expand source code
def min_regional(ctx, _):
    """Penalty: minimum share of regional generation."""
    shortfall = 0
    for rgn in ctx.regions:
        regional_demand = _regional_demand(rgn, ctx.demand)
        regional_generation = _regional_generation(rgn, ctx.generators)
        min_regional_generation = regional_demand * ctx.min_regional_generation
        shortfall += max(0, min_regional_generation - regional_generation)

    if shortfall > 0:
        reason = reasons['min-regional-gen']
    else:
        reason = 0
    return pow(shortfall, 3), reason
def reserves(ctx, args)

Penalty: minimum reserves.

Expand source code
def reserves(ctx, args):
    """Penalty: minimum reserves."""
    pen, reas = 0, 0
    for time in range(ctx.timesteps()):
        reserve, spilled = 0, 0
        for gen in ctx.generators:
            try:
                spilled += gen.series_spilled[time]
            except KeyError:
                # non-variable generators may not have spill data
                pass
            reserve += _calculate_reserve(gen, time)

        if reserve + spilled < args.reserves:
            reas |= reasons['reserves']
            pen += pow(args.reserves - reserve + spilled, 3)
    return pen, reas
def unserved(ctx, _)

Penalty: unserved energy.

Expand source code
def unserved(ctx, _):
    """Penalty: unserved energy."""
    minuse = ctx.total_demand() * (ctx.relstd / 100)
    use = max(0, ctx.unserved_energy() - minuse)
    reason = reasons['unserved'] if use > 0 else 0
    return pow(use, 3), reason