def generation_emissions_per_bus_rule(model, bus): bus_emission_reference = get_co2_emission_level_for_country( bus, refyear) bus_emission_target = (1-reduction_share_per_country[bus]) * bus_emission_reference \ * sum(net.snapshot_weightings['objective']) / 8760. bus_gens = net.generators[(net.generators.carrier.astype(bool)) & (net.generators.bus == bus)] generator_emissions_sum = 0. for tech in bus_gens.type.unique(): fuel, efficiency = get_tech_info(tech, ["fuel", "efficiency_ds"]) fuel_emissions_el = get_fuel_info(fuel, ['CO2']) fuel_emissions_thermal = fuel_emissions_el / efficiency gens = bus_gens[bus_gens.type == tech] for g in gens.index.values: for s in net.snapshots: generator_emissions_sum += model.generator_p[ g, s] * fuel_emissions_thermal.values[0] return generator_emissions_sum <= bus_emission_target
def generation_emissions_rule(model): generator_emissions_sum = 0. for tech in gens.type.unique(): fuel, efficiency = get_tech_info(tech, ["fuel", "efficiency_ds"]) fuel_emissions_el = get_fuel_info(fuel, ['CO2']) fuel_emissions_thermal = fuel_emissions_el / efficiency gen = gens[gens.index.str.contains(tech)] for g in gen.index.values: for s in network.snapshots: generator_emissions_sum += model.generator_p[ g, s] * fuel_emissions_thermal.values[0] return generator_emissions_sum <= co2_budget
def add_generators(network: pypsa.Network, tech: str) -> pypsa.Network: """ Add conventional generators to a Network instance. Parameters ---------- network: pypsa.Network A PyPSA Network instance with nodes associated to regions. tech: str Type of conventional generator (ccgt or ocgt) Returns ------- network: pypsa.Network Updated network """ logger.info(f"Adding {tech} generation.") assert hasattr(network.buses, "onshore_region"), "Some buses must be associated to an onshore region to add" \ "conventional generators." # Filter to keep only onshore buses # buses = network.buses[network.buses.onshore] buses = network.buses.dropna(subset=["onshore_region"], axis=0) capital_cost, marginal_cost = get_costs( tech, sum(network.snapshot_weightings['objective'])) # Get fuel type and efficiency fuel, efficiency = get_tech_info(tech, ["fuel", "efficiency_ds"]) network.madd("Generator", buses.index, suffix=f" Gen {tech}", bus=buses.index, p_nom_extendable=True, type=tech, carrier=fuel, efficiency=efficiency, marginal_cost=marginal_cost, capital_cost=capital_cost, x=buses.x.values, y=buses.y.values) return network
def add_co2_budget_per_country(net: pypsa.Network, co2_reduction_share: Dict[str, float], co2_reduction_refyear: int): """ Add CO2 budget per country. Parameters ---------- net: pypsa.Network A PyPSA Network instance with buses associated to regions co2_reduction_share: float Percentage of reduction of emission. co2_reduction_refyear: int Reference year from which the reduction in emission is computed. """ for bus in net.loads.bus: bus_emission_reference = get_co2_emission_level_for_country( bus, co2_reduction_refyear) co2_budget = (1-co2_reduction_share[bus]) * bus_emission_reference \ * sum(net.snapshot_weightings['objective']) / 8760. # Drop rows (gens) without an associated carrier (i.e., technologies not emitting) gens = net.generators[(net.generators.carrier.astype(bool)) & (net.generators.bus == bus)] gens_p = get_var(net, 'Generator', 'p')[gens.index] coefficients = pd.DataFrame(index=gens_p.index, columns=gens_p.columns, dtype=float) for tech in gens.type.unique(): fuel, efficiency = get_tech_info(tech, ["fuel", "efficiency_ds"]) fuel_emissions_el = get_fuel_info(fuel, ['CO2']) fuel_emissions_thermal = fuel_emissions_el / efficiency gens_with_tech = gens[gens.index.str.contains(tech)] coefficients[ gens_with_tech.index] = fuel_emissions_thermal.values[0] lhs = linexpr((coefficients, gens_p)).sum().sum() define_constraints(net, lhs, '<=', co2_budget, 'generation_emissions_global', bus)
def add_co2_budget_global(net: pypsa.Network, region: str, co2_reduction_share: float, co2_reduction_refyear: int): """ Add global CO2 budget. Parameters ---------- region: str Region over which the network is defined. net: pypsa.Network A PyPSA Network instance with buses associated to regions co2_reduction_share: float Percentage of reduction of emission. co2_reduction_refyear: int Reference year from which the reduction in emission is computed. """ co2_reference_kt = get_reference_emission_levels_for_region( region, co2_reduction_refyear) co2_budget = co2_reference_kt * (1 - co2_reduction_share) * sum( net.snapshot_weightings['objective']) / 8760. gens = net.generators[net.generators.carrier.astype(bool)] gens_p = get_var(net, 'Generator', 'p')[gens.index] coefficients = pd.DataFrame(index=gens_p.index, columns=gens_p.columns, dtype=float) for tech in gens.type.unique(): fuel, efficiency = get_tech_info(tech, ["fuel", "efficiency_ds"]) fuel_emissions_el = get_fuel_info(fuel, ['CO2']) fuel_emissions_thermal = fuel_emissions_el / efficiency gens_with_tech = gens[gens.index.str.contains(tech)] coefficients[gens_with_tech.index] = fuel_emissions_thermal.values[0] lhs = linexpr((coefficients, gens_p)).sum().sum() define_constraints(net, lhs, '<=', co2_budget, 'generation_emissions_global')
def add_generators(net: pypsa.Network, countries: List[str], use_ex_cap: bool = True, extendable: bool = False) -> pypsa.Network: """ Add nuclear generators to a PyPsa Network instance. Parameters ---------- net: pypsa.Network A Network instance with nodes associated to parameters: 'onshore' and 'region'. countries: List[str] Codes of countries over which the network is built use_ex_cap: bool (default: True) Whether to consider existing capacity or not extendable: bool (default: False) Whether generators are extendable Returns ------- network: pypsa.Network Updated network """ for attr in ["onshore_region"]: assert hasattr( net.buses, attr), f"Error: Buses must contain a '{attr}' attribute." # Nuclear plants can only be added onshore # onshore_buses = net.buses[net.buses.onshore] onshore_buses = net.buses.dropna(subset=["onshore_region"], axis=0) if len(onshore_buses) == 0: warn( "Warning: Trying to add nuclear to network without onshore buses.") return net gens = get_powerplants('nuclear', countries) buses_countries = list(onshore_buses.country) if hasattr( onshore_buses, 'country') else None gens["bus_id"] = match_powerplants_to_regions( gens, onshore_buses.onshore_region, shapes_countries=buses_countries, dist_threshold=50.0) # If no plants in the chosen countries, return directly the network if len(gens) == 0: return net logger.info( f"Adding {gens['Capacity'].sum() * 1e-3:.2f} GW of nuclear capacity " f"in {sorted(gens['ISO2'].unique())}.") if not use_ex_cap: gens.Capacity = 0. gens.Capacity /= 1000. # Convert MW to GW capital_cost, marginal_cost = get_costs( 'nuclear', sum(net.snapshot_weightings['objective'])) # Get fuel type, efficiency and ramp rates fuel, efficiency, ramp_rate, base_level = \ get_tech_info('nuclear', ["fuel", "efficiency_ds", "ramp_rate", "base_level"]) net.madd("Generator", "Gen nuclear " + gens.Name + " " + gens.bus_id, bus=gens.bus_id.values, p_nom=gens.Capacity.values, p_nom_min=gens.Capacity.values, p_nom_extendable=extendable, type='nuclear', carrier=fuel, efficiency=efficiency, marginal_cost=marginal_cost, capital_cost=capital_cost, ramp_limit_up=ramp_rate, ramp_limit_down=ramp_rate, p_min_pu=base_level, x=gens.lon.values, y=gens.lat.values) return net
def add_phs_plants(net: pypsa.Network, topology_type: str = "countries", extendable: bool = False, cyclic_sof: bool = True) -> pypsa.Network: """ Add pumped-hydro storage units to a PyPSA Network instance. Parameters ---------- net: pypsa.Network A Network instance. topology_type: str Can currently be countries (for one node per country topologies) or ehighway (for topologies based on ehighway) extendable: bool (default: False) Whether generators are extendable cyclic_sof: bool (default: True) Whether to set to True the cyclic_state_of_charge for the storage_unit component Returns ------- net: pypsa.Network Updated network """ check_assertions(net, topology_type) # Hydro generators can only be added onshore buses_onshore = net.buses.dropna(subset=["onshore_region"], axis=0) # Load capacities aggr_level = "countries" if topology_type == "countries" else "NUTS3" pow_cap, en_cap = get_phs_capacities(aggr_level) if topology_type == 'countries': # Extract only countries for which data is available countries_with_capacity = sorted( list(set(buses_onshore.country) & set(pow_cap.index))) buses_with_capacity_indexes = net.buses[net.buses.country.isin( countries_with_capacity)].index bus_pow_cap = pow_cap.loc[countries_with_capacity] bus_pow_cap.index = buses_with_capacity_indexes bus_en_cap = en_cap.loc[countries_with_capacity] bus_en_cap.index = buses_with_capacity_indexes else: # topology_type == 'ehighway bus_pow_cap, bus_en_cap = phs_inputs_nuts_to_ehighway( buses_onshore.index, pow_cap, en_cap) countries_with_capacity = set(bus_pow_cap.index.str[2:]) logger.info( f"Adding {bus_pow_cap.sum():.3f} GW of PHS hydro " f"with {bus_en_cap.sum():.3f} GWh of storage in {countries_with_capacity}." ) max_hours = bus_en_cap / bus_pow_cap # Get cost and efficiencies capital_cost, marginal_cost = get_costs( 'phs', sum(net.snapshot_weightings['objective'])) efficiency_dispatch, efficiency_store, self_discharge = \ get_tech_info('phs', ["efficiency_ds", "efficiency_ch", "efficiency_sd"]) self_discharge = round(1 - self_discharge, 4) net.madd("StorageUnit", bus_pow_cap.index, suffix=" Storage PHS", bus=bus_pow_cap.index, type='phs', p_nom=bus_pow_cap, p_nom_min=bus_pow_cap, p_nom_extendable=extendable, max_hours=max_hours.values, capital_cost=capital_cost, marginal_cost=marginal_cost, efficiency_store=efficiency_store, efficiency_dispatch=efficiency_dispatch, self_discharge=self_discharge, cyclic_state_of_charge=cyclic_sof, x=buses_onshore.loc[bus_pow_cap.index].x, y=buses_onshore.loc[bus_pow_cap.index].y) return net
def add_sto_plants(net: pypsa.Network, topology_type: str = "countries", extendable: bool = False, cyclic_sof: bool = True) -> pypsa.Network: """ Add run-of-river generators to a Network instance Parameters ---------- net: pypsa.Network A Network instance. topology_type: str Can currently be countries (for one node per country topologies) or ehighway (for topologies based on ehighway) extendable: bool (default: False) Whether generators are extendable cyclic_sof: bool (default: True) Whether to set to True the cyclic_state_of_charge for the storage_unit component Returns ------- net: pypsa.Network Updated network """ check_assertions(net, topology_type) # Hydro generators can only be added onshore buses_onshore = net.buses.dropna(subset=["onshore_region"], axis=0) # Load capacities and inflows aggr_level = "countries" if topology_type == "countries" else "NUTS3" pow_cap, en_cap = get_sto_capacities(aggr_level) inflows = get_sto_inflows(aggr_level, net.snapshots) if topology_type == 'countries': # Extract only countries for which data is available countries_with_capacity = sorted( list(set(buses_onshore.country) & set(pow_cap.index))) buses_with_capacity_indexes = net.buses[net.buses.country.isin( countries_with_capacity)].index bus_pow_cap = pow_cap.loc[countries_with_capacity] bus_pow_cap.index = buses_with_capacity_indexes bus_en_cap = en_cap.loc[countries_with_capacity] bus_en_cap.index = buses_with_capacity_indexes bus_inflows = inflows[countries_with_capacity] bus_inflows.columns = buses_with_capacity_indexes else: # topology_type == 'ehighway' bus_pow_cap, bus_en_cap, bus_inflows = \ sto_inputs_nuts_to_ehighway(buses_onshore.index, pow_cap, en_cap, inflows) countries_with_capacity = set(bus_pow_cap.index.str[2:]) logger.info( f"Adding {bus_pow_cap.sum():.2f} GW of STO hydro " f"with {bus_en_cap.sum() * 1e-3:.2f} TWh of storage in {countries_with_capacity}." ) bus_inflows = bus_inflows.round(3) max_hours = bus_en_cap / bus_pow_cap capital_cost, marginal_cost = get_costs( 'sto', sum(net.snapshot_weightings['objective'])) # Get efficiencies efficiency_dispatch = get_tech_info('sto', ['efficiency_ds'])["efficiency_ds"] net.madd("StorageUnit", bus_pow_cap.index, suffix=" Storage reservoir", bus=bus_pow_cap.index, type='sto', p_nom=bus_pow_cap, p_nom_min=bus_pow_cap, p_min_pu=0., p_nom_extendable=extendable, capital_cost=capital_cost, marginal_cost=marginal_cost, efficiency_store=0., efficiency_dispatch=efficiency_dispatch, cyclic_state_of_charge=cyclic_sof, max_hours=max_hours, inflow=bus_inflows, x=buses_onshore.loc[bus_pow_cap.index.values].x, y=buses_onshore.loc[bus_pow_cap.index.values].y) return net
def add_ror_plants(net: pypsa.Network, topology_type: str = "countries", extendable: bool = False) -> pypsa.Network: """ Add run-of-river generators to a Network instance. Parameters ---------- net: pypsa.Network A Network instance. topology_type: str Can currently be countries (for one node per country topologies) or ehighway (for topologies based on ehighway) extendable: bool (default: False) Whether generators are extendable Returns ------- net: pypsa.Network Updated network """ check_assertions(net, topology_type) # Hydro generators can only be added onshore buses_onshore = net.buses.dropna(subset=["onshore_region"], axis=0) # Load capacities and inflows aggr_level = "countries" if topology_type == "countries" else "NUTS3" pow_cap = get_ror_capacities(aggr_level) inflows = get_ror_inflows(aggr_level, net.snapshots) if topology_type == 'countries': # Extract only countries for which data is available countries_with_capacity = sorted( list(set(buses_onshore.country) & set(pow_cap.index))) buses_with_capacity_indexes = net.buses[net.buses.country.isin( countries_with_capacity)].index bus_pow_cap = pow_cap.loc[countries_with_capacity] bus_pow_cap.index = buses_with_capacity_indexes bus_inflows = inflows[countries_with_capacity] bus_inflows.columns = buses_with_capacity_indexes else: # topology_type == 'ehighway' bus_pow_cap, bus_inflows = \ ror_inputs_nuts_to_ehighway(buses_onshore.index, pow_cap, inflows) countries_with_capacity = set(bus_pow_cap.index.str[2:]) logger.info( f"Adding {bus_pow_cap.sum():.2f} GW of ROR hydro in {countries_with_capacity}." ) bus_inflows = bus_inflows.dropna().round(3) # Get cost and efficiencies capital_cost, marginal_cost = get_costs( 'ror', sum(net.snapshot_weightings['objective'])) efficiency = get_tech_info('ror', ["efficiency_ds"])["efficiency_ds"] net.madd("Generator", bus_pow_cap.index, suffix=" Generator ror", bus=bus_pow_cap.index, type='ror', p_nom=bus_pow_cap, p_nom_min=bus_pow_cap, p_nom_extendable=extendable, capital_cost=capital_cost, marginal_cost=marginal_cost, efficiency=efficiency, p_min_pu=0., p_max_pu=bus_inflows, x=buses_onshore.loc[bus_pow_cap.index].x, y=buses_onshore.loc[bus_pow_cap.index].y) return net
def add_batteries(network: pypsa.Network, battery_type: str, buses_ids: List[str] = None, fixed_duration: bool = False) -> pypsa.Network: """ Add a battery at each node of the network. Parameters ---------- network: pypsa.Network PyPSA network battery_type: str Type of battery to add buses_ids: List[str] IDs of the buses at which we want to add batteries. fixed_duration: bool Whether the battery storage is modelled with fixed duration. Returns ------- network: pypsa.Network Updated network """ logger.info(f"Adding {battery_type} storage.") buses = network.buses if buses_ids is not None: buses = buses.loc[buses_ids] # buses = network.buses[network.buses.onshore] # onshore_bus_indexes = pd.Index([bus_id for bus_id in buses.index if buses.loc[bus_id].onshore]) onshore_buses = buses.dropna(subset=["onshore_region"], axis=0) # Add batteries with fixed energy-power ratio if fixed_duration: capital_cost, marginal_cost = get_costs( battery_type, sum(network.snapshot_weightings['objective'])) efficiency_dispatch, efficiency_store, self_discharge = \ get_tech_info(battery_type, ["efficiency_ds", "efficiency_ch", "efficiency_sd"]) self_discharge = round(1 - self_discharge, 4) # Get max number of hours of storage max_hours = get_config_values(battery_type, ["max_hours"]) network.madd("StorageUnit", onshore_buses.index, suffix=f" StorageUnit {battery_type}", type=battery_type, bus=onshore_buses.index, p_nom_extendable=True, max_hours=max_hours, capital_cost=capital_cost, marginal_cost=marginal_cost, efficiency_dispatch=efficiency_dispatch, efficiency_store=efficiency_store, standing_loss=self_discharge) # Add batteries where energy and power are sized independently else: battery_type_power = battery_type + '_p' battery_type_energy = battery_type + '_e' capital_cost, marginal_cost = get_costs( battery_type_power, sum(network.snapshot_weightings['objective'])) capital_cost_e, marginal_cost_e = get_costs( battery_type_energy, sum(network.snapshot_weightings['objective'])) efficiency_dispatch, efficiency_store = get_tech_info( battery_type_power, ["efficiency_ds", "efficiency_ch"]) self_discharge = get_tech_info(battery_type_energy, ["efficiency_sd"]).astype(float) self_discharge = round(1 - self_discharge.values[0], 4) ctd_ratio = get_config_values(battery_type_power, ["ctd_ratio"]) network.madd("StorageUnit", onshore_buses.index, suffix=f" StorageUnit {battery_type}", type=battery_type, bus=onshore_buses.index, p_nom_extendable=True, capital_cost=capital_cost, marginal_cost=marginal_cost, capital_cost_e=capital_cost_e, marginal_cost_e=marginal_cost_e, efficiency_dispatch=efficiency_dispatch, efficiency_store=efficiency_store, standing_loss=self_discharge, ctd_ratio=ctd_ratio) storages = network.storage_units.index[network.storage_units.type == battery_type] for storage_to_replace in storages: replace_su_closed_loop(network, storage_to_replace) return network