def plot_states( figsize=(1400, 800), state_borders_kwargs=None, state_colors_args=None, state_tooltips_args=None, state_legends_kwargs=None, ): """Add states on canvas and optionally add colors, tooltips and legends. :param tuple figsize: size of the bokeh figure (in pixels). :param dict state_borders_kwargs: keyword argument(s) to be passed to :func:`postreise.add_state_borders`. :param dict state_colors_args: arguments to be passed to :func:`postreise.add_state_colors`. :param dict state_tooltips_args: argument(s) to be passed to :func:`postreise.add_state_tooltips`. :param dict state_legends_kwargs: keyword argument(s) to be passed to :func:`postreise.add_state_legends`. :raises TypeError: if ``state_borders_kwargs`` is not None or dict. if ``state_colors_args`` is not None or dict. if ``state_tooltips_args`` is not None or dict. if ``state_legends_kwargs`` is not None or dict. :return: (*bokeh.plotting.figure.Figure*) -- map of us states. """ for label in [ "state_borders_kwargs", "state_colors_args", "state_tooltips_args", "state_legends_kwargs", ]: var = eval(label) if var is not None and not isinstance(var, dict): raise TypeError(f"{label} must be a dict") # create canvas canvas = create_map_canvas(figsize) # add state borders if state_borders_kwargs is not None: _check_func_kwargs(add_state_borders, set(state_borders_kwargs), "state_borders_kwargs") canvas = add_state_borders(canvas, **state_borders_kwargs) else: canvas = add_state_borders(canvas) if state_colors_args is not None: canvas = add_state_colors(canvas, state_colors_args) if state_tooltips_args is not None: canvas = add_state_tooltips(canvas, state_tooltips_args["title"], state_tooltips_args["label"]) if state_legends_kwargs is not None: _check_func_kwargs(add_state_legends, set(state_legends_kwargs), "state_legends_kwargs") canvas = add_state_legends(canvas, **state_legends_kwargs) return canvas
def test_check_func_kwargs_errors(): def func(a=1, b=2, c=3): return a + b + c arg = ( ("a", "dummy"), (["a"], ["dummy"]), ([1, "b", "c"], "dummy"), ({"d", "e"}, "dummy"), ) for a in arg: with pytest.raises(TypeError): assert _check_func_kwargs(func, a[0], a[1])
def plot_powerflow_snapshot( scenario, hour, b2b_dclines=None, demand_centers=None, ac_branch_color="#8B36FF", dc_branch_color="#01D4ED", solar_color="#FFBB45", wind_color="#78D911", demand_color="gray", figsize=(1400, 800), circle_scale_factor=0.25, bg_width_scale_factor=0.001, pf_width_scale_factor=0.00125, arrow_pf_threshold=3000, arrow_dist_threshold=20, num_ac_arrows=1, num_dc_arrows=1, min_arrow_size=5, branch_alpha=0.5, state_borders_kwargs=None, x_range=None, y_range=None, legend_font_size=None, ): """Plot a snapshot of powerflow. :param powersimdata.scenario.scenario.Scenario scenario: scenario to plot. :param pandas.Timestamp/numpy.datetime64/datetime.datetime hour: snapshot interval. :param dict b2b_dclines: which DC lines are actually B2B facilities. Keys are: {"from", "to"}, values are iterables of DC line indices to plot (indices in "from" get plotted at the "from" end, and vice versa). :param pandas.DataFrame demand_centers: lat/lon centers at which to plot the demand from each load zone. :param str ac_branch_color: color to plot AC branches. :param str dc_branch_color: color to plot DC branches. :param str solar_color: color to plot solar generation. :param str wind_color: color to plot wind generation. :param str demand_color: color to plot demand. :param tuple figsize: size of the bokeh figure (in pixels). :param int/float circle_scale_factor: scale factor for demand/solar/wind circles. :param int/float bg_width_scale_factor: scale factor for grid capacities. :param int/float pf_width_scale_factor: scale factor for power flows. :param int/float arrow_pf_threshold: minimum power flow (MW) for adding arrows. :param int/float arrow_dist_threshold: minimum distance (miles) for adding arrows. :param int num_ac_arrows: number of arrows for each AC branch. :param int num_dc_arrows: number of arrows for each DC branch. :param int/float min_arrow_size: minimum arrow size. :param int/float branch_alpha: opaqueness of branches. :param dict state_borders_kwargs: keyword arguments to be passed to :func:`postreise.plot.plot_states.add_state_borders`. :param tuple(float, float) x_range: x range to zoom plot to (EPSG:3857). :param tuple(float, float) y_range: y range to zoom plot to (EPSG:3857). :param int/str legend_font_size: size to display legend specified as e.g. 12/'12pt'. :return: (*bokeh.plotting.figure*) -- power flow snapshot map. """ _check_scenario_is_in_analyze_state(scenario) _check_date_range_in_scenario(scenario, hour, hour) # Get scenario data grid = scenario.state.get_grid() bus = grid.bus plant = grid.plant # Augment the branch dataframe with extra info needed for plotting branch = grid.branch branch["pf"] = scenario.state.get_pf().loc[hour] branch = branch.query("pf != 0").copy() branch["dist"] = branch.apply(lambda x: haversine((x.from_lat, x.from_lon), (x.to_lat, x.to_lon)), axis=1) branch["arrow_size"] = branch["pf"].abs( ) * pf_width_scale_factor + min_arrow_size branch = project_branch(branch) # Augment the dcline dataframe with extra info needed for plotting dcline = grid.dcline dcline["pf"] = scenario.state.get_dcline_pf().loc[hour] dcline["from_lat"] = dcline.apply(lambda x: bus.loc[x.from_bus_id, "lat"], axis=1) dcline["from_lon"] = dcline.apply(lambda x: bus.loc[x.from_bus_id, "lon"], axis=1) dcline["to_lat"] = dcline.apply(lambda x: bus.loc[x.to_bus_id, "lat"], axis=1) dcline["to_lon"] = dcline.apply(lambda x: bus.loc[x.to_bus_id, "lon"], axis=1) dcline["dist"] = dcline.apply(lambda x: haversine((x.from_lat, x.from_lon), (x.to_lat, x.to_lon)), axis=1) dcline["arrow_size"] = dcline["pf"].abs( ) * pf_width_scale_factor + min_arrow_size dcline = project_branch(dcline) # Create a dataframe for demand plotting, if necessary if demand_centers is not None: demand = scenario.state.get_demand() demand_centers["demand"] = demand.loc[hour] demand_centers = project_bus(demand_centers) # create canvas canvas = create_map_canvas(figsize=figsize, x_range=x_range, y_range=y_range) # Add state borders default_state_borders_kwargs = {"fill_alpha": 0.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) if b2b_dclines is not None: # Append the pseudo AC lines to the branch dataframe, remove from dcline all_b2b_dclines = list(b2b_dclines["to"]) + list(b2b_dclines["from"]) pseudo_ac_lines = dcline.loc[all_b2b_dclines] pseudo_ac_lines["rateA"] = pseudo_ac_lines[["Pmin", "Pmax"]].abs().max(axis=1) branch = branch.append(pseudo_ac_lines) # Construct b2b dataframe so that all get plotted at their 'from' x/y b2b_from = dcline.loc[b2b_dclines["from"]] b2b_to = dcline.loc[b2b_dclines["to"]].rename( { "from_x": "to_x", "from_y": "to_y", "to_x": "from_x", "to_y": "from_y" }, axis=1, ) b2b = pd.concat([b2b_from, b2b_to]) dcline = dcline.loc[~dcline.index.isin(all_b2b_dclines)] # Plot grid background in grey canvas.multi_line( branch[["from_x", "to_x"]].to_numpy().tolist(), branch[["from_y", "to_y"]].to_numpy().tolist(), color="gray", alpha=branch_alpha, line_width=(branch["rateA"].abs() * bg_width_scale_factor), ) canvas.multi_line( dcline[["from_x", "to_x"]].to_numpy().tolist(), dcline[["from_y", "to_y"]].to_numpy().tolist(), color="gray", alpha=branch_alpha, line_width=(dcline[["Pmin", "Pmax"]].abs().max(axis=1) * bg_width_scale_factor), ) if b2b_dclines is not None: canvas.scatter( x=b2b.from_x, y=b2b.from_y, color="gray", alpha=0.5, marker="triangle", size=(b2b[["Pmin", "Pmax"]].abs().max(axis=1) * bg_width_scale_factor), ) fake_location = branch.iloc[0].drop("x").rename({ "from_x": "x", "from_y": "y" }) # Legend entries canvas.multi_line( (fake_location.x, fake_location.x), (fake_location.y, fake_location.y), color=dc_branch_color, alpha=branch_alpha, line_width=10, legend_label="HVDC powerflow", visible=False, ) canvas.multi_line( (fake_location.x, fake_location.x), (fake_location.y, fake_location.y), color=ac_branch_color, alpha=branch_alpha, line_width=10, legend_label="AC powerflow", visible=False, ) canvas.circle( fake_location.x, fake_location.y, color=solar_color, alpha=0.6, size=5, legend_label="Solar Gen.", visible=False, ) canvas.circle( fake_location.x, fake_location.y, color=wind_color, alpha=0.6, size=5, legend_label="Wind Gen.", visible=False, ) # Plot demand if demand_centers is not None: canvas.circle( fake_location.x, fake_location.y, color=demand_color, alpha=0.3, size=5, legend_label="Demand", visible=False, ) canvas.circle( demand_centers.x, demand_centers.y, color=demand_color, alpha=0.3, size=(demand_centers.demand * circle_scale_factor)**0.5, ) # Aggregate solar and wind for plotting plant_with_pg = plant.copy() plant_with_pg["pg"] = scenario.state.get_pg().loc[hour] grouped_solar = aggregate_plant_generation( plant_with_pg.query("type == 'solar'")) grouped_wind = aggregate_plant_generation( plant_with_pg.query("type == 'wind'")) # Plot solar, wind canvas.circle( grouped_solar.x, grouped_solar.y, color=solar_color, alpha=0.6, size=(grouped_solar.pg * circle_scale_factor)**0.5, ) canvas.circle( grouped_wind.x, grouped_wind.y, color=wind_color, alpha=0.6, size=(grouped_wind.pg * circle_scale_factor)**0.5, ) # Plot powerflow on AC branches canvas.multi_line( branch[["from_x", "to_x"]].to_numpy().tolist(), branch[["from_y", "to_y"]].to_numpy().tolist(), color=ac_branch_color, alpha=branch_alpha, line_width=(branch["pf"].abs() * pf_width_scale_factor), ) add_arrows( canvas, branch, color=ac_branch_color, pf_threshold=arrow_pf_threshold, dist_threshold=arrow_dist_threshold, n=num_ac_arrows, ) # Plot powerflow on DC lines canvas.multi_line( dcline[["from_x", "to_x"]].to_numpy().tolist(), dcline[["from_y", "to_y"]].to_numpy().tolist(), color=dc_branch_color, alpha=branch_alpha, line_width=(dcline["pf"].abs() * pf_width_scale_factor), ) add_arrows( canvas, dcline, color=dc_branch_color, pf_threshold=0, dist_threshold=0, n=num_dc_arrows, ) # B2Bs if b2b_dclines is not None: canvas.scatter( x=b2b.from_x, y=b2b.from_y, color=dc_branch_color, alpha=0.5, marker="triangle", size=(b2b["pf"].abs() * pf_width_scale_factor * 5), ) canvas.legend.location = "bottom_left" if legend_font_size is not None: if isinstance(legend_font_size, (int, float)): legend_font_size = f"{legend_font_size}pt" canvas.legend.label_text_font_size = legend_font_size return canvas
def map_carbon_emission_difference( scenario_1, scenario_2, coordinate_rounding=1, figsize=(1400, 800), color_increase=be_red, color_decrease=be_green, scale_factor=1, state_borders_kwargs=None, ): """Make map of difference in emissions between two scenarios at bus level. Color of markers indicates increase/decrease in emissions (``scenario_2`` w.r,t. ``scenario_1``) and size reflects relative difference in emissions. :param powersimdata.scenario.scenario.Scenario scenario_1: scenario instance. :param powersimdata.scenario.scenario.Scenario scenario_2: scenario instance. :param int coordinate_rounding: number of digits to round lat/lon for aggregation. :param tuple figsize: size of the bokeh figure (in pixels). :param str color_increase: color assigned to increase in emissions. :param str color_decrease: color associated to decrease in emissions. :param float scale_factor: scaling factor for size of emissions circles glyphs. :param dict state_borders_kwargs: keyword arguments to be passed to :func:`postreise.plot.plot_states.add_state_borders`. :return: (*bokeh.plotting.figure*) -- carbon emission map. :raises TypeError: if ``coordinate_rounding`` is not ``int``. if ``color_increase`` and ``color_decrease`` are not ``str``. if ``scale_factor`` is not ``int`` or ``float``. :raises ValueError: if ``coordinate_rounding`` is negative. if ``scale_factor`` is negative. """ _check_scenario_is_in_analyze_state(scenario_1) _check_scenario_is_in_analyze_state(scenario_2) 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(color_increase, str): raise TypeError("color_increase must be a str") if not isinstance(color_decrease, str): raise TypeError("color_decrease must be a str") 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") # create canvas canvas = create_map_canvas(figsize=figsize) # add state borders default_state_borders_kwargs = {"fill_alpha": 0.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) # add emission bus_emission_1 = combine_bus_info_and_emission(scenario_1) bus_emission_2 = combine_bus_info_and_emission(scenario_2) emission = bus_emission_1.merge( bus_emission_2, right_index=True, left_index=True, suffixes=("_1", "_2"), how="outer", ) emission.fillna(0, inplace=True) emission["amount"] = (emission["ng_2"] + emission["coal_2"] - emission["ng_1"] - emission["coal_1"]) emission["lon"] = emission["lon_1"].fillna(emission["lon_2"]) emission["lat"] = emission["lat_1"].fillna(emission["lat_2"]) emission = prepare_bus_data_difference(emission, coordinate_rounding, color_increase, color_decrease) canvas = add_emission(canvas, emission, scale_factor) return canvas
def map_carbon_emission_generator( scenario, coordinate_rounding=1, figsize=(1400, 800), color_coal="black", color_ng=be_purple, scale_factor=1, state_borders_kwargs=None, ): """Make map of carbon emissions at bus level. Color of markers indicates generator type and size reflects emissions level. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :param int coordinate_rounding: number of digits to round lat/lon for aggregation. :param tuple figsize: size of the bokeh figure (in pixels). :param str color_ng: color assigned for ng, default to BE purple. :param str color_coal: color associated with coal, default to black/gray. :param int/float scale_factor: scaling factor for size of emissions circles glyphs. :param dict state_borders_kwargs: keyword arguments to be passed to :func:`postreise.plot.plot_states.add_state_borders`. :return: (*bokeh.plotting.figure*) -- carbon emission map. :raises TypeError: if ``coordinate_rounding`` is not ``int``. if ``color_coal`` and ``color_ng`` are not ``str``. if ``scale_factor`` is not ``int`` or ``float``. :raises ValueError: if ``coordinate_rounding`` is negative. 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(color_coal, str): raise TypeError("color_coal must be a str") if not isinstance(color_ng, str): raise TypeError("color_ng must be a str") 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") # create canvas canvas = create_map_canvas(figsize=figsize) # add state borders default_state_borders_kwargs = {"fill_alpha": 0.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) # add emission bus_emission = combine_bus_info_and_emission(scenario) emission = prepare_bus_data_generator(bus_emission, coordinate_rounding, color_ng, color_coal) canvas = add_emission(canvas, emission, scale_factor) return canvas
def map_transmission_upgrades( scenario1, scenario2, b2b_indices=None, figsize=(1400, 800), x_range=None, y_range=None, state_borders_kwargs=None, legend_font_size=20, legend_location="bottom_left", **plot_kwargs, ): """Plot capacity differences for branches & HVDC lines between two scenarios. :param powersimdata.scenario.scenario.Scenario scenario1: first scenario. :param powersimdata.scenario.scenario.Scenario scenario2: second scenario. :param list/set/tuple b2b_indices: indices of HVDC lines which are back-to-backs. :param tuple figsize: size of the bokeh figure (in pixels). :param tuple x_range: x range to zoom plot to (EPSG:3857). :param tuple y_range: y range to zoom plot to (EPSG:3857). :param dict state_borders_kwargs: keyword arguments to be passed to :func:`postreise.plot.plot_states.add_state_borders`. :param int/float legend_font_size: font size for legend. :param str legend_location: location for legend. :param \\*\\*plot_kwargs: collected keyword arguments to be passed to :func:`add_transmission_upgrades`. :raises ValueError: if grid model and interconnect of scenarios differ. :return: (*bokeh.plotting.figure.Figure*) -- map with color-coded upgrades. """ # Validate inputs if not (scenario1.info["grid_model"] == scenario2.info["grid_model"] and scenario1.info["interconnect"] == scenario2.info["interconnect"]): raise ValueError( "Scenarios to compare must be same grid model & interconnect") # Pre-plot data processing grid1 = scenario1.state.get_grid() grid2 = scenario2.state.get_grid() branch_merge = calculate_branch_difference(grid1.branch, grid2.branch) dc_merge = calculate_dcline_difference(grid1, grid2) # Set up figure canvas = create_map_canvas(figsize=figsize, x_range=x_range, y_range=y_range) # Add state outlines default_state_borders_kwargs = { "line_color": "slategrey", "line_width": 1, "fill_alpha": 1, "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) # add transmission map canvas = add_transmission_upgrades(canvas, branch_merge, dc_merge, b2b_indices, **plot_kwargs) canvas.legend.location = legend_location canvas.legend.label_text_font_size = f"{legend_font_size}pt" return canvas
def map_interconnections( grid, branch_distance_cutoff=5, figsize=(1400, 800), branch_width_scale_factor=0.5, hvdc_width_scale_factor=1, b2b_size_scale_factor=50, state_borders_kwargs=None, ): """Map transmission lines color coded by interconnection. :param powersimdata.input.grid.Grid grid: grid object. :param int/float branch_distance_cutoff: distance cutoff for branch display. :param tuple figsize: size of the bokeh figure (in pixels). :param int/float branch_width_scale_factor: scale factor for branch capacities. :param int/float hvdc_width_scale_factor: scale factor for hvdc capacities. :param int/float b2b_size_scale_factor: scale factor for back-to_back capacities. :param dict state_borders_kwargs: keyword arguments to be passed to :func:`postreise.plot.plot_states.add_state_borders`. :return: (*bokeh.plotting.figure*) -- interconnection map with lines and nodes. :raises TypeError: if ``branch_device_cutoff`` is not ``float``. if ``branch_width_scale_factor`` is not ``int`` or ``float``. if ``hvdc_width_scale_factor`` is not ``int`` or ``float``. if ``b2b_size_scale_factor`` is not ``int`` or ``float``. :raises ValueError: if ``branch_device_cutoff`` is negative. if ``branch_width_scale_factor`` is negative. if ``hvdc_width_scale_factor`` is negative. if ``b2b_size_scale_factor`` is negative. if grid model is not supported. """ _check_grid_type(grid) if not isinstance(branch_distance_cutoff, (int, float)): raise TypeError("branch_distance_cutoff must be an int") if branch_distance_cutoff <= 0: raise ValueError("branch_distance_cutoff must be strictly positive") if not isinstance(branch_width_scale_factor, (int, float)): raise TypeError("branch_width_scale_factor must be a int/float") if branch_width_scale_factor < 0: raise ValueError("branch_width_scale_factor must be positive") if not isinstance(hvdc_width_scale_factor, (int, float)): raise TypeError("hvdc_width_scale_factor must be a int/float") if hvdc_width_scale_factor < 0: raise ValueError("hvdc_width_scale_factor must be positive") if not isinstance(b2b_size_scale_factor, (int, float)): raise TypeError("b2b_size_scale_factor must be a int/float") if b2b_size_scale_factor < 0: raise ValueError("b2b_size_scale_factor must be positive") # branches branch = grid.branch.copy() branch["to_coord"] = list(zip(branch["to_lat"], branch["to_lon"])) branch["from_coord"] = list(zip(branch["from_lat"], branch["from_lon"])) branch["dist"] = branch.apply( lambda row: distance.haversine(row["to_coord"], row["from_coord"]), axis=1) branch = branch.loc[branch["dist"] > branch_distance_cutoff] branch = project_branch(branch) branch_west = branch.loc[branch["interconnect"] == "Western"] branch_east = branch.loc[branch["interconnect"] == "Eastern"] branch_tx = branch.loc[branch["interconnect"] == "Texas"] # HVDC lines all_dcline = grid.dcline.copy() all_dcline["from_lon"] = grid.bus.loc[all_dcline["from_bus_id"], "lon"].values all_dcline["from_lat"] = grid.bus.loc[all_dcline["from_bus_id"], "lat"].values all_dcline["to_lon"] = grid.bus.loc[all_dcline["to_bus_id"], "lon"].values all_dcline["to_lat"] = grid.bus.loc[all_dcline["to_bus_id"], "lat"].values all_dcline = project_branch(all_dcline) if grid.grid_model == "usa_tamu": b2b_id = range(9) else: raise ValueError("grid model is not supported") dcline = all_dcline.iloc[~all_dcline.index.isin(b2b_id)] b2b = all_dcline.iloc[b2b_id] # create canvas canvas = create_map_canvas(figsize=figsize) # add state borders default_state_borders_kwargs = { "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) # add state tooltips state_counts = count_nodes_per_state(grid) state2label = { s: c for s, c in zip(state_counts.index, state_counts.to_numpy()) } canvas = add_state_tooltips(canvas, "nodes", state2label) canvas.multi_line( branch_west[["from_x", "to_x"]].to_numpy().tolist(), branch_west[["from_y", "to_y"]].to_numpy().tolist(), color="#006ff9", line_width=branch_west["rateA"].abs() * 1e-3 * branch_width_scale_factor, legend_label="Western", ) canvas.multi_line( branch_east[["from_x", "to_x"]].to_numpy().tolist(), branch_east[["from_y", "to_y"]].to_numpy().tolist(), color="#8B36FF", line_width=branch_east["rateA"].abs() * 1e-3 * branch_width_scale_factor, legend_label="Eastern", ) canvas.multi_line( branch_tx[["from_x", "to_x"]].to_numpy().tolist(), branch_tx[["from_y", "to_y"]].to_numpy().tolist(), color="#01D4ED", line_width=branch_tx["rateA"].abs() * 1e-3 * branch_width_scale_factor, legend_label="Texas", ) canvas.multi_line( dcline[["from_x", "to_x"]].to_numpy().tolist(), dcline[["from_y", "to_y"]].to_numpy().tolist(), color="#FF2370", line_width=dcline["Pmax"] * 1e-3 * hvdc_width_scale_factor, legend_label="HVDC", ) canvas.scatter( x=b2b["from_x"], y=b2b["from_y"], color="#FF2370", marker="triangle", size=b2b["Pmax"] * 1e-3 * b2b_size_scale_factor, legend_label="Back-to-Back", ) canvas.legend.location = "bottom_left" canvas.legend.label_text_font_size = "12pt" return canvas
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
def map_risk_bind( risk_or_bind, scenario=None, congestion_stats=None, branch=None, us_states_dat=None, vmin=None, vmax=None, color_bar_width=500, palette=None, all_branch_scale_factor=0.5, all_branch_min_width=0.2, select_branch_scale_factor=1, select_branch_min_width=2, figsize=(1400, 800), show_color_bar=True, state_borders_kwargs=None, ): """Make map showing risk or binding incidents on US states map. Either ``scenario`` XOR (``congestion_stats`` AND ``branch``) must be specified. :param str risk_or_bind: specify plotting "risk" or "bind". :param powersimdata.scenario.scenario.Scenario scenario: scenario to analyze. :param pandas.DataFrame congestion_stats: data frame as returned by :func:`postreise.analyze.transmission.utilization.generate_cong_stats`. :param pandas.DataFrame branch: branch data frame. :param int/float vmin: minimum value for color range. If None, use data minimum. :param int/float vmax: maximum value for color range. If None, use data maximum. :param int color_bar_width: width of color bar (pixels). :param iterable palette: sequence of colors used for color range, passed as `palette` kwarg to :func:`bokeh.transform.linear_cmap`. If None, default to `postreise.plot.colors.traffic_palette`. :param int/float all_branch_scale_factor: scale factor for unhighlighted branches (pixels/GW). :param int/float all_branch_min_width: minimum width for unhighlighted branches (pixels). :param int/float select_branch_scale_factor: scale factor for highlighted branches (pixels/GW). :param int/float select_branch_min_width: minimum width for highlighted branches (pixels). :param tuple(int, int) figsize: size of the bokeh figure (in pixels). :param bool show_color_bar: whether to render the color bar on the figure. :param dict state_borders_kwargs: keyword arguments to be passed to :func:`postreise.plot.plot_states.add_state_borders`. :raises ValueError: if (``scenario`` XOR (``congestion_stats`` AND ``branch``)) are not properly specified. :return: (*bokeh.plotting.figure*) -- map of lines with risk and bind incidents color coded. """ unit_labels = {"risk": "Risk (MWH)", "bind": "Binding incidents"} if risk_or_bind not in unit_labels: raise ValueError("risk_or_bind must be either 'risk' or 'bind'") risk_or_bind_units = unit_labels[risk_or_bind] # Check that we've appropriately specified: # `scenario` XOR (`congestion_stats` AND `branch`) if scenario is not None: branch = scenario.state.get_grid().branch congestion_stats = generate_cong_stats(scenario.state.get_pf(), branch) elif congestion_stats is not None and branch is not None: pass else: raise ValueError( "Either scenario XOR (congestion_stats AND branch) must be specified" ) if palette is None: palette = list(traffic_palette) # projection steps for mapping branch_congestion = pd.concat( [branch.loc[congestion_stats.index], congestion_stats], axis=1) branch_map_all = project_branch(branch) branch_congestion = branch_congestion[branch_congestion[risk_or_bind] > 0] branch_congestion.sort_values(by=[risk_or_bind]) branch_map = project_branch(branch_congestion) min_val = branch_congestion[risk_or_bind].min() if vmin is None else vmin max_val = branch_congestion[risk_or_bind].max() if vmax is None else vmax mapper = linear_cmap(field_name=risk_or_bind, palette=palette, low=min_val, high=max_val) multi_line_source = ColumnDataSource({ "xs": branch_map[["from_x", "to_x"]].values.tolist(), "ys": branch_map[["from_y", "to_y"]].values.tolist(), risk_or_bind: branch_map[risk_or_bind], "value": branch_map[risk_or_bind].round(), "cap": (branch_map["capacity"] * select_branch_scale_factor / 1000 + select_branch_min_width), "capacity": branch_map.rateA.round(), }) # Create canvas canvas = create_map_canvas(figsize=figsize) # Add state borders default_state_borders_kwargs = {"fill_alpha": 0.0, "background_map": True} 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) # Add color bar if show_color_bar: color_bar = ColorBar( color_mapper=mapper["transform"], width=color_bar_width, height=5, location=(0, 0), title=risk_or_bind_units, orientation="horizontal", padding=5, ) canvas.add_layout(color_bar, "center") canvas.multi_line( branch_map_all[["from_x", "to_x"]].to_numpy().tolist(), branch_map_all[["from_y", "to_y"]].to_numpy().tolist(), color="gray", line_width=(branch_map_all["rateA"] * all_branch_scale_factor / 1000 + all_branch_min_width), alpha=0.5, ) lines = canvas.multi_line("xs", "ys", color=mapper, line_width="cap", source=multi_line_source) hover = HoverTool( tooltips=[ ("Capacity MW", "@capacity"), (risk_or_bind_units, "@value"), ], renderers=[lines], ) canvas.add_tools(hover) return canvas
def map_utilization( scenario=None, utilization_df=None, branch=None, vmin=None, vmax=None, color_bar_width=500, palette=None, branch_scale_factor=0.5, branch_min_width=0.2, figsize=(1400, 800), show_color_bar=True, state_borders_kwargs=None, ): """Make map showing utilization. Utilization input can either be medians only, or can be normalized utilization dataframe. Either ``scenario`` XOR (``utilization_df`` AND ``branch``) must be specified. :param powersimdata.scenario.scenario.Scenario scenario: scenario to analyze. :param pandas.DataFrame utilization_df: utilization returned by :func:`postreise.analyze.transmission.utilization.get_utilization` :param pandas.DataFrame branch: branch data frame. :param int/float vmin: minimum value for color range. If None, use data minimum. :param int/float vmax: maximum value for color range. If None, use data maximum. :param int color_bar_width: width of color bar (pixels). :param iterable palette: sequence of colors used for color range, passed as `palette` kwarg to :func:`bokeh.transform.linear_cmap`. If None, default to `postreise.plot.colors.traffic_palette`. :param int/float branch_scale_factor: scale factor for branches (pixels/GW). :param int/float branch_min_width: minimum width for branches (pixels). :param tuple(int, int) figsize: size of the bokeh figure (in pixels). :param dict state_borders_kwargs: keyword arguments to be passed to :func:`postreise.plot.plot_states.add_state_borders`. :raises ValueError: if (``scenario`` XOR (``utilization_df`` AND ``branch``)) are not properly specified. :return: (*bokeh.plotting.figure*) -- map of lines with median utilization color coded. """ # Check that we've appropriately specified: # `scenario` XOR (`utilization_df` AND `branch`) if scenario is not None: branch = scenario.state.get_grid().branch utilization_df = get_utilization(branch, scenario.state.get_pf(), median=True) elif utilization_df is not None and branch is not None: pass else: raise ValueError( "Either scenario XOR (utilization_df AND branch) must be specified" ) if palette is None: palette = list(traffic_palette) branch_mask = branch.rateA != 0 median_util = utilization_df[branch.loc[branch_mask].index].median() branch_utilization = pd.concat( [branch.loc[branch_mask], median_util.rename("median_utilization")], axis=1) lines = branch_utilization.loc[( branch_utilization["branch_device_type"] == "Line")] min_val = lines["median_utilization"].min() if vmin is None else vmin max_val = lines["median_utilization"].max() if vmax is None else vmax mapper = linear_cmap( field_name="median_utilization", palette=palette, low=min_val, high=max_val, ) branch_map = project_branch(branch_utilization) branch_map = branch_map.sort_values(by=["median_utilization"]) branch_map = branch_map[~branch_map.isin([np.nan, np.inf, -np.inf]).any(1)] multi_line_source = ColumnDataSource({ "xs": branch_map[["from_x", "to_x"]].values.tolist(), "ys": branch_map[["from_y", "to_y"]].values.tolist(), "median_utilization": branch_map.median_utilization, "width": branch_map.rateA * branch_scale_factor / 1000 + branch_min_width, "util": branch_map.median_utilization.round(2), "capacity": branch_map.rateA.round(), }) # Create canvas canvas = create_map_canvas(figsize=figsize) # Add state borders default_state_borders_kwargs = {"fill_alpha": 0.0, "line_width": 2} 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) # Add color bar if show_color_bar: color_bar = ColorBar( color_mapper=mapper["transform"], width=color_bar_width, height=5, location=(0, 0), title="median utilization", orientation="horizontal", padding=5, ) canvas.add_layout(color_bar, "center") lines = canvas.multi_line("xs", "ys", color=mapper, line_width="width", source=multi_line_source) hover = HoverTool( tooltips=[ ("Capacity MW", "@capacity"), ("Utilization", "@util{f0.00}"), ], renderers=[lines], ) canvas.add_tools(hover) return canvas
def test_check_func_kwargs(): def func(a=1, b=2, c=3): return a * b * c assert _check_func_kwargs(func, ["a", "b"], "dummy") is None assert _check_func_kwargs(func, ["a", "b", "c"], "dummy") is None
def map_plant_capacity( scenario, resources, figsize=(1400, 800), x_range=None, y_range=None, disaggregation=None, state_borders_kwargs=None, min_capacity=1, size_factor=1, alpha=0.5, legend_font_size=12, legend_location="bottom_right", ): """Make map of plant capacities, optionally disaggregated by new/existing. Area is proportional to capacity. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :param iterable resources: which types of resources to plot. :param tuple figsize: size of the bokeh figure (in pixels). :param tuple x_range: x range to zoom plot to (EPSG:3857). :param tuple y_range: y range to zoom plot to (EPSG:3857). :param str disaggregation: method used to disaggregate plants: if "new_vs_existing_plants": separates plants into added vs. existing. if None, no disaggregation. :param dict state_borders_kwargs: keyword arguments to pass to :func:`postreise.plot.plot_states.add_state_borders`. :param float/int min_capacity: minimum bus capacity (MW) for markers to be plotted. :param float/int size_factor: scale size of glyphs. :param float/int alpha: opacity of circles (between 0 and 1). :param int/float legend_font_size: font size for legend. :param str legend_location: location for legend. :return: (*bokeh.plotting.figure.Figure*) -- map with color-coded upgrades. """ _check_scenario_is_in_analyze_state(scenario) # create canvas canvas = create_map_canvas(figsize=figsize, x_range=x_range, y_range=y_range) # add state borders default_state_borders_kwargs = { "line_color": "gray", "line_width": 2, "fill_alpha": 0, "background_map": True, } 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) # add plant capacity add_plant_capacity( canvas, scenario, resources, disaggregation, min_capacity, size_factor, alpha, ) canvas.legend.label_text_font_size = f"{legend_font_size}pt" canvas.legend.location = legend_location return canvas