# This plotting module has a corresponding demo notebook in
# PostREISE/postreise/plot/demo: lmp_map_demo.ipynb
from bokeh.models import (
BasicTicker,
ColorBar,
ColumnDataSource,
HoverTool,
LinearColorMapper,
)
from bokeh.palettes import Turbo256
from matplotlib.colors import BoundaryNorm
from powersimdata.scenario.check import _check_scenario_is_in_analyze_state
from postreise.plot.canvas import create_map_canvas
from postreise.plot.check import _check_func_kwargs
from postreise.plot.plot_states import add_state_borders
from postreise.plot.projection_helpers import project_bus
[docs]def map_lmp(
scenario,
coordinate_rounding=1,
lmp_min=20,
lmp_max=45,
num_ticks=6,
figsize=(1400, 800),
scale_factor=1,
state_borders_kwargs=None,
):
"""Plot average LMP at bus level.
:param powersimdata.scenario,scenario.Scenario scenario: scenario instance..
:param int coordinate_rounding: number of digits to round lat/lon for aggregation.
:param int lmp_min: minimum LMP to clamp plot range to.
:param int lmp_max: maximum LMP to clamp plot range to.
:param int num_ticks: number of ticks to display on the color bar.
:param tuple figsize: size of the bokeh figure (in pixels).
:param int/float scale_factor: scaling factor for size of circles centered on buses.
:param dict state_borders_kwargs: keyword arguments to be passed to
:func:`postreise.plot.plot_states.add_state_borders`.
:return: (*bokeh.plotting.figure*) -- LMP map.
:raises TypeError:
if ``coordinate_rounding`` is not ``int``.
if ``lmp_min`` and ``lmp_max`` are not ``int``.
if ``num_ticks`` is not ``int``.
if ``scale_factor`` is not ``int`` or ``float``.
:raises ValueError:
if ``coordinate_rounding`` is not positive.
if ``num_ticks`` is negative.
if ``lmp_min`` or ``lmp_max`` is negative or ``lmp_min`` >= ``lmp_max``.
if ``scale_factor`` is negative.
"""
_check_scenario_is_in_analyze_state(scenario)
if not isinstance(coordinate_rounding, int):
raise TypeError("coordinate_rounding must be an int")
if coordinate_rounding < 0:
raise ValueError("coordinate_rounding must be positive")
if not isinstance(lmp_min, int):
raise TypeError("lmp_min must be a int")
if not isinstance(lmp_max, int):
raise TypeError("lmp_max must be a int")
if lmp_min >= lmp_max:
raise ValueError("Must have lmp_min < lmp_max")
if not isinstance(scale_factor, (int, float)):
raise TypeError("scale_factor must be a int/float")
if scale_factor < 0:
raise ValueError("scale_factor must be positive")
grid = scenario.get_grid()
lmp = scenario.get_lmp()
bus_with_lmp = grid.bus.copy()
bus_with_lmp["lmp"] = lmp.mean()
return add_lmp(
bus_with_lmp,
coordinate_rounding,
lmp_min,
lmp_max,
num_ticks,
figsize,
scale_factor,
state_borders_kwargs,
)
[docs]def add_lmp(
bus_with_lmp,
coordinate_rounding,
lmp_min,
lmp_max,
num_ticks,
figsize,
scale_factor,
state_borders_kwargs,
):
"""Add LMP to canvas.
:param list bus_with_lmp: bus data frame with LMP values.
:param int coordinate_rounding: number of digits to round lat/lon for aggregation.
:param str file_name: name for output png file.
:param int lmp_min: minimum LMP to clamp plot range to.
:param int lmp_max: maximum LMP to clamp plot range to.
:param int num_ticks: number of ticks to display on the color bar.
:param tuple figsize: size of the bokeh figure (in pixels).
:param int/float scale_factor: scaling factor for size of circles centered on buses.
:param dict state_borders_kwargs: keyword arguments to be passed to
:func:`postreise.plot.plot_states.add_state_borders`.
:return: (*bokeh.plotting.figure*) -- canvas with LMP..
"""
# create canvas
canvas = create_map_canvas(figsize=figsize)
# add state borders
default_state_borders_kwargs = {
"line_color": "gray",
"line_width": 2,
"fill_alpha": 0,
"background_map": False,
}
all_state_borders_kwargs = (
{**default_state_borders_kwargs, **state_borders_kwargs}
if state_borders_kwargs is not None
else default_state_borders_kwargs
)
_check_func_kwargs(
add_state_borders, set(all_state_borders_kwargs), "state_borders_kwargs"
)
canvas = add_state_borders(canvas, **all_state_borders_kwargs)
# aggregate buses by lat/lon
grouped_bus = aggregate_bus_lmp(bus_with_lmp, coordinate_rounding)
# assign color
norm = BoundaryNorm(boundaries=range(lmp_min, lmp_max + 1), ncolors=256, clip=True)
grouped_bus["col"] = norm(grouped_bus["lmp"])
grouped_bus["col"] = grouped_bus["col"].apply(lambda x: Turbo256[x])
grouped_bus_info = {
"x": grouped_bus["x"],
"y": grouped_bus["y"],
"col": grouped_bus["col"],
"lmp": grouped_bus["lmp"].round(2),
}
circle = canvas.circle(
"x",
"y",
color="col",
size=2 * scale_factor,
alpha=0.2,
source=ColumnDataSource(grouped_bus_info),
)
hover = HoverTool(
tooltips=[
("$/MWh", "@lmp{1.11}"),
],
renderers=[circle],
)
canvas.add_tools(hover)
# Add color bar
cm = LinearColorMapper(palette="Turbo256", low=norm.vmin, high=norm.vmax)
cb = ColorBar(
color_mapper=cm,
ticker=BasicTicker(desired_num_ticks=num_ticks),
title="$/MWh",
title_standoff=8,
orientation="vertical",
location=(0, 0),
)
canvas.add_layout(cb, "left")
return canvas
[docs]def aggregate_bus_lmp(bus, coordinate_rounding):
"""Aggregate LMP for buses based on similar lat/lon coordinates.
:param pandas.DataFrame bus: data frame containing 'lat', 'lon', 'lmp' columns.
:param int coordinate_rounding: number of digits to round lat/lon for aggregation.
:return: (*pandas.DataFrame*) -- aggregated data frame.
"""
bus_w_xy = project_bus(bus)
bus_w_xy["lat"] = bus_w_xy["lat"].round(coordinate_rounding)
bus_w_xy["lon"] = bus_w_xy["lon"].round(coordinate_rounding)
aggregated = bus_w_xy.groupby(["lat", "lon"]).agg(
{"lmp": "mean", "x": "mean", "y": "mean"}
)
return aggregated