-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #71 from samgdotson/logical_flow
Logical flow
- Loading branch information
Showing
14 changed files
with
895 additions
and
69 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
:template: my_class.rst | ||
osier.DispatchModel | ||
osier.LogicDispatchModel | ||
osier.CapacityExpansion | ||
``` | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
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 | ||
|
||
|
||
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["Shortfall"] = 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 | ||
|
||
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 overproduction ' | ||
'(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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import pandas as pd | ||
import numpy as np | ||
from unyt import unyt_array, hr, MW, kW, GW | ||
import itertools as it | ||
from osier import Technology | ||
from osier.technology import _validate_quantity, _validate_unit | ||
from osier.utils import synchronize_units | ||
import warnings | ||
import logging | ||
|
||
_freq_opts = {'D': 'day', | ||
'H': 'hour', | ||
'S': 'second', | ||
'T': 'minute'} | ||
|
||
|
||
class OsierModel(): | ||
""" | ||
A class for instantiating energy models in Osier. | ||
""" | ||
|
||
def __init__(self, | ||
technology_list, | ||
net_demand, | ||
time_delta=None, | ||
power_units=MW, | ||
**kwargs): | ||
self.net_demand = net_demand | ||
self.time_delta = time_delta | ||
self.results = None | ||
self.objective = None | ||
|
||
if isinstance(net_demand, unyt_array): | ||
self.power_units = net_demand.units | ||
elif isinstance(net_demand, (np.ndarray, list)): | ||
self.power_units = power_units | ||
self.net_demand = np.array(self.net_demand) * self.power_units | ||
elif isinstance(net_demand, pd.core.series.Series): | ||
self.power_units = power_units | ||
self.net_demand = np.array(self.net_demand) * self.power_units | ||
else: | ||
self.power_units = power_units | ||
|
||
self.technology_list = synchronize_units( | ||
technology_list, | ||
unit_power=self.power_units, | ||
unit_time=self.time_delta.units) | ||
|
||
@property | ||
def time_delta(self): | ||
return self._time_delta | ||
|
||
@time_delta.setter | ||
def time_delta(self, value): | ||
if value: | ||
valid_quantity = _validate_quantity(value, dimension='time') | ||
self._time_delta = valid_quantity | ||
else: | ||
if isinstance(self.net_demand, pd.DataFrame): | ||
try: | ||
freq_list = list(self.net_demand.index.inferred_freq) | ||
freq_key = freq_list[-1] | ||
try: | ||
value = float(freq_list[0]) | ||
except ValueError: | ||
warnings.warn((f"Could not convert value " | ||
f"{freq_list[0]} to float. " | ||
"Setting to 1.0."), | ||
UserWarning) | ||
value = 1.0 | ||
self._time_delta = _validate_quantity( | ||
f"{value} {_freq_opts[freq_key]}", dimension='time') | ||
except KeyError: | ||
warnings.warn( | ||
(f"Could not infer time delta with freq {freq_key} " | ||
"from pandas dataframe. Setting delta to 1 hour."), | ||
UserWarning) | ||
self._time_delta = 1 * hr | ||
else: | ||
self._time_delta = 1 * hr |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.