mirror of
https://github.com/louisleroy5/trnslator.git
synced 2024-11-15 00:30:31 -05:00
1118 lines
32 KiB
Python
1118 lines
32 KiB
Python
import copy
|
|
import logging as lg
|
|
import os
|
|
import time
|
|
import warnings
|
|
from datetime import timedelta
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import tsam.timeseriesaggregation as tsam
|
|
from matplotlib import pyplot as plt, cm
|
|
from matplotlib.colors import LightSource
|
|
from pandas import Series, DataFrame, concat, MultiIndex, date_range
|
|
from sklearn import preprocessing
|
|
|
|
import translater
|
|
from translater import log, rmse, piecewise, settings
|
|
|
|
|
|
class EnergySeries(Series):
|
|
"""A Series object designed to store energy related data."""
|
|
|
|
@property
|
|
def _constructor(self):
|
|
return EnergySeries
|
|
|
|
_metadata = [
|
|
"name",
|
|
"bin_edges_",
|
|
"bin_scaling_factors_",
|
|
"profile_type",
|
|
"base_year",
|
|
"frequency",
|
|
"units",
|
|
"sort_values",
|
|
"to_units",
|
|
"converted_",
|
|
"concurrent_sort_",
|
|
]
|
|
|
|
def __new__(
|
|
cls,
|
|
data,
|
|
frequency=None,
|
|
units=None,
|
|
profile_type="undefinded",
|
|
index=None,
|
|
dtype=None,
|
|
copy=True,
|
|
name=None,
|
|
fastpath=False,
|
|
base_year=2018,
|
|
normalize=False,
|
|
sort_values=False,
|
|
ascending=False,
|
|
archetypes=None,
|
|
concurrent_sort=False,
|
|
to_units=None,
|
|
use_timeindex=False,
|
|
):
|
|
"""
|
|
Args:
|
|
data:
|
|
frequency:
|
|
units:
|
|
profile_type:
|
|
index:
|
|
dtype:
|
|
copy:
|
|
name:
|
|
fastpath:
|
|
base_year:
|
|
normalize:
|
|
sort_values:
|
|
ascending:
|
|
archetypes:
|
|
concurrent_sort:
|
|
to_units:
|
|
use_timeindex:
|
|
"""
|
|
self = super(EnergySeries, cls).__new__(cls)
|
|
return self
|
|
|
|
def __init__(
|
|
self,
|
|
data,
|
|
frequency=None,
|
|
units=None,
|
|
profile_type="undefinded",
|
|
index=None,
|
|
dtype=None,
|
|
copy=True,
|
|
name=None,
|
|
fastpath=False,
|
|
base_year=2018,
|
|
normalize=False,
|
|
sort_values=False,
|
|
ascending=False,
|
|
archetypes=None,
|
|
concurrent_sort=False,
|
|
to_units=None,
|
|
use_timeindex=False,
|
|
):
|
|
"""
|
|
Args:
|
|
data:
|
|
frequency:
|
|
units:
|
|
profile_type:
|
|
index:
|
|
dtype:
|
|
copy:
|
|
name:
|
|
fastpath:
|
|
base_year:
|
|
normalize:
|
|
sort_values:
|
|
ascending:
|
|
archetypes:
|
|
concurrent_sort:
|
|
to_units:
|
|
use_timeindex:
|
|
"""
|
|
super(EnergySeries, self).__init__(
|
|
data=data, index=index, dtype=dtype, name=name, copy=copy, fastpath=fastpath
|
|
)
|
|
self.bin_edges_ = None
|
|
self.bin_scaling_factors_ = None
|
|
self.profile_type = profile_type
|
|
self.frequency = frequency
|
|
self.base_year = base_year
|
|
self.units = settings.unit_registry.parse_expression(units).units
|
|
self.archetypes = archetypes
|
|
self.to_units = settings.unit_registry.parse_expression(to_units).units
|
|
self.converted_ = False
|
|
self.concurrent_sort_ = concurrent_sort
|
|
# handle sorting of the data
|
|
if sort_values:
|
|
self.is_sorted = True
|
|
if concurrent_sort:
|
|
self.concurrent_sort(ascending=ascending, inplace=True)
|
|
else:
|
|
self.sort_values(ascending=ascending, inplace=True)
|
|
self.reset_index(drop=True, inplace=True)
|
|
else:
|
|
self.is_sorted = False
|
|
|
|
# handle archetype names
|
|
if isinstance(self.index, MultiIndex):
|
|
self.archetypes = list(set(self.index.get_level_values(level=0)))
|
|
else:
|
|
self.archetypes = None
|
|
|
|
# handle normalization
|
|
if normalize:
|
|
self.normalize(inplace=True)
|
|
|
|
# handle unit conversion
|
|
if to_units and units:
|
|
self.unit_conversion(to_units=to_units, inplace=True)
|
|
|
|
# handle DateTimeIndex
|
|
if index is None and use_timeindex:
|
|
start_date = str(self.base_year) + "0101"
|
|
if isinstance(self.index, MultiIndex):
|
|
newindex = self.index # todo: finish this
|
|
else:
|
|
newindex = pd.date_range(
|
|
start=start_date, freq=self.frequency, periods=len(self)
|
|
)
|
|
self.index = newindex
|
|
|
|
@classmethod
|
|
def from_sqlite(
|
|
cls,
|
|
df,
|
|
name=None,
|
|
base_year=2018,
|
|
units=None,
|
|
normalize=False,
|
|
sort_values=False,
|
|
ascending=False,
|
|
concurrent_sort=False,
|
|
to_units=None,
|
|
agg_func="sum",
|
|
):
|
|
"""Create a.
|
|
|
|
Args:
|
|
df (DataFrame):
|
|
name:
|
|
base_year:
|
|
units:
|
|
normalize:
|
|
sort_values:
|
|
ascending:
|
|
concurrent_sort:
|
|
to_units:
|
|
agg_func (callable): The aggregation function to use in the case
|
|
that multiple values have the same index value. If a function,
|
|
must either work when passed a DataFrame or when passed to
|
|
DataFrame.apply. For a DataFrame, can pass a dict, if the keys
|
|
are DataFrame column names.
|
|
|
|
Accepted Combinations are:
|
|
- string function name
|
|
- function
|
|
- list of functions
|
|
- dict of column names -> functions (or list of functions)
|
|
"""
|
|
index = pd.to_datetime(
|
|
{
|
|
"year": base_year,
|
|
"month": df.Month,
|
|
"day": df.Day,
|
|
"hour": df.Hour,
|
|
"minute": df.Minute,
|
|
}
|
|
)
|
|
# Adjust timeindex by timedelta
|
|
index -= df.Interval.apply(lambda x: timedelta(minutes=x))
|
|
index = pd.DatetimeIndex(index)
|
|
# get data
|
|
data = df.Value
|
|
data.index = index
|
|
units = [units] if units else set(df.Units)
|
|
if len(units) > 1:
|
|
raise ValueError("The DataFrame contains mixed units: {}".format(units))
|
|
else:
|
|
units = next(iter(units), None)
|
|
# group data by index value (level=0) using the agg_func
|
|
if agg_func:
|
|
grouped_Data = data.groupby(level=0).agg(agg_func)
|
|
else:
|
|
df["DateTimeIndex"] = index
|
|
grouped_Data = df.set_index(["DateTimeIndex", "Name"]).Value
|
|
# Since we create the index, use_timeindex must be false
|
|
return cls(
|
|
grouped_Data.values,
|
|
name=name,
|
|
units=units,
|
|
index=grouped_Data.index,
|
|
use_timeindex=False,
|
|
base_year=base_year,
|
|
normalize=normalize,
|
|
sort_values=sort_values,
|
|
ascending=ascending,
|
|
concurrent_sort=concurrent_sort,
|
|
to_units=to_units,
|
|
)
|
|
|
|
def unit_conversion(self, to_units=None, inplace=False):
|
|
"""returns the multiplier to convert units
|
|
|
|
Args:
|
|
to_units (pint.Unit):
|
|
inplace:
|
|
"""
|
|
reg = settings.unit_registry
|
|
if to_units is None:
|
|
to_units = self.to_units
|
|
else:
|
|
to_units = reg.parse_expression(to_units).units
|
|
cdata = reg.Quantity(self.values, self.units).to(to_units).m
|
|
result = self.apply(lambda x: x)
|
|
result.update(pd.Series(cdata, index=result.index))
|
|
result.__class__ = EnergySeries
|
|
result.converted_ = True
|
|
result.units = to_units
|
|
if inplace:
|
|
self._update_inplace(result)
|
|
self.__finalize__(result)
|
|
else:
|
|
return result
|
|
|
|
def concurrent_sort(self, ascending=False, inplace=False, level=0):
|
|
"""
|
|
Args:
|
|
ascending:
|
|
inplace:
|
|
level:
|
|
"""
|
|
if isinstance(self.index, MultiIndex):
|
|
concurrent = self.unstack(level=level)
|
|
concurrent_sum = concurrent.sum(axis=1)
|
|
|
|
sortedIdx = concurrent_sum.sort_values(ascending=ascending).index
|
|
|
|
result = concurrent.loc[sortedIdx, :]
|
|
result.index = concurrent.index
|
|
result = result.stack().swaplevel()
|
|
|
|
if inplace:
|
|
self.update(result)
|
|
self.__finalize__(result)
|
|
else:
|
|
return result # todo: make sure results has all the metadata
|
|
|
|
def normalize(self, feature_range=(0, 1), inplace=False):
|
|
"""Returns a normalized EnergySeries
|
|
|
|
Args:
|
|
feature_range:
|
|
inplace:
|
|
"""
|
|
scaler = preprocessing.MinMaxScaler(feature_range=feature_range)
|
|
if self.archetypes:
|
|
result = concat(
|
|
{
|
|
name: Series(
|
|
scaler.fit_transform(sub.values.reshape(-1, 1)).ravel()
|
|
)
|
|
for name, sub in self.groupby(level=0)
|
|
}
|
|
).sort_index()
|
|
result = self._constructor(result)
|
|
else:
|
|
result = Series(scaler.fit_transform(self.values.reshape(-1, 1)).ravel())
|
|
result = self._constructor(result)
|
|
result.units = settings.unit_registry.dimensionless
|
|
if inplace:
|
|
self._update_inplace(result)
|
|
else:
|
|
return result # todo: make sure result has all the metadata
|
|
|
|
def ldc_source(self, SCOPH=4, SCOPC=4):
|
|
"""Returns the Load Duration Curve from the source side of theoretical
|
|
Heat Pumps
|
|
|
|
Args:
|
|
SCOPH: Seasonal COP in Heating
|
|
SCOPC: Seasonal COP in Cooling
|
|
|
|
Returns:
|
|
(EnergySeries) Load Duration Curve
|
|
"""
|
|
|
|
result = self.ldc.apply(
|
|
lambda x: x * (1 - 1 / SCOPH) if x > 0 else x * (1 + 1 / SCOPC)
|
|
)
|
|
return result
|
|
|
|
def source_side(self, SCOPH=None, SCOPC=None):
|
|
"""Returns the Source Side EnergySeries given a Seasonal COP. Negative
|
|
values are considered like Cooling Demand.
|
|
|
|
Args:
|
|
SCOPH: Seasonal COP in Heating
|
|
SCOPC: Seasonal COP in Cooling
|
|
|
|
Returns:
|
|
(EnergySeries) Load Duration Curve
|
|
"""
|
|
if SCOPC or SCOPH:
|
|
result = self.apply(
|
|
lambda x: x * (1 - 1 / SCOPH) if SCOPH else x * (1 + 1 / SCOPC)
|
|
)
|
|
return result
|
|
else:
|
|
raise ValueError("Please provide a SCOPH or a SCOPC")
|
|
|
|
def discretize_tsam(
|
|
self,
|
|
resolution=None,
|
|
noTypicalPeriods=10,
|
|
hoursPerPeriod=24,
|
|
clusterMethod="hierarchical",
|
|
evalSumPeriods=False,
|
|
sortValues=False,
|
|
sameMean=False,
|
|
rescaleClusterPeriods=True,
|
|
weightDict=None,
|
|
extremePeriodMethod="None",
|
|
solver="glpk",
|
|
roundOutput=None,
|
|
addPeakMin=None,
|
|
addPeakMax=None,
|
|
addMeanMin=None,
|
|
addMeanMax=None,
|
|
):
|
|
"""uses tsam
|
|
|
|
Args:
|
|
resolution:
|
|
noTypicalPeriods:
|
|
hoursPerPeriod:
|
|
clusterMethod:
|
|
evalSumPeriods:
|
|
sortValues:
|
|
sameMean:
|
|
rescaleClusterPeriods:
|
|
weightDict:
|
|
extremePeriodMethod:
|
|
solver:
|
|
roundOutput:
|
|
addPeakMin:
|
|
addPeakMax:
|
|
addMeanMin:
|
|
addMeanMax:
|
|
"""
|
|
try:
|
|
import tsam.timeseriesaggregation as tsam
|
|
except ImportError:
|
|
raise ImportError("tsam is required for discretize_tsam()")
|
|
if not isinstance(self.index, pd.DatetimeIndex):
|
|
raise TypeError("To use tsam, index of series must be a " "DateTimeIndex")
|
|
if isinstance(self, Series):
|
|
timeSeries = pd.DataFrame(self)
|
|
else:
|
|
timeSeries = self.copy()
|
|
agg = tsam.TimeSeriesAggregation(
|
|
timeSeries,
|
|
resolution=resolution,
|
|
noTypicalPeriods=noTypicalPeriods,
|
|
hoursPerPeriod=hoursPerPeriod,
|
|
clusterMethod=clusterMethod,
|
|
evalSumPeriods=evalSumPeriods,
|
|
sortValues=sortValues,
|
|
sameMean=sameMean,
|
|
rescaleClusterPeriods=rescaleClusterPeriods,
|
|
weightDict=weightDict,
|
|
extremePeriodMethod=extremePeriodMethod,
|
|
solver=solver,
|
|
roundOutput=roundOutput,
|
|
addPeakMin=addPeakMin,
|
|
addPeakMax=addPeakMax,
|
|
addMeanMin=addMeanMin,
|
|
addMeanMax=addMeanMax,
|
|
)
|
|
|
|
agg.createTypicalPeriods()
|
|
results = agg.predictOriginalData()
|
|
results = EnergySeries(results.iloc[:, 0])
|
|
# results._internal_names += agg.clusterOrder
|
|
return results.__finalize__(self)
|
|
|
|
def discretize(self, n_bins=3, inplace=False):
|
|
"""Retruns a discretized pandas.Series
|
|
|
|
Args:
|
|
n_bins (int): Number of bins or steps to discretize the function
|
|
inplace (bool): if True, perform operation in-place
|
|
"""
|
|
try:
|
|
from scipy.optimize import minimize
|
|
from itertools import chain
|
|
except ImportError:
|
|
raise ImportError(
|
|
"The sklearn package must be installed to " "use this optional feature."
|
|
)
|
|
if self.archetypes:
|
|
# if multiindex, group and apply operation on each group.
|
|
# combine at the end
|
|
results = {}
|
|
edges = {}
|
|
ampls = {}
|
|
for name, sub in self.groupby(level=0):
|
|
hour_of_min = sub.time_at_min[1]
|
|
|
|
sf = [1 / (i * 1.01) for i in range(1, n_bins + 1)]
|
|
sf.extend([sub.min()])
|
|
sf_bounds = [(0, sub.max()) for i in range(0, n_bins + 1)]
|
|
hours = [
|
|
hour_of_min - hour_of_min * 1 / (i * 1.01)
|
|
for i in range(1, n_bins + 1)
|
|
]
|
|
# Todo hours need to work fow datetime index
|
|
hours.extend([len(sub)])
|
|
hours_bounds = [(0, len(sub)) for i in range(0, n_bins + 1)]
|
|
|
|
start_time = time.time()
|
|
log("discretizing EnergySeries {}".format(name), lg.DEBUG)
|
|
res = minimize(
|
|
rmse,
|
|
np.array(hours + sf),
|
|
args=(sub.values),
|
|
method="L-BFGS-B",
|
|
bounds=hours_bounds + sf_bounds,
|
|
options=dict(disp=True),
|
|
)
|
|
log(
|
|
"Completed discretization in {:,.2f} seconds".format(
|
|
time.time() - start_time
|
|
),
|
|
lg.DEBUG,
|
|
)
|
|
edges[name] = res.x[0 : n_bins + 1]
|
|
ampls[name] = res.x[n_bins + 1 :]
|
|
results[name] = Series(piecewise(res.x))
|
|
self.bin_edges_ = Series(edges).apply(Series)
|
|
self.bin_scaling_factors_ = DataFrame(ampls)
|
|
|
|
result = concat(results)
|
|
else:
|
|
hour_of_min = self.time_at_min
|
|
|
|
sf = [1 / (i * 1.01) for i in range(1, n_bins + 1)]
|
|
sf.extend([self.min()])
|
|
sf_bounds = [(0, self.max()) for i in range(0, n_bins + 1)]
|
|
hours = [
|
|
hour_of_min - hour_of_min * 1 / (i * 1.01) for i in range(1, n_bins + 1)
|
|
]
|
|
hours.extend([len(self)])
|
|
hours_bounds = [(0, len(self)) for i in range(0, n_bins + 1)]
|
|
|
|
start_time = time.time()
|
|
# log('discretizing EnergySeries {}'.format(name), lg.DEBUG)
|
|
res = minimize(
|
|
rmse,
|
|
np.array(hours + sf),
|
|
args=(self.values),
|
|
method="L-BFGS-B",
|
|
bounds=hours_bounds + sf_bounds,
|
|
options=dict(disp=True),
|
|
)
|
|
log(
|
|
"Completed discretization in {:,.2f} seconds".format(
|
|
time.time() - start_time
|
|
),
|
|
lg.DEBUG,
|
|
)
|
|
edges = res.x[0 : n_bins + 1]
|
|
ampls = res.x[n_bins + 1 :]
|
|
result = Series(piecewise(res.x))
|
|
bin_edges = Series(edges).apply(Series)
|
|
self.bin_edges_ = bin_edges
|
|
bin_edges.loc[-1, 0] = 0
|
|
bin_edges.sort_index(inplace=True)
|
|
bin_edges = bin_edges.diff().dropna()
|
|
bin_edges = bin_edges.round()
|
|
self.bin_scaling_factors_ = DataFrame(
|
|
{"duration": bin_edges[0], "scaling factor": ampls}
|
|
)
|
|
self.bin_scaling_factors_.index = np.round(edges).astype(int)
|
|
|
|
if inplace:
|
|
self.update(result)
|
|
self.__class__ = EnergySeries
|
|
self.__finalize__(result)
|
|
else:
|
|
result.__class__ = EnergySeries
|
|
return result.__finalize__(self)
|
|
|
|
def unstack(self, level=-1, fill_value=None):
|
|
"""
|
|
Args:
|
|
level:
|
|
fill_value:
|
|
"""
|
|
from pandas.core.reshape.reshape import unstack
|
|
|
|
result = unstack(self, level, fill_value)
|
|
result.__class__ = translater.EnergyDataFrame
|
|
return result.__finalize__(self)
|
|
|
|
def stack(self, level=-1, dropna=True):
|
|
"""
|
|
Args:
|
|
level:
|
|
dropna:
|
|
"""
|
|
from pandas.core.reshape.reshape import stack, stack_multiple
|
|
|
|
if isinstance(level, (tuple, list)):
|
|
result = stack_multiple(self, level, dropna=dropna)
|
|
return self.__finalize__(result)
|
|
else:
|
|
result = stack(self, level, dropna=dropna)
|
|
return self.__finalize__(result)
|
|
|
|
def plot3d(self, *args, **kwargs):
|
|
"""Generate a plot of the EnergySeries.
|
|
|
|
Wraps the ``plot_energyseries()`` function, and documentation is
|
|
copied from there.
|
|
|
|
Args:
|
|
*args:
|
|
**kwargs:
|
|
"""
|
|
return plot_energyseries(self, *args, **kwargs)
|
|
|
|
def plot2d(self, *args, **kwargs):
|
|
"""
|
|
Args:
|
|
*args:
|
|
**kwargs:
|
|
"""
|
|
return plot_energyseries_map(self, **kwargs)
|
|
|
|
# @property
|
|
# def units(self):
|
|
# return self.units.units
|
|
|
|
@property
|
|
def p_max(self):
|
|
if isinstance(self.index, MultiIndex):
|
|
return self.groupby(level=0).max()
|
|
else:
|
|
return self.max()
|
|
|
|
@property
|
|
def monthly(self):
|
|
if isinstance(self.index, MultiIndex):
|
|
return self.groupby(level=0).max()
|
|
else:
|
|
datetimeindex = date_range(
|
|
freq=self.frequency,
|
|
start="{}-01-01".format(self.base_year),
|
|
periods=self.size,
|
|
)
|
|
self_copy = self.copy()
|
|
self_copy.index = datetimeindex
|
|
self_copy = self_copy.resample("M").mean()
|
|
self_copy.frequency = "M"
|
|
return EnergySeries(self_copy, frequency="M", units=self.units)
|
|
|
|
@property
|
|
def capacity_factor(self):
|
|
max = self.max()
|
|
mean = self.mean()
|
|
return mean / max
|
|
|
|
@property
|
|
def bin_edges(self):
|
|
return self.bin_edges_
|
|
|
|
@property
|
|
def time_at_min(self):
|
|
return self.idxmin()
|
|
|
|
@property
|
|
def bin_scaling_factors(self):
|
|
return self.bin_scaling_factors_
|
|
|
|
@property
|
|
def duration_scaling_factor(self):
|
|
return list(map(tuple, self.bin_scaling_factors.values))
|
|
|
|
@property
|
|
def ldc(self):
|
|
nb_points = len(self)
|
|
newdata = self.sort_values(ascending=False).reset_index(drop=True)
|
|
return newdata.__finalize__(self)
|
|
|
|
@property
|
|
def nseries(self):
|
|
if self.data.ndim == 1:
|
|
return 1
|
|
else:
|
|
return self.data.shape[1]
|
|
|
|
|
|
def save_and_show(
|
|
fig, ax, save, show, close, filename, file_format, dpi, axis_off, extent
|
|
):
|
|
"""Save a figure to disk and show it, as specified.
|
|
|
|
Args:
|
|
fig (matplotlib.figure.Figure): the figure
|
|
ax (matplotlib.axes.Axes or list(matplotlib.axes.Axes)): the axes
|
|
save (bool): whether to save the figure to disk or not
|
|
show (bool): whether to display the figure or not
|
|
close (bool): close the figure (only if show equals False) to prevent
|
|
display
|
|
filename (string): the name of the file to save
|
|
file_format (string): the format of the file to save (e.g., 'jpg',
|
|
'png', 'svg')
|
|
dpi (int): the resolution of the image file if saving (Dots per inch)
|
|
axis_off (bool): if True matplotlib axis was turned off by plot_graph so
|
|
constrain the saved figure's extent to the interior of the axis
|
|
extent:
|
|
|
|
Returns:
|
|
(tuple) fig, ax
|
|
"""
|
|
# save the figure if specified
|
|
|
|
if save:
|
|
start_time = time.time()
|
|
|
|
# create the save folder if it doesn't already exist
|
|
if not os.path.exists(settings.imgs_folder):
|
|
os.makedirs(settings.imgs_folder)
|
|
path_filename = os.path.join(
|
|
settings.imgs_folder, os.extsep.join([filename, file_format])
|
|
)
|
|
|
|
if not isinstance(ax, (np.ndarray, list)):
|
|
ax = [ax]
|
|
if file_format == "svg":
|
|
for ax in ax:
|
|
# if the file_format is svg, prep the fig/ax a bit for saving
|
|
ax.axis("off")
|
|
ax.set_position([0, 0, 1, 1])
|
|
ax.patch.set_alpha(0.0)
|
|
fig.patch.set_alpha(0.0)
|
|
fig.savefig(
|
|
path_filename,
|
|
bbox_inches=0,
|
|
format=file_format,
|
|
facecolor=fig.get_facecolor(),
|
|
transparent=True,
|
|
)
|
|
else:
|
|
if extent is None:
|
|
if len(ax) == 1:
|
|
if axis_off:
|
|
for ax in ax:
|
|
# if axis is turned off, constrain the saved
|
|
# figure's extent to the interior of the axis
|
|
extent = ax.get_window_extent().transformed(
|
|
fig.dpi_scale_trans.inverted()
|
|
)
|
|
else:
|
|
extent = "tight"
|
|
fig.savefig(
|
|
path_filename,
|
|
dpi=dpi,
|
|
bbox_inches=extent,
|
|
format=file_format,
|
|
facecolor=fig.get_facecolor(),
|
|
transparent=True,
|
|
)
|
|
log(
|
|
"Saved the figure to disk in {:,.2f} seconds".format(
|
|
time.time() - start_time
|
|
)
|
|
)
|
|
|
|
# show the figure if specified
|
|
if show:
|
|
start_time = time.time()
|
|
plt.show()
|
|
# fig.show()
|
|
log("Showed the plot in {:,.2f} seconds".format(time.time() - start_time))
|
|
# if show=False, close the figure if close=True to prevent display
|
|
elif close:
|
|
plt.close()
|
|
|
|
return fig, ax
|
|
|
|
|
|
def plot_energyseries(
|
|
energy_series,
|
|
kind="polygon",
|
|
axis_off=True,
|
|
cmap=None,
|
|
fig_height=None,
|
|
fig_width=6,
|
|
show=True,
|
|
view_angle=-60,
|
|
save=False,
|
|
close=False,
|
|
dpi=300,
|
|
file_format="png",
|
|
color=None,
|
|
axes=None,
|
|
vmin=None,
|
|
vmax=None,
|
|
filename=None,
|
|
timeStepsPerPeriod=24,
|
|
**kwargs
|
|
):
|
|
"""
|
|
Args:
|
|
energy_series (EnergySeries):
|
|
kind (str):
|
|
axis_off (bool):
|
|
cmap:
|
|
fig_height (float):
|
|
fig_width (float):
|
|
show (bool):
|
|
view_angle (float):
|
|
save (bool):
|
|
close (bool):
|
|
dpi (int):
|
|
file_format (str):
|
|
color (str):
|
|
axes:
|
|
vmin (float):
|
|
vmax (float):
|
|
filename (str):
|
|
timeStepsPerPeriod (int): The number of discrete timesteps which
|
|
describe one period.
|
|
**kwargs:
|
|
"""
|
|
if energy_series.empty:
|
|
warnings.warn(
|
|
"The EnergyProgile you are attempting to plot is "
|
|
"empty. Nothing has been displayed.",
|
|
UserWarning,
|
|
)
|
|
return axes
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
# noinspection PyUnresolvedReferences
|
|
from mpl_toolkits.mplot3d import Axes3D
|
|
|
|
if isinstance(energy_series.index, pd.MultiIndex):
|
|
groups = energy_series.groupby(level=0)
|
|
nax = len(groups)
|
|
else:
|
|
nax = 1
|
|
groups = [("unnamed", energy_series)]
|
|
|
|
if fig_height is None:
|
|
fig_height = fig_width * nax
|
|
|
|
# Set up plot
|
|
fig, axes = plt.subplots(
|
|
nax,
|
|
1,
|
|
subplot_kw=dict(projection="3d"),
|
|
figsize=(fig_width, fig_height),
|
|
dpi=dpi,
|
|
)
|
|
if not isinstance(axes, np.ndarray):
|
|
axes = [axes]
|
|
|
|
for ax, (name, profile) in zip(axes, groups):
|
|
values = profile.values
|
|
|
|
vmin = values.min() if vmin is None else vmin
|
|
vmax = values.max() if vmax is None else vmax
|
|
|
|
if kind == "polygon":
|
|
import tsam.timeseriesaggregation as tsam
|
|
|
|
z, _ = tsam.unstackToPeriods(profile, timeStepsPerPeriod=timeStepsPerPeriod)
|
|
nrows, ncols = z.shape
|
|
|
|
xs = z.columns
|
|
zs = z.index.values
|
|
|
|
verts = []
|
|
for i in zs:
|
|
ys = z.iloc[int(i), :]
|
|
verts.append(_polygon_under_graph(xs, ys))
|
|
|
|
_plot_poly_collection(
|
|
ax,
|
|
verts,
|
|
zs,
|
|
edgecolors=kwargs.get("edgecolors", None),
|
|
facecolors=kwargs.get("facecolors", None),
|
|
linewidths=kwargs.get("linewidths", None),
|
|
cmap=cmap,
|
|
)
|
|
elif kind == "surface":
|
|
import tsam.timeseriesaggregation as tsam
|
|
|
|
z, _ = tsam.unstackToPeriods(profile, timeStepsPerPeriod=timeStepsPerPeriod)
|
|
nrows, ncols = z.shape
|
|
x = z.columns
|
|
y = z.index.values
|
|
|
|
x, y = np.meshgrid(x, y)
|
|
_plot_surface(ax, x, y, z.values, cmap=cmap, **kwargs)
|
|
elif kind == "contour":
|
|
import tsam.timeseriesaggregation as tsam
|
|
|
|
z, _ = tsam.unstackToPeriods(profile, timeStepsPerPeriod=timeStepsPerPeriod)
|
|
nrows, ncols = z.shape
|
|
x = z.columns
|
|
y = z.index.values
|
|
|
|
x, y = np.meshgrid(x, y)
|
|
_plot_contour(ax, x, y, z.values, cmap=cmap, **kwargs)
|
|
else:
|
|
raise NameError('plot kind "{}" is not supported'.format(kind))
|
|
|
|
if filename is None:
|
|
filename = "unnamed"
|
|
|
|
# set the extent of the figure
|
|
ax.set_xlim3d(-1, ncols)
|
|
ax.set_xlabel(kwargs.get("xlabel", "hour of day"))
|
|
ax.set_ylim3d(-1, nrows)
|
|
ax.set_ylabel(kwargs.get("ylabel", "day of year"))
|
|
ax.set_zlim3d(vmin, vmax)
|
|
z_label = "{} [{:~P}]".format(
|
|
energy_series.name if energy_series.name is not None else "Z",
|
|
energy_series.units,
|
|
)
|
|
ax.set_zlabel(z_label)
|
|
|
|
# configure axis appearance
|
|
xaxis = ax.xaxis
|
|
yaxis = ax.yaxis
|
|
zaxis = ax.zaxis
|
|
|
|
xaxis.get_major_formatter().set_useOffset(False)
|
|
yaxis.get_major_formatter().set_useOffset(False)
|
|
zaxis.get_major_formatter().set_useOffset(False)
|
|
|
|
# if axis_off, turn off the axis display set the margins to zero and
|
|
# point the ticks in so there's no space around the plot
|
|
if axis_off:
|
|
ax.axis("off")
|
|
ax.margins(0)
|
|
ax.tick_params(which="both", direction="in")
|
|
xaxis.set_visible(False)
|
|
yaxis.set_visible(False)
|
|
zaxis.set_visible(False)
|
|
fig.canvas.draw()
|
|
if view_angle is not None:
|
|
ax.view_init(30, view_angle)
|
|
ax.set_proj_type(kwargs.get("proj_type", "persp"))
|
|
fig.canvas.draw()
|
|
fig, axes = save_and_show(
|
|
fig=fig,
|
|
ax=axes,
|
|
save=save,
|
|
show=show,
|
|
close=close,
|
|
filename=filename,
|
|
file_format=file_format,
|
|
dpi=dpi,
|
|
axis_off=axis_off,
|
|
extent=None,
|
|
)
|
|
return fig, axes
|
|
|
|
|
|
def plot_energyseries_map(
|
|
data,
|
|
periodlength=24,
|
|
subplots=False,
|
|
vmin=None,
|
|
vmax=None,
|
|
axis_off=True,
|
|
cmap="RdBu",
|
|
fig_height=None,
|
|
fig_width=6,
|
|
show=True,
|
|
view_angle=-60,
|
|
save=False,
|
|
close=False,
|
|
dpi=300,
|
|
file_format="png",
|
|
color=None,
|
|
ax=None,
|
|
filename="untitled",
|
|
extent="tight",
|
|
sharex=False,
|
|
sharey=False,
|
|
layout=None,
|
|
layout_type="vertical",
|
|
**kwargs
|
|
):
|
|
"""
|
|
Args:
|
|
data (EnergySeries or EnergyDataFrame):
|
|
periodlength:
|
|
subplots:
|
|
vmin:
|
|
vmax:
|
|
axis_off:
|
|
cmap:
|
|
fig_height:
|
|
fig_width:
|
|
show:
|
|
view_angle:
|
|
save:
|
|
close:
|
|
dpi:
|
|
file_format:
|
|
color:
|
|
ax:
|
|
filename:
|
|
extent:
|
|
sharex:
|
|
sharey:
|
|
layout:
|
|
layout_type:
|
|
**kwargs:
|
|
"""
|
|
if fig_height is None:
|
|
fig_height = fig_width / 3
|
|
figsize = (fig_width, fig_height)
|
|
|
|
if not ax:
|
|
if subplots:
|
|
if isinstance(data, EnergySeries):
|
|
data = data.unstack(level=0)
|
|
n = data.shape[1]
|
|
else:
|
|
n = 1
|
|
fig, axes = plt.subplots(
|
|
nrows=n, ncols=1, figsize=(fig_width, fig_height), dpi=dpi
|
|
)
|
|
else:
|
|
fig = ax.get_figure()
|
|
if figsize is not None:
|
|
fig.set_size_inches(figsize)
|
|
axes = ax
|
|
|
|
stacked, timeindex = tsam.unstackToPeriods(copy.deepcopy(data), periodlength)
|
|
cmap = plt.get_cmap(cmap)
|
|
im = axes.imshow(
|
|
stacked.values.T, interpolation="nearest", vmin=vmin, vmax=vmax, cmap=cmap
|
|
)
|
|
axes.set_aspect("auto")
|
|
axes.set_ylabel("Hour")
|
|
plt.xlabel("Day")
|
|
|
|
# fig.subplots_adjust(right=1.1)
|
|
cbar = fig.colorbar(im, ax=axes)
|
|
cbar.set_label("{} [{:~P}]".format(data.name, data.units))
|
|
|
|
fig, axes = save_and_show(
|
|
fig, axes, save, show, close, filename, file_format, dpi, axis_off, extent
|
|
)
|
|
|
|
return fig, axes
|
|
|
|
|
|
def _plot_poly_collection(
|
|
ax, verts, zs=None, color=None, cmap=None, vmin=None, vmax=None, **kwargs
|
|
):
|
|
"""
|
|
Args:
|
|
ax:
|
|
verts:
|
|
zs:
|
|
color:
|
|
cmap:
|
|
vmin:
|
|
vmax:
|
|
**kwargs:
|
|
"""
|
|
from matplotlib.collections import PolyCollection
|
|
|
|
# if None in zs:
|
|
# zs = None
|
|
|
|
# color=None overwrites specified facecolor/edgecolor with default color
|
|
if color is not None:
|
|
kwargs["color"] = color
|
|
import matplotlib as mpl
|
|
|
|
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
|
|
|
|
poly = PolyCollection(verts, **kwargs)
|
|
if zs is not None:
|
|
poly.set_array(np.asarray(zs))
|
|
poly.set_cmap(cmap)
|
|
poly.set_clim(vmin, vmax)
|
|
|
|
ax.add_collection3d(poly, zs=zs, zdir="y")
|
|
# ax.autoscale_view()
|
|
return poly
|
|
|
|
|
|
def _plot_surface(ax, x, y, z, cmap=None, **kwargs):
|
|
"""
|
|
Args:
|
|
ax:
|
|
x:
|
|
y:
|
|
z:
|
|
cmap:
|
|
**kwargs:
|
|
"""
|
|
if cmap is None:
|
|
cmap = cm.gist_earth
|
|
|
|
ls = LightSource(270, 45)
|
|
# To use a custom hillshading mode, override the built-in shading and pass
|
|
# in the rgb colors of the shaded surface calculated from "shade".
|
|
rgb = ls.shade(z, cmap=cm.get_cmap(cmap), vert_exag=0.1, blend_mode="soft")
|
|
surf = ax.plot_surface(
|
|
x,
|
|
y,
|
|
z,
|
|
rstride=1,
|
|
cstride=1,
|
|
facecolors=rgb,
|
|
linewidth=0,
|
|
antialiased=False,
|
|
shade=False,
|
|
**kwargs
|
|
)
|
|
return surf
|
|
|
|
|
|
def _plot_contour(ax, x, y, z, cmap=None, **kwargs):
|
|
"""
|
|
Args:
|
|
ax:
|
|
x:
|
|
y:
|
|
z:
|
|
cmap:
|
|
**kwargs:
|
|
"""
|
|
if cmap is None:
|
|
cmap = cm.gist_earth
|
|
surf = ax.contour3D(x, y, z, 150, cmap=cmap, **kwargs)
|
|
return surf
|
|
|
|
|
|
def _polygon_under_graph(xlist, ylist):
|
|
"""Construct the vertex list which defines the polygon filling the space
|
|
under the (xlist, ylist) line graph. Assumes the xs are in ascending order.
|
|
|
|
Args:
|
|
xlist:
|
|
ylist:
|
|
"""
|
|
return [(xlist[0], 0.0), *zip(xlist, ylist), (xlist[-1], 0.0)]
|
|
|
|
|
|
EnergySeries.plot3d.__doc__ = plot_energyseries.__doc__
|
|
EnergySeries.plot2d.__doc__ = plot_energyseries_map.__doc__
|