Merge pull request #739 from PyPSA/complete-losses

Lossy bidirectional links
This commit is contained in:
Fabian Neumann 2024-01-03 16:54:02 +01:00 committed by GitHub
commit cd3eddcc3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 121 additions and 2 deletions

View File

@ -487,6 +487,16 @@ sector:
electricity_distribution_grid: true
electricity_distribution_grid_cost_factor: 1.0
electricity_grid_connection: true
transmission_efficiency:
DC:
efficiency_static: 0.98
efficiency_per_1000km: 0.977
H2 pipeline:
efficiency_per_1000km: 1 # 0.979
compression_per_1000km: 0.019
gas pipeline:
efficiency_per_1000km: 1 #0.977
compression_per_1000km: 0.01
H2_network: true
gas_network: false
H2_retrofit: false

View File

@ -107,6 +107,11 @@ electricity_distribution _grid,--,"{true, false}",Add a simplified representatio
electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of the electricity distribution grid
,,,
electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar
transmission_efficiency,,,Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links.
-- {carrier},--,str,The carrier of the link.
-- -- efficiency_static,p.u.,float,Length-independent transmission efficiency.
-- -- efficiency_per_1000km,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$)
-- -- compression_per_1000km,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus.
H2_network,--,"{true, false}",Add option for new hydrogen pipelines
gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm <https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation.html#networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation>`_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well."
H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen.

1 Unit Values Description
107 electricity_distribution _grid_cost_factor Multiplies the investment cost of the electricity distribution grid
108
109 electricity_grid _connection -- {true, false} Add the cost of electricity grid connection for onshore wind and solar
110 transmission_efficiency Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links.
111 -- {carrier} -- str The carrier of the link.
112 -- -- efficiency_static p.u. float Length-independent transmission efficiency.
113 -- -- efficiency_per_1000km p.u. per 1000 km float Length-dependent transmission efficiency ($\eta^{\text{length}}$)
114 -- -- compression_per_1000km p.u. per 1000 km float Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus.
115 H2_network -- {true, false} Add option for new hydrogen pipelines
116 gas_network -- {true, false} Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm <https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation.html#networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation>`_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well.
117 H2_retrofit -- {true, false} Add option for retrofiting existing pipelines to transport hydrogen.

View File

@ -10,6 +10,13 @@ Release Notes
Upcoming Release
================
* Add option to specify losses for bidirectional links, e.g. pipelines or HVDC
links, in configuration file under ``sector: transmission_efficiency:``. Users
can specify static or length-dependent values as well as a length-dependent
electricity demand for compression, which is implemented as a multi-link to
the local electricity buses. The bidirectional links will then be split into
two unidirectional links with linked capacities.
* Pin ``snakemake`` version to below 8.0.0, as the new version is not yet
supported by ``pypsa-eur``.

View File

@ -3394,6 +3394,57 @@ def set_temporal_aggregation(n, opts, solver_name):
return n
def lossy_bidirectional_links(n, carrier, efficiencies={}):
"Split bidirectional links into two unidirectional links to include transmission losses."
carrier_i = n.links.query("carrier == @carrier").index
if (
not any((v != 1.0) or (v >= 0) for v in efficiencies.values())
or carrier_i.empty
):
return
efficiency_static = efficiencies.get("efficiency_static", 1)
efficiency_per_1000km = efficiencies.get("efficiency_per_1000km", 1)
compression_per_1000km = efficiencies.get("compression_per_1000km", 0)
logger.info(
f"Specified losses for {carrier} transmission "
f"(static: {efficiency_static}, per 1000km: {efficiency_per_1000km}, compression per 1000km: {compression_per_1000km}). "
"Splitting bidirectional links."
)
n.links.loc[carrier_i, "p_min_pu"] = 0
n.links.loc[
carrier_i, "efficiency"
] = efficiency_static * efficiency_per_1000km ** (
n.links.loc[carrier_i, "length"] / 1e3
)
rev_links = (
n.links.loc[carrier_i].copy().rename({"bus0": "bus1", "bus1": "bus0"}, axis=1)
)
rev_links["length_original"] = rev_links["length"]
rev_links["capital_cost"] = 0
rev_links["length"] = 0
rev_links["reversed"] = True
rev_links.index = rev_links.index.map(lambda x: x + "-reversed")
n.links = pd.concat([n.links, rev_links], sort=False)
n.links["reversed"] = n.links["reversed"].fillna(False)
n.links["length_original"] = n.links["length_original"].fillna(n.links.length)
# do compression losses after concatenation to take electricity consumption at bus0 in either direction
carrier_i = n.links.query("carrier == @carrier").index
if compression_per_1000km > 0:
n.links.loc[carrier_i, "bus2"] = n.links.loc[carrier_i, "bus0"].map(
n.buses.location
) # electricity
n.links.loc[carrier_i, "efficiency2"] = (
-compression_per_1000km * n.links.loc[carrier_i, "length_original"] / 1e3
)
if __name__ == "__main__":
if "snakemake" not in globals():
from _helpers import mock_snakemake
@ -3569,6 +3620,18 @@ if __name__ == "__main__":
if options["electricity_grid_connection"]:
add_electricity_grid_connection(n, costs)
for k, v in options["transmission_efficiency"].items():
lossy_bidirectional_links(n, k, v)
# Workaround: Remove lines with conflicting (and unrealistic) properties
# cf. https://github.com/PyPSA/pypsa-eur/issues/444
if snakemake.config["solving"]["options"]["transmission_losses"]:
idx = n.lines.query("num_parallel == 0").index
logger.info(
f"Removing {len(idx)} line(s) with properties conflicting with transmission losses functionality."
)
n.mremove("Line", idx)
first_year_myopic = (snakemake.params.foresight in ["myopic", "perfect"]) and (
snakemake.params.planning_horizons[0] == investment_year
)

View File

@ -687,6 +687,35 @@ def add_battery_constraints(n):
n.model.add_constraints(lhs == 0, name="Link-charger_ratio")
def add_lossy_bidirectional_link_constraints(n):
if not n.links.p_nom_extendable.any() or not "reversed" in n.links.columns:
return
n.links["reversed"] = n.links.reversed.fillna(0).astype(bool)
carriers = n.links.loc[n.links.reversed, "carrier"].unique()
forward_i = n.links.query(
"carrier in @carriers and ~reversed and p_nom_extendable"
).index
def get_backward_i(forward_i):
return pd.Index(
[
re.sub(r"-(\d{4})$", r"-reversed-\1", s)
if re.search(r"-\d{4}$", s)
else s + "-reversed"
for s in forward_i
]
)
backward_i = get_backward_i(forward_i)
lhs = n.model["Link-p_nom"].loc[backward_i]
rhs = n.model["Link-p_nom"].loc[forward_i]
n.model.add_constraints(lhs == rhs, name="Link-bidirectional_sync")
def add_chp_constraints(n):
electric = (
n.links.index.str.contains("urban central")
@ -745,9 +774,13 @@ def add_pipe_retrofit_constraint(n):
"""
Add constraint for retrofitting existing CH4 pipelines to H2 pipelines.
"""
gas_pipes_i = n.links.query("carrier == 'gas pipeline' and p_nom_extendable").index
if "reversed" not in n.links.columns:
n.links["reversed"] = False
gas_pipes_i = n.links.query(
"carrier == 'gas pipeline' and p_nom_extendable and ~reversed"
).index
h2_retrofitted_i = n.links.query(
"carrier == 'H2 pipeline retrofitted' and p_nom_extendable"
"carrier == 'H2 pipeline retrofitted' and p_nom_extendable and ~reversed"
).index
if h2_retrofitted_i.empty or gas_pipes_i.empty:
@ -786,6 +819,7 @@ def extra_functionality(n, snapshots):
if "EQ" in o:
add_EQ_constraints(n, o)
add_battery_constraints(n)
add_lossy_bidirectional_link_constraints(n)
add_pipe_retrofit_constraint(n)
if n._multi_invest:
add_carbon_constraint(n, snapshots)