def _prepare_busmap(bus_info_and_emission, color_ng, color_coal, agg, type1="natural gas", type2="coal"): """Prepare data with amount of emissions and type for hover tips :param pandas.DataFrame bus_info_and_emission: info and emission of buses as returned by :func:`combine_bus_info_and_emission`. :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 boolean agg: if true, aggregates points by lat lon within a given radius :param str type1: label for hover over tool tips, first color/type (usual choices: natural gas or increase if making a diff map) :param str type2: label for hover over tool tips, second color/type (usual choices: coal or decrease if making diff map) :return: (pandas.DataFrame) -- data frame with amount and tye fields """ bus_map = bus_info_and_emission bus_map["color"] = "" bus_map["type"] = "" bus_map.loc[(bus_map["ng"] > 0), "color"] = color_ng bus_map.loc[(bus_map["coal"] > 0), "color"] = color_coal bus_map.loc[(bus_map["ng"] > 0), "type"] = type1 bus_map.loc[(bus_map["coal"] > 0), "type"] = type2 bus_map = project_bus(bus_map) bus_map1 = group_lat_lon(bus_map, agg=agg) bus_map1["amount"] = bus_map1["ng"] + bus_map1["coal"] return bus_map1
def _get_shadowprice_data(scenario_id): """Gets data necessary for plotting shadow price :param str/int scenario_id: scenario id :return: (*tuple*) -- interconnect as a str, bus data as a data frame, lmp data as a data frame, branch data as a data frame and congestion data as a data frame """ s = Scenario(scenario_id) interconnect = s.info["interconnect"] interconnect = " ".join(interconnect.split("_")) s_grid = s.state.get_grid() # Get bus and add location data bus_map = project_bus(s_grid.bus) # get branch and add location data branch_map = project_branch(s_grid.branch) # get congestion congu = s.state.get_congu() congl = s.state.get_congl() cong_abs = pd.DataFrame( np.maximum(congu.to_numpy(), congl.to_numpy()), columns=congu.columns, index=congu.index, ) return interconnect, bus_map, s.state.get_lmp(), branch_map, cong_abs
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
def aggregate_plant_generation(plant, coordinate_rounding=0): """Aggregate generation for plants based on similar lat/lon coordinates. :param int coordinate_rounding: number of digits to round lat & lon for aggregation. :param pandas.DataFrame plant: data frame containing: 'lat', 'lon', 'x', 'y', 'pg'. :return: (*pandas.DataFrame*) -- data frame aggregated to the desired precision. """ plant_w_xy = project_bus(plant) plant_w_xy["lat"] = plant_w_xy["lat"].round(coordinate_rounding) plant_w_xy["lon"] = plant_w_xy["lon"].round(coordinate_rounding) aggregated = plant_w_xy.groupby(["lat", "lon"]).agg({ "pg": "sum", "x": "mean", "y": "mean" }) return aggregated
def aggregate_bus_emission_difference(bus, coordinate_rounding): """Aggregate emission for buses based on similar lat/lon coordinates :param pandas.DataFrame bus: bus data frame containing 'coal', 'ng', 'lat', 'lon' 'type' and 'color' 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({ "amount": "sum", "x": "mean", "y": "mean" }) aggregated = aggregated.reset_index() return aggregated
def map_lmp(s_grid, lmp, us_states_dat=None, lmp_min=20, lmp_max=45, is_website=False): """Plots average LMP by color coding buses :param powersimdata.input.grid.Grid s_grid: scenario grid :param pandas.DataFrame lmp: locational marginal prices calculated for the scenario :param dict us_states_dat: dictionary of state border lats/lons. If None, get from :func:`postreise.plot.plot_states.get_state_borders`. :param inf/float lmp_min: minimum LMP to clamp plot range to. :param inf/float lmp_max: maximum LMP to clamp plot range to. :param bool is_website: changes text/legend formatting to look better on the website :return: (*bokeh.models.layout.Row*) bokeh map visual in row layout """ if us_states_dat is None: us_states_dat = get_state_borders() bus = project_bus(s_grid.bus) bus_segments = _construct_bus_data(bus, lmp, lmp_min, lmp_max) return _construct_shadowprice_visuals( bus_segments, us_states_dat, lmp_min, lmp_max, is_website )
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_plant_capacity(scenario, us_states_dat=None, size_factor=1): """Makes map of renewables from change table 'new plants'. Size/area indicates capacity. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :param dict us_states_dat: dictionary of state border lats/lons. If None, get from :func:`postreise.plot.plot_states.get_state_borders`. :param float/int size_factor: scale size of glyphs. """ _check_scenario_is_in_analyze_state(scenario) # prepare data from the change table to select new plants ct = scenario.state.get_ct() # check that there are new plants, check scenario is in analyze state if "new_plant" not in ct.keys(): raise ValueError( "There are no new plants added in the selected scenario. Please choose a different scenario." ) if us_states_dat is None: us_states_dat = get_state_borders() newplant = pd.DataFrame(ct["new_plant"]) newplant = newplant.set_index("bus_id") newplant = newplant[newplant.Pmax > 0] # merge with bus info to get coordinates gridscen = scenario.state.get_grid() bus_of_interest = gridscen.bus.loc[list(set(newplant.index))] bus_capacity = bus_of_interest.merge(newplant, right_index=True, left_index=True) bus_map = project_bus(bus_capacity) bus_map1 = bus_map.loc[bus_map.Pmax > 1] rar_df = bus_map1.loc[(bus_map1.type_y == "solar") | (bus_map1.type_y == "wind")] # group by coordinates rar_df = rar_df.groupby(["lat", "lon"]).agg({ "Pmax": "sum", "x": "mean", "y": "mean" }) a, b = project_borders(us_states_dat) rar_source = ColumnDataSource({ "x": rar_df["x"], "y": rar_df["y"], "capacity": (rar_df["Pmax"] * size_factor)**0.5, "capacitymw": rar_df["Pmax"], }) tools: str = "pan,wheel_zoom,reset,save" p = figure( tools=tools, x_axis_location=None, y_axis_location=None, plot_width=800, plot_height=800, output_backend="webgl", sizing_mode="stretch_both", match_aspect=True, ) # for legend, plot behind tiles p.circle( -8.1e6, 5.2e6, fill_color=be_green, color=be_green, alpha=0.5, size=50, legend_label="Renewable Capacity Added", ) p.add_tile(get_provider(Vendors.CARTODBPOSITRON_RETINA)) # state borders p.patches(a, b, fill_alpha=0.0, line_color="gray", line_width=2) # capacity circles circle = p.circle( "x", "y", fill_color=be_green, color=be_green, alpha=0.8, size="capacity", source=rar_source, ) p.legend.label_text_font_size = "12pt" p.legend.location = "bottom_right" hover = HoverTool( tooltips=[ ("Capacity (MW)", "@capacitymw"), ], renderers=[circle], ) p.add_tools(hover) return p
def map_carbon_emission_bar( bus_info_and_emission, scenario_name, color_coal="black", color_ng="purple", us_states_dat=None, size_factor=1.0, ): """Makes map of carbon emissions, color code by fuel type. Size/area indicates emissions. :param dict us_states_dat: dictionary of state border lats/lons. If None, get from :func:`postreise.plot.plot_states.get_state_borders`. :param pandas.DataFrame bus_info_and_emission: info and emission of buses by :func:`combine_bus_info_and_emission`. :param str scenario_name: name of scenario for labeling. :param str color_coal: color assigned for coal :param str color_ng: color assigned for natural gas :param float size_factor: height scale for bars """ if us_states_dat is None: us_states_dat = get_state_borders() bus_map = project_bus(bus_info_and_emission) bus_map = group_zone(bus_map) a, b = project_borders(us_states_dat) # plotting adjustment constants ha = 85000 size_factor = size_factor * 0.02 bus_source = ColumnDataSource({ "x": bus_map["x"], "x2": bus_map["x"] + ha * 2, "y": bus_map["y"], "coaly": bus_map["y"] + bus_map["coal"] * size_factor, "ngy": bus_map["y"] + bus_map["ng"] * size_factor, }) # Set up figure tools: str = "pan,wheel_zoom,reset,hover,save" p = figure( title=scenario_name, tools=tools, x_axis_location=None, y_axis_location=None, plot_width=800, plot_height=800, ) p_legend = figure( x_axis_location=None, y_axis_location=None, toolbar_location=None, plot_width=200, plot_height=200, y_range=(0, 2), x_range=(0, 2), ) p.add_tile(get_provider(Vendors.CARTODBPOSITRON_RETINA)) # state borders p.patches(a, b, fill_alpha=0.0, line_color="black", line_width=2) # emissions bars width = 150000.0 alpha = 0.7 p.vbar( x="x2", bottom="y", top="coaly", width=width, color=color_coal, source=bus_source, alpha=alpha, ) p.vbar( x="x", bottom="y", top="ngy", width=width, color=color_ng, source=bus_source, alpha=alpha, ) bus_map = pd.DataFrame(bus_map) # citation fields cit_x = [] cit_y = [] cit_text = [] # constants for adjustments of text labels va = 1000 m = 1000000 # x values are offset so vbars are side by side. cit_x.append([i - ha * 1.5 for i in bus_map.x.values.tolist()]) cit_x.append([i + ha for i in bus_map.x.values.tolist()]) cit_y.append( np.add( [i + va * 2 for i in bus_map.y.values.tolist()], [i * size_factor for i in bus_map.ng.values.tolist()], )) cit_y.append( np.add( [i + va * 2 for i in bus_map.y.values.tolist()], [i * size_factor for i in bus_map.coal.values.tolist()], )) cit_text.append( [round(num, 1) for num in [i / m for i in bus_map.ng.values.tolist()]]) cit_text.append([ round(num, 1) for num in [i / m for i in bus_map.coal.values.tolist()] ]) for j in range(0, len(cit_x)): for i in range(0, len(cit_x[j])): citation = Label( x=cit_x[j][i], y=cit_y[j][i], x_units="data", y_units="data", text_font_size="20pt", text=str(cit_text[j][i]), render_mode="css", border_line_color="black", border_line_alpha=0, background_fill_color="white", background_fill_alpha=0.8, ) p.add_layout(citation) # custom legend p_legend.square(1, 1, fill_color="white", color="white", size=600) p_legend.square(1, 1, fill_color="white", color="black", size=400) p_legend.square(0.4, 0.8, fill_color="black", color="black", size=40, alpha=0.7) p_legend.square(0.4, 0.2, fill_color="purple", color="purple", size=40, alpha=0.7) source = ColumnDataSource(data=dict( x=[0.7, 0.7, 0.05], y=[0.6, 0.1, 1.5], text=["Coal", "Natural Gas", "Emissions (Million Tons)"], )) labels = LabelSet( x="x", y="y", text="text", source=source, level="glyph", x_offset=0, y_offset=0, render_mode="css", text_font_size="20pt", ) p_legend.add_layout(labels) return row(p_legend, p)
def map_carbon_emission_comparison(bus_info_and_emission_1, bus_info_and_emission_2, web=True): """Makes map of carbon emissions, color code by fuel type, size/area indicates emissions Also, returns data frame enclosing emission released by thermal generators. :param pandas.DataFrame bus_info_and_emission_1: info and emission of buses for 1st scenario returned by :func:`combine_bus_info_and_emission`. :param pandas.DataFrame bus_info_and_emission_2: info and emission of buses for 2nd scenario as returned by :func:`combine_bus_info_and_emission`. :param boolean web: if true, optimizes figure for web-based presentation :return: (pandas.DataFrame) -- comparison map indicating increase or decrease in emission """ # merge bus_info_and_emission_1 = bus_info_and_emission_1.fillna(0) bus_info_and_emission_2 = bus_info_and_emission_2.fillna(0) bus_info_and_emission_1["amt"] = (bus_info_and_emission_1["ng"] + bus_info_and_emission_1["coal"]) bus_info_and_emission_2["amt"] = (bus_info_and_emission_2["ng"] + bus_info_and_emission_2["coal"]) bus_map = bus_info_and_emission_1.merge( bus_info_and_emission_2, right_index=True, left_index=True, suffixes=("_x", "_y"), how="outer", ) # fill 0s so we can subtract bus_map = bus_map.fillna(0) bus_map["amt_dif"] = bus_map["amt_x"] - bus_map["amt_y"] bus_map["lon"] = bus_map["lon_x"].fillna(bus_map["lon_y"]) bus_map["lat"] = bus_map["lat_x"].fillna(bus_map["lat_y"]) bus_map["coal"] = 0 bus_map["ng"] = 0 bus_map.loc[bus_map.amt_dif > 0, ["coal"]] = bus_map["amt_dif"] bus_map.loc[bus_map.amt_dif < 0, ["ng"]] = abs(bus_map["amt_dif"]) bus_map2 = bus_map[bus_map.amt_dif != 0] bus_map2 = project_bus(bus_map2) bus_map2 = _prepare_busmap(bus_map2, be_green, be_red, agg=False, type1="increase", type2="decrease") p = map_carbon_emission( bus_map2, be_green, be_red, u"Less tons CO\u2082", u"More tons CO\u2082", web=web, type1="increase", type2="decrease", ) return p
def add_plant_capacity( canvas, scenario, resources, disaggregation=None, min_capacity=1, size_factor=1, alpha=0.5, ): """Adds renewables capacity to a plot. :param bokeh.plotting.figure.Figure canvas: canvas to plot capacities onto. :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 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). :raises ValueError: if ``disaggregation`` is not 'new_vs_existing_plants' or None. :return: (*bokeh.plotting.figure.Figure*) -- map with color-coded upgrades. """ ct = scenario.get_ct() grid = scenario.get_grid() grid.plant["lat"] = grid.plant["lat"].round(3) grid.plant["lon"] = grid.plant["lon"].round(3) grid.plant = project_bus(grid.plant.query("type in @resources")) type_colors = grid.model_immutables.plants["type2color"] xy_capacity = {} if disaggregation is None: grouped_capacities = grid.plant.groupby(["x", "y", "type"]).sum().Pmax grouped_capacities = grouped_capacities.reset_index() xy_capacity["all"] = grouped_capacities.query("Pmax > 0") elif disaggregation == "new_vs_existing_plants": if "new_plant" in ct.keys(): num_new_plants = len(ct["new_plant"]) scaled_plants = grid.plant.iloc[:-num_new_plants] new_plants = grid.plant.iloc[-num_new_plants:] grouped_new_capacities = new_plants.groupby(["x", "y", "type"]).sum().Pmax grouped_new_capacities = grouped_new_capacities.reset_index() xy_capacity["new"] = grouped_new_capacities.query("Pmax > 0") else: scaled_plants = grid.plant new_plants = pd.DataFrame(columns=grid.plant.columns) xy_capacity["new"] = pd.DataFrame( columns=["x", "y", "type", "Pmax"]) grouped_capacities = scaled_plants.groupby(["x", "y", "type"]).sum().Pmax grouped_capacities = grouped_capacities.reset_index() xy_capacity["existing"] = grouped_capacities.query("Pmax > 0") else: raise ValueError(f"Unknown disaggregation method: {disaggregation}") # capacity circles renderers = [] for tranche, plants in xy_capacity.items(): for resource in sorted(resources): if disaggregation is None: legend_label = f"{resource} capacity" elif disaggregation == "new_vs_existing_plants": legend_label = f"{resource} capacity of {tranche} plants" if resource not in plants.type.unique(): print(f"no {resource} plants for grouping: {tranche}") continue matching_plants = plants.query("type == @resource") data = { "x": matching_plants["x"], "y": matching_plants["y"], "capacity": matching_plants["Pmax"], "radius": matching_plants["Pmax"]**0.5 * size_factor, } circle = canvas.circle( "x", "y", color=mcolors.to_hex(type_colors[resource]), alpha=0.8, size="radius", source=ColumnDataSource(data), legend_label=legend_label, ) renderers.append(circle) hover = HoverTool( tooltips=[ ("Capacity (MW)", "@capacity"), ], renderers=renderers, ) canvas.add_tools(hover) return canvas