diff --git a/README.md b/README.md index 8b87badd..cca64381 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,13 @@ To carry out transmission congestion analyses per scenario: 2. calculate congestion statistics; 3. display the data. -The ***[utilization_demo.ipynb][utilization]*** notebook shows the steps for -downloading, calculating and plotting the various statistics. Note that since +The ***[utilization_demo.ipynb][utilization]*** notebook shows the steps for +downloading, calculating and plotting the various statistics. Note that since the plot outputs are meant to be interactive, they may not render on GitHub. ### B. Transmission Congestion (Surplus) Analysis The congestion surplus for each hour can be calculated by calling -``` +```python postreise.analyze.transmission.congestion.calculate_congestion_surplus(scenario) ``` where `scenario` is a powersimdata.scenario.scenario.Scenario object in Analyze @@ -36,14 +36,14 @@ state. ### C. Carbon Analysis The hourly CO2 emissions from a scenario may be analyzed by calling -``` +```python postreise.analyze.generation.carbon.generate_carbon_stats(scenario) ``` -where `scenario` is a powersimdata.scenario.scenario.Scenario instance in the +where `scenario` is a powersimdata.scenario.scenario.Scenario instance in the analyze state. The resulting data frame can be summed by generator type and bus by calling -``` +```python postreise.analyze.generation.carbon.summarize_carbon_by_bus(carbon, plant) ``` where `carbon` is a pandas.DataFrame as returned by `generate_carbon_stats` and @@ -52,51 +52,78 @@ where `carbon` is a pandas.DataFrame as returned by `generate_carbon_stats` and ### D. Curtailment Analysis The level of curtailment for a Scenario may be calculated in several ways. + + #### I. Calculating Time Series To calculate the time-series curtailment for each solar and wind generator, call +```python +from postreise.analyze.generation.curtailment import calculate_curtailment_time_series +calculate_curtailment_time_series(scenario) ``` -postreise.analyze.generation.curtailment.calculate_curtailment_time_series(scenario) +where `scenario` is a `powersimdata.scenario.scenario.Scenario` instance in the +analyze state. If you call: +```python +postreise.analyze.generation.curtailment import calculate_curtailment_time_series_by_resources +calculate_curtailment_time_series_by_resources(scenario, resources={"solar", "wind"}) ``` -where `scenario` is a powersimdata.scenario.scenario.Scenario instance in the -analyze state. To calculate the curtailment just for wind or solar, call -``` -postreise.analyze.generation.curtailment.calculate_curtailment_time_series(scenario, resources={'wind'}) -``` -or +you will obtain a dictionary where the keys are solar and wind and the values are +the curtailment time-series for the associated resource. + +You can also get the curtailment time-series by areas by calling: +```python +from postreise.analyze.generation.curtailment import calculate_curtailment_time_series_by_areas +calculate_curtailment_time_series_by_areas(scenario, + areas={"interconnect": {"Western", "Texas"}, + "state": {"Washington", "California", "Idaho"}, + "loadzone": {"Bay Area", "El Paso", "Far West"}}) ``` -postreise.analyze.generation.curtailment.calculate_curtailment_time_series(scenario, resources={'solar'}) +this returns a dictionary where the keys are the areas and the values are the +curtailment time-series for solar and wind in the associated area. + +Finally, you can group the curtailment time-series by areas and resources as follows: +```python +postreise.analyze.generation.curtailment import calculate_curtailment_time_series_by_areas_and_resources +calculate_curtailment_time_series_by_areas_and_resources(scenario, + areas={"interconnect": {"Western", "Texas"}, + "state": {"Washington", "California", "Idaho"}, + "loadzone": {"Bay Area", "El Paso", "Far West"}}, + resources={"solar", "wind"}) ``` +in that case the it returns a dictionary where the keys are the areas and the values +are a dictionary where the keys are the resources and the values the curtailment +time-series for the associated area-resource pair. + + #### II. Summarizing Time Series: Plant => Bus/Location A curtailment data frame with plants as columns can be further summarized by bus or by location (substation) with: -``` -postreise.analyze.generation.curtailment.summarize_curtailment_by_bus(curtailment, grid) +```python +postreise.analyze.generation.curtailment.summarize_curtailment_by_bus(scenario) ``` or - -``` -postreise.analyze.generation.curtailment.summarize_curtailment_by_location(curtailment, grid) +```python +postreise.analyze.generation.curtailment.summarize_curtailment_by_location(scenario) ``` -where `curtailment` is a pandas.DataFrame as returned by -`calculate_curtailment_time_series` and `grid` is a -powersimdata.input.grid.Grid instance. + #### III. Calculating Annual Curtailment Percentage An annual average curtailment value can be found for all wind/solar plants with -``` +```python postreise.analyze.generation.curtailment.calculate_curtailment_percentage(scenario) ``` -where `scenario` is a powersimdata.scenario.scenario.Scenario instance in the +where `scenario` is a `powersimdata.scenario.scenario.Scenario` instance in the analyze state. To calculate the average curtailment just for wind or solar, call -``` -postreise.analyze.generation.curtailment.calculate_curtailment_percentage(scenario, resources={'wind'}) +```python +from postreise.analyze.generation.curtailment import calculate_curtailment_percentage +calculate_curtailment_percentage(scenario, resources={'wind'}) ``` or -``` -postreise.analyze.generation.curtailment.calculate_curtailment_percentage(scenario, resources={'solar'}) +```python +from ostreise.analyze.generation.curtailment import calculate_curtailment_percentage +calculate_curtailment_percentage(scenario, resources={'solar'}) ``` @@ -118,7 +145,7 @@ the same zone and using the same resource. * Calculate the capacity factor of one resource in one zone and show result using box plots. * Map the shadowprice and congested branches for a given hour -* Tornado plot: Horizontal bar chart styled to show both positive and negative +* Tornado plot: Horizontal bar chart styled to show both positive and negative values cleanly. Check out the notebooks within the [demo][plot_notebooks] folder. @@ -129,14 +156,14 @@ Check out the notebooks within the [demo][plot_notebooks] folder. The `plot_carbon_map` module is used to plot carbon emissions on a map. There are two ways it can be used: -* Map carbon emissions per bus, size scaled to emissions quantity (tons) and +* Map carbon emissions per bus, size scaled to emissions quantity (tons) and color coded by fuel type. * Map carbon emissions per bus for two scenarios and compare. -Comparison map color codes by increase vs. decrease from first to second +Comparison map color codes by increase vs. decrease from first to second scenario analyzed. -The `plot_carbon_bar` module is used to make barcharts comparing carbon +The `plot_carbon_bar` module is used to make barcharts comparing carbon emissions of two scenarios. -The `projection_helpers.py` module contains helper functions such as +The `projection_helpers.py` module contains helper functions such as re-projection, necessary for mapping. diff --git a/postreise/analyze/check.py b/postreise/analyze/check.py index 23167505..0463fc87 100644 --- a/postreise/analyze/check.py +++ b/postreise/analyze/check.py @@ -1,21 +1,15 @@ import datetime + import numpy as np import pandas as pd - -from powersimdata.scenario.scenario import Scenario -from powersimdata.scenario.analyze import Analyze from powersimdata.input.grid import Grid +from powersimdata.network.usa_tamu.constants import zones from powersimdata.network.usa_tamu.constants.plants import ( all_resources, renewable_resources, ) -from powersimdata.network.usa_tamu.constants.zones import ( - loadzone, - state2loadzone, - abv2loadzone, - interconnect2loadzone, - abv2state, -) +from powersimdata.scenario.analyze import Analyze +from powersimdata.scenario.scenario import Scenario def _check_data_frame(df, label): @@ -85,7 +79,8 @@ def _check_areas_and_format(areas): state name(s)/abbreviation(s) or interconnect(s). :raises TypeError: if areas is not a list/tuple/set of str. :raises ValueError: if areas is empty or not valid. - :return: (*set*) -- areas as a set. + :return: (*set*) -- areas as a set. State abbreviations are converted to state + names. """ if isinstance(areas, str): areas = {areas} @@ -97,20 +92,15 @@ def _check_areas_and_format(areas): raise TypeError("areas must be a str or a list/tuple/set of str") if len(areas) == 0: raise ValueError("areas must be non-empty") - all_areas = ( - loadzone - | set(abv2loadzone.keys()) - | set(state2loadzone.keys()) - | set(interconnect2loadzone.keys()) - ) + all_areas = zones.loadzone | zones.abv | zones.state | zones.interconnect if not areas <= all_areas: diff = areas - all_areas raise ValueError("invalid area(s): %s" % " | ".join(diff)) - abv_in_areas = [z for z in areas if z in abv2state.keys()] + abv_in_areas = [z for z in areas if z in zones.abv] for a in abv_in_areas: areas.remove(a) - areas.add(abv2state[a]) + areas.add(zones.abv2state[a]) return areas @@ -154,28 +144,90 @@ def _check_resources_are_renewable_and_format(resources): return resources -def _check_resources_are_in_grid(resources, grid): +def _check_areas_are_in_grid_and_format(areas, grid): + """Ensure that list of areas are in grid. + + :param dict areas: keys are area types: '*loadzone*', '*state*' or '*interconnect*'. + Values are str/list/tuple/set of areas. + :param powersimdata.input.grid.Grid grid: Grid instance. + :return: (*dict*) -- modified areas dictionary. Keys are area types ('*loadzone*', + '*state*' or '*interconnect*'). State abbreviations, if present, are converted + to state names. Values are areas as a set. + :raises TypeError: if areas is not a dict or its keys are not str. + :raises ValueError: if area type is invalid, an area in not in grid or an + invalid loadzone/state/interconnect is passed. + """ + _check_grid(grid) + if not isinstance(areas, dict): + raise TypeError("areas must be a dict") + + areas_formatted = {} + for a in areas.keys(): + if a in ["loadzone", "state", "interconnect"]: + areas_formatted[a] = set() + + all_loadzones = set() + for k, v in areas.items(): + if not isinstance(k, str): + raise TypeError("area type must be a str") + elif k == "interconnect": + interconnects = _check_areas_and_format(v) + for i in interconnects: + try: + all_loadzones.update(zones.interconnect2loadzone[i]) + except KeyError: + raise ValueError("invalid interconnect: %s" % i) + areas_formatted["interconnect"].update(interconnects) + elif k == "state": + states = _check_areas_and_format(v) + for s in states: + try: + all_loadzones.update(zones.state2loadzone[s]) + except KeyError: + raise ValueError("invalid state: %s" % s) + areas_formatted["state"].update(states) + elif k == "loadzone": + loadzones = _check_areas_and_format(v) + for l in loadzones: + if l not in zones.loadzone: + raise ValueError("invalid load zone: %s" % l) + all_loadzones.update(loadzones) + areas_formatted["loadzone"].update(loadzones) + else: + raise ValueError("invalid area type") + + valid_loadzones = set(grid.plant["zone_name"].unique()) + if not all_loadzones <= valid_loadzones: + diff = all_loadzones - valid_loadzones + raise ValueError("%s not in in grid" % " | ".join(diff)) + + return areas_formatted + + +def _check_resources_are_in_grid_and_format(resources, grid): """Ensure that resource(s) is represented in at least one generator in the grid used for the scenario. :param str/list/tuple/set resources: resource(s) to analyze. :param powersimdata.input.grid.Grid grid: a Grid instance. + :return: (*set*) -- resources as a set. :raises ValueError: if resources is not used in scenario. """ + _check_grid(grid) resources = _check_resources_and_format(resources) valid_resources = set(grid.plant["type"].unique()) if not resources <= valid_resources: diff = resources - valid_resources raise ValueError("%s not in in grid" % " | ".join(diff)) + return resources def _check_plants_are_in_grid(plant_id, grid): """Ensure that list of plant id are in grid. - :param list/tuple/set plant_id: list of plant_id. + :param list/tuple/set plant_id: list of plant id. :param powersimdata.input.grid.Grid grid: Grid instance. - :raises TypeError: if plant_id is not a list of int or str and grid is not a Grid - object. + :raises TypeError: if plant_id is not a list of int or str. :raises ValueError: if plant id is not in network. """ _check_grid(grid) @@ -184,8 +236,6 @@ def _check_plants_are_in_grid(plant_id, grid): and all([isinstance(p, (int, str)) for p in plant_id]) ): raise TypeError("plant_id must be a a list/tuple/set of int or str") - if not isinstance(grid, Grid): - raise TypeError("grid must be powersimdata.input.grid.Grid object") if not set([str(p) for p in plant_id]) <= set([str(i) for i in grid.plant.index]): raise ValueError("plant_id must be subset of plant index") @@ -313,20 +363,3 @@ def _check_gencost(gencost): for c in coef_columns: if c not in gencost.columns: raise ValueError("gencost of order {0} must have column {1}".format(n, c)) - - -def _check_curtailment(curtailment, grid): - """Ensure that curtailment is a dict of data frames, and that each key is - represented in at least one generator in grid. - - :param dict curtailment: curtailment data. - :param powersimdata.input.grid.Grid grid: a Grid object. - :raises TypeError: if curtailment is not a dict and values are not data frames. - """ - if not isinstance(curtailment, dict): - raise TypeError("curtailment must be a dict") - _check_grid(grid) - resources = _check_resources_are_renewable_and_format(list(curtailment.keys())) - _check_resources_are_in_grid(resources, grid) - if not all([isinstance(v, pd.DataFrame) for v in curtailment.values()]): - raise TypeError("curtailment values must be a data frame") diff --git a/postreise/analyze/generation/capacity_value.py b/postreise/analyze/generation/capacity_value.py index 18f43b9d..f73ef373 100644 --- a/postreise/analyze/generation/capacity_value.py +++ b/postreise/analyze/generation/capacity_value.py @@ -1,7 +1,6 @@ from postreise.analyze.check import ( _check_scenario_is_in_analyze_state, - _check_resources_and_format, - _check_resources_are_in_grid, + _check_resources_are_in_grid_and_format, _check_number_hours_to_analyze, ) @@ -18,8 +17,7 @@ def calculate_NLDC(scenario, resources, hours=100): """ _check_scenario_is_in_analyze_state(scenario) grid = scenario.state.get_grid() - resources = _check_resources_and_format(resources) - _check_resources_are_in_grid(resources, grid) + resources = _check_resources_are_in_grid_and_format(resources, grid) _check_number_hours_to_analyze(scenario, hours) # Then calculate capacity value @@ -46,8 +44,7 @@ def calculate_net_load_peak(scenario, resources, hours=100): """ _check_scenario_is_in_analyze_state(scenario) grid = scenario.state.get_grid() - resources = _check_resources_and_format(resources) - _check_resources_are_in_grid(resources, grid) + resources = _check_resources_are_in_grid_and_format(resources, grid) _check_number_hours_to_analyze(scenario, hours) # Then calculate capacity value diff --git a/postreise/analyze/generation/curtailment.py b/postreise/analyze/generation/curtailment.py index 76270f6b..b2661c34 100644 --- a/postreise/analyze/generation/curtailment.py +++ b/postreise/analyze/generation/curtailment.py @@ -1,105 +1,202 @@ import pandas as pd - from postreise.analyze.check import ( - _check_scenario_is_in_analyze_state, - _check_grid, + _check_areas_are_in_grid_and_format, _check_resources_are_renewable_and_format, - _check_resources_are_in_grid, - _check_curtailment, + _check_scenario_is_in_analyze_state, ) from postreise.analyze.helpers import ( + get_plant_id_for_resources, + decompose_plant_data_frame_into_resources, + decompose_plant_data_frame_into_areas, + decompose_plant_data_frame_into_areas_and_resources, + decompose_plant_data_frame_into_resources_and_areas, summarize_plant_to_bus, summarize_plant_to_location, ) +from powersimdata.network.usa_tamu.constants import zones from powersimdata.network.usa_tamu.constants.plants import renewable_resources -# What is the name of the function in scenario.state to get the profiles? -# The set of keys to in dict defines the set of possible curtailment resources. -_resource_func = { - "solar": "get_solar", - "wind": "get_wind", - "wind_offshore": "get_wind", -} +def calculate_curtailment_time_series(scenario): + """Calculate a time series of curtailment for renewable resources. + :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. + :return: (*pandas.DataFrame*) -- time series of curtailment + """ + _check_scenario_is_in_analyze_state(scenario) + grid = scenario.state.get_grid() + pg = scenario.state.get_pg() -def calculate_curtailment_time_series(scenario, resources=None): + plant_id = get_plant_id_for_resources( + renewable_resources.intersection(set(grid.plant.type)), grid + ) + profiles = pd.concat( + [scenario.state.get_solar(), scenario.state.get_wind()], axis=1 + ) + + curtailment = (profiles[plant_id] - pg[plant_id]).clip(lower=0).round(6) + return curtailment + + +def calculate_curtailment_time_series_by_resources(scenario, resources=None): """Calculate a time series of curtailment for a set of valid resources. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :param str/tuple/list/set resources: names of resources to analyze. Default is - all resources which can be curtailed, defined in _resource_func. - :return: (*dict*) -- keys are resources, values are pandas.DataFrames - indexed by (datetime, plant) where plant is only plants of matching type. + all renewable esources. + :return: (*dict*) -- keys are resources, values are data frames indexed by + (datetime, plant) where plant is only plants of matching type. """ - _check_scenario_is_in_analyze_state(scenario) + curtailment = calculate_curtailment_time_series(scenario) grid = scenario.state.get_grid() + if resources is None: - resources = renewable_resources + resources = renewable_resources.intersection(set(grid.plant.type)) else: resources = _check_resources_are_renewable_and_format(resources) - _check_resources_are_in_grid(resources, grid) - # Get input dataframes from scenario object - pg = scenario.state.get_pg() - profile_functions = {_resource_func[r] for r in resources} - relevant_profiles = pd.concat( - [getattr(scenario.state, p)() for p in profile_functions], axis=1 + curtailment_by_resources = decompose_plant_data_frame_into_resources( + curtailment, resources, grid ) - # Calculate differences for each resource - curtailment = {} - for r in resources: - ren_plants = grid.plant.groupby("type").get_group(r).index - curtailment[r] = relevant_profiles[ren_plants] - pg[ren_plants] + return curtailment_by_resources - return curtailment +def calculate_curtailment_time_series_by_areas(scenario, areas=None): + """Calculate a time series of curtailment for a set of valid areas. + + :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. + :param dict areas: keys are area types ('*loadzone*', '*state*' or + '*interconnect*'), values are a list of areas. Default is the interconnect of + the scenario. Default is the scenario interconnect. + :return: (*dict*) -- keys are areas, values are data frames indexed by + (datetime, plant) where plant is only renewable plants in specified area. + """ + curtailment = calculate_curtailment_time_series(scenario) + grid = scenario.state.get_grid() + + areas = ( + _check_areas_are_in_grid_and_format(areas, grid) + if areas is not None + else {"interconnecy": grid.interconnect} + ) + + curtailment_by_areas = decompose_plant_data_frame_into_areas( + curtailment, areas, grid + ) + + return curtailment_by_areas -def calculate_curtailment_percentage(scenario, resources=None): + +def calculate_curtailment_percentage_by_resources(scenario, resources=None): """Calculate scenario-long average curtailment for selected resources. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :param str/tuple/list/set resources: names of resources to analyze. Default is - all resources which can be curtailed. + all renewable resources. :return: (*float*) -- Average curtailment fraction over the scenario. """ - _check_scenario_is_in_analyze_state(scenario) + curtailment = calculate_curtailment_time_series_by_resources(scenario, resources) + resources = set(curtailment.keys()) grid = scenario.state.get_grid() + + total_curtailment = {r: curtailment[r].sum().sum() for r in resources} + + profiles = pd.concat( + [scenario.state.get_solar(), scenario.state.get_wind()], axis=1 + ) + total_potential = profiles.groupby(grid.plant.type, axis=1).sum().sum() + + curtailment_percentage = ( + sum(v for v in total_curtailment.values()) + / total_potential.loc[resources].sum() + ) + + return curtailment_percentage + + +def calculate_curtailment_time_series_by_areas_and_resources( + scenario, areas=None, resources=None +): + """Calculate a time series of curtailment for a set of valid areas and resources. + + :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. + :param dict areas: keys are area types ('*loadzone*', '*state*' or + '*interconnect*'), values are a list of areas. Default is the interconnect of + the scenario. Default is the scenario interconnect. + :param str/tuple/list/set resources: names of resources to analyze. Default is + all renewable resources. + :return: (*dict*) -- keys are areas, values are dictionaries whose keys are + resources and values are data frames indexed by (datetime, plant) where plant + is only plants of matching type and located in area. + """ + curtailment = calculate_curtailment_time_series(scenario) + grid = scenario.state.get_grid() + + areas = ( + _check_areas_are_in_grid_and_format(areas, grid) + if areas is not None + else {"interconnecy": grid.interconnect} + ) + if resources is None: - resources = renewable_resources + resources = renewable_resources.intersection(set(grid.plant.type)) else: resources = _check_resources_are_renewable_and_format(resources) - _check_resources_are_in_grid(resources, grid) - curtailment = calculate_curtailment_time_series(scenario, resources) - rentype_total_curtailment = {r: curtailment[r].sum().sum() for r in resources} + curtailment_by_areas_and_resources = ( + decompose_plant_data_frame_into_areas_and_resources( + curtailment, areas, resources, grid + ) + ) + return curtailment_by_areas_and_resources - # Build a set of the profile methods we will call - profile_methods = {_resource_func[r] for r in resources} - # Build one mega-profile dataframe that contains all profiles of interest - mega_profile = pd.concat([getattr(scenario.state, p)() for p in profile_methods]) - # Calculate total energy for each resource - rentype_total_potential = mega_profile.groupby(grid.plant.type, axis=1).sum().sum() - # Calculate curtailment percentage by dividing total curtailment by total potential - curtailment_percentage = ( - sum(v for v in rentype_total_curtailment.values()) - / rentype_total_potential.loc[list(resources)].sum() +def calculate_curtailment_time_series_by_resources_and_areas( + scenario, areas=None, resources=None +): + """Calculate a time series of curtailment for a set of valid resources and areas. + + :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. + :param str/tuple/list/set resources: names of resources to analyze. Default is + all renewable resources. :param dict areas: keys are area types ('*loadzone*', '*state*' or + '*interconnect*'), values are a list of areas. Default is the interconnect of + the scenario. Default is the scenario interconnect. + :return: (*dict*) -- keys are areas, values are dictionaries whose keys are + resources and values are data frames indexed by (datetime, plant) where plant + is only plants of matching type and located in area. + """ + curtailment = calculate_curtailment_time_series(scenario) + grid = scenario.state.get_grid() + + areas = ( + _check_areas_are_in_grid_and_format(areas, grid) + if areas is not None + else {"interconnecy": grid.interconnect} ) - return curtailment_percentage + if resources is None: + resources = renewable_resources.intersection(set(grid.plant.type)) + else: + resources = _check_resources_are_renewable_and_format(resources) + + curtailment_by_resources_and_areas = ( + decompose_plant_data_frame_into_resources_and_areas( + curtailment, resources, areas, grid + ) + ) + return curtailment_by_resources_and_areas -def summarize_curtailment_by_bus(curtailment, grid): +def summarize_curtailment_by_bus(scenario): """Calculate total curtailment for selected resources, by bus. - :param dict curtailment: keys are resources, values are data frame. - :param powersimdata.input.grid.Grid grid: Grid instance. + :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :return: (*dict*) -- keys are resources, values are dict of (bus: curtailment vector). """ - _check_curtailment(curtailment, grid) + curtailment = calculate_curtailment_time_series_by_resources(scenario) + grid = scenario.state.get_grid() bus_curtailment = { ren_type: summarize_plant_to_bus(curtailment_df, grid).sum().to_dict() @@ -109,15 +206,15 @@ def summarize_curtailment_by_bus(curtailment, grid): return bus_curtailment -def summarize_curtailment_by_location(curtailment, grid): +def summarize_curtailment_by_location(scenario): """Calculate total curtailment for selected resources, by location. - :param dict curtailment: keys are resources, values are data frame. - :param powersimdata.input.grid.Grid grid: Grid instance. + :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :return: (*dict*) -- keys are resources, values are dict of ((lat, lon): curtailment vector). """ - _check_curtailment(curtailment, grid) + curtailment = calculate_curtailment_time_series_by_resources(scenario) + grid = scenario.state.get_grid() location_curtailment = { ren_type: summarize_plant_to_location(curtailment_df, grid).sum().to_dict() diff --git a/postreise/analyze/generation/summarize.py b/postreise/analyze/generation/summarize.py index b57e4b33..2dfcef6d 100644 --- a/postreise/analyze/generation/summarize.py +++ b/postreise/analyze/generation/summarize.py @@ -5,7 +5,7 @@ from powersimdata.scenario.scenario import Scenario from powersimdata.scenario.analyze import Analyze from powersimdata.network.usa_tamu.constants.zones import ( - interconnect2state, + interconnect2abv, abv2state, loadzone2state, loadzone2interconnect, @@ -87,8 +87,8 @@ def summarize_hist_gen(hist_gen_raw: pd.DataFrame, all_resources: list) -> pd.Da :param list all_resources: list of resources from the scenario :return: (*pandas.DataFrame*) historical generation per resource """ - western = [abv2state[s] for s in interconnect2state["Western"]] - eastern = [abv2state[s] for s in interconnect2state["Eastern"]] + western = [abv2state[s] for s in interconnect2abv["Western"]] + eastern = [abv2state[s] for s in interconnect2abv["Eastern"]] filtered_colnames = [k for k in label2type.keys() if label2type[k] in all_resources] result = hist_gen_raw.copy() diff --git a/postreise/analyze/generation/tests/test_curtailment.py b/postreise/analyze/generation/tests/test_curtailment.py index edb7f348..53463d5f 100644 --- a/postreise/analyze/generation/tests/test_curtailment.py +++ b/postreise/analyze/generation/tests/test_curtailment.py @@ -5,8 +5,8 @@ from powersimdata.tests.mock_scenario import MockScenario from postreise.analyze.generation.curtailment import ( - calculate_curtailment_time_series, - calculate_curtailment_percentage, + calculate_curtailment_time_series_by_resources, + calculate_curtailment_percentage_by_resources, summarize_curtailment_by_bus, summarize_curtailment_by_location, ) @@ -81,46 +81,45 @@ def _check_curtailment_vs_expected(self, curtailment, expected): def test_calculate_curtailment_time_series_solar(self): expected_return = {"solar": mock_curtailment["solar"]} - curtailment = calculate_curtailment_time_series(scenario, resources=("solar",)) - self._check_curtailment_vs_expected(curtailment, expected_return) - - def test_calculate_curtailment_time_series_wind_tuple(self): - expected_return = {"wind": mock_curtailment["wind"]} - curtailment = calculate_curtailment_time_series(scenario, resources=("wind",)) - self._check_curtailment_vs_expected(curtailment, expected_return) - - def test_calculate_curtailment_time_series_wind_set(self): - expected_return = {"wind": mock_curtailment["wind"]} - curtailment = calculate_curtailment_time_series(scenario, resources={"wind"}) + curtailment = calculate_curtailment_time_series_by_resources( + scenario, resources=("solar",) + ) self._check_curtailment_vs_expected(curtailment, expected_return) - def test_calculate_curtailment_time_series_wind_list(self): + def test_calculate_curtailment_time_series_wind_argument_type(self): expected_return = {"wind": mock_curtailment["wind"]} - curtailment = calculate_curtailment_time_series(scenario, resources=["wind"]) - self._check_curtailment_vs_expected(curtailment, expected_return) + arg = ( + (scenario, "wind"), + (scenario, ("wind")), + (scenario, ["wind"]), + (scenario, {"wind"}), + ) + for a in arg: + curtailment = calculate_curtailment_time_series_by_resources(a[0], a[1]) + self._check_curtailment_vs_expected(curtailment, expected_return) def test_calculate_curtailment_time_series_default(self): expected_return = mock_curtailment - curtailment = calculate_curtailment_time_series(scenario) + curtailment = calculate_curtailment_time_series_by_resources(scenario) self._check_curtailment_vs_expected(curtailment, expected_return) def test_calculate_curtailment_time_series_solar_wind_tuple(self): expected_return = {r: mock_curtailment[r] for r in ("solar", "wind")} - curtailment = calculate_curtailment_time_series( + curtailment = calculate_curtailment_time_series_by_resources( scenario, resources=("solar", "wind") ) self._check_curtailment_vs_expected(curtailment, expected_return) def test_calculate_curtailment_time_series_solar_wind_set(self): expected_return = {r: mock_curtailment[r] for r in ("solar", "wind")} - curtailment = calculate_curtailment_time_series( + curtailment = calculate_curtailment_time_series_by_resources( scenario, resources={"solar", "wind"} ) self._check_curtailment_vs_expected(curtailment, expected_return) def test_calculate_curtailment_time_series_wind_solar_list(self): expected_return = {r: mock_curtailment[r] for r in ("solar", "wind")} - curtailment = calculate_curtailment_time_series( + curtailment = calculate_curtailment_time_series_by_resources( scenario, resources=["wind", "solar"] ) self._check_curtailment_vs_expected(curtailment, expected_return) @@ -129,7 +128,7 @@ def test_calculate_curtailment_time_series_wind_solar_list(self): class TestCheckResourceInScenario(unittest.TestCase): def test_error_geothermal_curtailment(self): with self.assertRaises(ValueError): - curtailment = calculate_curtailment_time_series( + curtailment = calculate_curtailment_time_series_by_resources( scenario, resources=("geothermal",) ) @@ -138,7 +137,7 @@ def test_error_no_solar(self): no_solar_grid_attrs = {"plant": no_solar_mock_plant} no_solar_scenario = MockScenario(no_solar_grid_attrs) with self.assertRaises(ValueError): - curtailment = calculate_curtailment_time_series( + curtailment = calculate_curtailment_time_series_by_resources( no_solar_scenario, resources=("solar",) ) @@ -146,33 +145,33 @@ def test_error_no_solar(self): class TestCalculateCurtailmentPercentage(unittest.TestCase): def test_calculate_curtailment_percentage_solar(self): expected_return = 3.5 / 25 - total_curtailment = calculate_curtailment_percentage( + total_curtailment = calculate_curtailment_percentage_by_resources( scenario, resources=("solar",) ) self.assertAlmostEqual(total_curtailment, expected_return) def test_calculate_curtailment_percentage_wind(self): expected_return = 0.5 / 7 - total_curtailment = calculate_curtailment_percentage( + total_curtailment = calculate_curtailment_percentage_by_resources( scenario, resources=("wind",) ) self.assertAlmostEqual(total_curtailment, expected_return) def test_calculate_curtailment_percentage_wind_offshore(self): expected_return = 2.5 / 16 - total_curtailment = calculate_curtailment_percentage( + total_curtailment = calculate_curtailment_percentage_by_resources( scenario, resources=("wind_offshore",) ) self.assertAlmostEqual(total_curtailment, expected_return) def test_calculate_curtailment_percentage_default(self): expected_return = 6.5 / 48 - total_curtailment = calculate_curtailment_percentage(scenario) + total_curtailment = calculate_curtailment_percentage_by_resources(scenario) self.assertAlmostEqual(total_curtailment, expected_return) def test_calculate_curtailment_percentage_solar_wind(self): expected_return = 4 / 32 - total_curtailment = calculate_curtailment_percentage( + total_curtailment = calculate_curtailment_percentage_by_resources( scenario, resources=("solar", "wind") ) self.assertAlmostEqual(total_curtailment, expected_return) @@ -180,23 +179,21 @@ def test_calculate_curtailment_percentage_solar_wind(self): class TestSummarizeCurtailmentByBus(unittest.TestCase): def test_summarize_curtailment_by_bus(self): - grid = scenario.state.get_grid() expected_return = { "solar": {1: 1, 2: 2.5}, "wind": {3: 0.5}, "wind_offshore": {4: 2.5}, } - bus_curtailment = summarize_curtailment_by_bus(mock_curtailment, grid) + bus_curtailment = summarize_curtailment_by_bus(scenario) self.assertEqual(bus_curtailment, expected_return) class TestSummarizeCurtailmentByLocation(unittest.TestCase): def test_summarize_curtailment_by_location(self): - grid = scenario.state.get_grid() expected_return = { "solar": {(47.6, 122.3): 3.5}, "wind": {(37.8, 122.4): 0.5}, "wind_offshore": {(37.8, 122.4): 2.5}, } - location_curtailment = summarize_curtailment_by_location(mock_curtailment, grid) + location_curtailment = summarize_curtailment_by_location(scenario) self.assertEqual(location_curtailment, expected_return) diff --git a/postreise/analyze/helpers.py b/postreise/analyze/helpers.py index ce43dbbb..95722dca 100644 --- a/postreise/analyze/helpers.py +++ b/postreise/analyze/helpers.py @@ -1,18 +1,22 @@ -import pandas as pd +from collections import defaultdict -from powersimdata.input.grid import Grid +import pandas as pd from postreise.analyze.check import ( - _check_plants_are_in_grid, + _check_areas_are_in_grid_and_format, _check_data_frame, _check_grid, + _check_plants_are_in_grid, + _check_resources_are_in_grid_and_format, ) +from powersimdata.input.grid import Grid +from powersimdata.network.usa_tamu.constants import zones def get_resources_in_grid(grid): """Get resources in grid. :param powersimdata.input.grid.Grid grid: a Grid instance. - :return: (*set*) -- all resources in grid. + :return: (*set*) -- name of all resources in grid. """ _check_grid(grid) resources = set(grid.plant["type"].unique()) @@ -23,17 +27,224 @@ def get_active_resources_in_grid(grid): """Get active resources in grid. :param powersimdata.input.grid.Grid grid: a Grid instance. - :return: (*set*) -- active resources in grid. + :return: (*set*) -- name of active resources in grid. """ _check_grid(grid) active_resources = set(grid.plant.loc[grid.plant["Pmax"] > 0].type.unique()) return active_resources +def get_plant_id_for_resources(resources, grid): + """Get plant id for plants fueled by resource(s). + + :param str/list/tuple/set resources: name of resource(s). + :param powersimdata.input.grid.Grid grid: Grid instance. + :return: (*set*) -- list of plant id. + """ + resources = _check_resources_are_in_grid_and_format(resources, grid) + plant = grid.plant + plant_id = plant[(plant.type.isin(resources))].index + return set(plant_id) + + +def get_plant_id_in_loadzones(loadzones, grid): + """Get plant id for plants in loadzone(s). + + :param str/list/tuple/set loadzones: name of load zone(s). + :param powersimdata.input.grid.Grid grid: Grid instance. + :return: (*set*) -- list of plant id. + """ + areas = _check_areas_are_in_grid_and_format({"loadzone": loadzones}, grid) + plant = grid.plant + plant_id = plant[(plant.zone_name.isin(areas["loadzone"]))].index + return set(plant_id) + + +def get_plant_id_in_interconnects(interconnects, grid): + """Get plant id for plants in interconnect(s). + + :param str/list/tuple/set interconnects: name of interconnect(s). + :param powersimdata.input.grid.Grid grid: Grid instance. + :return: (*set*) -- list of plant id + """ + areas = _check_areas_are_in_grid_and_format({"interconnect": interconnects}, grid) + loadzones = set.union( + *(zones.interconnect2loadzone[i] for i in areas["interconnect"]) + ) + + plant = grid.plant + plant_id = plant[(plant.zone_name.isin(loadzones))].index + return set(plant_id) + + +def get_plant_id_in_states(states, grid): + """Get plant id for plants in state(s). + + :param str/list/tuple/set states: states(s) name or abbreviation(s). + :param powersimdata.input.grid.Grid grid: Grid instance. + :return: (*set*) -- list of plant id. + """ + + areas = _check_areas_are_in_grid_and_format({"state": states}, grid) + loadzones = set.union(*(zones.state2loadzone[i] for i in areas["state"])) + + plant = grid.plant + plant_id = plant[(plant.zone_name.isin(loadzones))].index + return set(plant_id) + + +def get_plant_id_for_resources_in_loadzones(resources, loadzones, grid): + """Get plant id for plants fueled by resource(s) in load zone(s). + + :param str/list/tuple/set resources: name of resource(s). + :param str/list/tuple/set loadzones: name of load zone(s). + :param powersimdata.input.grid.Grid grid: a Grid instance. + :return: (*set*) -- list of plant id. + """ + plant_id = get_plant_id_for_resources(resources, grid) & get_plant_id_in_loadzones( + loadzones, grid + ) + return set(plant_id) + + +def get_plant_id_for_resources_in_interconnects(resources, interconnects, grid): + """Get plant id for for plants fueled by resource(s) in interconnect(s). + + :param str/list/tuple/set resources: name of resource(s). + :param str/list/tuple/set interconnects: name of interconnect(s). + :param powersimdata.input.grid.Grid grid: a Grid instance. + :return: (*set*) -- list of plant id. + """ + plant_id = get_plant_id_for_resources( + resources, grid + ) & get_plant_id_in_interconnects(interconnects, grid) + return set(plant_id) + + +def get_plant_id_for_resources_in_states(resources, states, grid): + """Get plant id for for plants fueled by resource(s) in state(s). + + :param str/list/tuple/set resources: name of resource(s). + :param str/list/tuple/set interconnects: state(s) name or abbreviation. + :param powersimdata.input.grid.Grid grid: a Grid instance. + :return: (*set*) -- list of plant id + """ + plant_id = get_plant_id_for_resources(resources, grid) & get_plant_id_in_states( + states, grid + ) + return set(plant_id) + + +def decompose_plant_data_frame_into_resources(df, resources, grid): + """Take a plant-column data frame and decompose it into plant-column data frames + for each resource. + + :param pandas.DataFrame df: data frame, columns are plant id in grid. + :param str/list/tuple/set resources: resource(s) to use for decomposition. + :param powersimdata.input.grid.Grid grid: Grid instance. + :return: (*dict*) -- keys are resources, values are plant-column data frames. + """ + _check_data_frame(df, "PG") + plant_id = set(df.columns) + _check_plants_are_in_grid(plant_id, grid) + resources = _check_resources_are_in_grid_and_format(resources, grid) + + df_resources = { + r: df[get_plant_id_for_resources(r, grid) & plant_id].sort_index(axis=1) + for r in resources + } + return df_resources + + +def decompose_plant_data_frame_into_areas(df, areas, grid): + """Take a plant-column data frame and decompose it into plant-column data frames + for areas. + + :param pandas.DataFrame df: data frame, columns are plant id in grid. + :param dict areas: areas to use for decomposition. Keys are area types + ('*loadzone*', '*state*', or '*interconnect*'), values are + str/list/tuple/set of areas. + :param powersimdata.input.grid.Grid grid: Grid instance. + :return: (*dict*) -- keys are areas, values are plant-column data frames. + """ + _check_data_frame(df, "PG") + plant_id = set(df.columns) + _check_plants_are_in_grid(plant_id, grid) + areas = _check_areas_are_in_grid_and_format(areas, grid) + + plant = grid.plant + df_areas = {} + for k, v in areas.items(): + if k == "interconnect": + for i in v: + name = "%s interconnect" % " - ".join(i.split("_")) + df_areas[name] = df[get_plant_id_in_interconnects(i, grid) & plant_id] + elif k == "state": + for s in v: + df_areas[s] = df[get_plant_id_in_states(s, grid) & plant_id] + elif k == "loadzone": + for l in v: + df_areas[l] = df[get_plant_id_in_loadzones(l, grid) & plant_id] + + return df_areas + + +def decompose_plant_data_frame_into_areas_and_resources(df, areas, resources, grid): + """Take a plant-column data frame and decompose it into plant-column data frames + for each resources-areas combinations. + + :param pandas.DataFrame df: data frame, columns are plant id in grid. + :param dict areas: areas to use for decomposition. Keys are area types + ('*loadzone*', '*state*' or '*interconnect*'), values are + str/list/tuple/set of areas. + :param str/list/tuple/set resources: resource(s) to use for decomposition. + :param powersimdata.input.grid.Grid grid: Grid instance. + :return: (*dict*) -- keys are areas, values are dictionaries whose keys are + resources and values are data frames indexed by (datetime, plant) where plant + include only plants of matching type and located in area. + """ + # areas_resources = defaultdict(dict) + df_areas_resources = {} + resources = _check_resources_are_in_grid_and_format(resources, grid) + + for a, df_a in decompose_plant_data_frame_into_areas(df, areas, grid).items(): + df_areas_resources[a] = decompose_plant_data_frame_into_resources( + df_a, resources, grid + ) + + return df_areas_resources + + +def decompose_plant_data_frame_into_resources_and_areas(df, resources, areas, grid): + """Take a plant-column data frame and decompose it into plant-column data frames + for each resources-areas combinations. + + :param pandas.DataFrame df: data frame, columns are plant id in grid. + :param str/list/tuple/set resources: resource(s) to use for decomposition. + :param dict areas: areas to use for decomposition. Keys are area types + ('*loadzone*', '*state*', '*state_abv*' or '*interconnect*'), values are + str/list/tuple/set of areas. + :param powersimdata.input.grid.Grid grid: Grid instance. + :return: (*dict*) -- keys are resources, values are dictionaries whose keys are + areas and values are data frames indexed by (datetime, plant) where plant + include only plants of matching type and located in area. + """ + resources_areas = defaultdict(dict) + + areas_resources = decompose_plant_data_frame_into_areas_and_resources( + df, areas, resources, grid + ) + for a in areas_resources.keys(): + for r in areas_resources[a].keys(): + resources_areas[r].update({a: areas_resources[a][r]}) + + return resources_areas + + def summarize_plant_to_bus(df, grid, all_buses=False): """Take a plant-column data frame and sum to a bus-column data frame. - :param pandas.DataFrame df: dataframe, columns are plant id in Grid. + :param pandas.DataFrame df: dataframe, columns are plant id in grid. :param powersimdata.input.grid.Grid grid: Grid instance. :param boolean all_buses: return all buses in grid, not just plant buses. :return: (*pandas.DataFrame*) -- index as df input, columns are buses. @@ -56,7 +267,7 @@ def summarize_plant_to_bus(df, grid, all_buses=False): def summarize_plant_to_location(df, grid): """Take a plant-column data frame and sum to a location-column data frame. - :param pandas.DataFrame df: dataframe, columns are plant id in Grid. + :param pandas.DataFrame df: dataframe, columns are plant id in grid. :param powersimdata.input.grid.Grid grid: Grid instance. :return: (*pandas.DataFrame*) -- index: df index, columns: location tuples. """ diff --git a/postreise/analyze/tests/test_check.py b/postreise/analyze/tests/test_check.py index eef26830..909089f1 100644 --- a/postreise/analyze/tests/test_check.py +++ b/postreise/analyze/tests/test_check.py @@ -11,7 +11,8 @@ _check_areas_and_format, _check_resources_and_format, _check_resources_are_renewable_and_format, - _check_resources_are_in_grid, + _check_areas_are_in_grid_and_format, + _check_resources_are_in_grid_and_format, _check_plants_are_in_grid, _check_number_hours_to_analyze, _check_date, @@ -20,7 +21,6 @@ _check_epsilon, _check_gencost, _check_time_series, - _check_curtailment, ) @@ -43,6 +43,23 @@ "ng", "solar", ], + "interconnect": ["Western"] * 3 + ["Texas"] * 8 + ["Eastern"] * 4, + "zone_name": [ + "Washington", + "El Paso", + "Bay Area", + ] + + [ + "Far West", + "North", + "West", + "South", + "North Central", + "South Central", + "Coast", + "East", + ] + + ["Kentucky", "Nebraska", "East Texas", "Texas Panhandle"], } mock_gencost = { @@ -221,16 +238,51 @@ def test_check_resources_are_renewable_and_format(): _check_resources_are_renewable_and_format({"wind"}) -def test_check_resources_are_in_grid_argument_value(): +def test_check_areas_are_in_grid_and_format_argument_type(): + arg = (({"Texas", "El Paso"}, grid), ({123: "Nebraska"}, grid)) + for a in arg: + with pytest.raises(TypeError): + _check_areas_are_in_grid_and_format(a[0], a[1]) + + +def test_check_areas_are_in_grid_and_format_argument_value(): + arg = ( + ({"county": "Kentucky"}, grid), + ({"state": "California"}, grid), + ({"loadzone": "Texas"}, grid), + ({"state": "El Paso"}, grid), + ({"interconnect": "Nebraska"}, grid), + ) + for a in arg: + with pytest.raises(ValueError): + _check_areas_are_in_grid_and_format(a[0], a[1]) + + +def test_check_areas_are_in_grid_and_format(): + assert _check_areas_are_in_grid_and_format( + { + "state": {"Washington", "Kentucky", "NE", "TX", "WA"}, + "loadzone": ["Washington", "East", "El Paso", "Bay Area"], + "interconnect": "Texas", + }, + grid, + ) == { + "interconnect": {"Texas"}, + "state": {"Washington", "Kentucky", "Nebraska", "Texas"}, + "loadzone": {"Washington", "East", "El Paso", "Bay Area"}, + } + + +def test_check_resources_are_in_grid_and_format_argument_value(): arg = (({"solar", "dfo"}, grid), ({"uranium"}, grid)) for a in arg: with pytest.raises(ValueError): - _check_resources_are_in_grid(a[0], a[1]) + _check_resources_are_in_grid_and_format(a[0], a[1]) -def test_check_resources_are_in_grid(): - _check_resources_are_in_grid({"solar", "coal", "hydro"}, grid) - _check_resources_are_in_grid(["solar", "ng", "hydro", "nuclear"], grid) +def test_check_resources_are_in_grid_and_format(): + _check_resources_are_in_grid_and_format({"solar", "coal", "hydro"}, grid) + _check_resources_are_in_grid_and_format(["solar", "ng", "hydro", "nuclear"], grid) def test_check_plants_are_in_grid_argument_type(): @@ -368,31 +420,3 @@ def test_check_gencost_argument_value(): def test_check_gencost(): gencost = grid.gencost["after"] _check_gencost(gencost) - - -def test_check_curtailment_argument_type(): - curtailment = { - "solar": pd.DataFrame( - {1: 100, 2: 20, 3: 0}, - index=pd.date_range("2018-01-01", periods=3, freq="H"), - ), - "wind": [50, 5, 13], - } - arg = (1, ["solar", "wind"], curtailment) - for a in arg: - with pytest.raises(TypeError): - _check_curtailment() - - -def check_curtailment(): - curtaiment = { - "solar": pd.DataFrame( - {1: 100, 2: 20, 3: 0}, - index=pd.date_range("2018-01-01", periods=3, freq="H"), - ), - "wind": pd.DataFrame( - {1: 50, 2: 5, 3: 13}, - index=pd.date_range("2018-01-01", periods=3, freq="H"), - ), - } - _check_curtailment(curtailment) diff --git a/postreise/analyze/tests/test_helpers.py b/postreise/analyze/tests/test_helpers.py index c2ba1264..b7c0f4c6 100644 --- a/postreise/analyze/tests/test_helpers.py +++ b/postreise/analyze/tests/test_helpers.py @@ -1,16 +1,25 @@ import unittest -import pytest -from numpy.testing import assert_array_equal, assert_array_almost_equal import pandas as pd - -from powersimdata.tests.mock_grid import MockGrid +import pytest +from numpy.testing import assert_array_almost_equal, assert_array_equal from postreise.analyze.helpers import ( - get_resources_in_grid, get_active_resources_in_grid, + get_plant_id_for_resources, + get_plant_id_in_loadzones, + get_plant_id_in_interconnects, + get_plant_id_in_states, + get_plant_id_for_resources_in_interconnects, + get_plant_id_for_resources_in_loadzones, + get_plant_id_for_resources_in_states, + get_resources_in_grid, summarize_plant_to_bus, summarize_plant_to_location, ) +from powersimdata.tests.mock_grid import MockGrid +from powersimdata.input.grid import Grid +from powersimdata.network.usa_tamu.constants import zones + # plant_id is the index mock_plant = { @@ -19,6 +28,7 @@ "lat": [47.6, 47.6, 37.8, 37.8], "lon": [122.3, 122.3, 122.4, 122.4], "type": ["coal", "ng", "coal", "solar"], + "Pmin": [0, 50, 0, 0], "Pmax": [0, 300, 0, 50], } @@ -117,13 +127,220 @@ def test_summarize_location(self): self._check_dataframe_matches(loc_data, expected_return) -def test_get_resources_in_grid(): - grid = MockGrid(grid_attrs) - resources = get_resources_in_grid(grid) - assert resources == {"ng", "coal", "solar"} +class TestResourcesInGrid(unittest.TestCase): + def setUp(self): + self.grid = MockGrid(grid_attrs) + + def test_get_resources_in_grid(self): + assert get_resources_in_grid(self.grid) == {"ng", "coal", "solar"} + + def test_get_active_resources_in_grid(self): + assert get_active_resources_in_grid(self.grid) == {"ng", "solar"} + + +@pytest.fixture(scope="module") +def grid(): + return Grid(["USA"]) + + +def test_get_plant_id_for_resources_argument_type(grid): + arg = ((1, grid), ([1, 2, 3], grid), ("nuclear", 1)) + for a in arg: + with pytest.raises(TypeError): + get_plant_id_for_resources(a[0], a[1]) + + +def test_get_plant_id_for_resources_argument_value(grid): + arg = (("uranium", grid), (["uranium", "plutonium"], grid)) + for a in arg: + with pytest.raises(ValueError): + get_plant_id_for_resources(a[0], a[1]) + + +def test_get_plant_id_for_resources(grid): + arg = (("nuclear", grid), (["solar", "ng"], grid)) + expected = (["nuclear"], ["solar", "ng"]) + for a, e in zip(arg, expected): + plant_id = get_plant_id_for_resources(a[0], a[1]) + assert set(grid.plant.loc[plant_id].type) == set(e) + + +def test_get_plant_id_in_loadzones_argument_type(grid): + arg = ((1, grid), ([1, 2, 3], grid), ("Nevada", 1), ("Far West", 1)) + for a in arg: + with pytest.raises(TypeError): + get_plant_id_in_loadzones(a[0], a[1]) + + +def test_get_plant_id_in_loadzones_argument_value(grid): + arg = (("France", grid), (["Alberta", "British Columbia"], grid)) + for a in arg: + with pytest.raises(ValueError): + get_plant_id_in_loadzones(a[0], a[1]) + + +def test_get_plant_id_in_loadzones(grid): + arg = (("Oregon", grid), (["Kentucky", "Montana Western", "El Paso"], grid)) + expected = (["Oregon"], ["Kentucky", "Montana Western", "El Paso"]) + for a, e in zip(arg, expected): + plant_id = get_plant_id_in_loadzones(a[0], a[1]) + assert set(grid.plant.loc[plant_id].zone_name) == set(e) + + +def test_get_plant_id_in_interconnects_argument_type(grid): + arg = ((1, grid), ([1, 2, 3], grid), ("Eastern", 1)) + for a in arg: + with pytest.raises(TypeError): + get_plant_id_in_interconnects(a[0], a[1]) + + +def test_get_plant_id_in_interconnects_argument_value(grid): + arg = (("ERCOT", grid), (["CAISO", "Western"], grid)) + for a in arg: + with pytest.raises(ValueError): + get_plant_id_in_interconnects(a[0], a[1]) + + +def test_get_plant_id_in_interconnects(grid): + arg = (("Western", grid), (["Texas_Western", "Eastern"], grid)) + expected = (["Western"], ["Texas", "Western", "Eastern"]) + for a, e in zip(arg, expected): + plant_id = get_plant_id_in_interconnects(a[0], a[1]) + assert set(grid.plant.loc[plant_id].interconnect) == set(e) + +def test_get_plant_id_in_states_argument_type(grid): + arg = ((1, grid), ([1, 2, 3], grid), ("California", 1)) + for a in arg: + with pytest.raises(TypeError): + get_plant_id_in_states(a[0], a[1]) -def test_get_active_resources_in_grid(): - grid = MockGrid(grid_attrs) - resources = get_active_resources_in_grid(grid) - assert resources == {"ng", "solar"} + +def test_get_plant_id_in_states_argument_value(grid): + arg = (("Western", grid), (["Far West", "New Mexico Eastern"], grid)) + for a in arg: + with pytest.raises(ValueError): + get_plant_id_in_states(a[0], a[1]) + + +def test_get_plant_id_in_states(grid): + arg = (("TX", grid), (["Washington", "OR", "Idaho"], grid)) + expected = (({44, 45, 216} | set(range(301, 309))), {201, 202, 214}) + for a, e in zip(arg, expected): + plant_id = get_plant_id_in_states(a[0], a[1]) + assert set(grid.plant.loc[plant_id].zone_id) == e + + +def test_get_plant_id_for_resources_in_loadzones_argument_type(grid): + arg = ((1, 1, grid), ([1, 2, 3], {4, 5, 5}, grid), ("solar", "Utah", 1)) + for a in arg: + with pytest.raises(TypeError): + get_plant_id_for_resources_in_loadzones(a[0], a[1], a[2]) + + +def test_get_plant_id_for_resources_in_loadzones_argument_value(grid): + arg = ( + (["solar", "hydro", "wind"], "Western", grid), + ("plutonium", "Kentucky", grid), + ) + for a in arg: + with pytest.raises(ValueError): + get_plant_id_for_resources_in_loadzones(a[0], a[1], a[2]) + + +def test_get_plant_id_for_resources_in_loadzones(grid): + arg = ( + ("solar", ["Utah", "Montana Western"], grid), + (["nuclear", "wind"], ["Colorado", "El Paso"], grid), + (["wind", "solar"], "Oregon", grid), + (["coal", "ng"], ["South Carolina", "Ohio River", "Maine"], grid), + ) + expected = ( + (["solar"], ["Utah"]), + (["wind"], ["Colorado"]), + (["wind", "solar"], ["Oregon"]), + (["coal", "ng"], ["South Carolina", "Ohio River", "Maine"]), + ) + for a, e in zip(arg, expected): + plant_id = get_plant_id_for_resources_in_loadzones(a[0], a[1], a[2]) + assert set(grid.plant.loc[plant_id].type) == set(e[0]) + assert set(grid.plant.loc[plant_id].zone_name) == set(e[1]) + + +def test_get_plant_id_for_resources_in_interconnects_argument_type(grid): + arg = ( + (1, 1, grid), + ([1, 2, 3], {4, 5, 5}, grid), + (["solar", "ng", "gothermal"], "Texas_Western", 1), + ) + for a in arg: + with pytest.raises(TypeError): + get_plant_id_for_resources_in_interconnects(a[0], a[1], a[2]) + + +def test_get_plant_id_for_resources_in_interconnects_argument_value(grid): + arg = ( + (["nuclear", "hydro"], "coal", grid), + (["plutonium", "coal"], ["Eastern", "Texas"], grid), + ) + for a in arg: + with pytest.raises(ValueError): + get_plant_id_for_resources_in_interconnects(a[0], a[1], a[2]) + + +def test_get_plant_id_for_resources_in_interconnects(grid): + arg = ( + ("solar", ["Western"], grid), + (["nuclear", "wind"], ["Texas_Western"], grid), + (["geothermal"], ["Western", "Eastern"], grid), + ) + expected = ( + (["solar"], ["Western"]), + (["nuclear", "wind"], ["Texas", "Western"]), + (["geothermal"], ["Western"]), + ) + for a, e in zip(arg, expected): + plant_id = get_plant_id_for_resources_in_interconnects(a[0], a[1], a[2]) + assert set(grid.plant.loc[plant_id].type) == set(e[0]) + assert set(grid.plant.loc[plant_id].interconnect) == set(e[1]) + + +def test_get_plant_id_for_resources_in_states_argument_type(grid): + arg = ( + (1, 1, grid), + ([1, 2, 3], {4, 5, 5}, grid), + (["solar", "ng", "gothermal"], ["New Mexico", "California"], 1), + ) + for a in arg: + with pytest.raises(TypeError): + get_plant_id_for_resources_in_states(a[0], a[1], a[2]) + + +def test_get_plant_id_for_resources_in_states_argument_value(grid): + arg = ( + (["nuclear", "hydro"], "Eastern", grid), + (["plutonium", "coal"], ["Illinois", "FL"], grid), + ) + for a in arg: + with pytest.raises(ValueError): + get_plant_id_for_resources_in_states(a[0], a[1], a[2]) + + +def test_get_plant_id_for_resources_in_states(grid): + arg = ( + (["solar", "wind", "nuclear"], ["California", "TX"], grid), + (["nuclear", "wind"], ["Washington"], grid), + (["geothermal"], ["Nevada", "Massachusetts", "Mississippi"], grid), + ) + expected = ( + ( + ["solar", "wind", "nuclear"], + set(range(203, 208)) | {44, 45, 216} | set(range(301, 309)), + ), + (["nuclear", "wind"], [201]), + (["geothermal"], [208]), + ) + for a, e in zip(arg, expected): + plant_id = get_plant_id_for_resources_in_states(a[0], a[1], a[2]) + assert set(grid.plant.loc[plant_id].type) == set(e[0]) + assert set(grid.plant.loc[plant_id].zone_id) == set(e[1])