Source code for oemof.solph._energy_system

# -*- coding: utf-8 -*-

"""
solph version of oemof.network.energy_system

SPDX-FileCopyrightText: Uwe Krien <krien@uni-bremen.de>
SPDX-FileCopyrightText: Simon Hilpert
SPDX-FileCopyrightText: Cord Kaldemeyer
SPDX-FileCopyrightText: Stephan Günther
SPDX-FileCopyrightText: Birgit Schachler
SPDX-FileCopyrightText: Johannes Kochems

SPDX-License-Identifier: MIT

"""

import calendar
import datetime
import warnings

import numpy as np
import pandas as pd
from oemof.network import energy_system as es
from oemof.tools import debugging


[docs]class EnergySystem(es.EnergySystem): """A variant of the class EnergySystem from <oemof.network.network.energy_system.EnergySystem> specially tailored to solph. In order to work in tandem with solph, instances of this class always use solph.GROUPINGS <oemof.solph.GROUPINGS>. If custom groupings are supplied via the `groupings` keyword argument, solph.GROUPINGS <oemof.solph.GROUPINGS> is prepended to those. If you know what you are doing and want to use solph without solph.GROUPINGS <oemof.solph.GROUPINGS>, you can just use EnergySystem <oemof.network.network.energy_system.EnergySystem>` of oemof.network directly. Parameters ---------- timeindex : pandas.DatetimeIndex timeincrement : iterable infer_last_interval : bool Add an interval to the last time point. The end time of this interval is unknown so it does only work for an equidistant DatetimeIndex with a 'freq' attribute that is not None. The parameter has no effect on the timeincrement parameter. periods : list or None The periods of a multi-period model. If this is explicitly specified, it leads to creating a multi-period model, providing a respective user warning as a feedback. list of pd.date_range objects carrying the timeindex for the respective period; For a standard model, periods are not (to be) declared, i.e. None. A list with one entry is derived, i.e. [0]. use_remaining_value : bool If True, compare the remaining value of an investment to the original value (only applicable for multi-period models) kwargs """ def __init__( self, timeindex=None, timeincrement=None, infer_last_interval=None, periods=None, use_remaining_value=False, groupings=None, ): # Doing imports at runtime is generally frowned upon, but should work # for now. See the TODO in :func:`constraint_grouping # <oemof.solph.groupings.constraint_grouping>` for more information. from oemof.solph import GROUPINGS if groupings is None: groupings = [] groupings = GROUPINGS + groupings if not ( isinstance(timeindex, pd.DatetimeIndex) or isinstance(timeindex, type(None)) ): msg = ( "Parameter 'timeindex' has to be of type " "pandas.DatetimeIndex or NoneType and not of type {0}" ) raise TypeError(msg.format(type(timeindex))) if infer_last_interval is None and timeindex is not None: msg = ( "The default behaviour will change in future versions.\n" "At the moment the last interval of an equidistant time " "index is added implicitly by default. Set " "'infer_last_interval' explicitly 'True' or 'False' to avoid " "this warning. In future versions 'False' will be the default" "behaviour" ) warnings.warn(msg, FutureWarning) infer_last_interval = True if infer_last_interval is True and timeindex is not None: # Add one time interval to the timeindex by adding one time point. if timeindex.freq is None: msg = ( "You cannot infer the last interval if the 'freq' " "attribute of your DatetimeIndex is None. Set " " 'infer_last_interval=False' or specify a DatetimeIndex " "with a valid frequency." ) raise AttributeError(msg) timeindex = timeindex.union( pd.date_range( timeindex[-1] + timeindex.freq, periods=1, freq=timeindex.freq, ) ) # catch wrong combinations and infer timeincrement from timeindex. if timeincrement is not None and timeindex is not None: if periods is None: msg = ( "Specifying the timeincrement and the timeindex parameter " "at the same time is not allowed since these might be " "conflicting to each other." ) raise AttributeError(msg) else: msg = ( "Ensure that your timeindex and timeincrement are " "consistent." ) warnings.warn(msg, debugging.ExperimentalFeatureWarning) elif timeindex is not None and timeincrement is None: df = pd.DataFrame(timeindex) timedelta = df.diff() timeincrement = timedelta / np.timedelta64(1, "h") # we want a series (squeeze) # without the first item (no delta defined for first entry) # but starting with index 0 (reset) timeincrement = timeincrement.squeeze()[1:].reset_index(drop=True) if timeincrement is not None and (pd.Series(timeincrement) <= 0).any(): msg = ( "The time increment is inconsistent. Negative values and zero " "are not allowed.\nThis is caused by a inconsistent " "timeincrement parameter or an incorrect timeindex." ) raise TypeError(msg) super().__init__( groupings=groupings, timeindex=timeindex, timeincrement=timeincrement, ) self.periods = periods if self.periods is not None: msg = ( "CAUTION! You specified the 'periods' attribute for your " "energy system.\n This will lead to creating " "a multi-period optimization modeling which can be " "used e.g. for long-term investment modeling.\n" "Please be aware that the feature is experimental as of " "now. If you find anything suspicious or any bugs, " "please report them." ) warnings.warn(msg, debugging.ExperimentalFeatureWarning) self._extract_periods_years() self._extract_periods_matrix() self._extract_end_year_of_optimization() self.use_remaining_value = use_remaining_value def _extract_periods_years(self): """Map years in optimization to respective period based on time indices Attribute `periods_years` of type list is set. It contains the year of the start of each period, relative to the start of the optimization run and starting with 0. """ periods_years = [0] start_year = self.periods[0].min().year for k, v in enumerate(self.periods): if k >= 1: periods_years.append(v.min().year - start_year) self.periods_years = periods_years def _extract_periods_matrix(self): """Determines a matrix describing the temporal distance to each period. Attribute `periods_matrix` of type list np.array is set. Rows represent investment/commissioning periods, columns represent decommissioning periods. The values describe the temporal distance between each investment period to each decommissioning period. """ periods_matrix = [] period_years = np.array(self.periods_years) for v in period_years: row = period_years - v row = np.where(row < 0, 0, row) periods_matrix.append(row) self.periods_matrix = np.array(periods_matrix) def _extract_end_year_of_optimization(self): """Extract the end of the optimization in years Attribute `end_year_of_optimization` of int is set. """ duration_last_period = self.get_period_duration(-1) self.end_year_of_optimization = ( self.periods_years[-1] + duration_last_period )
[docs] def get_period_duration(self, period): """Get duration of a period in full years Parameters ---------- period : int Period for which the duration in years shall be obtained Returns ------- int Duration of the period """ return ( self.periods[period].max().year - self.periods[period].min().year + 1 )
[docs]def create_time_index( year: int = None, interval: float = 1, number: int = None, start: datetime.datetime or datetime.date = None, ): """ Create a datetime index for one year. Notes ----- To create 8760 hourly intervals for a non leap year a datetime index with 8761 time points need to be created. So the number of time steps is always the number of intervals plus one. Parameters ---------- year : int, datetime The year of the index. If number and start is set the year parameter is ignored. interval : float The time interval in hours e.g. 0.5 for 30min or 2 for a two hour interval (default: 1). number : int The number of time intervals. By default number is calculated to create an index of one year. For a shorter or longer period the number of intervals can be set by the user. start : datetime.datetime or datetime.date Optional start time. If start is not set, 00:00 of the first day of the given year is the start time. Examples -------- >>> len(create_time_index(2014)) 8761 >>> len(create_time_index(2012)) # leap year 8785 >>> len(create_time_index(2014, interval=0.5)) 17521 >>> len(create_time_index(2014, interval=0.5, number=10)) 11 >>> len(create_time_index(2014, number=10)) 11 >>> str(create_time_index(2014, interval=0.5, number=10)[-1]) '2014-01-01 05:00:00' >>> str(create_time_index(2014, interval=2, number=10)[-1]) '2014-01-01 20:00:00' """ if number is None: if calendar.isleap(year): hoy = 8784 else: hoy = 8760 number = round(hoy / interval) if start is None: start = f"1/1/{year}" return pd.date_range(start, periods=number + 1, freq=f"{interval}H")