Module nemo.generators

Simulated electricity generators for the NEMO framework.

Expand source code
# Copyright (C) 2011, 2012, 2013, 2014, 2022 Ben Elliston
# Copyright (C) 2014, 2015, 2016 The University of New South Wales
# Copyright (C) 2016, 2017 IT Power (Australia)
#
# 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.

"""Simulated electricity generators for the NEMO framework."""

# pylint: disable=too-many-lines
# We use class names here that upset Pylint.
# pylint: disable=invalid-name

from math import isclose

import numpy as np
import pandas as pd
import requests
from matplotlib.patches import Patch

from nemo import polygons, storage
from nemo.utils import currency, thousands, ureg


class Generator():
    """Base generator class."""

    # Is the generator a rotating machine?
    synchronous_p = True
    """Is this a synchronous generator?"""

    storage_p = False
    """A generator is not capable of storage by default."""

    def __init__(self, polygon, capacity, label=None):
        """
        Construct a base Generator.

        Arguments: installed polygon, installed capacity, descriptive label.
        """
        assert capacity >= 0
        self.setters = [(self.set_capacity, 0, 40)]
        self.label = self.__class__.__name__ if label is None else label
        self.capacity = capacity
        self.polygon = polygon

        # Sanity check polygon argument.
        assert not isinstance(polygon, polygons.regions.Region)
        assert 0 < polygon <= polygons.NUMPOLYGONS, polygon

        # Time series of dispatched power and spills
        self.series_power = {}
        self.series_spilled = {}

    def series(self):
        """Return generation and spills series."""
        return {'power': pd.Series(self.series_power, dtype=float),
                'spilled': pd.Series(self.series_spilled, dtype=float)}

    def step(self, hour, demand):
        """Step the generator by one hour."""
        raise NotImplementedError

    def region(self):
        """Return the region the generator is in."""
        return polygons.region(self.polygon)

    def capcost(self, costs):
        """Return the capital cost."""
        return costs.capcost_per_kw[type(self)] * self.capacity * 1000

    def opcost(self, costs):
        """Return the annual operating and maintenance cost."""
        return self.fixed_om_costs(costs) + \
            sum(self.series_power.values()) * self.opcost_per_mwh(costs)

    def fixed_om_costs(self, costs):
        """Return the fixed O&M costs."""
        return costs.fixed_om_costs[type(self)] * self.capacity * 1000

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        return costs.opcost_per_mwh[type(self)]

    def reset(self):
        """Reset the generator."""
        self.series_power.clear()
        self.series_spilled.clear()

    def capfactor(self):
        """Capacity factor of this generator (in %)."""
        supplied = sum(self.series_power.values())
        hours = len(self.series_power)
        try:
            capfactor = supplied / (self.capacity * hours) * 100
            return capfactor
        except ZeroDivisionError:
            return float('nan')

    def lcoe(self, costs, years):
        """Calculate the LCOE in $/MWh."""
        total_cost = self.capcost(costs) / costs.annuityf * years \
            + self.opcost(costs)
        supplied = sum(self.series_power.values())
        if supplied > 0:
            cost_per_mwh = total_cost / supplied
            return cost_per_mwh
        return np.inf

    def summary(self, context):
        """Return a summary of the generator activity."""
        costs = context.costs
        supplied = sum(self.series_power.values()) * ureg.MWh
        string = f'supplied {supplied.to_compact()}'
        if self.capacity > 0:
            if self.capfactor() > 0:
                string += f', CF {self.capfactor():.1f}%'
        if sum(self.series_spilled.values()) > 0:
            spilled = sum(self.series_spilled.values()) * ureg.MWh
            string += f', surplus {spilled.to_compact()}'
        if self.capcost(costs) > 0:
            string += f', capcost {currency(self.capcost(costs))}'
        if self.opcost(costs) > 0:
            string += f', opcost {currency(self.opcost(costs))}'
        lcoe = self.lcoe(costs, context.years())
        if np.isfinite(lcoe) and lcoe > 0:
            string += f', LCOE {currency(int(lcoe))}'
        return string

    def set_capacity(self, cap):
        """Change the capacity of the generator to cap GW."""
        self.capacity = cap * 1000

    def __str__(self):
        """Return a short string representation of the generator."""
        return f'{self.label} ({self.region()}:{self.polygon}), ' + \
            str(self.capacity * ureg.MW)

    def __repr__(self):
        """Return a representation of the generator."""
        return self.__str__()


# This class is not to be confused with storage.py.
# This class will go away soon.

class Storage():
    """A class to give a generator storage capability."""

    storage_p = True
    """This generator is capable of storage."""

    def __init__(self):
        """Storage constructor."""
        # Time series of charges
        self.series_charge = {}
        self.series_soc = {}

    def soc(self):
        """Return the storage SOC (state of charge)."""
        raise NotImplementedError

    def record(self, hour, energy):
        """Record storage."""
        if hour not in self.series_charge:
            self.series_charge[hour] = 0
        self.series_charge[hour] += energy
        self.series_soc[hour] = self.soc()

    def charge_capacity(self, gen, hour):
        """Return available storage capacity.

        Since a storage-capable generator can be called on multiple
        times to store energy in a single timestep, we keep track of
        how much remaining capacity is available for charging in the
        given timestep.
        """
        try:
            result = gen.capacity - self.series_charge[hour]
            if result < 0 or isclose(result, 0, abs_tol=1e-6):
                result = 0
            assert result >= 0
            return result
        except KeyError:
            return gen.capacity

    def series(self):
        """Return generation and spills series."""
        return {'charge': pd.Series(self.series_charge, dtype=float),
                'soc': pd.Series(self.series_soc, dtype=float)}

    def store(self, hour, power):
        """Abstract method to ensure that derived classes define this."""
        raise NotImplementedError

    def reset(self):
        """Reset a generator with storage."""
        self.series_charge.clear()
        self.series_soc.clear()


class TraceGenerator(Generator):
    """A generator that gets its hourly dispatch from a CSV trace file."""

    csvfilename = None
    csvdata = None

    def __init__(self, polygon, capacity, label=None, build_limit=None):
        """Construct a generator with a specified trace file."""
        Generator.__init__(self, polygon, capacity, label)
        if build_limit is not None:
            # Override default capacity limit with build_limit
            _, _, limit = self.setters[0]
            self.setters = [(self.set_capacity, 0, min(build_limit, limit))]

    def step(self, hour, demand):
        """Step method for any generator using traces."""
        # self.generation must be defined by derived classes
        # pylint: disable=no-member
        generation = self.generation[hour] * self.capacity
        # optimised version of min() because TraceGenerator is a
        # heavily used class
        power = generation if generation < demand else demand
        spilled = generation - power
        self.series_power[hour] = power
        self.series_spilled[hour] = spilled
        return power, spilled


class CSVTraceGenerator(TraceGenerator):
    """A generator that gets its hourly dispatch from a CSV trace file."""

    csvfilename = None
    csvdata = None

    def __init__(self, polygon, capacity, filename, column, label=None,
                 build_limit=None):
        """Construct a generator with a specified trace file."""
        TraceGenerator.__init__(self, polygon, capacity, label, build_limit)
        cls = self.__class__
        if cls.csvfilename != filename:
            # Optimisation:
            # Only if the filename changes do we invoke genfromtxt.
            if not filename.startswith('http'):
                # Local file path
                traceinput = filename
            else:
                try:
                    resp = requests.request('GET', filename, timeout=5)
                except requests.exceptions.Timeout as exc:
                    raise TimeoutError(f'timeout fetching {filename}') from exc
                if not resp.ok:
                    msg = f'HTTP {resp.status_code}: {filename}'
                    raise ConnectionError(msg)
                traceinput = resp.text.splitlines()
            cls.csvdata = np.genfromtxt(traceinput, encoding='UTF-8',
                                        delimiter=',')
            cls.csvdata = np.maximum(0, cls.csvdata)
            # check all elements are not NaNs
            assert np.all(~np.isnan(cls.csvdata)), \
                f'Trace file {filename} contains NaNs; inspect file'
            cls.csvfilename = filename
        self.generation = cls.csvdata[::, column]


class Wind(CSVTraceGenerator):
    """Wind power."""

    patch = Patch(facecolor='#417505')
    """Patch for plotting"""
    synchronous_p = False
    """Is this a synchronous generator?"""


class WindOffshore(Wind):
    """Offshore wind power."""

    patch = Patch(facecolor='darkgreen')
    """Colour for plotting"""


class PV(CSVTraceGenerator):
    """Solar photovoltaic (PV) model."""

    synchronous_p = False
    """Is this a synchronous generator?"""


class PV1Axis(PV):
    """Single-axis tracking PV."""

    patch = Patch(facecolor='#fed500')
    """Colour for plotting"""


class Behind_Meter_PV(PV):
    """Behind the meter PV."""

    patch = Patch(facecolor='#ffe03d')


class CST(CSVTraceGenerator):
    """Concentrating solar thermal (CST) model."""

    patch = Patch(facecolor='orange')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, solarmult, shours, filename,
                 column, label=None, build_limit=None):
        """
        Construct a CST generator.

        Arguments include capacity (in MW), sm (solar multiple) and
        shours (hours of storage).
        """
        CSVTraceGenerator.__init__(self, polygon, capacity, filename, column,
                                   label)
        self.maxstorage = None
        self.stored = None
        self.set_storage(shours)
        self.set_multiple(solarmult)

    def set_capacity(self, cap):
        """Change the capacity of the generator to cap GW."""
        Generator.set_capacity(self, cap)
        self.maxstorage = self.capacity * self.shours

    def set_multiple(self, solarmult):
        """Change the solar multiple of a CST plant."""
        self.solarmult = solarmult

    def set_storage(self, shours):
        """Change the storage capacity of a CST plant."""
        self.shours = shours
        self.maxstorage = self.capacity * shours
        self.stored = 0.5 * self.maxstorage

    def step(self, hour, demand):
        """Step method for CST generators."""
        generation = self.generation[hour] * self.capacity * self.solarmult
        remainder = min(self.capacity, demand)
        if generation > remainder:
            to_storage = generation - remainder
            generation -= to_storage
            self.stored += to_storage
            self.stored = min(self.stored, self.maxstorage)
        else:
            from_storage = min(remainder - generation, self.stored)
            generation += from_storage
            self.stored -= from_storage
            assert self.stored >= 0
        assert self.stored <= self.maxstorage
        assert self.stored >= 0
        self.series_power[hour] = generation
        self.series_spilled[hour] = 0

        # This can happen due to rounding errors.
        generation = min(generation, demand)
        return generation, 0

    def reset(self):
        """Reset the generator."""
        Generator.reset(self)
        self.stored = 0.5 * self.maxstorage

    def summary(self, context):
        """Return a summary of the generator activity."""
        return Generator.summary(self, context) + \
            f', solar mult {self.solarmult:.2f}' + \
            f', {self.shours}h storage'


class ParabolicTrough(CST):
    """Parabolic trough CST generator.

    This stub class allows differentiated CST costs in costs.py.
    """


class CentralReceiver(CST):
    """Central receiver CST generator.

    This stub class allows differentiated CST costs in costs.py.
    """


class Fuelled(Generator):
    """The class of generators that consume fuel."""

    def __init__(self, polygon, capacity, label):
        """Construct a fuelled generator."""
        Generator.__init__(self, polygon, capacity, label)
        self.runhours = 0

    def reset(self):
        """Reset the generator."""
        Generator.reset(self)
        self.runhours = 0

    def step(self, hour, demand):
        """Step method for fuelled generators."""
        power = min(self.capacity, demand)
        if power > 0:
            self.runhours += 1
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        return power, 0

    def summary(self, context):
        """Return a summary of the generator activity."""
        return Generator.summary(self, context) + \
            f', ran {thousands(self.runhours)} hours'


class Hydro(Fuelled):
    """Hydro power stations."""

    patch = Patch(facecolor='#4582b4')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, label=None):
        """Construct a hydroelectric generator."""
        Fuelled.__init__(self, polygon, capacity, label)
        # capacity is in MW, but build limit is in GW
        self.setters = [(self.set_capacity, 0, capacity / 1000.)]


class PumpedHydroPump(Storage, Generator):
    """Pumped hydro (pump side) model."""

    patch = Patch(facecolor='darkblue')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, reservoirs, rte=0.8, label=None):
        """Construct a pumped hydro storage generator."""
        if not isinstance(reservoirs, storage.PumpedHydroStorage):
            raise TypeError
        Storage.__init__(self)
        Generator.__init__(self, polygon, capacity, label)
        self.reservoirs = reservoirs
        self.rte = rte

    def step(self, hour, demand):
        """Return 0 as this is not a generator."""
        return 0, 0

    def series(self):
        """Return the combined series."""
        dict1 = Hydro.series(self)
        dict2 = Storage.series(self)
        dict1.update(dict2)
        return dict1

    def soc(self):
        """Return the pumped hydro SOC (state of charge)."""
        return self.reservoirs.soc()

    def store(self, hour, power):
        """Pump water uphill for one hour."""
        if self.reservoirs.last_gen == hour:
            # Can't pump and generate in the same hour.
            return 0
        power = min(self.charge_capacity(self, hour), power,
                    self.capacity)

        stored = self.reservoirs.charge(power * self.rte)
        if stored < power * self.rte:
            power = (self.reservoirs.maxstorage - self.reservoirs.storage) \
                / self.rte

        if power > 0:
            self.record(hour, power)
            self.reservoirs.last_pump = hour
        return power

    def reset(self):
        """Reset the generator."""
        Generator.reset(self)
        Storage.reset(self)
        self.reservoirs.reset()

    def summary(self, context):
        """Return a summary of the generator activity."""
        stg = (self.reservoirs.maxstorage * ureg.MWh).to_compact()
        return Generator.summary(self, context) + \
            f', charged {thousands(len(self.series_charge))} hours' + \
            f', {stg} storage'


class PumpedHydroTurbine(Hydro):
    """Pumped storage hydro (generator side) model."""

    patch = Patch(facecolor='powderblue')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, reservoirs, label=None):
        """Construct a pumped hydro storage generator."""
        if not isinstance(reservoirs, storage.PumpedHydroStorage):
            raise TypeError
        Hydro.__init__(self, polygon, capacity, label)
        self.reservoirs = reservoirs

    def step(self, hour, demand):
        """Step method for pumped hydro storage."""
        power = min(self.reservoirs.storage, self.capacity, demand)
        if self.reservoirs.last_pump == hour:
            # Can't pump and generate in the same hour.
            self.series_power[hour] = 0
            self.series_spilled[hour] = 0
            return 0, 0

        self.reservoirs.discharge(power)
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        if power > 0:
            self.runhours += 1
            self.reservoirs.last_gen = hour
        return power, 0


class Biofuel(Fuelled):
    """Model of open cycle gas turbines burning biofuel."""

    patch = Patch(facecolor='wheat')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, label=None):
        """Construct a biofuel generator."""
        Fuelled.__init__(self, polygon, capacity, label)

    def capcost(self, costs):
        """Return the capital cost (of an OCGT)."""
        return costs.capcost_per_kw[OCGT] * self.capacity * 1000

    def fixed_om_costs(self, costs):
        """Return the fixed O&M costs (of an OCGT)."""
        return costs.fixed_om_costs[OCGT] * self.capacity * 1000

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[OCGT]
        fuel_cost = costs.bioenergy_price_per_gj * (3.6 / .31)  # 31% heat rate
        return vom + fuel_cost


class Biomass(Fuelled):
    """Model of steam turbine burning solid biomass."""

    patch = Patch(facecolor='#1d7a7a')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, label=None, heatrate=0.3):
        """Construct a biomass generator."""
        Fuelled.__init__(self, polygon, capacity, label)
        self.heatrate = heatrate

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        fuel_cost = costs.bioenergy_price_per_gj * (3.6 / self.heatrate)
        return vom + fuel_cost


class Fossil(Fuelled):
    """Base class for GHG emitting power stations."""

    patch = Patch(facecolor='grey')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, intensity, label=None):
        """
        Construct a fossil fuelled generator.

        Greenhouse gas emissions intensity is given in tonnes per MWh.
        """
        Fuelled.__init__(self, polygon, capacity, label)
        self.intensity = intensity

    def summary(self, context):
        """Return a summary of the generator activity."""
        generation = sum(self.series_power.values()) * ureg.MWh
        emissions = generation * self.intensity * (ureg.t / ureg.MWh)
        return Fuelled.summary(self, context) + \
            f', {emissions.to("Mt")} CO2'


class Black_Coal(Fossil):
    """Black coal power stations with no CCS."""

    patch = Patch(facecolor='#121212')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, intensity=0.773, label=None):
        """Construct a black coal generator."""
        Fossil.__init__(self, polygon, capacity, intensity, label)

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        fuel_cost = costs.coal_price_per_gj * 8.57
        total_opcost = vom + fuel_cost + self.intensity * costs.carbon
        return total_opcost


class OCGT(Fossil):
    """Open cycle gas turbine (OCGT) model."""

    patch = Patch(facecolor='#ffcd96')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, intensity=0.7, label=None):
        """Construct an OCGT generator."""
        Fossil.__init__(self, polygon, capacity, intensity, label)

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        fuel_cost = costs.gas_price_per_gj * 11.61
        total_opcost = vom + fuel_cost + self.intensity * costs.carbon
        return total_opcost


class CCGT(Fossil):
    """Combined cycle gas turbine (CCGT) model."""

    patch = Patch(facecolor='#fdb462')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, intensity=0.4, label=None):
        """Construct a CCGT generator."""
        Fossil.__init__(self, polygon, capacity, intensity, label)

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        fuel_cost = costs.gas_price_per_gj * 6.92
        total_opcost = vom + fuel_cost + self.intensity * costs.carbon
        return total_opcost


class CCS(Fossil):
    """Base class of carbon capture and storage (CCS)."""

    def __init__(self, polygon, capacity, intensity, capture, label=None):
        """Construct a CCS generator.

        Emissions capture rate is given in the range 0 to 1.
        """
        Fossil.__init__(self, polygon, capacity, intensity, label)
        assert 0 <= capture <= 1
        self.capture = capture

    def summary(self, context):
        """Return a summary of the generator activity."""
        generation = sum(self.series_power.values()) * ureg.MWh
        emissions = generation * self.intensity * (ureg.t / ureg.MWh)
        captured = emissions * self.capture
        return Fossil.summary(self, context) + \
            f', {captured.to("Mt")} captured'


class Coal_CCS(CCS):
    """Coal with CCS."""

    def __init__(self, polygon, capacity, intensity=0.8, capture=0.85,
                 label=None):
        """Construct a coal CCS generator.

        Emissions capture rate is given in the range 0 to 1.
        """
        CCS.__init__(self, polygon, capacity, intensity, capture, label)

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        # thermal efficiency 31.4% (AETA 2012)
        fuel_cost = costs.coal_price_per_gj * (3.6 / 0.314)
        # t CO2/MWh
        emissions_rate = 0.103
        total_opcost = vom + fuel_cost + \
            (emissions_rate * costs.carbon) + \
            (self.intensity * self.capture * costs.ccs_storage_per_t)
        return total_opcost


class CCGT_CCS(CCS):
    """CCGT with CCS."""

    def __init__(self, polygon, capacity, intensity=0.4, capture=0.85,
                 label=None):
        """Construct a CCGT (with CCS) generator.

        Emissions capture rate is given in the range 0 to 1.
        """
        CCS.__init__(self, polygon, capacity, intensity, capture, label)

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        # thermal efficiency 43.1% (AETA 2012)
        fuel_cost = costs.gas_price_per_gj * (3.6 / 0.431)
        total_opcost = vom + fuel_cost + \
            (self.intensity * (1 - self.capture) * costs.carbon) + \
            (self.intensity * self.capture * costs.ccs_storage_per_t)
        return total_opcost


class Diesel(Fossil):
    """Diesel genset model."""

    patch = Patch(facecolor='#f35020')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, intensity=1.0, kwh_per_litre=3.3,
                 label=None):
        """Construct a diesel generator."""
        Fossil.__init__(self, polygon, capacity, intensity, label)
        self.kwh_per_litre = kwh_per_litre

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        litres_per_mwh = (1 / self.kwh_per_litre) * 1000
        fuel_cost = costs.diesel_price_per_litre * litres_per_mwh
        total_opcost = vom + fuel_cost + self.intensity * costs.carbon
        return total_opcost


class BatteryLoad(Storage, Generator):
    """Battery storage (load side)."""

    patch = Patch(facecolor='#b2daef')
    """Colour for plotting"""
    synchronous_p = False
    """Is this a synchronous generator?"""

    def __init__(self, polygon, capacity, battery, label=None,
                 discharge_hours=None, rte=0.95):
        """
        Construct a battery load (battery charging).

        battery must be an instance of storage.BatteryStorage.
        discharge_hours is a list of hours when discharging can occur
          (or, rather, when charging cannot occur).
        """
        Storage.__init__(self)
        Generator.__init__(self, polygon, capacity, label)
        if not isinstance(battery, storage.BatteryStorage):
            raise TypeError
        self.battery = battery
        self.rte = rte
        shours = battery.maxstorage / capacity
        assert shours in [1, 2, 4, 8]
        self.discharge_hours = discharge_hours \
            if discharge_hours is not None else range(18, 24)

    def step(self, hour, demand):
        """Return 0 as this is not a generator."""
        return 0, 0

    def store(self, hour, power):
        """Store power."""
        assert power > 0, f'{power} is <= 0'

        if self.battery.full_p() or \
           hour % 24 in self.discharge_hours:
            return 0

        power = min(self.charge_capacity(self, hour), power,
                    self.capacity)
        stored = self.battery.charge(power * self.rte)
        if power > 0:
            self.record(hour, stored / self.rte)
        return stored / self.rte

    def reset(self):
        """Reset the generator."""
        Generator.reset(self)
        Storage.reset(self)
        self.battery.reset()

    def series(self):
        """Return the combined series."""
        dict1 = Generator.series(self)
        dict2 = Storage.series(self)
        dict1.update(dict2)
        return dict1

    def soc(self):
        """Return the battery SOC (state of charge)."""
        return self.battery.soc()

    # Battery costs are all calculated on the discharge side.
    def capcost(self, costs):
        """Return the capital cost."""
        return 0

    def fixed_om_costs(self, costs):
        """Return the fixed O&M costs."""
        return 0

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        return 0

    def summary(self, context):
        """Return a summary of the generator activity."""
        mwh = self.battery.maxstorage * ureg.MWh
        return Generator.summary(self, context) + \
            f', charged {thousands(len(self.series_charge))} hours' + \
            f', {mwh.to_compact()} storage'


class Battery(Generator):
    """Battery storage (of any type)."""

    patch = Patch(facecolor='#00a2fa')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, battery, label=None,
                 discharge_hours=None):
        """
        Construct a battery generator.

        battery must be an instance of storage.BatteryStorage.
        discharge_hours is a list of hours when discharging can occur.
        """
        if not isinstance(battery, storage.BatteryStorage):
            raise TypeError
        Generator.__init__(self, polygon, capacity, label)
        self.battery = battery
        self.runhours = 0
        self.discharge_hours = discharge_hours \
            if discharge_hours is not None else range(18, 24)

    def step(self, hour, demand):
        """Specialised step method for batteries."""
        if self.battery.empty_p() or \
           hour % 24 not in self.discharge_hours:
            self.series_power[hour] = 0
            self.series_spilled[hour] = 0
            return 0, 0

        power = min(self.battery.storage, self.capacity, demand)
        self.battery.discharge(power)
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        if power > 0:
            self.runhours += 1
        return power, 0

    def reset(self):
        """Reset the generator."""
        Generator.reset(self)
        self.battery.reset()

    def soc(self):
        """Return the battery SOC (state of charge)."""
        return self.battery.soc()

    def capcost(self, costs):
        """Return the capital cost."""
        kwh = self.battery.maxstorage * 1000
        shours = self.battery.maxstorage / self.capacity
        assert shours in [1, 2, 4, 8]
        cost_per_kwh = costs.totcost_per_kwh[type(self)][shours]
        return kwh * cost_per_kwh

    def fixed_om_costs(self, costs):
        """Return the fixed O&M costs."""
        return 0

    def opcost_per_mwh(self, costs):
        """
        Return the variable O&M costs.

        Per-kWh costs for batteries are included in the capital cost.
        """
        return 0

    def summary(self, context):
        """Return a summary of the generator activity."""
        return Generator.summary(self, context) + \
            f', ran {thousands(self.runhours)} hours'


class Geothermal(CSVTraceGenerator):
    """Geothermal power plant."""

    patch = Patch(facecolor='indianred')
    """Colour for plotting"""

    def step(self, hour, demand):
        """Specialised step method for geothermal generators.

        Geothermal power plants do not spill.
        """
        generation = self.generation[hour] * self.capacity
        power = min(generation, demand)
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        return power, 0


class Geothermal_HSA(Geothermal):
    """Hot sedimentary aquifer (HSA) geothermal model."""


class Geothermal_EGS(Geothermal):
    """Enhanced geothermal systems (EGS) geothermal model."""


class DemandResponse(Generator):
    """
    Load shedding generator.

    >>> dr = DemandResponse(polygons.WILDCARD, 500, 1500)
    """

    patch = Patch(facecolor='white')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, cost_per_mwh, label=None):
        """
        Construct a demand response 'generator'.

        The demand response opportunity cost is given by
        cost_per_mwh. There is assumed to be no capital cost.
        """
        Generator.__init__(self, polygon, capacity, label)
        self.setters = []
        self.runhours = 0
        self.maxresponse = 0
        self.cost_per_mwh = cost_per_mwh

    def step(self, hour, demand):
        """
        Specialised step method for demand response.

        >>> dr = DemandResponse(polygons.WILDCARD, 500, 1500)
        >>> dr.step(hour=0, demand=200)
        (200, 0)
        >>> dr.runhours
        1
        """
        power = min(self.capacity, demand)
        self.maxresponse = max(self.maxresponse, power)
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        if power > 0:
            self.runhours += 1
        return power, 0

    def reset(self):
        """Reset the generator."""
        Generator.reset(self)
        self.runhours = 0
        self.maxresponse = 0

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        return self.cost_per_mwh

    def summary(self, context):
        """Return a summary of the generator activity."""
        return Generator.summary(self, context) + \
            f', max response {self.maxresponse} MW' + \
            f', ran {thousands(self.runhours)} hours'


class Block(Generator):
    """A simple block generator."""

    patch = Patch(facecolor='darkgreen')
    """Colour for plotting"""

    def step(self, hour, demand):
        """Step method for GreenPower."""
        power = min(self.capacity, demand)
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        return power, 0


class Electrolyser(Storage, Generator):
    """A hydrogen electrolyser."""

    patch = Patch(facecolor='teal')
    """Colour for plotting"""

    def __init__(self, tank, polygon, capacity, efficiency=0.8, label=None):
        """
        Construct a hydrogen electrolyser.

        Arguments include the associated storage vessel (the 'tank'),
        the capacity of the electrolyser (in MW) and electrolysis
        conversion efficiency.
        """
        if not isinstance(tank, storage.HydrogenStorage):
            raise TypeError
        Storage.__init__(self)
        Generator.__init__(self, polygon, capacity, label)
        self.efficiency = efficiency
        self.tank = tank
        self.setters += [(self.tank.set_storage, 0, 10000)]

    def soc(self):
        """Return the hydrogen tank state of charge (SOC)."""
        return self.tank.soc()

    def series(self):
        """Return the combined series."""
        dict1 = Generator.series(self)
        dict2 = Storage.series(self)
        dict1.update(dict2)
        return dict1

    def step(self, hour, demand):
        """Return 0 as this is not a generator."""
        return 0, 0

    def reset(self):
        """Reset the generator."""
        Storage.reset(self)
        Generator.reset(self)

    def store(self, _, power):
        """Store power."""
        power = min(power, self.capacity)
        stored = self.tank.charge(power * self.efficiency)
        return stored / self.efficiency


class HydrogenGT(Fuelled):
    """A combustion turbine fuelled by hydrogen."""

    patch = Patch(facecolor='violet')
    """Colour for plotting"""

    def __init__(self, tank, polygon, capacity, efficiency=0.36, label=None):
        """
        Construct a HydrogenGT object.

        >>> h = storage.HydrogenStorage(1000, 'test')
        >>> gt = HydrogenGT(h, 1, 100, efficiency=0.5)
        >>> print(gt)
        HydrogenGT (QLD1:1), 100.00 MW
        >>> gt.step(0, 100) # discharge 100 MWh-e of hydrogen
        (100.0, 0)
        >>> gt.step(0, 100) # discharge another 100 MWh-e of hydrogen
        (100.0, 0)
        >>> h.storage == (1000 / 2.) - (200 / gt.efficiency)
        True
        """
        assert isinstance(tank, storage.HydrogenStorage)
        Fuelled.__init__(self, polygon, capacity, label)
        self.tank = tank
        self.efficiency = efficiency

    def step(self, hour, demand):
        """Step method for hydrogen comubstion turbine generators."""
        # calculate hydrogen requirement
        hydrogen = min(self.capacity, demand) / self.efficiency
        # discharge that amount of hydrogen
        power = self.tank.discharge(hydrogen) * self.efficiency
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        if power > 0:
            self.runhours += 1
        return power, 0

    def capcost(self, costs):
        """Return the capital cost (of an OCGT)."""
        return costs.capcost_per_kw[OCGT] * self.capacity * 1000

    def fixed_om_costs(self, costs):
        """Return the fixed O&M costs (of an OCGT)."""
        return costs.fixed_om_costs[OCGT] * self.capacity * 1000

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs (of an OCGT)."""
        return costs.opcost_per_mwh[OCGT]

Classes

class Battery (polygon, capacity, battery, label=None, discharge_hours=None)

Battery storage (of any type).

Construct a battery generator.

battery must be an instance of storage.BatteryStorage. discharge_hours is a list of hours when discharging can occur.

Expand source code
class Battery(Generator):
    """Battery storage (of any type)."""

    patch = Patch(facecolor='#00a2fa')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, battery, label=None,
                 discharge_hours=None):
        """
        Construct a battery generator.

        battery must be an instance of storage.BatteryStorage.
        discharge_hours is a list of hours when discharging can occur.
        """
        if not isinstance(battery, storage.BatteryStorage):
            raise TypeError
        Generator.__init__(self, polygon, capacity, label)
        self.battery = battery
        self.runhours = 0
        self.discharge_hours = discharge_hours \
            if discharge_hours is not None else range(18, 24)

    def step(self, hour, demand):
        """Specialised step method for batteries."""
        if self.battery.empty_p() or \
           hour % 24 not in self.discharge_hours:
            self.series_power[hour] = 0
            self.series_spilled[hour] = 0
            return 0, 0

        power = min(self.battery.storage, self.capacity, demand)
        self.battery.discharge(power)
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        if power > 0:
            self.runhours += 1
        return power, 0

    def reset(self):
        """Reset the generator."""
        Generator.reset(self)
        self.battery.reset()

    def soc(self):
        """Return the battery SOC (state of charge)."""
        return self.battery.soc()

    def capcost(self, costs):
        """Return the capital cost."""
        kwh = self.battery.maxstorage * 1000
        shours = self.battery.maxstorage / self.capacity
        assert shours in [1, 2, 4, 8]
        cost_per_kwh = costs.totcost_per_kwh[type(self)][shours]
        return kwh * cost_per_kwh

    def fixed_om_costs(self, costs):
        """Return the fixed O&M costs."""
        return 0

    def opcost_per_mwh(self, costs):
        """
        Return the variable O&M costs.

        Per-kWh costs for batteries are included in the capital cost.
        """
        return 0

    def summary(self, context):
        """Return a summary of the generator activity."""
        return Generator.summary(self, context) + \
            f', ran {thousands(self.runhours)} hours'

Ancestors

Class variables

var patch

Colour for plotting

Methods

def opcost_per_mwh(self, costs)

Return the variable O&M costs.

Per-kWh costs for batteries are included in the capital cost.

Expand source code
def opcost_per_mwh(self, costs):
    """
    Return the variable O&M costs.

    Per-kWh costs for batteries are included in the capital cost.
    """
    return 0
def soc(self)

Return the battery SOC (state of charge).

Expand source code
def soc(self):
    """Return the battery SOC (state of charge)."""
    return self.battery.soc()
def step(self, hour, demand)

Specialised step method for batteries.

Expand source code
def step(self, hour, demand):
    """Specialised step method for batteries."""
    if self.battery.empty_p() or \
       hour % 24 not in self.discharge_hours:
        self.series_power[hour] = 0
        self.series_spilled[hour] = 0
        return 0, 0

    power = min(self.battery.storage, self.capacity, demand)
    self.battery.discharge(power)
    self.series_power[hour] = power
    self.series_spilled[hour] = 0
    if power > 0:
        self.runhours += 1
    return power, 0

Inherited members

class BatteryLoad (polygon, capacity, battery, label=None, discharge_hours=None, rte=0.95)

Battery storage (load side).

Construct a battery load (battery charging).

battery must be an instance of storage.BatteryStorage. discharge_hours is a list of hours when discharging can occur (or, rather, when charging cannot occur).

Expand source code
class BatteryLoad(Storage, Generator):
    """Battery storage (load side)."""

    patch = Patch(facecolor='#b2daef')
    """Colour for plotting"""
    synchronous_p = False
    """Is this a synchronous generator?"""

    def __init__(self, polygon, capacity, battery, label=None,
                 discharge_hours=None, rte=0.95):
        """
        Construct a battery load (battery charging).

        battery must be an instance of storage.BatteryStorage.
        discharge_hours is a list of hours when discharging can occur
          (or, rather, when charging cannot occur).
        """
        Storage.__init__(self)
        Generator.__init__(self, polygon, capacity, label)
        if not isinstance(battery, storage.BatteryStorage):
            raise TypeError
        self.battery = battery
        self.rte = rte
        shours = battery.maxstorage / capacity
        assert shours in [1, 2, 4, 8]
        self.discharge_hours = discharge_hours \
            if discharge_hours is not None else range(18, 24)

    def step(self, hour, demand):
        """Return 0 as this is not a generator."""
        return 0, 0

    def store(self, hour, power):
        """Store power."""
        assert power > 0, f'{power} is <= 0'

        if self.battery.full_p() or \
           hour % 24 in self.discharge_hours:
            return 0

        power = min(self.charge_capacity(self, hour), power,
                    self.capacity)
        stored = self.battery.charge(power * self.rte)
        if power > 0:
            self.record(hour, stored / self.rte)
        return stored / self.rte

    def reset(self):
        """Reset the generator."""
        Generator.reset(self)
        Storage.reset(self)
        self.battery.reset()

    def series(self):
        """Return the combined series."""
        dict1 = Generator.series(self)
        dict2 = Storage.series(self)
        dict1.update(dict2)
        return dict1

    def soc(self):
        """Return the battery SOC (state of charge)."""
        return self.battery.soc()

    # Battery costs are all calculated on the discharge side.
    def capcost(self, costs):
        """Return the capital cost."""
        return 0

    def fixed_om_costs(self, costs):
        """Return the fixed O&M costs."""
        return 0

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        return 0

    def summary(self, context):
        """Return a summary of the generator activity."""
        mwh = self.battery.maxstorage * ureg.MWh
        return Generator.summary(self, context) + \
            f', charged {thousands(len(self.series_charge))} hours' + \
            f', {mwh.to_compact()} storage'

Ancestors

Class variables

var patch

Colour for plotting

Methods

def reset(self)

Reset the generator.

Expand source code
def reset(self):
    """Reset the generator."""
    Generator.reset(self)
    Storage.reset(self)
    self.battery.reset()
def series(self)

Return the combined series.

Expand source code
def series(self):
    """Return the combined series."""
    dict1 = Generator.series(self)
    dict2 = Storage.series(self)
    dict1.update(dict2)
    return dict1
def soc(self)

Return the battery SOC (state of charge).

Expand source code
def soc(self):
    """Return the battery SOC (state of charge)."""
    return self.battery.soc()
def step(self, hour, demand)

Return 0 as this is not a generator.

Expand source code
def step(self, hour, demand):
    """Return 0 as this is not a generator."""
    return 0, 0
def store(self, hour, power)

Store power.

Expand source code
def store(self, hour, power):
    """Store power."""
    assert power > 0, f'{power} is <= 0'

    if self.battery.full_p() or \
       hour % 24 in self.discharge_hours:
        return 0

    power = min(self.charge_capacity(self, hour), power,
                self.capacity)
    stored = self.battery.charge(power * self.rte)
    if power > 0:
        self.record(hour, stored / self.rte)
    return stored / self.rte

Inherited members

class Behind_Meter_PV (polygon, capacity, filename, column, label=None, build_limit=None)

Behind the meter PV.

Construct a generator with a specified trace file.

Expand source code
class Behind_Meter_PV(PV):
    """Behind the meter PV."""

    patch = Patch(facecolor='#ffe03d')

Ancestors

Class variables

var patch

Inherited members

class Biofuel (polygon, capacity, label=None)

Model of open cycle gas turbines burning biofuel.

Construct a biofuel generator.

Expand source code
class Biofuel(Fuelled):
    """Model of open cycle gas turbines burning biofuel."""

    patch = Patch(facecolor='wheat')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, label=None):
        """Construct a biofuel generator."""
        Fuelled.__init__(self, polygon, capacity, label)

    def capcost(self, costs):
        """Return the capital cost (of an OCGT)."""
        return costs.capcost_per_kw[OCGT] * self.capacity * 1000

    def fixed_om_costs(self, costs):
        """Return the fixed O&M costs (of an OCGT)."""
        return costs.fixed_om_costs[OCGT] * self.capacity * 1000

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[OCGT]
        fuel_cost = costs.bioenergy_price_per_gj * (3.6 / .31)  # 31% heat rate
        return vom + fuel_cost

Ancestors

Class variables

var patch

Colour for plotting

Methods

def capcost(self, costs)

Return the capital cost (of an OCGT).

Expand source code
def capcost(self, costs):
    """Return the capital cost (of an OCGT)."""
    return costs.capcost_per_kw[OCGT] * self.capacity * 1000
def fixed_om_costs(self, costs)

Return the fixed O&M costs (of an OCGT).

Expand source code
def fixed_om_costs(self, costs):
    """Return the fixed O&M costs (of an OCGT)."""
    return costs.fixed_om_costs[OCGT] * self.capacity * 1000

Inherited members

class Biomass (polygon, capacity, label=None, heatrate=0.3)

Model of steam turbine burning solid biomass.

Construct a biomass generator.

Expand source code
class Biomass(Fuelled):
    """Model of steam turbine burning solid biomass."""

    patch = Patch(facecolor='#1d7a7a')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, label=None, heatrate=0.3):
        """Construct a biomass generator."""
        Fuelled.__init__(self, polygon, capacity, label)
        self.heatrate = heatrate

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        fuel_cost = costs.bioenergy_price_per_gj * (3.6 / self.heatrate)
        return vom + fuel_cost

Ancestors

Class variables

var patch

Colour for plotting

Inherited members

class Black_Coal (polygon, capacity, intensity=0.773, label=None)

Black coal power stations with no CCS.

Construct a black coal generator.

Expand source code
class Black_Coal(Fossil):
    """Black coal power stations with no CCS."""

    patch = Patch(facecolor='#121212')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, intensity=0.773, label=None):
        """Construct a black coal generator."""
        Fossil.__init__(self, polygon, capacity, intensity, label)

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        fuel_cost = costs.coal_price_per_gj * 8.57
        total_opcost = vom + fuel_cost + self.intensity * costs.carbon
        return total_opcost

Ancestors

Inherited members

class Block (polygon, capacity, label=None)

A simple block generator.

Construct a base Generator.

Arguments: installed polygon, installed capacity, descriptive label.

Expand source code
class Block(Generator):
    """A simple block generator."""

    patch = Patch(facecolor='darkgreen')
    """Colour for plotting"""

    def step(self, hour, demand):
        """Step method for GreenPower."""
        power = min(self.capacity, demand)
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        return power, 0

Ancestors

Class variables

var patch

Colour for plotting

Methods

def step(self, hour, demand)

Step method for GreenPower.

Expand source code
def step(self, hour, demand):
    """Step method for GreenPower."""
    power = min(self.capacity, demand)
    self.series_power[hour] = power
    self.series_spilled[hour] = 0
    return power, 0

Inherited members

class CCGT (polygon, capacity, intensity=0.4, label=None)

Combined cycle gas turbine (CCGT) model.

Construct a CCGT generator.

Expand source code
class CCGT(Fossil):
    """Combined cycle gas turbine (CCGT) model."""

    patch = Patch(facecolor='#fdb462')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, intensity=0.4, label=None):
        """Construct a CCGT generator."""
        Fossil.__init__(self, polygon, capacity, intensity, label)

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        fuel_cost = costs.gas_price_per_gj * 6.92
        total_opcost = vom + fuel_cost + self.intensity * costs.carbon
        return total_opcost

Ancestors

Inherited members

class CCGT_CCS (polygon, capacity, intensity=0.4, capture=0.85, label=None)

CCGT with CCS.

Construct a CCGT (with CCS) generator.

Emissions capture rate is given in the range 0 to 1.

Expand source code
class CCGT_CCS(CCS):
    """CCGT with CCS."""

    def __init__(self, polygon, capacity, intensity=0.4, capture=0.85,
                 label=None):
        """Construct a CCGT (with CCS) generator.

        Emissions capture rate is given in the range 0 to 1.
        """
        CCS.__init__(self, polygon, capacity, intensity, capture, label)

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        # thermal efficiency 43.1% (AETA 2012)
        fuel_cost = costs.gas_price_per_gj * (3.6 / 0.431)
        total_opcost = vom + fuel_cost + \
            (self.intensity * (1 - self.capture) * costs.carbon) + \
            (self.intensity * self.capture * costs.ccs_storage_per_t)
        return total_opcost

Ancestors

Inherited members

class CCS (polygon, capacity, intensity, capture, label=None)

Base class of carbon capture and storage (CCS).

Construct a CCS generator.

Emissions capture rate is given in the range 0 to 1.

Expand source code
class CCS(Fossil):
    """Base class of carbon capture and storage (CCS)."""

    def __init__(self, polygon, capacity, intensity, capture, label=None):
        """Construct a CCS generator.

        Emissions capture rate is given in the range 0 to 1.
        """
        Fossil.__init__(self, polygon, capacity, intensity, label)
        assert 0 <= capture <= 1
        self.capture = capture

    def summary(self, context):
        """Return a summary of the generator activity."""
        generation = sum(self.series_power.values()) * ureg.MWh
        emissions = generation * self.intensity * (ureg.t / ureg.MWh)
        captured = emissions * self.capture
        return Fossil.summary(self, context) + \
            f', {captured.to("Mt")} captured'

Ancestors

Subclasses

Inherited members

class CST (polygon, capacity, solarmult, shours, filename, column, label=None, build_limit=None)

Concentrating solar thermal (CST) model.

Construct a CST generator.

Arguments include capacity (in MW), sm (solar multiple) and shours (hours of storage).

Expand source code
class CST(CSVTraceGenerator):
    """Concentrating solar thermal (CST) model."""

    patch = Patch(facecolor='orange')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, solarmult, shours, filename,
                 column, label=None, build_limit=None):
        """
        Construct a CST generator.

        Arguments include capacity (in MW), sm (solar multiple) and
        shours (hours of storage).
        """
        CSVTraceGenerator.__init__(self, polygon, capacity, filename, column,
                                   label)
        self.maxstorage = None
        self.stored = None
        self.set_storage(shours)
        self.set_multiple(solarmult)

    def set_capacity(self, cap):
        """Change the capacity of the generator to cap GW."""
        Generator.set_capacity(self, cap)
        self.maxstorage = self.capacity * self.shours

    def set_multiple(self, solarmult):
        """Change the solar multiple of a CST plant."""
        self.solarmult = solarmult

    def set_storage(self, shours):
        """Change the storage capacity of a CST plant."""
        self.shours = shours
        self.maxstorage = self.capacity * shours
        self.stored = 0.5 * self.maxstorage

    def step(self, hour, demand):
        """Step method for CST generators."""
        generation = self.generation[hour] * self.capacity * self.solarmult
        remainder = min(self.capacity, demand)
        if generation > remainder:
            to_storage = generation - remainder
            generation -= to_storage
            self.stored += to_storage
            self.stored = min(self.stored, self.maxstorage)
        else:
            from_storage = min(remainder - generation, self.stored)
            generation += from_storage
            self.stored -= from_storage
            assert self.stored >= 0
        assert self.stored <= self.maxstorage
        assert self.stored >= 0
        self.series_power[hour] = generation
        self.series_spilled[hour] = 0

        # This can happen due to rounding errors.
        generation = min(generation, demand)
        return generation, 0

    def reset(self):
        """Reset the generator."""
        Generator.reset(self)
        self.stored = 0.5 * self.maxstorage

    def summary(self, context):
        """Return a summary of the generator activity."""
        return Generator.summary(self, context) + \
            f', solar mult {self.solarmult:.2f}' + \
            f', {self.shours}h storage'

Ancestors

Subclasses

Class variables

var patch

Colour for plotting

Methods

def set_multiple(self, solarmult)

Change the solar multiple of a CST plant.

Expand source code
def set_multiple(self, solarmult):
    """Change the solar multiple of a CST plant."""
    self.solarmult = solarmult
def set_storage(self, shours)

Change the storage capacity of a CST plant.

Expand source code
def set_storage(self, shours):
    """Change the storage capacity of a CST plant."""
    self.shours = shours
    self.maxstorage = self.capacity * shours
    self.stored = 0.5 * self.maxstorage
def step(self, hour, demand)

Step method for CST generators.

Expand source code
def step(self, hour, demand):
    """Step method for CST generators."""
    generation = self.generation[hour] * self.capacity * self.solarmult
    remainder = min(self.capacity, demand)
    if generation > remainder:
        to_storage = generation - remainder
        generation -= to_storage
        self.stored += to_storage
        self.stored = min(self.stored, self.maxstorage)
    else:
        from_storage = min(remainder - generation, self.stored)
        generation += from_storage
        self.stored -= from_storage
        assert self.stored >= 0
    assert self.stored <= self.maxstorage
    assert self.stored >= 0
    self.series_power[hour] = generation
    self.series_spilled[hour] = 0

    # This can happen due to rounding errors.
    generation = min(generation, demand)
    return generation, 0

Inherited members

class CSVTraceGenerator (polygon, capacity, filename, column, label=None, build_limit=None)

A generator that gets its hourly dispatch from a CSV trace file.

Construct a generator with a specified trace file.

Expand source code
class CSVTraceGenerator(TraceGenerator):
    """A generator that gets its hourly dispatch from a CSV trace file."""

    csvfilename = None
    csvdata = None

    def __init__(self, polygon, capacity, filename, column, label=None,
                 build_limit=None):
        """Construct a generator with a specified trace file."""
        TraceGenerator.__init__(self, polygon, capacity, label, build_limit)
        cls = self.__class__
        if cls.csvfilename != filename:
            # Optimisation:
            # Only if the filename changes do we invoke genfromtxt.
            if not filename.startswith('http'):
                # Local file path
                traceinput = filename
            else:
                try:
                    resp = requests.request('GET', filename, timeout=5)
                except requests.exceptions.Timeout as exc:
                    raise TimeoutError(f'timeout fetching {filename}') from exc
                if not resp.ok:
                    msg = f'HTTP {resp.status_code}: {filename}'
                    raise ConnectionError(msg)
                traceinput = resp.text.splitlines()
            cls.csvdata = np.genfromtxt(traceinput, encoding='UTF-8',
                                        delimiter=',')
            cls.csvdata = np.maximum(0, cls.csvdata)
            # check all elements are not NaNs
            assert np.all(~np.isnan(cls.csvdata)), \
                f'Trace file {filename} contains NaNs; inspect file'
            cls.csvfilename = filename
        self.generation = cls.csvdata[::, column]

Ancestors

Subclasses

Class variables

var csvdata
var csvfilename

Inherited members

class CentralReceiver (polygon, capacity, solarmult, shours, filename, column, label=None, build_limit=None)

Central receiver CST generator.

This stub class allows differentiated CST costs in costs.py.

Construct a CST generator.

Arguments include capacity (in MW), sm (solar multiple) and shours (hours of storage).

Expand source code
class CentralReceiver(CST):
    """Central receiver CST generator.

    This stub class allows differentiated CST costs in costs.py.
    """

Ancestors

Inherited members

class Coal_CCS (polygon, capacity, intensity=0.8, capture=0.85, label=None)

Coal with CCS.

Construct a coal CCS generator.

Emissions capture rate is given in the range 0 to 1.

Expand source code
class Coal_CCS(CCS):
    """Coal with CCS."""

    def __init__(self, polygon, capacity, intensity=0.8, capture=0.85,
                 label=None):
        """Construct a coal CCS generator.

        Emissions capture rate is given in the range 0 to 1.
        """
        CCS.__init__(self, polygon, capacity, intensity, capture, label)

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        # thermal efficiency 31.4% (AETA 2012)
        fuel_cost = costs.coal_price_per_gj * (3.6 / 0.314)
        # t CO2/MWh
        emissions_rate = 0.103
        total_opcost = vom + fuel_cost + \
            (emissions_rate * costs.carbon) + \
            (self.intensity * self.capture * costs.ccs_storage_per_t)
        return total_opcost

Ancestors

Inherited members

class DemandResponse (polygon, capacity, cost_per_mwh, label=None)

Load shedding generator.

>>> dr = DemandResponse(polygons.WILDCARD, 500, 1500)

Construct a demand response 'generator'.

The demand response opportunity cost is given by cost_per_mwh. There is assumed to be no capital cost.

Expand source code
class DemandResponse(Generator):
    """
    Load shedding generator.

    >>> dr = DemandResponse(polygons.WILDCARD, 500, 1500)
    """

    patch = Patch(facecolor='white')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, cost_per_mwh, label=None):
        """
        Construct a demand response 'generator'.

        The demand response opportunity cost is given by
        cost_per_mwh. There is assumed to be no capital cost.
        """
        Generator.__init__(self, polygon, capacity, label)
        self.setters = []
        self.runhours = 0
        self.maxresponse = 0
        self.cost_per_mwh = cost_per_mwh

    def step(self, hour, demand):
        """
        Specialised step method for demand response.

        >>> dr = DemandResponse(polygons.WILDCARD, 500, 1500)
        >>> dr.step(hour=0, demand=200)
        (200, 0)
        >>> dr.runhours
        1
        """
        power = min(self.capacity, demand)
        self.maxresponse = max(self.maxresponse, power)
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        if power > 0:
            self.runhours += 1
        return power, 0

    def reset(self):
        """Reset the generator."""
        Generator.reset(self)
        self.runhours = 0
        self.maxresponse = 0

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        return self.cost_per_mwh

    def summary(self, context):
        """Return a summary of the generator activity."""
        return Generator.summary(self, context) + \
            f', max response {self.maxresponse} MW' + \
            f', ran {thousands(self.runhours)} hours'

Ancestors

Class variables

var patch

Colour for plotting

Methods

def step(self, hour, demand)

Specialised step method for demand response.

>>> dr = DemandResponse(polygons.WILDCARD, 500, 1500)
>>> dr.step(hour=0, demand=200)
(200, 0)
>>> dr.runhours
1
Expand source code
def step(self, hour, demand):
    """
    Specialised step method for demand response.

    >>> dr = DemandResponse(polygons.WILDCARD, 500, 1500)
    >>> dr.step(hour=0, demand=200)
    (200, 0)
    >>> dr.runhours
    1
    """
    power = min(self.capacity, demand)
    self.maxresponse = max(self.maxresponse, power)
    self.series_power[hour] = power
    self.series_spilled[hour] = 0
    if power > 0:
        self.runhours += 1
    return power, 0

Inherited members

class Diesel (polygon, capacity, intensity=1.0, kwh_per_litre=3.3, label=None)

Diesel genset model.

Construct a diesel generator.

Expand source code
class Diesel(Fossil):
    """Diesel genset model."""

    patch = Patch(facecolor='#f35020')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, intensity=1.0, kwh_per_litre=3.3,
                 label=None):
        """Construct a diesel generator."""
        Fossil.__init__(self, polygon, capacity, intensity, label)
        self.kwh_per_litre = kwh_per_litre

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        litres_per_mwh = (1 / self.kwh_per_litre) * 1000
        fuel_cost = costs.diesel_price_per_litre * litres_per_mwh
        total_opcost = vom + fuel_cost + self.intensity * costs.carbon
        return total_opcost

Ancestors

Inherited members

class Electrolyser (tank, polygon, capacity, efficiency=0.8, label=None)

A hydrogen electrolyser.

Construct a hydrogen electrolyser.

Arguments include the associated storage vessel (the 'tank'), the capacity of the electrolyser (in MW) and electrolysis conversion efficiency.

Expand source code
class Electrolyser(Storage, Generator):
    """A hydrogen electrolyser."""

    patch = Patch(facecolor='teal')
    """Colour for plotting"""

    def __init__(self, tank, polygon, capacity, efficiency=0.8, label=None):
        """
        Construct a hydrogen electrolyser.

        Arguments include the associated storage vessel (the 'tank'),
        the capacity of the electrolyser (in MW) and electrolysis
        conversion efficiency.
        """
        if not isinstance(tank, storage.HydrogenStorage):
            raise TypeError
        Storage.__init__(self)
        Generator.__init__(self, polygon, capacity, label)
        self.efficiency = efficiency
        self.tank = tank
        self.setters += [(self.tank.set_storage, 0, 10000)]

    def soc(self):
        """Return the hydrogen tank state of charge (SOC)."""
        return self.tank.soc()

    def series(self):
        """Return the combined series."""
        dict1 = Generator.series(self)
        dict2 = Storage.series(self)
        dict1.update(dict2)
        return dict1

    def step(self, hour, demand):
        """Return 0 as this is not a generator."""
        return 0, 0

    def reset(self):
        """Reset the generator."""
        Storage.reset(self)
        Generator.reset(self)

    def store(self, _, power):
        """Store power."""
        power = min(power, self.capacity)
        stored = self.tank.charge(power * self.efficiency)
        return stored / self.efficiency

Ancestors

Class variables

var patch

Colour for plotting

Methods

def reset(self)

Reset the generator.

Expand source code
def reset(self):
    """Reset the generator."""
    Storage.reset(self)
    Generator.reset(self)
def series(self)

Return the combined series.

Expand source code
def series(self):
    """Return the combined series."""
    dict1 = Generator.series(self)
    dict2 = Storage.series(self)
    dict1.update(dict2)
    return dict1
def soc(self)

Return the hydrogen tank state of charge (SOC).

Expand source code
def soc(self):
    """Return the hydrogen tank state of charge (SOC)."""
    return self.tank.soc()
def step(self, hour, demand)

Return 0 as this is not a generator.

Expand source code
def step(self, hour, demand):
    """Return 0 as this is not a generator."""
    return 0, 0
def store(self, _, power)

Store power.

Expand source code
def store(self, _, power):
    """Store power."""
    power = min(power, self.capacity)
    stored = self.tank.charge(power * self.efficiency)
    return stored / self.efficiency

Inherited members

class Fossil (polygon, capacity, intensity, label=None)

Base class for GHG emitting power stations.

Construct a fossil fuelled generator.

Greenhouse gas emissions intensity is given in tonnes per MWh.

Expand source code
class Fossil(Fuelled):
    """Base class for GHG emitting power stations."""

    patch = Patch(facecolor='grey')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, intensity, label=None):
        """
        Construct a fossil fuelled generator.

        Greenhouse gas emissions intensity is given in tonnes per MWh.
        """
        Fuelled.__init__(self, polygon, capacity, label)
        self.intensity = intensity

    def summary(self, context):
        """Return a summary of the generator activity."""
        generation = sum(self.series_power.values()) * ureg.MWh
        emissions = generation * self.intensity * (ureg.t / ureg.MWh)
        return Fuelled.summary(self, context) + \
            f', {emissions.to("Mt")} CO2'

Ancestors

Subclasses

Class variables

var patch

Colour for plotting

Inherited members

class Fuelled (polygon, capacity, label)

The class of generators that consume fuel.

Construct a fuelled generator.

Expand source code
class Fuelled(Generator):
    """The class of generators that consume fuel."""

    def __init__(self, polygon, capacity, label):
        """Construct a fuelled generator."""
        Generator.__init__(self, polygon, capacity, label)
        self.runhours = 0

    def reset(self):
        """Reset the generator."""
        Generator.reset(self)
        self.runhours = 0

    def step(self, hour, demand):
        """Step method for fuelled generators."""
        power = min(self.capacity, demand)
        if power > 0:
            self.runhours += 1
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        return power, 0

    def summary(self, context):
        """Return a summary of the generator activity."""
        return Generator.summary(self, context) + \
            f', ran {thousands(self.runhours)} hours'

Ancestors

Subclasses

Methods

def step(self, hour, demand)

Step method for fuelled generators.

Expand source code
def step(self, hour, demand):
    """Step method for fuelled generators."""
    power = min(self.capacity, demand)
    if power > 0:
        self.runhours += 1
    self.series_power[hour] = power
    self.series_spilled[hour] = 0
    return power, 0

Inherited members

class Generator (polygon, capacity, label=None)

Base generator class.

Construct a base Generator.

Arguments: installed polygon, installed capacity, descriptive label.

Expand source code
class Generator():
    """Base generator class."""

    # Is the generator a rotating machine?
    synchronous_p = True
    """Is this a synchronous generator?"""

    storage_p = False
    """A generator is not capable of storage by default."""

    def __init__(self, polygon, capacity, label=None):
        """
        Construct a base Generator.

        Arguments: installed polygon, installed capacity, descriptive label.
        """
        assert capacity >= 0
        self.setters = [(self.set_capacity, 0, 40)]
        self.label = self.__class__.__name__ if label is None else label
        self.capacity = capacity
        self.polygon = polygon

        # Sanity check polygon argument.
        assert not isinstance(polygon, polygons.regions.Region)
        assert 0 < polygon <= polygons.NUMPOLYGONS, polygon

        # Time series of dispatched power and spills
        self.series_power = {}
        self.series_spilled = {}

    def series(self):
        """Return generation and spills series."""
        return {'power': pd.Series(self.series_power, dtype=float),
                'spilled': pd.Series(self.series_spilled, dtype=float)}

    def step(self, hour, demand):
        """Step the generator by one hour."""
        raise NotImplementedError

    def region(self):
        """Return the region the generator is in."""
        return polygons.region(self.polygon)

    def capcost(self, costs):
        """Return the capital cost."""
        return costs.capcost_per_kw[type(self)] * self.capacity * 1000

    def opcost(self, costs):
        """Return the annual operating and maintenance cost."""
        return self.fixed_om_costs(costs) + \
            sum(self.series_power.values()) * self.opcost_per_mwh(costs)

    def fixed_om_costs(self, costs):
        """Return the fixed O&M costs."""
        return costs.fixed_om_costs[type(self)] * self.capacity * 1000

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        return costs.opcost_per_mwh[type(self)]

    def reset(self):
        """Reset the generator."""
        self.series_power.clear()
        self.series_spilled.clear()

    def capfactor(self):
        """Capacity factor of this generator (in %)."""
        supplied = sum(self.series_power.values())
        hours = len(self.series_power)
        try:
            capfactor = supplied / (self.capacity * hours) * 100
            return capfactor
        except ZeroDivisionError:
            return float('nan')

    def lcoe(self, costs, years):
        """Calculate the LCOE in $/MWh."""
        total_cost = self.capcost(costs) / costs.annuityf * years \
            + self.opcost(costs)
        supplied = sum(self.series_power.values())
        if supplied > 0:
            cost_per_mwh = total_cost / supplied
            return cost_per_mwh
        return np.inf

    def summary(self, context):
        """Return a summary of the generator activity."""
        costs = context.costs
        supplied = sum(self.series_power.values()) * ureg.MWh
        string = f'supplied {supplied.to_compact()}'
        if self.capacity > 0:
            if self.capfactor() > 0:
                string += f', CF {self.capfactor():.1f}%'
        if sum(self.series_spilled.values()) > 0:
            spilled = sum(self.series_spilled.values()) * ureg.MWh
            string += f', surplus {spilled.to_compact()}'
        if self.capcost(costs) > 0:
            string += f', capcost {currency(self.capcost(costs))}'
        if self.opcost(costs) > 0:
            string += f', opcost {currency(self.opcost(costs))}'
        lcoe = self.lcoe(costs, context.years())
        if np.isfinite(lcoe) and lcoe > 0:
            string += f', LCOE {currency(int(lcoe))}'
        return string

    def set_capacity(self, cap):
        """Change the capacity of the generator to cap GW."""
        self.capacity = cap * 1000

    def __str__(self):
        """Return a short string representation of the generator."""
        return f'{self.label} ({self.region()}:{self.polygon}), ' + \
            str(self.capacity * ureg.MW)

    def __repr__(self):
        """Return a representation of the generator."""
        return self.__str__()

Subclasses

Class variables

var storage_p

A generator is not capable of storage by default.

var synchronous_p

Is this a synchronous generator?

Methods

def capcost(self, costs)

Return the capital cost.

Expand source code
def capcost(self, costs):
    """Return the capital cost."""
    return costs.capcost_per_kw[type(self)] * self.capacity * 1000
def capfactor(self)

Capacity factor of this generator (in %).

Expand source code
def capfactor(self):
    """Capacity factor of this generator (in %)."""
    supplied = sum(self.series_power.values())
    hours = len(self.series_power)
    try:
        capfactor = supplied / (self.capacity * hours) * 100
        return capfactor
    except ZeroDivisionError:
        return float('nan')
def fixed_om_costs(self, costs)

Return the fixed O&M costs.

Expand source code
def fixed_om_costs(self, costs):
    """Return the fixed O&M costs."""
    return costs.fixed_om_costs[type(self)] * self.capacity * 1000
def lcoe(self, costs, years)

Calculate the LCOE in $/MWh.

Expand source code
def lcoe(self, costs, years):
    """Calculate the LCOE in $/MWh."""
    total_cost = self.capcost(costs) / costs.annuityf * years \
        + self.opcost(costs)
    supplied = sum(self.series_power.values())
    if supplied > 0:
        cost_per_mwh = total_cost / supplied
        return cost_per_mwh
    return np.inf
def opcost(self, costs)

Return the annual operating and maintenance cost.

Expand source code
def opcost(self, costs):
    """Return the annual operating and maintenance cost."""
    return self.fixed_om_costs(costs) + \
        sum(self.series_power.values()) * self.opcost_per_mwh(costs)
def opcost_per_mwh(self, costs)

Return the variable O&M costs.

Expand source code
def opcost_per_mwh(self, costs):
    """Return the variable O&M costs."""
    return costs.opcost_per_mwh[type(self)]
def region(self)

Return the region the generator is in.

Expand source code
def region(self):
    """Return the region the generator is in."""
    return polygons.region(self.polygon)
def reset(self)

Reset the generator.

Expand source code
def reset(self):
    """Reset the generator."""
    self.series_power.clear()
    self.series_spilled.clear()
def series(self)

Return generation and spills series.

Expand source code
def series(self):
    """Return generation and spills series."""
    return {'power': pd.Series(self.series_power, dtype=float),
            'spilled': pd.Series(self.series_spilled, dtype=float)}
def set_capacity(self, cap)

Change the capacity of the generator to cap GW.

Expand source code
def set_capacity(self, cap):
    """Change the capacity of the generator to cap GW."""
    self.capacity = cap * 1000
def step(self, hour, demand)

Step the generator by one hour.

Expand source code
def step(self, hour, demand):
    """Step the generator by one hour."""
    raise NotImplementedError
def summary(self, context)

Return a summary of the generator activity.

Expand source code
def summary(self, context):
    """Return a summary of the generator activity."""
    costs = context.costs
    supplied = sum(self.series_power.values()) * ureg.MWh
    string = f'supplied {supplied.to_compact()}'
    if self.capacity > 0:
        if self.capfactor() > 0:
            string += f', CF {self.capfactor():.1f}%'
    if sum(self.series_spilled.values()) > 0:
        spilled = sum(self.series_spilled.values()) * ureg.MWh
        string += f', surplus {spilled.to_compact()}'
    if self.capcost(costs) > 0:
        string += f', capcost {currency(self.capcost(costs))}'
    if self.opcost(costs) > 0:
        string += f', opcost {currency(self.opcost(costs))}'
    lcoe = self.lcoe(costs, context.years())
    if np.isfinite(lcoe) and lcoe > 0:
        string += f', LCOE {currency(int(lcoe))}'
    return string
class Geothermal (polygon, capacity, filename, column, label=None, build_limit=None)

Geothermal power plant.

Construct a generator with a specified trace file.

Expand source code
class Geothermal(CSVTraceGenerator):
    """Geothermal power plant."""

    patch = Patch(facecolor='indianred')
    """Colour for plotting"""

    def step(self, hour, demand):
        """Specialised step method for geothermal generators.

        Geothermal power plants do not spill.
        """
        generation = self.generation[hour] * self.capacity
        power = min(generation, demand)
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        return power, 0

Ancestors

Subclasses

Class variables

var patch

Colour for plotting

Methods

def step(self, hour, demand)

Specialised step method for geothermal generators.

Geothermal power plants do not spill.

Expand source code
def step(self, hour, demand):
    """Specialised step method for geothermal generators.

    Geothermal power plants do not spill.
    """
    generation = self.generation[hour] * self.capacity
    power = min(generation, demand)
    self.series_power[hour] = power
    self.series_spilled[hour] = 0
    return power, 0

Inherited members

class Geothermal_EGS (polygon, capacity, filename, column, label=None, build_limit=None)

Enhanced geothermal systems (EGS) geothermal model.

Construct a generator with a specified trace file.

Expand source code
class Geothermal_EGS(Geothermal):
    """Enhanced geothermal systems (EGS) geothermal model."""

Ancestors

Inherited members

class Geothermal_HSA (polygon, capacity, filename, column, label=None, build_limit=None)

Hot sedimentary aquifer (HSA) geothermal model.

Construct a generator with a specified trace file.

Expand source code
class Geothermal_HSA(Geothermal):
    """Hot sedimentary aquifer (HSA) geothermal model."""

Ancestors

Inherited members

class Hydro (polygon, capacity, label=None)

Hydro power stations.

Construct a hydroelectric generator.

Expand source code
class Hydro(Fuelled):
    """Hydro power stations."""

    patch = Patch(facecolor='#4582b4')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, label=None):
        """Construct a hydroelectric generator."""
        Fuelled.__init__(self, polygon, capacity, label)
        # capacity is in MW, but build limit is in GW
        self.setters = [(self.set_capacity, 0, capacity / 1000.)]

Ancestors

Subclasses

Class variables

var patch

Colour for plotting

Inherited members

class HydrogenGT (tank, polygon, capacity, efficiency=0.36, label=None)

A combustion turbine fuelled by hydrogen.

Construct a HydrogenGT object.

>>> h = storage.HydrogenStorage(1000, 'test')
>>> gt = HydrogenGT(h, 1, 100, efficiency=0.5)
>>> print(gt)
HydrogenGT (QLD1:1), 100.00 MW
>>> gt.step(0, 100) # discharge 100 MWh-e of hydrogen
(100.0, 0)
>>> gt.step(0, 100) # discharge another 100 MWh-e of hydrogen
(100.0, 0)
>>> h.storage == (1000 / 2.) - (200 / gt.efficiency)
True
Expand source code
class HydrogenGT(Fuelled):
    """A combustion turbine fuelled by hydrogen."""

    patch = Patch(facecolor='violet')
    """Colour for plotting"""

    def __init__(self, tank, polygon, capacity, efficiency=0.36, label=None):
        """
        Construct a HydrogenGT object.

        >>> h = storage.HydrogenStorage(1000, 'test')
        >>> gt = HydrogenGT(h, 1, 100, efficiency=0.5)
        >>> print(gt)
        HydrogenGT (QLD1:1), 100.00 MW
        >>> gt.step(0, 100) # discharge 100 MWh-e of hydrogen
        (100.0, 0)
        >>> gt.step(0, 100) # discharge another 100 MWh-e of hydrogen
        (100.0, 0)
        >>> h.storage == (1000 / 2.) - (200 / gt.efficiency)
        True
        """
        assert isinstance(tank, storage.HydrogenStorage)
        Fuelled.__init__(self, polygon, capacity, label)
        self.tank = tank
        self.efficiency = efficiency

    def step(self, hour, demand):
        """Step method for hydrogen comubstion turbine generators."""
        # calculate hydrogen requirement
        hydrogen = min(self.capacity, demand) / self.efficiency
        # discharge that amount of hydrogen
        power = self.tank.discharge(hydrogen) * self.efficiency
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        if power > 0:
            self.runhours += 1
        return power, 0

    def capcost(self, costs):
        """Return the capital cost (of an OCGT)."""
        return costs.capcost_per_kw[OCGT] * self.capacity * 1000

    def fixed_om_costs(self, costs):
        """Return the fixed O&M costs (of an OCGT)."""
        return costs.fixed_om_costs[OCGT] * self.capacity * 1000

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs (of an OCGT)."""
        return costs.opcost_per_mwh[OCGT]

Ancestors

Class variables

var patch

Colour for plotting

Methods

def capcost(self, costs)

Return the capital cost (of an OCGT).

Expand source code
def capcost(self, costs):
    """Return the capital cost (of an OCGT)."""
    return costs.capcost_per_kw[OCGT] * self.capacity * 1000
def fixed_om_costs(self, costs)

Return the fixed O&M costs (of an OCGT).

Expand source code
def fixed_om_costs(self, costs):
    """Return the fixed O&M costs (of an OCGT)."""
    return costs.fixed_om_costs[OCGT] * self.capacity * 1000
def opcost_per_mwh(self, costs)

Return the variable O&M costs (of an OCGT).

Expand source code
def opcost_per_mwh(self, costs):
    """Return the variable O&M costs (of an OCGT)."""
    return costs.opcost_per_mwh[OCGT]
def step(self, hour, demand)

Step method for hydrogen comubstion turbine generators.

Expand source code
def step(self, hour, demand):
    """Step method for hydrogen comubstion turbine generators."""
    # calculate hydrogen requirement
    hydrogen = min(self.capacity, demand) / self.efficiency
    # discharge that amount of hydrogen
    power = self.tank.discharge(hydrogen) * self.efficiency
    self.series_power[hour] = power
    self.series_spilled[hour] = 0
    if power > 0:
        self.runhours += 1
    return power, 0

Inherited members

class OCGT (polygon, capacity, intensity=0.7, label=None)

Open cycle gas turbine (OCGT) model.

Construct an OCGT generator.

Expand source code
class OCGT(Fossil):
    """Open cycle gas turbine (OCGT) model."""

    patch = Patch(facecolor='#ffcd96')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, intensity=0.7, label=None):
        """Construct an OCGT generator."""
        Fossil.__init__(self, polygon, capacity, intensity, label)

    def opcost_per_mwh(self, costs):
        """Return the variable O&M costs."""
        vom = costs.opcost_per_mwh[type(self)]
        fuel_cost = costs.gas_price_per_gj * 11.61
        total_opcost = vom + fuel_cost + self.intensity * costs.carbon
        return total_opcost

Ancestors

Inherited members

class PV (polygon, capacity, filename, column, label=None, build_limit=None)

Solar photovoltaic (PV) model.

Construct a generator with a specified trace file.

Expand source code
class PV(CSVTraceGenerator):
    """Solar photovoltaic (PV) model."""

    synchronous_p = False
    """Is this a synchronous generator?"""

Ancestors

Subclasses

Inherited members

class PV1Axis (polygon, capacity, filename, column, label=None, build_limit=None)

Single-axis tracking PV.

Construct a generator with a specified trace file.

Expand source code
class PV1Axis(PV):
    """Single-axis tracking PV."""

    patch = Patch(facecolor='#fed500')
    """Colour for plotting"""

Ancestors

Class variables

var patch

Colour for plotting

Inherited members

class ParabolicTrough (polygon, capacity, solarmult, shours, filename, column, label=None, build_limit=None)

Parabolic trough CST generator.

This stub class allows differentiated CST costs in costs.py.

Construct a CST generator.

Arguments include capacity (in MW), sm (solar multiple) and shours (hours of storage).

Expand source code
class ParabolicTrough(CST):
    """Parabolic trough CST generator.

    This stub class allows differentiated CST costs in costs.py.
    """

Ancestors

Inherited members

class PumpedHydroPump (polygon, capacity, reservoirs, rte=0.8, label=None)

Pumped hydro (pump side) model.

Construct a pumped hydro storage generator.

Expand source code
class PumpedHydroPump(Storage, Generator):
    """Pumped hydro (pump side) model."""

    patch = Patch(facecolor='darkblue')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, reservoirs, rte=0.8, label=None):
        """Construct a pumped hydro storage generator."""
        if not isinstance(reservoirs, storage.PumpedHydroStorage):
            raise TypeError
        Storage.__init__(self)
        Generator.__init__(self, polygon, capacity, label)
        self.reservoirs = reservoirs
        self.rte = rte

    def step(self, hour, demand):
        """Return 0 as this is not a generator."""
        return 0, 0

    def series(self):
        """Return the combined series."""
        dict1 = Hydro.series(self)
        dict2 = Storage.series(self)
        dict1.update(dict2)
        return dict1

    def soc(self):
        """Return the pumped hydro SOC (state of charge)."""
        return self.reservoirs.soc()

    def store(self, hour, power):
        """Pump water uphill for one hour."""
        if self.reservoirs.last_gen == hour:
            # Can't pump and generate in the same hour.
            return 0
        power = min(self.charge_capacity(self, hour), power,
                    self.capacity)

        stored = self.reservoirs.charge(power * self.rte)
        if stored < power * self.rte:
            power = (self.reservoirs.maxstorage - self.reservoirs.storage) \
                / self.rte

        if power > 0:
            self.record(hour, power)
            self.reservoirs.last_pump = hour
        return power

    def reset(self):
        """Reset the generator."""
        Generator.reset(self)
        Storage.reset(self)
        self.reservoirs.reset()

    def summary(self, context):
        """Return a summary of the generator activity."""
        stg = (self.reservoirs.maxstorage * ureg.MWh).to_compact()
        return Generator.summary(self, context) + \
            f', charged {thousands(len(self.series_charge))} hours' + \
            f', {stg} storage'

Ancestors

Class variables

var patch

Colour for plotting

Methods

def reset(self)

Reset the generator.

Expand source code
def reset(self):
    """Reset the generator."""
    Generator.reset(self)
    Storage.reset(self)
    self.reservoirs.reset()
def series(self)

Return the combined series.

Expand source code
def series(self):
    """Return the combined series."""
    dict1 = Hydro.series(self)
    dict2 = Storage.series(self)
    dict1.update(dict2)
    return dict1
def soc(self)

Return the pumped hydro SOC (state of charge).

Expand source code
def soc(self):
    """Return the pumped hydro SOC (state of charge)."""
    return self.reservoirs.soc()
def step(self, hour, demand)

Return 0 as this is not a generator.

Expand source code
def step(self, hour, demand):
    """Return 0 as this is not a generator."""
    return 0, 0
def store(self, hour, power)

Pump water uphill for one hour.

Expand source code
def store(self, hour, power):
    """Pump water uphill for one hour."""
    if self.reservoirs.last_gen == hour:
        # Can't pump and generate in the same hour.
        return 0
    power = min(self.charge_capacity(self, hour), power,
                self.capacity)

    stored = self.reservoirs.charge(power * self.rte)
    if stored < power * self.rte:
        power = (self.reservoirs.maxstorage - self.reservoirs.storage) \
            / self.rte

    if power > 0:
        self.record(hour, power)
        self.reservoirs.last_pump = hour
    return power

Inherited members

class PumpedHydroTurbine (polygon, capacity, reservoirs, label=None)

Pumped storage hydro (generator side) model.

Construct a pumped hydro storage generator.

Expand source code
class PumpedHydroTurbine(Hydro):
    """Pumped storage hydro (generator side) model."""

    patch = Patch(facecolor='powderblue')
    """Colour for plotting"""

    def __init__(self, polygon, capacity, reservoirs, label=None):
        """Construct a pumped hydro storage generator."""
        if not isinstance(reservoirs, storage.PumpedHydroStorage):
            raise TypeError
        Hydro.__init__(self, polygon, capacity, label)
        self.reservoirs = reservoirs

    def step(self, hour, demand):
        """Step method for pumped hydro storage."""
        power = min(self.reservoirs.storage, self.capacity, demand)
        if self.reservoirs.last_pump == hour:
            # Can't pump and generate in the same hour.
            self.series_power[hour] = 0
            self.series_spilled[hour] = 0
            return 0, 0

        self.reservoirs.discharge(power)
        self.series_power[hour] = power
        self.series_spilled[hour] = 0
        if power > 0:
            self.runhours += 1
            self.reservoirs.last_gen = hour
        return power, 0

Ancestors

Methods

def step(self, hour, demand)

Step method for pumped hydro storage.

Expand source code
def step(self, hour, demand):
    """Step method for pumped hydro storage."""
    power = min(self.reservoirs.storage, self.capacity, demand)
    if self.reservoirs.last_pump == hour:
        # Can't pump and generate in the same hour.
        self.series_power[hour] = 0
        self.series_spilled[hour] = 0
        return 0, 0

    self.reservoirs.discharge(power)
    self.series_power[hour] = power
    self.series_spilled[hour] = 0
    if power > 0:
        self.runhours += 1
        self.reservoirs.last_gen = hour
    return power, 0

Inherited members

class Storage

A class to give a generator storage capability.

Storage constructor.

Expand source code
class Storage():
    """A class to give a generator storage capability."""

    storage_p = True
    """This generator is capable of storage."""

    def __init__(self):
        """Storage constructor."""
        # Time series of charges
        self.series_charge = {}
        self.series_soc = {}

    def soc(self):
        """Return the storage SOC (state of charge)."""
        raise NotImplementedError

    def record(self, hour, energy):
        """Record storage."""
        if hour not in self.series_charge:
            self.series_charge[hour] = 0
        self.series_charge[hour] += energy
        self.series_soc[hour] = self.soc()

    def charge_capacity(self, gen, hour):
        """Return available storage capacity.

        Since a storage-capable generator can be called on multiple
        times to store energy in a single timestep, we keep track of
        how much remaining capacity is available for charging in the
        given timestep.
        """
        try:
            result = gen.capacity - self.series_charge[hour]
            if result < 0 or isclose(result, 0, abs_tol=1e-6):
                result = 0
            assert result >= 0
            return result
        except KeyError:
            return gen.capacity

    def series(self):
        """Return generation and spills series."""
        return {'charge': pd.Series(self.series_charge, dtype=float),
                'soc': pd.Series(self.series_soc, dtype=float)}

    def store(self, hour, power):
        """Abstract method to ensure that derived classes define this."""
        raise NotImplementedError

    def reset(self):
        """Reset a generator with storage."""
        self.series_charge.clear()
        self.series_soc.clear()

Subclasses

Class variables

var storage_p

This generator is capable of storage.

Methods

def charge_capacity(self, gen, hour)

Return available storage capacity.

Since a storage-capable generator can be called on multiple times to store energy in a single timestep, we keep track of how much remaining capacity is available for charging in the given timestep.

Expand source code
def charge_capacity(self, gen, hour):
    """Return available storage capacity.

    Since a storage-capable generator can be called on multiple
    times to store energy in a single timestep, we keep track of
    how much remaining capacity is available for charging in the
    given timestep.
    """
    try:
        result = gen.capacity - self.series_charge[hour]
        if result < 0 or isclose(result, 0, abs_tol=1e-6):
            result = 0
        assert result >= 0
        return result
    except KeyError:
        return gen.capacity
def record(self, hour, energy)

Record storage.

Expand source code
def record(self, hour, energy):
    """Record storage."""
    if hour not in self.series_charge:
        self.series_charge[hour] = 0
    self.series_charge[hour] += energy
    self.series_soc[hour] = self.soc()
def reset(self)

Reset a generator with storage.

Expand source code
def reset(self):
    """Reset a generator with storage."""
    self.series_charge.clear()
    self.series_soc.clear()
def series(self)

Return generation and spills series.

Expand source code
def series(self):
    """Return generation and spills series."""
    return {'charge': pd.Series(self.series_charge, dtype=float),
            'soc': pd.Series(self.series_soc, dtype=float)}
def soc(self)

Return the storage SOC (state of charge).

Expand source code
def soc(self):
    """Return the storage SOC (state of charge)."""
    raise NotImplementedError
def store(self, hour, power)

Abstract method to ensure that derived classes define this.

Expand source code
def store(self, hour, power):
    """Abstract method to ensure that derived classes define this."""
    raise NotImplementedError
class TraceGenerator (polygon, capacity, label=None, build_limit=None)

A generator that gets its hourly dispatch from a CSV trace file.

Construct a generator with a specified trace file.

Expand source code
class TraceGenerator(Generator):
    """A generator that gets its hourly dispatch from a CSV trace file."""

    csvfilename = None
    csvdata = None

    def __init__(self, polygon, capacity, label=None, build_limit=None):
        """Construct a generator with a specified trace file."""
        Generator.__init__(self, polygon, capacity, label)
        if build_limit is not None:
            # Override default capacity limit with build_limit
            _, _, limit = self.setters[0]
            self.setters = [(self.set_capacity, 0, min(build_limit, limit))]

    def step(self, hour, demand):
        """Step method for any generator using traces."""
        # self.generation must be defined by derived classes
        # pylint: disable=no-member
        generation = self.generation[hour] * self.capacity
        # optimised version of min() because TraceGenerator is a
        # heavily used class
        power = generation if generation < demand else demand
        spilled = generation - power
        self.series_power[hour] = power
        self.series_spilled[hour] = spilled
        return power, spilled

Ancestors

Subclasses

Class variables

var csvdata
var csvfilename

Methods

def step(self, hour, demand)

Step method for any generator using traces.

Expand source code
def step(self, hour, demand):
    """Step method for any generator using traces."""
    # self.generation must be defined by derived classes
    # pylint: disable=no-member
    generation = self.generation[hour] * self.capacity
    # optimised version of min() because TraceGenerator is a
    # heavily used class
    power = generation if generation < demand else demand
    spilled = generation - power
    self.series_power[hour] = power
    self.series_spilled[hour] = spilled
    return power, spilled

Inherited members

class Wind (polygon, capacity, filename, column, label=None, build_limit=None)

Wind power.

Construct a generator with a specified trace file.

Expand source code
class Wind(CSVTraceGenerator):
    """Wind power."""

    patch = Patch(facecolor='#417505')
    """Patch for plotting"""
    synchronous_p = False
    """Is this a synchronous generator?"""

Ancestors

Subclasses

Class variables

var patch

Patch for plotting

Inherited members

class WindOffshore (polygon, capacity, filename, column, label=None, build_limit=None)

Offshore wind power.

Construct a generator with a specified trace file.

Expand source code
class WindOffshore(Wind):
    """Offshore wind power."""

    patch = Patch(facecolor='darkgreen')
    """Colour for plotting"""

Ancestors

Inherited members