def test_change_time_zone_argument_value(): arg = ( (ts_as_data_frame, "US/Japan"), (ts_as_series.resample("D").sum(), "GMT+8"), ) for a in arg: with pytest.raises(ValueError): change_time_zone(a[0], a[1])
def test_change_time_zone_argument_type(): arg = ( (ts_as_series, 1), (ts_as_data_frame, {1, 2, 3}), (ts_as_series.to_dict(), "US/Eastern"), ) for a in arg: with pytest.raises(TypeError): change_time_zone(a[0], a[1])
def test_change_time_zone(): arg = (ts_as_data_frame, ts_as_series) for a in arg: ts_idx = change_time_zone(a, "US/Pacific").index assert ts_idx.tz != pytz.timezone("UTC") assert ts_idx.tz == pytz.timezone("US/Pacific") assert set(ts_idx.year.unique()) == {2015, 2016} # assign time zone before calling function ts_idx = change_time_zone(a.tz_localize("UTC"), "US/Pacific").index assert ts_idx.tz != pytz.timezone("UTC") assert ts_idx.tz == pytz.timezone("US/Pacific") assert set(ts_idx.year.unique()) == {2015, 2016}
def sum_generation_by_type_zone( scenario: Scenario, time_range=None, time_zone=None ) -> pd.DataFrame: """Get total generation for each generator type and load zone combination. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire time range is used for the given scenario. :param str time_zone: new time zone. :return: (*pandas.DataFrame*) -- total generation, indexed by {type, zone}. """ _check_scenario_is_in_analyze_state(scenario) pg = scenario.state.get_pg() if time_zone: pg = change_time_zone(pg, time_zone) if time_range: pg = slice_time_series(pg, time_range[0], time_range[1]) grid = scenario.state.get_grid() plant = grid.plant summed_gen_series = pg.sum().groupby([plant.type, plant.zone_id]).sum() summed_gen_dataframe = summed_gen_series.unstack().fillna(value=0) return summed_gen_dataframe
def plot_generation_time_series_stack( scenario, area, resources, area_type=None, time_range=None, time_zone="utc", time_freq="H", show_demand=True, show_net_demand=True, normalize=False, t2c=None, t2l=None, t2hc=None, title=None, label_fontsize=20, title_fontsize=22, tick_fontsize=15, legend_fontsize=18, save=False, filename=None, filepath=None, ): """Generate time series generation stack plot in a certain area of a scenario. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance :param str area: one of *loadzone*, *state*, *state abbreviation*, *interconnect*, *'all'* :param str/list resources: one or a list of resources. *'solar_curtailment'*, *'wind_curtailment'*, *'wind_offshore_curtailment'* are valid entries together with all available generator types in the area. The order of the resources determines the stack order in the figure. :param str area_type: one of *'loadzone'*, *'state'*, *'state_abbr'*, *'interconnect'* :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire time range is used for the given scenario. :param str time_zone: new time zone. :param str time_freq: frequency. Either *'D'* (day), *'W'* (week), *'M'* (month). :param bool show_demand: show demand line in the plot or not, default is True. :param bool show_net_demand: show net demand line in the plot or not, default is True. :param bool normalize: normalize the generation based on capacity or not, default is False. :param dict t2c: user specified color of resource type to overwrite type2color default dict. key: resource type, value: color code. :param dict t2l: user specified label of resource type to overwrite type2label default dict. key: resource type, value: label. :param dict t2hc: user specified color of curtailable resource hatches to overwrite type2hatchcolor default dict. key: resource type, valid keys are *'wind_curtailment'*, *'solar_curtailment'*, *'wind_offshore_curtailment'*, value: color code. :param str title: user specified title of the figure, default is set to be area. :param float label_fontsize: user specified label fontsize, default is 20. :param float title_fontsize: user specified title fontsize, default is 22. :param float tick_fontsize: user specified ticks of axes fontsize, default is 15. :param float legend_fontsize: user specified legend fontsize, default is 18. :param bool save: save the generated figure or not, default is False. :param str filename: if save is True, user specified filename, use area if None. :param str filepath: if save is True, user specified filepath, use current directory if None. """ _check_scenario_is_in_analyze_state(scenario) mi = ModelImmutables(scenario.info["grid_model"]) type2color = mi.plants["type2color"] type2label = mi.plants["type2label"] type2hatchcolor = mi.plants["type2hatchcolor"] if t2c: type2color.update(t2c) if t2l: type2label.update(t2l) if t2hc: type2hatchcolor.update(t2hc) pg_stack = get_generation_time_series_by_resources(scenario, area, resources, area_type=area_type) capacity = get_capacity_by_resources(scenario, area, resources, area_type=area_type) demand = get_demand_time_series(scenario, area, area_type=area_type) net_demand = get_net_demand_time_series(scenario, area, area_type=area_type) capacity_ts = pd.Series(capacity.sum(), index=pg_stack.index) curtailable_resources = { "solar_curtailment", "wind_curtailment", "wind_offshore_curtailment", } if curtailable_resources & set(resources): curtailment = get_curtailment_time_series(scenario, area, area_type=area_type) for r in curtailable_resources: if r in resources and r in curtailment.columns: pg_stack[r] = curtailment[r] if time_zone != "utc": pg_stack = change_time_zone(pg_stack, time_zone) demand = change_time_zone(demand, time_zone) net_demand = change_time_zone(net_demand, time_zone) capacity_ts = change_time_zone(capacity_ts, time_zone) if not time_range: time_range = ( pd.Timestamp(scenario.info["start_date"]), pd.Timestamp(scenario.info["end_date"]), ) pg_stack = slice_time_series(pg_stack, time_range[0], time_range[1]) demand = slice_time_series(demand, time_range[0], time_range[1]) net_demand = slice_time_series(net_demand, time_range[0], time_range[1]) capacity_ts = slice_time_series(capacity_ts, time_range[0], time_range[1]) if time_freq != "H": pg_stack = resample_time_series(pg_stack, time_freq) demand = resample_time_series(demand, time_freq) net_demand = resample_time_series(net_demand, time_freq) capacity_ts = resample_time_series(capacity_ts, time_freq) if "storage" in resources: pg_storage = get_storage_time_series(scenario, area, area_type=area_type) capacity_storage = get_storage_capacity(scenario, area, area_type=area_type) capacity_storage_ts = pd.Series(capacity_storage, index=pg_storage.index) if time_zone != "utc": pg_storage = change_time_zone(pg_storage, time_zone) capacity_storage_ts = change_time_zone(capacity_storage_ts, time_zone) if time_range != ("2016-01-01 00:00:00", "2016-12-31 23:00:00"): pg_storage = slice_time_series(pg_storage, time_range[0], time_range[1]) capacity_storage_ts = slice_time_series(capacity_storage_ts, time_range[0], time_range[1]) if time_freq != "H": pg_storage = resample_time_series(pg_storage, time_freq) capacity_storage_ts = resample_time_series(capacity_storage_ts, time_freq) pg_stack["storage"] = pg_storage.clip(lower=0) capacity_ts += capacity_storage_ts fig, (ax, ax_storage) = plt.subplots( 2, 1, figsize=(20, 15), sharex="row", gridspec_kw={ "height_ratios": [3, 1], "hspace": 0.02 }, ) plt.subplots_adjust(wspace=0) if normalize: pg_storage = pg_storage.divide(capacity_storage_ts, axis="index") ax_storage.set_ylabel("Normalized Storage", fontsize=label_fontsize) else: ax_storage.set_ylabel("Energy Storage (MW)", fontsize=label_fontsize) ax_storage = pg_storage.plot(color=type2color["storage"], lw=4, ax=ax_storage) ax_storage.fill_between( pg_storage.index.values, 0, pg_storage.values, color=type2color["storage"], alpha=0.5, ) # Erase year in xticklabels xt_with_year = list(ax_storage.__dict__["date_axis_info"][0]) xt_with_year[-1] = b"%b" ax_storage.__dict__["date_axis_info"][0] = tuple(xt_with_year) ax_storage.tick_params(axis="both", which="both", labelsize=tick_fontsize) ax_storage.set_xlabel("") for a in fig.get_axes(): a.label_outer() else: fig = plt.figure(figsize=(20, 10)) ax = fig.gca() if normalize: pg_stack = pg_stack.divide(capacity_ts, axis="index") demand = demand.divide(capacity_ts, axis="index") net_demand = net_demand.divide(capacity_ts, axis="index") ax.set_ylabel("Normalized Generation", fontsize=label_fontsize) else: pg_stack = pg_stack.divide(1e6, axis="index") demand = demand.divide(1e6, axis="index") net_demand = net_demand.divide(1e6, axis="index") ax.set_ylabel("Daily Energy TWh", fontsize=label_fontsize) available_resources = [r for r in resources if r in pg_stack.columns] pg_stack[available_resources].clip(0, None).plot.area(color=type2color, linewidth=0, alpha=0.7, ax=ax, sharex="row") if show_demand: demand.plot(color="red", lw=4, ax=ax) if show_net_demand: net_demand.plot(color="red", ls="--", lw=2, ax=ax) if not title: title = area ax.set_title("%s" % title, fontsize=title_fontsize) ax.grid(color="black", axis="y") if "storage" not in resources: # Erase year in xticklabels xt_with_year = list(ax.__dict__["date_axis_info"][0]) xt_with_year[-1] = b"%b" ax.__dict__["date_axis_info"][0] = tuple(xt_with_year) ax.set_xlabel("") ax.tick_params(which="both", labelsize=tick_fontsize) ax.set_ylim([ min(0, 1.1 * net_demand.min()), max(ax.get_ylim()[1], 1.1 * demand.max()), ]) handles, labels = ax.get_legend_handles_labels() if show_demand: labels[0] = "Demand" if show_net_demand: labels[1] = "Net Demand" label_offset = show_demand + show_net_demand labels = [type2label[l] if l in type2label else l for l in labels] # Add hatches for r in curtailable_resources: if r in available_resources: ind = available_resources.index(r) ax.fill_between( pg_stack[available_resources].index.values, pg_stack[available_resources].iloc[:, :ind + 1].sum(axis=1), pg_stack[available_resources].iloc[:, :ind].sum(axis=1), color="none", hatch="//", edgecolor=type2hatchcolor[r], linewidth=0.0, ) handles[ind + label_offset] = mpatches.Patch( facecolor=type2color[r], hatch="//", edgecolor=type2hatchcolor[r], linewidth=0.0, ) ax.legend( handles[::-1], labels[::-1], frameon=2, prop={"size": legend_fontsize}, loc="upper left", bbox_to_anchor=(1, 1), ) if save: if not filename: filename = area if not filepath: filepath = os.path.join(os.getcwd(), filename) plt.savefig(f"{filepath}.pdf", bbox_inches="tight", pad_inches=0)
def plot_curtailment_time_series( scenario, area, resources, area_type=None, time_range=None, time_zone="utc", time_freq="H", show_demand=True, percentage=True, t2c=None, t2l=None, title=None, label_fontsize=20, title_fontsize=22, tick_fontsize=15, legend_fontsize=18, save=False, filename=None, filepath=None, ): """Generate time series curtailment plot of each specified resource in a certain area of a scenario. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance :param str area: one of *loadzone*, *state*, *state abbreviation*, *interconnect*, *'all'* :param str/list resources: one or a list of resources. :param str area_type: one of *'loadzone'*, *'state'*, *'state_abbr'*, *'interconnect'* :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire time range is used for the given scenario. :param str time_zone: new time zone. :param str time_freq: frequency. Either *'D'* (day), *'W'* (week), *'M'* (month). :param bool show_demand: show demand line in the plot or not, default is True. :param bool percentage: plot the curtailment in terms of percentage or not, default is True. :param dict t2c: user specified color of resource type to overwrite type2color default dict. key: resource type, value: color code. :param dict t2l: user specified label of resource type to overwrite type2label default dict. key: resource type, value: label. :param str title: user specified title of the figure. :param float label_fontsize: user specified label fontsize, default is 20. :param float title_fontsize: user specified title fontsize, default is 22. :param float tick_fontsize: user specified ticks of axes fontsize, default is 15. :param float legend_fontsize: user specified legend fontsize, default is 18. :param bool save: save the generated figure or not, default is False. :param str filename: if save is True, user specified filename, use area if None. :param str filepath: if save is True, user specified filepath, use current directory if None. """ _check_scenario_is_in_analyze_state(scenario) resources = _check_resources_and_format( resources, grid_model=scenario.info["grid_model"]) mi = ModelImmutables(scenario.info["grid_model"]) type2color = mi.plants["type2color"] type2label = mi.plants["type2label"] if t2c: type2color.update(t2c) if t2l: type2label.update(t2l) resource_pg = get_generation_time_series_by_resources(scenario, area, resources, area_type=area_type) demand = get_demand_time_series(scenario, area, area_type=area_type) curtailment = get_curtailment_time_series(scenario, area, area_type=area_type) resource_pg = change_time_zone(resource_pg, time_zone) demand = change_time_zone(demand, time_zone) curtailment = change_time_zone(curtailment, time_zone) if not time_range: time_range = ( pd.Timestamp(scenario.info["start_date"], tz="utc"), pd.Timestamp(scenario.info["end_date"], tz="utc"), ) resource_pg = slice_time_series(resource_pg, time_range[0], time_range[1]) demand = slice_time_series(demand, time_range[0], time_range[1]) curtailment = slice_time_series(curtailment, time_range[0], time_range[1]) if time_freq != "H": resource_pg = resample_time_series(resource_pg, time_freq) demand = resample_time_series(demand, time_freq) curtailment = resample_time_series(curtailment, time_freq) for r in resource_pg.columns: curtailment[r + "_curtailment" + "_mean"] = curtailment[r + "_curtailment"].mean() curtailment[ r + "_available"] = curtailment[r + "_curtailment"] + resource_pg[r] curtailment[r + "_curtailment" + "_percentage"] = (curtailment[r + "_curtailment"] / curtailment[r + "_available"] * 100) curtailment[r + "_curtailment" + "_percentage" + "_mean"] = curtailment[r + "_curtailment" + "_percentage"].mean() for r in resources: if r not in resource_pg.columns: raise ValueError(f"{r} is invalid in {area}!") fig = plt.figure(figsize=(20, 10)) ax = fig.gca() title_text = f"{area} {r.capitalize()}" if not title else title plt.title(title_text, fontsize=title_fontsize) cr = r + "_curtailment" if percentage: key1, key2 = f"{cr}_percentage", f"{cr}_percentage_mean" else: key1, key2 = cr, f"{cr}_mean" curtailment[key1].plot( ax=ax, lw=4, alpha=0.7, color=type2color[cr], label=type2label[cr], ) curtailment[key2].plot( ax=ax, ls="--", lw=4, alpha=0.7, color=type2color[cr], label=type2label[cr] + " Mean", ) ax_twin = ax.twinx() curtailment[r + "_available"].plot( ax=ax_twin, lw=4, alpha=0.7, color=type2color[r], label=f"{type2label[r]} Energy Available", ) if show_demand: demand.plot(ax=ax_twin, lw=4, alpha=0.7, color="red", label="Demand") # Erase year in xticklabels xt_with_year = list(ax_twin.__dict__["date_axis_info"][0]) xt_with_year[-1] = b"%b" ax_twin.__dict__["date_axis_info"][0] = tuple(xt_with_year) ax_twin.set_xlabel("") ax_twin.tick_params(which="both", labelsize=tick_fontsize) ax_twin.yaxis.get_offset_text().set_fontsize(tick_fontsize) ax_twin.set_ylabel("MWh", fontsize=label_fontsize) ax_twin.legend(loc="upper right", prop={"size": legend_fontsize}) ax.tick_params(which="both", labelsize=tick_fontsize) ax.yaxis.get_offset_text().set_fontsize(tick_fontsize) ax.grid(color="black", axis="y") ax.set_xlabel("") if percentage: ax.set_ylabel("Curtailment [%]", fontsize=label_fontsize) else: ax.set_ylabel("Curtailment", fontsize=label_fontsize) ax.legend(loc="upper left", prop={"size": legend_fontsize}) if save: if not filename: filename = f"{area.lower()}_{r}_curtailment" if not filepath: filepath = os.path.join(os.getcwd(), filename) plt.savefig(f"{filepath}.pdf", bbox_inches="tight", pad_inches=0)
def plot_heatmap( series, time_zone=None, time_zone_label=None, title=None, cmap="PiYG", scale=None, save_filename=None, origin="upper", vmin=None, vmax=None, cbar_format=None, cbar_tick_values=None, cbar_label=None, cbar_tick_labels=None, contour_levels=None, figsize=(16, 8), ): """Show time-series values via an imshow where each column is one color-coded day. :param pandas.Series series: a time-series of values to be color-coded. :param str time_zone: a time zone to be passed as `tz` kwarg to :func:`postreise.analyze.time.change_time_zone`. :param str time_zone_label: a time zone label to be added to the y axis label. :param str title: a title to be added to the figure. :param str/matplotlib.colors.Colormap cmap: colormap specification to be passed as `cmap` kwarg to :func:`matplotlib.pyplot.imshow`. :param int/float scale: a scaling factor to be applied to the series values. :param str save_filename: a path to save the figure to. :param str origin: the vertical location of the origin, either "upper" or "lower". :param int/float vmin: Minimum value for coloring, to be passed as `vmin` kwarg to :func:`matplotlib.pyplot.imshow`. :param int/float vmax: Maximum value for coloring, to be passed as `vmax` kwarg to :func:`matplotlib.pyplot.imshow`. :param str/matplotlib.ticker.Formatter cbar_format: a formatter for colorbar labels, to be passed as `format` kwarg to :func:`matplotlib.pyplot.colorbar`. :param iterable cbar_tick_values: colorbar tick locations, to be passed as `ticks` kwarg to :func:`matplotlib.pyplot.colorbar`. :param str cbar_label: axis label for colorbar. :param iterable cbar_tick_labels: colorbar tick labels. :param iterable contour_levels: values at which to draw contours, passed as `levels` kwarg to :func:`matplotlib.pyplot.contour`. :param tuple(int/float, int/float) figsize: size of figure. """ _check_time_series(series, "series") df = series.to_frame(name="values").asfreq("H") year = df.index[0].year if time_zone is not None: df = change_time_zone(df, time_zone) df["date"] = df.index.date df["hour"] = df.index.hour df_reshaped = pd.pivot( df, index="date", columns="hour", values="values", ) xlims = mdates.date2num([df_reshaped.index[0], df_reshaped.index[-1]]) ylims = mdates.date2num( [dt.datetime(year, 1, 1, 0), dt.datetime(year, 1, 1, 23)]) if scale is not None: df_reshaped *= scale fig = plt.figure(figsize=figsize) ax = fig.add_subplot() # if necessary, flip ylims so labels follow data from top to bottom extent = [*xlims, *ylims ] if origin == "lower" else [*xlims, ylims[1], ylims[0]] im = plt.imshow( df_reshaped.T, cmap=cmap, aspect="auto", extent=extent, origin=origin, vmin=vmin, vmax=vmax, ) if contour_levels is not None: ax.contour(df_reshaped.T, extent=extent, levels=contour_levels, origin=origin) date_format = mdates.DateFormatter("%m/%d") ax.xaxis_date() ax.xaxis.set_major_formatter(date_format) ax.set_xlabel("Date") time_format = mdates.DateFormatter("%H:%M") ax.yaxis_date() ax.yaxis.set_major_formatter(time_format) y_axis_label = "Time" if time_zone_label is None else f"Time {time_zone_label}" ax.set_ylabel(y_axis_label) cbar = fig.colorbar(im, format=cbar_format, ticks=cbar_tick_values) if cbar_label is not None: cbar.set_label(cbar_label) if title is not None: plt.title(title) if cbar_tick_labels is not None: cbar.ax.set_yticklabels(cbar_tick_labels) if save_filename is not None: plt.savefig(save_filename, bbox_inches="tight")
def plot_scatter_capacity_vs_curtailment( scenario, area, resources, time_zone="utc", time_range=None, area_type=None, between_time=None, dayofweek=None, markersize=50, fontsize=20, title=None, percentage=False, show_plot=True, ): """Generate for a given scenario the scatter plot of the capacity (x-axis) vs curtailment as a fraction of available resources (y-axis) of generators located in area and fueled by resources over a time range. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance :param str area: one of *loadzone*, *state*, *state abbreviation*, *interconnect*, *'all'* :param str/list resources: one or a list of resources. :param str time_zone: new time zone, default to be *'utc'*. :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire time range is used for the given scenario. :param str area_type: one of *'loadzone'*, *'state'*, *'state_abbr'*, *'interconnect'* :param list between_time: specify the start hour and end hour of each day inclusively, default to None, which includes every hour of a day. Note that if the end hour is set before the start hour, the complementary hours of a day are picked. :param set dayofweek: specify the interest days of week, which is a subset of integers in [0, 6] with 0 being Monday and 6 being Sunday, default to None, which includes every day of a week. :param int/float markersize: marker size, default to 50. :param int/float fontsize: font size, default to 20. :param str title: user specified figure title, default to None. :param bool percentage: show capacity factor in percentage or not, default to False :param bool show_plot: show the plot or not, default to True. :return: (*tuple*) -- the first entry is matplotlib.axes.Axes object of the plot, the second entry is the capacity weighted average of curtailment over the selected time range. :raises TypeError: if markersize is not an integer or a float and/or if fontsize is not an integer or a float and/or if title is provided but not in a string format. """ if not isinstance(markersize, (int, float)): raise TypeError("markersize should be either an integer or a float") if not isinstance(fontsize, (int, float)): raise TypeError("fontsize should be either an integer or a float") if title is not None and not isinstance(title, str): raise TypeError("title should be a string") resources = _check_resources_are_renewable_and_format( resources, grid_model=scenario.info["grid_model"]) curtailment = calculate_curtailment_time_series(scenario) plant_list = get_plant_id_for_resources_in_area(scenario, area, resources, area_type=area_type) curtailment = curtailment[set(plant_list) & set(curtailment.columns)] curtailment = change_time_zone(curtailment, time_zone) if not time_range: time_range = ( pd.Timestamp(scenario.info["start_date"], tz="utc"), pd.Timestamp(scenario.info["end_date"], tz="utc"), ) curtailment = slice_time_series( curtailment, time_range[0], time_range[1], between_time=between_time, dayofweek=dayofweek, ) profiles = pd.concat( [scenario.state.get_solar(), scenario.state.get_wind()], axis=1) curtailment = curtailment.sum() / profiles[curtailment.columns].sum() if percentage: curtailment = (curtailment * 100).round(2) total_cap = get_capacity_by_resources(scenario, area, resources, area_type=area_type).sum() plant_df = scenario.state.get_grid().plant.loc[plant_list] if total_cap == 0: data_avg = 0 else: data_avg = (plant_df["Pmax"] * curtailment).sum() / total_cap _, ax = plt.subplots(figsize=[20, 10]) ax.scatter(plant_df["Pmax"], curtailment, s=markersize) ax.plot(plant_df["Pmax"], [data_avg] * len(plant_df.index), c="red") ax.grid() if title is None: ax.set_title( f"{area} " f'{" ".join(resources) if isinstance(resources, (list, set)) else resources}' ) else: ax.set_title(title) ax.set_xlabel("Capacity (MW)") if percentage: ax.set_ylabel("Curtailment %") else: ax.set_ylabel("Curtailment") for item in ([ax.title, ax.xaxis.label, ax.yaxis.label] + ax.get_xticklabels() + ax.get_yticklabels()): item.set_fontsize(fontsize) if show_plot: plt.show() return ax, data_avg