Package nemo
The National Electricity Market Optimiser (NEMO).
Sub-modules
nemo.configfile
-
Configuration file processing (eg, filenames).
nemo.context
-
Implementation of the Context class …
nemo.costs
-
Generation technology costs.
nemo.generators
-
Simulated electricity generators for the NEMO framework.
nemo.nem
-
A National Electricity Market (NEM) simulation.
nemo.penalties
-
Penalty functions for the optimisation.
nemo.polygons
-
Support code for the 43 polygons of the AEMO study.
nemo.regions
-
Market regions consisting of one or more polygons.
nemo.scenarios
-
Supply side scenarios.
nemo.sim
-
The core of the simulation engine.
nemo.storage
-
Storage classes …
nemo.types
-
Useful internal types.
nemo.utils
-
Utility functions (eg, plotting).
Functions
def plot(context, filename=None, xlim=None, *, spills=False, showlegend=True)
-
Expand source code
def plot(context, filename=None, xlim=None, *, spills=False, showlegend=True): """Produce a pretty plot of supply and demand.""" if xlim is not None and not isinstance(xlim, tuple): raise ValueError(xlim) if xlim is None: starttime = context.demand.index[0] ninety_days = 24 * 90 if context.timesteps() > ninety_days: endtime = starttime + timedelta(days=90) else: endtime = context.demand.index[-1] timerange = (starttime, endtime) else: if len(xlim) != 2: raise ValueError(xlim) timerange = xlim _figure(context, spills, showlegend, timerange) if not filename: plt.show() else: plt.savefig(filename)
Produce a pretty plot of supply and demand.
def run(context, starthour=None, endhour=None)
-
Expand source code
def run(context, starthour=None, endhour=None): """Run the simulation.""" if not isinstance(context.regions, list): raise TypeError if starthour is None: starthour = context.demand.index.min() if endhour is None: endhour = context.demand.index.max() date_range = pd.date_range(starthour, endhour, freq='h') _sim(context, date_range) # Calculate unserved energy. agg_demand = context.demand.sum(axis=1) agg_generation = context.generation.sum(axis=1) unserved = agg_demand - agg_generation # Ignore unserved events very close to 0 (rounding errors) context.unserved = unserved[~np.isclose(unserved, 0)]
Run the simulation.
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.