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.