Source code for postreise.analyze.generation.emissions

import numpy as np
import pandas as pd
from powersimdata.input.check import _check_grid_type, _check_time_series
from powersimdata.network.model import ModelImmutables
from powersimdata.scenario.check import _check_scenario_is_in_analyze_state

from postreise.analyze.generation.costs import calculate_costs


[docs]def generate_emissions_stats(scenario, pollutant="carbon", method="simple"): """Calculate hourly emissions for each generator. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :param str pollutant: pollutant to analyze. :param str method: selected method to handle no-load fuel consumption. :return: (*pandas.DataFrame*) -- emissions data frame. index: timestamps, column: plant id, values: emission in tons. .. note:: method descriptions: - 'simple' uses a fixed ratio of CO2 to MWh - 'always-on' uses generator heat-rate curves including non-zero intercepts - 'decommit' uses generator heat-rate curves but de-commits generators if they are off (detected by pg < 1 MW). """ _check_scenario_is_in_analyze_state(scenario) mi = ModelImmutables(scenario.info["grid_model"]) allowed_methods = { "carbon": {"simple", "always-on", "decommit"}, "nox": {"simple"}, "so2": {"simple"}, } emissions_per_mwh = { "carbon": mi.plants["carbon_per_mwh"], "nox": mi.plants["nox_per_mwh"], "so2": mi.plants["so2_per_mwh"], } if pollutant not in allowed_methods.keys(): raise ValueError("Unknown pollutant for generate_emissions_stats()") if not isinstance(method, str): raise TypeError("method must be a str") if method not in allowed_methods[pollutant]: err_msg = f"method for {pollutant} must be one of: {allowed_methods[pollutant]}" raise ValueError(err_msg) pg = scenario.get_pg() grid = scenario.get_grid() emissions = pd.DataFrame(np.zeros_like(pg), index=pg.index, columns=pg.columns) if method == "simple": for fuel, val in emissions_per_mwh[pollutant].items(): indices = (grid.plant["type"] == fuel).to_numpy() emissions.loc[:, indices] = pg.loc[:, indices] * val / 1000 else: decommit = method == "decommit" costs = calculate_costs( pg=pg, gencost=grid.gencost["before"], decommit=decommit ) heat = np.zeros_like(costs) for fuel, val in mi.plants["carbon_per_mmbtu"].items(): indices = (grid.plant["type"] == fuel).to_numpy() heat[:, indices] = ( costs.iloc[:, indices] / grid.plant["GenFuelCost"].values[indices] ) emissions.loc[:, indices] = heat[:, indices] * val * 44 / 12 / 1000 return emissions
[docs]def summarize_emissions_by_bus(emissions, grid): """Calculate total emissions by generator type and bus. :param pandas.DataFrame emissions: hourly emissions by generator as returned by :func:`generate_emissions_stats`. :param powersimdata.input.grid.Grid grid: grid object. :return: (*dict*) -- annual emissions by fuel and bus. """ _check_time_series(emissions, "emissions") if (emissions < -1e-3).any(axis=None): raise ValueError("emissions must be non-negative") _check_grid_type(grid) plant = grid.plant # sum by generator plant_totals = emissions.sum() # set up output data structure plant_buses = plant["bus_id"].unique() bus_totals_by_type = { f: {b: 0 for b in plant_buses} for f in grid.model_immutables.plants["carbon_resources"] } # sum by fuel by bus for p in plant_totals.index: plant_type = plant.loc[p, "type"] if plant_type not in grid.model_immutables.plants["carbon_resources"]: continue plant_bus = plant.loc[p, "bus_id"] bus_totals_by_type[plant_type][plant_bus] += plant_totals.loc[p] # filter out buses whose emissions are zero bus_totals_by_type = { r: {b: v for b, v in bus_totals_by_type[r].items() if v > 0} for r in grid.model_immutables.plants["carbon_resources"] } return bus_totals_by_type
[docs]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.get_grid() ) carbon_by_bus_2 = summarize_emissions_by_bus( generate_emissions_stats(scenario_2), scenario_2.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)