Source code for osier.models.logic_dispatch

from osier import OsierModel
from osier.utils import get_tech_names
from copy import deepcopy
from unyt import unyt_array, MW
import pandas as pd
import numpy as np
import warnings

LARGE_NUMBER = 1e20


[docs] class LogicDispatchModel(OsierModel): """ The :class:`LogicDispatchModel` class creates and solves a basic dispatch model from the perspective of a "grid operator." Parameters ---------- technology_list : list of :class:`osier.Technology` The list of :class:`Technology` objects to dispatch -- i.e. decide how much energy each technology should produce. net_demand : list, :class:`numpy.ndarray`, :class:`unyt.array.unyt_array`, :class:`pandas.DataFrame` The remaining energy demand to be fulfilled by the technologies in :attr:`technology_list`. The `values` of an object passed as `net_demand` are used to create a supply constraint. See :attr:`oversupply` and :attr:`undersupply`. If a :class:`pandas.DataFrame` is passed, :mod:`osier` will try inferring a `time_delta` from the dataframe index. Otherwise, the :attr:`time_delta` must be passed or the default is used. time_delta : str, :class:`unyt.unyt_quantity`, float, int Specifies the amount of time between two time slices. The default is one hour. Can be overridden by specifying a unit with the value. For example: >>> time_delta = "5 minutes" >>> from unyt import days >>> time_delta = 30*days would both work. power_units : str, :class:`unyt.unit_object` Specifies the units for the power demand. The default is :attr:`MW`. Can be overridden by specifying a unit with the value. verbosity : Optional, int Sets the logging level for the simulation. Accepts `logging.LEVEL` or integer where LEVEL is {10:DEBUG, 20:INFO, 30:WARNING, 40:ERROR, 50:CRITICAL}. curtailment : boolean Indicates if the model should enable a curtailment option. allow_blackout : boolean If True, a "reliability" technology is added to the model that will fulfill the mismatch in supply and demand. This reliability technology has a variable cost of 1e4 $/MWh. The value must be higher than the variable cost of any other technology to prevent a pathological preference for blackouts. Default is False. Attributes ---------- objective : float The result of the model's objective function. Only instantiated after :meth:`DispatchModel.solve()` is called. technology_list : list of :class:`osier.Technology` A _sorted_ list of technologies. original_order : list of str A list of technology names to preserve the intended order of the technologies. """ def __init__(self, technology_list, net_demand, allow_blackout=False, curtailment=True, verbosity=50, *args, **kwargs): super().__init__(technology_list=technology_list, net_demand=net_demand, *args, **kwargs) self.technology_list = technology_list self.technology_list.sort() self.original_order = get_tech_names(technology_list) self.cost_history = np.zeros(len(net_demand)) self.covered_demand = None self.objective = None self.results = None self.verbosity = verbosity self.allow_blackout = allow_blackout self.curtailment = curtailment def _reset_all(self): for t in self.technology_list: t.reset_history() return def _format_results(self): data = {} for t in self.technology_list: data[f"{t.technology_name}"] = unyt_array( t.power_history).to_ndarray() if t.technology_type == 'storage': data[f"{t.technology_name}_level"] = unyt_array( t.storage_history).to_ndarray() data[f"{t.technology_name}_charge"] = unyt_array( t.charge_history).to_ndarray() data["Curtailment"] = np.array( [v if v <= 0 else 0 for v in self.covered_demand]) data["LoadLoss"] = np.array( [v if v > 0 else 0 for v in self.covered_demand]) self.results = pd.DataFrame(data) return def _calculate_objective(self): self.objective = sum(np.array(t.power_history).sum() * t.variable_cost.to_value() for t in self.technology_list) return
[docs] def solve(self): """ This function executes the model solve with a rule-based approach. """ self.covered_demand = self.net_demand.copy() self._reset_all() try: for i, v in enumerate(self.covered_demand): for t in self.technology_list: power_out = t.power_output(v, time_delta=self.time_delta) v -= power_out self.covered_demand[i] = v if not self.allow_blackout and (v > 0): if self.verbosity <= 20: print('solve failed -- unmet demand') raise ValueError if not self.curtailment and (v < 0): if self.verbosity <= 20: print( ('solve failed -- ' 'too much supply ' '(no curtailment allowed)')) raise ValueError self._format_results() self._calculate_objective() except ValueError: if self.verbosity <= 30: warnings.warn( (f"Infeasible or no solution." f"Objective set to {LARGE_NUMBER}") ) self.objective = LARGE_NUMBER return