Module nemo.context

Implementation of the Context class.

A simulation context encapsulates all simulation state ensuring that there is never any residual state left behind after a simulation run. It also allows multiple contexts to be compared after individual simulation runs.

Classes

class Context
Expand source code
class Context:
    """All simulation state is kept in a Context object."""

    # pylint: disable=too-many-instance-attributes
    def __init__(self):
        """Initialise a default context."""
        self.verbose = False
        self.regions = regions.All
        self.startdate = startdate
        # Number of timesteps is determined by the number of demand rows.
        self.hours = len(hourly_regional_demand)

        self.relstd = 0.002  # 0.002% unserved energy
        self.generators = [generators.CCGT(polygons.WILDCARD, 20000),
                           generators.OCGT(polygons.WILDCARD, 20000)]
        self.storages = None
        self.demand = hourly_demand.copy()
        self.spill = pd.DataFrame()
        self.generation = pd.DataFrame()
        self.unserved = pd.DataFrame()
        # System non-synchronous penetration limit
        self.nsp_limit = float(configfile.get('limits', 'nonsync-penetration'))
        self.costs = costs.NullCosts()

    def years(self):
        """Return the number of years from the number of simulation hours."""
        return self.hours / (365 * 24)

    def timesteps(self):
        """Return the number of timesteps."""
        return len(self.demand)

    def total_demand(self):
        """Return the total demand from the data frame."""
        return self.demand.to_numpy().sum()

    def unserved_energy(self):
        """Return the total unserved energy."""
        return self.unserved.to_numpy().sum()

    def surplus_energy(self):
        """Return total surplus energy."""
        return self.spill.to_numpy().sum()

    def unserved_percent(self):
        """Return the total unserved energy as a percentage of total demand."""
        # We can't catch ZeroDivision because numpy emits a warning
        # (which we would rather not suppress).
        if self.total_demand() == 0:
            return np.nan
        return self.unserved_energy() / self.total_demand() * 100

    def set_capacities(self, caps):
        """Set generator capacities from a list."""
        num = 0
        for gen in self.generators:
            for (setter, min_cap, max_cap) in gen.setters:
                # keep parameters within bounds
                newval = max(min(caps[num], max_cap), min_cap)
                setter(newval)
                num += 1
        # Check every parameter has been set.
        msg = f'{num} != {len(caps)}'
        if num != len(caps):
            raise ValueError(msg)

    def __str__(self):
        """Make a human-readable representation of the context."""
        string = ""
        if self.regions != regions.All:
            string += f'Regions: {self.regions}\n'
        if self.verbose:
            string += 'Generators:' + '\n'
            for gen in self.generators:
                string += f'\t{gen}'
                summary = gen.summary(self)
                if summary is not None:
                    string += f'\n\t   {summary}\n'
                else:
                    string += '\n'
        string += f'Timesteps: {self.hours} h\n'
        total_demand = (self.total_demand() * ureg.MWh).to_compact()
        string += f'Demand energy: {total_demand}\n'
        surplus_energy = (self.surplus_energy() * ureg.MWh).to_compact()
        string += f'Unstored surplus energy: {surplus_energy}\n'
        if self.surplus_energy() > 0:
            spill_series = self.spill[self.spill.sum(axis=1) > 0]
            string += 'Timesteps with unused surplus energy: '
            string += f'{len(spill_series)}\n'

        if self.unserved.empty:
            string += 'No unserved energy'
        else:
            string += f'Unserved energy: {self.unserved_percent():.3f}%\n'
            if self.unserved_percent() > self.relstd * 1.001:
                string += 'WARNING: reliability standard exceeded\n'
            string += f'Unserved total hours: {len(self.unserved)}\n'

            # A subtle trick: generate a date range and then subtract
            # it from the timestamps of unserved events.  This will
            # produce a run of time deltas (for each consecutive hour,
            # the time delta between this timestamp and the
            # corresponding row from the range will be
            # constant). Group by the deltas.
            date_range = pd.date_range(self.unserved.index[0],
                                       periods=len(self.unserved.index),
                                       freq='h')
            deltas = self.unserved.groupby(self.unserved.index - date_range)
            unserved_events = [k for k, g in deltas]
            string += 'Number of unserved energy events: '
            string += f'{len(unserved_events)}\n'
            if not self.unserved.empty:
                umin = (self.unserved.min() * ureg.MW).to_compact()
                umax = (self.unserved.max() * ureg.MW).to_compact()
                string += f'Shortfalls (min, max): ({umin}, {umax})'
        return string

All simulation state is kept in a Context object.

Initialise a default context.

Methods

def set_capacities(self, caps)
Expand source code
def set_capacities(self, caps):
    """Set generator capacities from a list."""
    num = 0
    for gen in self.generators:
        for (setter, min_cap, max_cap) in gen.setters:
            # keep parameters within bounds
            newval = max(min(caps[num], max_cap), min_cap)
            setter(newval)
            num += 1
    # Check every parameter has been set.
    msg = f'{num} != {len(caps)}'
    if num != len(caps):
        raise ValueError(msg)

Set generator capacities from a list.

def surplus_energy(self)
Expand source code
def surplus_energy(self):
    """Return total surplus energy."""
    return self.spill.to_numpy().sum()

Return total surplus energy.

def timesteps(self)
Expand source code
def timesteps(self):
    """Return the number of timesteps."""
    return len(self.demand)

Return the number of timesteps.

def total_demand(self)
Expand source code
def total_demand(self):
    """Return the total demand from the data frame."""
    return self.demand.to_numpy().sum()

Return the total demand from the data frame.

def unserved_energy(self)
Expand source code
def unserved_energy(self):
    """Return the total unserved energy."""
    return self.unserved.to_numpy().sum()

Return the total unserved energy.

def unserved_percent(self)
Expand source code
def unserved_percent(self):
    """Return the total unserved energy as a percentage of total demand."""
    # We can't catch ZeroDivision because numpy emits a warning
    # (which we would rather not suppress).
    if self.total_demand() == 0:
        return np.nan
    return self.unserved_energy() / self.total_demand() * 100

Return the total unserved energy as a percentage of total demand.

def years(self)
Expand source code
def years(self):
    """Return the number of years from the number of simulation hours."""
    return self.hours / (365 * 24)

Return the number of years from the number of simulation hours.