def carbon_diff(scenario_1, scenario_2): """Prints percentage change in carbon emissions of two scenarios. :param powersimdata.scenario.scenario.Scenario scenario_1: scenario instance. :param powersimdata.scenario.scenario.Scenario scenario_2: scenario instance. :return: (*float*) -- relative difference in emission in percent. """ carbon_by_bus_1 = summarize_emissions_by_bus( generate_emissions_stats(scenario_1), scenario_1.state.get_grid()) carbon_by_bus_2 = summarize_emissions_by_bus( generate_emissions_stats(scenario_2), scenario_2.state.get_grid()) sum_1 = sum(carbon_by_bus_1["coal"].values()) + sum( carbon_by_bus_1["ng"].values()) sum_2 = sum(carbon_by_bus_2["coal"].values()) + sum( carbon_by_bus_2["ng"].values()) return 100 * (1 - sum_2 / sum_1)
def plot_carbon_bar(*args, labels=None, labels_size=15, show_plot=True): """Make bar chart of carbon emissions by fuel type for n scenarios. :param powersimdata.scenario.scenario.Scenario args: scenario instances. :param list/tuple/set labels: labels on plot. Default to scenario id. :param int labels_size: size of labels. :param bool show_plot: whether to save the plot. :raises ValueError: if ``args`` are not scenario instances. if ``labels`` has a different length than the number of passed scenarios. :raises TypeError: if ``labels`` is not an iterable. if ``labels_size`` is not an int. :return: (*tuple*) -- the figure elements that can be further modified. """ if not all([isinstance(a, Scenario) for a in args]): raise ValueError("all inputs must be Scenario objects") if labels is not None and not isinstance(labels, (list, tuple, set)): raise TypeError("labels must be a list/tuple/set") if labels is not None and len(args) != len(labels): raise ValueError("labels must have same length as number of scenarios") if not isinstance(labels_size, int): raise TypeError("labels_size must be an integer") labels = tuple([s.info["id"] for s in args]) if labels is None else tuple(labels) carbon_val = {"coal": [], "ng": []} for i, s in enumerate(args): grid = s.state.get_grid() carbon_by_bus = summarize_emissions_by_bus(generate_emissions_stats(s), grid) carbon_val["coal"].append(sum(carbon_by_bus["coal"].values())) carbon_val["ng"].append(sum(carbon_by_bus["ng"].values())) fig, (ax1, ax2) = plt.subplots(1, 2, sharey=True, figsize=(14, len(labels))) y_pos = np.arange(len(labels)) for a, f, c, t in zip( [ax1, ax2], ["coal", "ng"], ["black", "purple"], ["Coal: CO$_2$ Emissions", "Natural Gas: CO$_2$ Emissions"], ): a.barh(y_pos, carbon_val[f], align="center", alpha=0.25, color=c) a.set_xlabel("Tons", fontsize=labels_size + 3) a.set_title(t, y=1.04, fontsize=labels_size + 5) a.tick_params(axis="both", labelsize=labels_size) plt.yticks(y_pos, labels) plt.subplots_adjust(wspace=0.1) if show_plot: plt.show() return ax1, ax2
def test_calculate_so2_simple(self, scenario): expected_values = np.array( [ [0, 0, 0, 0, 0], [0, 0, 3.0000e-05, 3.8600e-03, 1.0945e-02], [0, 0, 6.0000e-05, 7.7200e-03, 2.1890e-02], [0, 0, 9.0000e-05, 1.1580e-02, 3.2835e-02], ] ) nox = generate_emissions_stats(scenario, pollutant="so2", method="simple") assert_array_almost_equal( expected_values, nox.to_numpy(), err_msg="Values do not match expected" )
def test_calculate_nox_simple(self, scenario): expected_values = np.array( [ [0, 0, 0, 0, 0], [0, 0, 0.000537, 0.002632, 0.007685], [0, 0, 0.001074, 0.005264, 0.015370], [0, 0, 0.001611, 0.007896, 0.023055], ] ) nox = generate_emissions_stats(scenario, pollutant="nox", method="simple") assert_array_almost_equal( expected_values, nox.to_numpy(), err_msg="Values do not match expected" )
def combine_bus_info_and_emission(scenario): """Build data frame needed for plotting carbon emitted by thermal generators. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :return: (*pandas.DataFrame*) -- bus data frame with emission. """ grid = scenario.state.get_grid() carbon_by_bus = summarize_emissions_by_bus( generate_emissions_stats(scenario), grid) selected = grid.bus.loc[pd.DataFrame.from_dict(carbon_by_bus).index] bus_w_emission = selected.merge(pd.DataFrame.from_dict(carbon_by_bus), right_index=True, left_index=True) return bus_w_emission
def test_carbon_calc_always_on(self, scenario, mock_plant): carbon = generate_emissions_stats(scenario, method="always-on") _test_emissions_structure(carbon, mock_plant, scenario.state.get_pg()) # check specific values expected_values = np.array( [ [0, 0, 4.82, 8.683333, 6.77], [0, 0, 6.6998, 13.546000, 11.8475], [0, 0, 9.4472, 21.1873333, 20.3100], [0, 0, 13.0622, 31.6073333, 32.1575], ] ) assert_array_almost_equal( expected_values, carbon.to_numpy(), err_msg="Values do not match expected" )
def test_carbon_calc_simple(self, scenario, mock_plant): carbon = generate_emissions_stats(scenario, method="simple") _test_emissions_structure(carbon, mock_plant, scenario.state.get_pg()) # check specific values expected_values = np.array( [ [0, 0, 0, 0, 0], [0, 0, 1.407, 4.004, 4.2], [0, 0, 2.814, 8.008, 8.4], [0, 0, 4.221, 12.012, 12.6], ] ) assert_array_almost_equal( expected_values, carbon.to_numpy(), err_msg="Values do not match expected" )
def test_calculate_so2_disallowed_method(self, scenario): with pytest.raises(ValueError): generate_emissions_stats(scenario, pollutant="so2", method="always-on")
def test_calculate_nox_disallowed_method(self, scenario): with pytest.raises(ValueError): generate_emissions_stats(scenario, pollutant="nox", method="decommit")
def plot_n_scenarios(*args): """For 1-to-n scenarios, plot stacked energy and carbon side-by-side. :param powersimdata.scenario.scenario.Scenario args: scenario instances. :raises ValueError: if arguments are not scenario instances. """ if not all([isinstance(a, Scenario) for a in args]): raise ValueError("all inputs must be Scenario objects") # Build dictionaries of calculated data for each scenario scenario_numbers = [a.info["id"] for a in args] first_id = scenario_numbers[0] scenarios = {id: scen for (id, scen) in zip(scenario_numbers, args)} grid = { id: scenario.state.get_grid() for id, scenario in scenarios.items() } plant = {k: v.plant for k, v in grid.items()} # First scenario is chosen to set fuel colors type2color = ModelImmutables( args[0].info["grid_model"]).plants["type2color"] carbon_by_type, energy_by_type = {}, {} for id, scenario in scenarios.items(): # Calculate raw numbers annual_plant_energy = scenario.state.get_pg().sum() raw_energy_by_type = annual_plant_energy.groupby(plant[id].type).sum() annual_plant_carbon = generate_emissions_stats(scenario).sum() raw_carbon_by_type = annual_plant_carbon.groupby(plant[id].type).sum() # Drop fuels with zero energy (e.g. all offshore_wind scaled to 0 MW) energy_by_type[id] = raw_energy_by_type[raw_energy_by_type != 0] carbon_by_type[id] = raw_carbon_by_type[raw_energy_by_type != 0] # Carbon multiplier is inverse of carbon intensity, to scale bar heights carbon_multiplier = energy_by_type[first_id].sum( ) / carbon_by_type[first_id].sum() # Determine the fuel types with generation in either scenario fuels = list(set.union(*[set(v.index) for v in energy_by_type.values()])) # Fill zeros in scenarios without a fuel when it's present in another for id in scenario_numbers: energy_by_type[id] = energy_by_type[id].reindex(fuels, fill_value=0) carbon_by_type[id] = carbon_by_type[id].reindex(fuels, fill_value=0) # Re-order according to plotting preferences fuels = [f for f in all_possible if f in fuels] # Re-assess subsets renewable_fuels = [f for f in possible_renewable if f in fuels] clean_fuels = [f for f in possible_clean if f in fuels] carbon_fuels = [f for f in possible_carbon if f in fuels] dropped_fuels = (set(possible_clean) | set(possible_carbon)) - ( set(clean_fuels) | set(carbon_fuels)) print("no energy in any grid(s), dropping: %s" % ", ".join(dropped_fuels)) fuel_series = { f: sum( [[energy_by_type[s][f], carbon_by_type[s][f] * carbon_multiplier] for s in scenario_numbers], [], ) for f in fuels } num_bars = 2 * len(scenario_numbers) ind = np.arange(num_bars) width = 0.5 line_alpha = 0.25 line_offsets = np.array((0.25, 0.75)) # Strart plotting fig = plt.figure(figsize=(10, 8)) ax = fig.add_subplot(111) # Plot bars patches = {} bottom = np.zeros(num_bars) for i, f in enumerate(fuels): patches[i] = plt.bar(ind, fuel_series[f], width, bottom=bottom, color=type2color[f]) bottom += fuel_series[f] # Plot lines for i, fuel in enumerate(carbon_fuels): for j, s in enumerate(scenario_numbers): cumulative_energy = sum( [energy_by_type[s][carbon_fuels[k]] for k in range(i + 1)]) cumulative_scaled_carbon = carbon_multiplier * sum( [carbon_by_type[s][carbon_fuels[k]] for k in range(i + 1)]) ys = (cumulative_energy, cumulative_scaled_carbon) xs = line_offsets + 2 * j plt.plot(xs, ys, "k-", alpha=line_alpha) # Labeling energy_fmt = "{0:,.0f} TWh\n{1:.0f}% renewable\n{2:.0f}% carbon-free\n" carbon_fmt = "{0:,.0f} million\ntons CO2\n" energy_total = {s: energy_by_type[s].sum() for s in scenarios} for j, s in enumerate(scenario_numbers): carbon_total = carbon_by_type[s].sum() renewable_energy = sum([energy_by_type[s][f] for f in renewable_fuels]) clean_energy = sum([energy_by_type[s][f] for f in clean_fuels]) renewable_share = renewable_energy / energy_total[s] * 100 clean_share = clean_energy / energy_total[s] * 100 annotation_str = energy_fmt.format(energy_total[s] / 1e6, renewable_share, clean_share) annotation_x = 2 * (j - 0.08) plt.annotate(xy=(annotation_x, energy_total[s]), text=annotation_str) annotation_str = carbon_fmt.format(carbon_total / 1e6) annotation_x = 2 * (j - 0.08) + 1 annotation_y = carbon_total * carbon_multiplier plt.annotate(xy=(annotation_x, annotation_y), text=annotation_str) # Plot formatting plt.ylim((0, max(energy_total.values()) * 1.2)) labels = sum([["%s Energy" % id, "%s Carbon" % id] for id in scenario_numbers], []) plt.xticks(ind, labels) plt.yticks([], []) ax.tick_params(axis="x", labelsize=12) # Invert default legend order to match stack ordering plt.legend( handles=(patches[i] for i in range(len(fuels) - 1, -1, -1)), labels=fuels[::-1], fontsize=12, bbox_to_anchor=(1.02, 1), loc="upper left", ) plt.tight_layout() plt.show()