def test_good_scenario(self): mock_plant = { "plant_id": ["A", "B", "C", "D"], "ramp_30": [2.5, 5, 10, 25], } mock_scenario = MockScenario({"plant": mock_plant}) _check_scenario_is_in_analyze_state(mock_scenario)
def calculate_NLDC(scenario, resources, hours=100): """Calculate the capacity value of a class of resources by comparing the mean of the top N hour of absolute demand to the mean of the top N hours of net demand. NLDC = 'Net Load Duration Curve'. :param powersimdata.scenario.scenario.Scenario scenario: analyzed scenario. :param str/list/tuple/set resources: one or more resources to analyze. :param int hours: number of hours to analyze. :return: (*float*) -- difference between peak demand and peak net demand. """ _check_scenario_is_in_analyze_state(scenario) grid = scenario.state.get_grid() resources = _check_resources_are_in_grid_and_format(resources, grid) _check_number_hours_to_analyze(scenario, hours) # Then calculate capacity value total_demand = scenario.state.get_demand().sum(axis=1) prev_peak = total_demand.sort_values(ascending=False).head(hours).mean() plant_groupby = grid.plant.groupby("type") plant_indices = sum( [plant_groupby.get_group(r).index.tolist() for r in resources], []) resource_generation = scenario.state.get_pg()[plant_indices].sum(axis=1) net_demand = total_demand - resource_generation net_peak = net_demand.sort_values(ascending=False).head(hours).mean() return prev_peak - net_peak
def generate_emissions_stats(scenario, pollutant="carbon", method="simple"): """Generate emissions statistics from the input generation data. 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). :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. """ _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.state.get_pg() grid = scenario.state.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 elif method in ("decommit", "always-on"): decommit = True if method == "decommit" else False costs = calc_costs(pg, 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[:, indices] / grid.plant["GenFuelCost"].values[indices]) emissions.loc[:, indices] = heat[:, indices] * val * 44 / 12 / 1000 else: raise Exception("I should not be able to get here") return emissions
def test_bad_scenario_state(self): mock_plant = { "plant_id": ["A", "B", "C", "D"], "ramp_30": [2.5, 5, 10, 25], } mock_scenario = MockScenario({"plant": mock_plant}) mock_scenario.state = "Create" with self.assertRaises(ValueError): _check_scenario_is_in_analyze_state(mock_scenario)
def pmin_constraints(scenario, epsilon=1e-3): """Identify time periods in which generators are at minimum power. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :param float epsilon: allowable 'fuzz' for whether constraint is binding. :return: (*pandas.DataFrame*) -- Boolean data frame of same shape as PG. """ _check_scenario_is_in_analyze_state(scenario) _check_epsilon(epsilon) pg = scenario.state.get_pg() grid = scenario.state.get_grid() pmin = grid.plant["Pmin"] binding_pmin_constraints = (pg - pmin) <= epsilon return binding_pmin_constraints
def ramp_constraints(scenario, epsilon=1e-3): """Identify time periods in which generators have binding ramp constraints. .. note:: The first time period will always return *False* for each column. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :param float epsilon: allowable 'fuzz' for whether constraint is binding. :return: (*pandas.DataFrame*) -- Boolean dataframe of same shape as PG. """ _check_scenario_is_in_analyze_state(scenario) _check_epsilon(epsilon) pg = scenario.state.get_pg() grid = scenario.state.get_grid() ramp = grid.plant["ramp_30"] diff = pg.diff(axis=0) binding_ramp_constraints = (ramp * 2 - abs(diff)) <= epsilon return binding_ramp_constraints
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() plant_id = get_plant_id_for_resources( grid.model_immutables.plants["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_net_load_peak(scenario, resources, hours=100): """Calculate the capacity value of a class of resources by averaging the power generated in the top N hours of net load peak. :param powersimdata.scenario.scenario.Scenario scenario: analyzed scenario. :param str/list/tuple/set resources: one or more resources to analyze. :param int hours: number of hours to analyze. :return: (*float*) -- resource capacity during hours of peak net demand. """ _check_scenario_is_in_analyze_state(scenario) grid = scenario.state.get_grid() resources = _check_resources_are_in_grid_and_format(resources, grid) _check_number_hours_to_analyze(scenario, hours) # Then calculate capacity value total_demand = scenario.state.get_demand().sum(axis=1) plant_groupby = grid.plant.groupby("type") plant_indices = sum( [plant_groupby.get_group(r).index.tolist() for r in resources], []) resource_generation = scenario.state.get_pg()[plant_indices].sum(axis=1) net_demand = total_demand - resource_generation top_hours = net_demand.sort_values(ascending=False).head(hours).index return resource_generation[top_hours].mean()
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 plot_generation_time_series_stack( scenario, area, resources, area_type=None, time_range=None, time_zone="utc", time_freq="H", show_demand=True, show_net_demand=True, normalize=False, t2c=None, t2l=None, t2hc=None, title=None, label_fontsize=20, title_fontsize=22, tick_fontsize=15, legend_fontsize=18, save=False, filename=None, filepath=None, ): """Generate time series generation stack plot in a certain area of a scenario. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance :param str area: one of *loadzone*, *state*, *state abbreviation*, *interconnect*, *'all'* :param str/list resources: one or a list of resources. *'solar_curtailment'*, *'wind_curtailment'*, *'wind_offshore_curtailment'* are valid entries together with all available generator types in the area. The order of the resources determines the stack order in the figure. :param str area_type: one of *'loadzone'*, *'state'*, *'state_abbr'*, *'interconnect'* :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire time range is used for the given scenario. :param str time_zone: new time zone. :param str time_freq: frequency. Either *'D'* (day), *'W'* (week), *'M'* (month). :param bool show_demand: show demand line in the plot or not, default is True. :param bool show_net_demand: show net demand line in the plot or not, default is True. :param bool normalize: normalize the generation based on capacity or not, default is False. :param dict t2c: user specified color of resource type to overwrite type2color default dict. key: resource type, value: color code. :param dict t2l: user specified label of resource type to overwrite type2label default dict. key: resource type, value: label. :param dict t2hc: user specified color of curtailable resource hatches to overwrite type2hatchcolor default dict. key: resource type, valid keys are *'wind_curtailment'*, *'solar_curtailment'*, *'wind_offshore_curtailment'*, value: color code. :param str title: user specified title of the figure, default is set to be area. :param float label_fontsize: user specified label fontsize, default is 20. :param float title_fontsize: user specified title fontsize, default is 22. :param float tick_fontsize: user specified ticks of axes fontsize, default is 15. :param float legend_fontsize: user specified legend fontsize, default is 18. :param bool save: save the generated figure or not, default is False. :param str filename: if save is True, user specified filename, use area if None. :param str filepath: if save is True, user specified filepath, use current directory if None. """ _check_scenario_is_in_analyze_state(scenario) mi = ModelImmutables(scenario.info["grid_model"]) type2color = mi.plants["type2color"] type2label = mi.plants["type2label"] type2hatchcolor = mi.plants["type2hatchcolor"] if t2c: type2color.update(t2c) if t2l: type2label.update(t2l) if t2hc: type2hatchcolor.update(t2hc) pg_stack = get_generation_time_series_by_resources(scenario, area, resources, area_type=area_type) capacity = get_capacity_by_resources(scenario, area, resources, area_type=area_type) demand = get_demand_time_series(scenario, area, area_type=area_type) net_demand = get_net_demand_time_series(scenario, area, area_type=area_type) capacity_ts = pd.Series(capacity.sum(), index=pg_stack.index) curtailable_resources = { "solar_curtailment", "wind_curtailment", "wind_offshore_curtailment", } if curtailable_resources & set(resources): curtailment = get_curtailment_time_series(scenario, area, area_type=area_type) for r in curtailable_resources: if r in resources and r in curtailment.columns: pg_stack[r] = curtailment[r] if time_zone != "utc": pg_stack = change_time_zone(pg_stack, time_zone) demand = change_time_zone(demand, time_zone) net_demand = change_time_zone(net_demand, time_zone) capacity_ts = change_time_zone(capacity_ts, time_zone) if not time_range: time_range = ( pd.Timestamp(scenario.info["start_date"]), pd.Timestamp(scenario.info["end_date"]), ) pg_stack = slice_time_series(pg_stack, time_range[0], time_range[1]) demand = slice_time_series(demand, time_range[0], time_range[1]) net_demand = slice_time_series(net_demand, time_range[0], time_range[1]) capacity_ts = slice_time_series(capacity_ts, time_range[0], time_range[1]) if time_freq != "H": pg_stack = resample_time_series(pg_stack, time_freq) demand = resample_time_series(demand, time_freq) net_demand = resample_time_series(net_demand, time_freq) capacity_ts = resample_time_series(capacity_ts, time_freq) if "storage" in resources: pg_storage = get_storage_time_series(scenario, area, area_type=area_type) capacity_storage = get_storage_capacity(scenario, area, area_type=area_type) capacity_storage_ts = pd.Series(capacity_storage, index=pg_storage.index) if time_zone != "utc": pg_storage = change_time_zone(pg_storage, time_zone) capacity_storage_ts = change_time_zone(capacity_storage_ts, time_zone) if time_range != ("2016-01-01 00:00:00", "2016-12-31 23:00:00"): pg_storage = slice_time_series(pg_storage, time_range[0], time_range[1]) capacity_storage_ts = slice_time_series(capacity_storage_ts, time_range[0], time_range[1]) if time_freq != "H": pg_storage = resample_time_series(pg_storage, time_freq) capacity_storage_ts = resample_time_series(capacity_storage_ts, time_freq) pg_stack["storage"] = pg_storage.clip(lower=0) capacity_ts += capacity_storage_ts fig, (ax, ax_storage) = plt.subplots( 2, 1, figsize=(20, 15), sharex="row", gridspec_kw={ "height_ratios": [3, 1], "hspace": 0.02 }, ) plt.subplots_adjust(wspace=0) if normalize: pg_storage = pg_storage.divide(capacity_storage_ts, axis="index") ax_storage.set_ylabel("Normalized Storage", fontsize=label_fontsize) else: ax_storage.set_ylabel("Energy Storage (MW)", fontsize=label_fontsize) ax_storage = pg_storage.plot(color=type2color["storage"], lw=4, ax=ax_storage) ax_storage.fill_between( pg_storage.index.values, 0, pg_storage.values, color=type2color["storage"], alpha=0.5, ) # Erase year in xticklabels xt_with_year = list(ax_storage.__dict__["date_axis_info"][0]) xt_with_year[-1] = b"%b" ax_storage.__dict__["date_axis_info"][0] = tuple(xt_with_year) ax_storage.tick_params(axis="both", which="both", labelsize=tick_fontsize) ax_storage.set_xlabel("") for a in fig.get_axes(): a.label_outer() else: fig = plt.figure(figsize=(20, 10)) ax = fig.gca() if normalize: pg_stack = pg_stack.divide(capacity_ts, axis="index") demand = demand.divide(capacity_ts, axis="index") net_demand = net_demand.divide(capacity_ts, axis="index") ax.set_ylabel("Normalized Generation", fontsize=label_fontsize) else: pg_stack = pg_stack.divide(1e6, axis="index") demand = demand.divide(1e6, axis="index") net_demand = net_demand.divide(1e6, axis="index") ax.set_ylabel("Daily Energy TWh", fontsize=label_fontsize) available_resources = [r for r in resources if r in pg_stack.columns] pg_stack[available_resources].clip(0, None).plot.area(color=type2color, linewidth=0, alpha=0.7, ax=ax, sharex="row") if show_demand: demand.plot(color="red", lw=4, ax=ax) if show_net_demand: net_demand.plot(color="red", ls="--", lw=2, ax=ax) if not title: title = area ax.set_title("%s" % title, fontsize=title_fontsize) ax.grid(color="black", axis="y") if "storage" not in resources: # Erase year in xticklabels xt_with_year = list(ax.__dict__["date_axis_info"][0]) xt_with_year[-1] = b"%b" ax.__dict__["date_axis_info"][0] = tuple(xt_with_year) ax.set_xlabel("") ax.tick_params(which="both", labelsize=tick_fontsize) ax.set_ylim([ min(0, 1.1 * net_demand.min()), max(ax.get_ylim()[1], 1.1 * demand.max()), ]) handles, labels = ax.get_legend_handles_labels() if show_demand: labels[0] = "Demand" if show_net_demand: labels[1] = "Net Demand" label_offset = show_demand + show_net_demand labels = [type2label[l] if l in type2label else l for l in labels] # Add hatches for r in curtailable_resources: if r in available_resources: ind = available_resources.index(r) ax.fill_between( pg_stack[available_resources].index.values, pg_stack[available_resources].iloc[:, :ind + 1].sum(axis=1), pg_stack[available_resources].iloc[:, :ind].sum(axis=1), color="none", hatch="//", edgecolor=type2hatchcolor[r], linewidth=0.0, ) handles[ind + label_offset] = mpatches.Patch( facecolor=type2color[r], hatch="//", edgecolor=type2hatchcolor[r], linewidth=0.0, ) ax.legend( handles[::-1], labels[::-1], frameon=2, prop={"size": legend_fontsize}, loc="upper left", bbox_to_anchor=(1, 1), ) if save: if not filename: filename = area if not filepath: filepath = os.path.join(os.getcwd(), filename) plt.savefig(f"{filepath}.pdf", bbox_inches="tight", pad_inches=0)
def test_check_scenario_is_in_analyze(): _check_scenario_is_in_analyze_state(scenario)
def test_check_scenario_is_in_analyze_state_argument_value(): input = MockScenario() input.state = "Create" with pytest.raises(ValueError): _check_scenario_is_in_analyze_state(input)
def test_check_scenario_is_in_analyze_state_argument_type(): arg = (1, grid) for a in arg: with pytest.raises(TypeError): _check_scenario_is_in_analyze_state(a)
def plot_curtailment_time_series( scenario, area, resources, area_type=None, time_range=None, time_zone="utc", time_freq="H", show_demand=True, percentage=True, t2c=None, t2l=None, title=None, label_fontsize=20, title_fontsize=22, tick_fontsize=15, legend_fontsize=18, save=False, filename=None, filepath=None, ): """Generate time series curtailment plot of each specified resource in a certain area of a scenario. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance :param str area: one of *loadzone*, *state*, *state abbreviation*, *interconnect*, *'all'* :param str/list resources: one or a list of resources. :param str area_type: one of *'loadzone'*, *'state'*, *'state_abbr'*, *'interconnect'* :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire time range is used for the given scenario. :param str time_zone: new time zone. :param str time_freq: frequency. Either *'D'* (day), *'W'* (week), *'M'* (month). :param bool show_demand: show demand line in the plot or not, default is True. :param bool percentage: plot the curtailment in terms of percentage or not, default is True. :param dict t2c: user specified color of resource type to overwrite type2color default dict. key: resource type, value: color code. :param dict t2l: user specified label of resource type to overwrite type2label default dict. key: resource type, value: label. :param str title: user specified title of the figure. :param float label_fontsize: user specified label fontsize, default is 20. :param float title_fontsize: user specified title fontsize, default is 22. :param float tick_fontsize: user specified ticks of axes fontsize, default is 15. :param float legend_fontsize: user specified legend fontsize, default is 18. :param bool save: save the generated figure or not, default is False. :param str filename: if save is True, user specified filename, use area if None. :param str filepath: if save is True, user specified filepath, use current directory if None. """ _check_scenario_is_in_analyze_state(scenario) resources = _check_resources_and_format( resources, grid_model=scenario.info["grid_model"] ) mi = ModelImmutables(scenario.info["grid_model"]) type2color = mi.plants["type2color"] type2label = mi.plants["type2label"] if t2c: type2color.update(t2c) if t2l: type2label.update(t2l) resource_pg = get_generation_time_series_by_resources( scenario, area, resources, area_type=area_type ) demand = get_demand_time_series(scenario, area, area_type=area_type) curtailment = get_curtailment_time_series(scenario, area, area_type=area_type) if time_zone != "utc": resource_pg = change_time_zone(resource_pg, time_zone) demand = change_time_zone(demand, time_zone) curtailment = change_time_zone(curtailment, time_zone) if not time_range: time_range = ( pd.Timestamp(scenario.info["start_date"]), pd.Timestamp(scenario.info["end_date"]), ) resource_pg = slice_time_series(resource_pg, time_range[0], time_range[1]) demand = slice_time_series(demand, time_range[0], time_range[1]) curtailment = slice_time_series(curtailment, time_range[0], time_range[1]) if time_freq != "H": resource_pg = resample_time_series(resource_pg, time_freq) demand = resample_time_series(demand, time_freq) curtailment = resample_time_series(curtailment, time_freq) for r in resource_pg.columns: curtailment[r + "_curtailment" + "_mean"] = curtailment[ r + "_curtailment" ].mean() curtailment[r + "_available"] = curtailment[r + "_curtailment"] + resource_pg[r] curtailment[r + "_curtailment" + "_percentage"] = ( curtailment[r + "_curtailment"] / curtailment[r + "_available"] * 100 ) curtailment[r + "_curtailment" + "_percentage" + "_mean"] = curtailment[ r + "_curtailment" + "_percentage" ].mean() for r in resources: if r not in resource_pg.columns: raise ValueError(f"{r} is invalid in {area}!") fig = plt.figure(figsize=(20, 10)) ax = fig.gca() title_text = f"{area} {r.capitalize()}" if not title else title plt.title(title_text, fontsize=title_fontsize) cr = r + "_curtailment" if percentage: key1, key2 = f"{cr}_percentage", f"{cr}_percentage_mean" else: key1, key2 = cr, f"{cr}_mean" curtailment[key1].plot( ax=ax, lw=4, alpha=0.7, color=type2color[cr], label=type2label[cr], ) curtailment[key2].plot( ax=ax, ls="--", lw=4, alpha=0.7, color=type2color[cr], label=type2label[cr] + " Mean", ) ax_twin = ax.twinx() curtailment[r + "_available"].plot( ax=ax_twin, lw=4, alpha=0.7, color=type2color[r], label=f"{type2label[r]} Energy Available", ) if show_demand: demand.plot(ax=ax_twin, lw=4, alpha=0.7, color="red", label="Demand") # Erase year in xticklabels xt_with_year = list(ax_twin.__dict__["date_axis_info"][0]) xt_with_year[-1] = b"%b" ax_twin.__dict__["date_axis_info"][0] = tuple(xt_with_year) ax_twin.set_xlabel("") ax_twin.tick_params(which="both", labelsize=tick_fontsize) ax_twin.yaxis.get_offset_text().set_fontsize(tick_fontsize) ax_twin.set_ylabel("MWh", fontsize=label_fontsize) ax_twin.legend(loc="upper right", prop={"size": legend_fontsize}) ax.tick_params(which="both", labelsize=tick_fontsize) ax.yaxis.get_offset_text().set_fontsize(tick_fontsize) ax.grid(color="black", axis="y") ax.set_xlabel("") if percentage: ax.set_ylabel("Curtailment [%]", fontsize=label_fontsize) else: ax.set_ylabel("Curtailment", fontsize=label_fontsize) ax.legend(loc="upper left", prop={"size": legend_fontsize}) if save: if not filename: filename = f"{area.lower()}_{r}_curtailment" if not filepath: filepath = os.path.join(os.getcwd(), filename) plt.savefig(f"{filepath}.pdf", bbox_inches="tight", pad_inches=0)
def test_bad_scenario_type(self): with self.assertRaises(TypeError): _check_scenario_is_in_analyze_state("307")