From 921e92f71df8f76f3de1109bedd50b72fb800edb Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 22 Feb 2023 18:36:41 +0100 Subject: [PATCH 1/6] time-varying efficiencies and standing losses for all components --- doc/release_notes.rst | 6 +++ pypsa/component_attrs/generators.csv | 2 +- pypsa/component_attrs/storage_units.csv | 6 +-- pypsa/component_attrs/stores.csv | 2 +- pypsa/linopf.py | 18 ++++---- pypsa/opf.py | 59 ++++++++++++++----------- pypsa/optimization/constraints.py | 8 ++-- pypsa/pf.py | 20 ++++----- test/test_lopf_basic_constraints.py | 12 ++--- 9 files changed, 72 insertions(+), 61 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index d5365a8ae..114ab07f2 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -14,6 +14,12 @@ Upcoming Release * Fixed interference of io routines with linopy optimisation [`#564 `_, `#567 `_] +* Efficiencies and standing losses of stores, storage units and generators can + now be specified as time-varying attributes (``efficiency``, + ``efficiency_dispatch``, ``efficiency_store``, ``standing_loss``). For + example, this allows specifying temperature-dependent generator efficiencies + or evaporation in hydro reservoirs. + PyPSA 0.22.1 (15th February 2023) ================================= diff --git a/pypsa/component_attrs/generators.csv b/pypsa/component_attrs/generators.csv index 11ca07031..4b3f472eb 100644 --- a/pypsa/component_attrs/generators.csv +++ b/pypsa/component_attrs/generators.csv @@ -17,7 +17,7 @@ marginal_cost,static or series,currency/MWh,0,Marginal cost of production of 1 M build_year,int,year,0,build year,Input (optional) lifetime,float,years,inf,lifetime,Input (optional) capital_cost,float,currency/MW,0,Capital cost of extending p_nom by 1 MW.,Input (optional) -efficiency,float,per unit,1,"Ratio between primary energy and electrical energy, e.g. takes value 0.4 MWh_elec/MWh_thermal for gas. This is required for global constraints on primary energy in OPF.",Input (optional) +efficiency,static or series,per unit,1,"Ratio between primary energy and electrical energy, e.g. takes value 0.4 MWh_elec/MWh_thermal for gas. This is required for global constraints on primary energy in OPF.",Input (optional) committable,boolean,n/a,False,Use unit commitment (only possible if p_nom is not extendable).,Input (optional) start_up_cost,float,currency,0,Cost to start up the generator. Only read if committable is True.,Input (optional) shut_down_cost,float,currency,0,Cost to shut down the generator. Only read if committable is True.,Input (optional) diff --git a/pypsa/component_attrs/storage_units.csv b/pypsa/component_attrs/storage_units.csv index 407488aa9..d4d87c8b3 100644 --- a/pypsa/component_attrs/storage_units.csv +++ b/pypsa/component_attrs/storage_units.csv @@ -23,9 +23,9 @@ state_of_charge_set,static or series,MWh,NaN,State of charge set points for snap cyclic_state_of_charge,boolean,n/a,False,"Switch: if True, then state_of_charge_initial is ignored and the initial state of charge is set to the final state of charge for the group of snapshots in the OPF (soc[-1] = soc[len(snapshots)-1]).",Input (optional) cyclic_state_of_charge_per_period,boolean,n/a,True,"Switch: if True, then the cyclic constraints are applied to each period (first snapshot level if multiindexed) separately.",Input (optional) max_hours,float,hours,1,Maximum state of charge capacity in terms of hours at full output capacity p_nom,Input (optional) -efficiency_store,float,per unit,1,Efficiency of storage on the way into the storage.,Input (optional) -efficiency_dispatch,float,per unit,1,Efficiency of storage on the way out of the storage.,Input (optional) -standing_loss,float,per unit,0,Losses per hour to state of charge.,Input (optional) +efficiency_store,static or series,per unit,1,Efficiency of storage on the way into the storage.,Input (optional) +efficiency_dispatch,static or series,per unit,1,Efficiency of storage on the way out of the storage.,Input (optional) +standing_loss,static or series,per unit,0,Losses per hour to state of charge.,Input (optional) inflow,static or series,MW,0,"Inflow to the state of charge, e.g. due to river inflow in hydro reservoir.",Input (optional) p,series,MW,0,active power at bus (positive if net generation),Output p_dispatch,series,MW,0,active power dispatch at bus,Output diff --git a/pypsa/component_attrs/stores.csv b/pypsa/component_attrs/stores.csv index a856a5794..83f6503fa 100644 --- a/pypsa/component_attrs/stores.csv +++ b/pypsa/component_attrs/stores.csv @@ -18,7 +18,7 @@ q_set,static or series,MVar,0,reactive power set point (for PF),Input (optional) sign,float,n/a,1,power sign,Input (optional) marginal_cost,static or series,currency/MWh,0,Marginal cost of production of 1 MWh.,Input (optional) capital_cost,float,currency/MWh,0,Capital cost of extending e_nom by 1 MWh.,Input (optional) -standing_loss,float,per unit,0,Losses per hour to energy.,Input (optional) +standing_loss,static or series,per unit,0,Losses per hour to energy.,Input (optional) build_year,int,year,0,build year,Input (optional) lifetime,float,years,inf,lifetime,Input (optional) p,series,MW,0,active power at bus (positive if net generation),Output diff --git a/pypsa/linopf.py b/pypsa/linopf.py index e30c53e2e..b49ade04c 100644 --- a/pypsa/linopf.py +++ b/pypsa/linopf.py @@ -52,7 +52,7 @@ write_objective, ) from pypsa.pf import _as_snapshots -from pypsa.pf import get_switchable_as_dense as get_as_dense +from pypsa.descriptors import get_switchable_as_dense as get_as_dense agg_group_kwargs = ( dict(numeric_only=False) if parse(pd.__version__) >= Version("1.3") else {} @@ -543,9 +543,9 @@ def define_storage_unit_constraints(n, sns): # elapsed hours eh = expand_series(n.snapshot_weightings.stores[sns], sus_i) # efficiencies - eff_stand = expand_series(1 - n.df(c).standing_loss, sns).T.pow(eh) - eff_dispatch = expand_series(n.df(c).efficiency_dispatch, sns).T - eff_store = expand_series(n.df(c).efficiency_store, sns).T + eff_stand = (1 - get_as_dense(n, c, "standing_loss", sns)).pow(eh) + eff_dispatch = get_as_dense(n, c, "efficiency_dispatch", sns) + eff_store = get_as_dense(n, c, "efficiency_store", sns) soc = get_var(n, c, "state_of_charge") @@ -655,7 +655,7 @@ def define_store_constraints(n, sns): # elapsed hours eh = expand_series(n.snapshot_weightings.stores[sns], stores_i) # elapsed hours - eff_stand = expand_series(1 - n.df(c).standing_loss, sns).T.pow(eh) + eff_stand = (1 - get_as_dense(n, c, "standing_loss", sns)).pow(eh) e = get_var(n, c, "e") @@ -821,11 +821,9 @@ def get_period(n, glc, sns): # generators gens = n.generators.query("carrier in @emissions.index") if not gens.empty: - em_pu = gens.carrier.map(emissions) / gens.efficiency - em_pu = ( - weightings["generators"].to_frame("weightings") - @ em_pu.to_frame("weightings").T - ).loc[period] + efficiency = get_as_dense(n, "Generator", "efficiency", inds=gens.index) + em_pu = gens.carrier.map(emissions) / efficiency + em_pu = em_pu.multiply(weightings.generators, axis=0).loc[period] p = get_var(n, "Generator", "p").loc[sns, gens.index].loc[period] vals = linexpr((em_pu, p), as_pandas=False) diff --git a/pypsa/opf.py b/pypsa/opf.py index 940dc0c05..9fd571061 100644 --- a/pypsa/opf.py +++ b/pypsa/opf.py @@ -41,10 +41,10 @@ class PersistentSolver: from pypsa.descriptors import ( allocate_series_dataframes, - get_switchable_as_dense, get_switchable_as_iter, zsum, ) +from pypsa.descriptors import get_switchable_as_dense as get_as_dense from pypsa.opt import ( LConstraint, LExpression, @@ -109,8 +109,8 @@ def define_generator_variables_constraints(network, snapshots): start_i = network.snapshots.get_loc(snapshots[0]) - p_min_pu = get_switchable_as_dense(network, "Generator", "p_min_pu", snapshots) - p_max_pu = get_switchable_as_dense(network, "Generator", "p_max_pu", snapshots) + p_min_pu = get_as_dense(network, "Generator", "p_min_pu", snapshots) + p_max_pu = get_as_dense(network, "Generator", "p_max_pu", snapshots) ## Define generator dispatch variables ## @@ -689,8 +689,8 @@ def define_storage_variables_constraints(network, snapshots): ## Define storage dispatch variables ## - p_max_pu = get_switchable_as_dense(network, "StorageUnit", "p_max_pu", snapshots) - p_min_pu = get_switchable_as_dense(network, "StorageUnit", "p_min_pu", snapshots) + p_max_pu = get_as_dense(network, "StorageUnit", "p_max_pu", snapshots) + p_min_pu = get_as_dense(network, "StorageUnit", "p_min_pu", snapshots) bounds = {(su, sn): (0, None) for su in ext_sus_i for sn in snapshots} bounds.update( @@ -733,7 +733,7 @@ def su_p_store_bounds(model, su_name, snapshot): free_pyomo_initializers(network.model.storage_p_store) ## Define spillage variables only for hours with inflow>0. ## - inflow = get_switchable_as_dense(network, "StorageUnit", "inflow", snapshots) + inflow = get_as_dense(network, "StorageUnit", "inflow", snapshots) spill_sus_i = sus.index[inflow.max() > 0] # skip storage units without any inflow inflow_gt0_b = inflow > 0 spill_bounds = { @@ -834,10 +834,14 @@ def su_p_lower(model, su_name, snapshot): # store the combinations with a fixed soc fixed_soc = {} - state_of_charge_set = get_switchable_as_dense( + state_of_charge_set = get_as_dense( network, "StorageUnit", "state_of_charge_set", snapshots ) + standing_loss = get_as_dense(network, "StorageUnit", "standing_loss").T + eff_dispatch = get_as_dense(network, "StorageUnit", "efficiency_dispatch").T + eff_store = get_as_dense(network, "StorageUnit", "efficiency_store").T + for su in sus.index: for i, sn in enumerate(snapshots): soc[su, sn] = [[], "==", 0.0] @@ -847,13 +851,13 @@ def su_p_lower(model, su_name, snapshot): if i == 0 and not sus.at[su, "cyclic_state_of_charge"]: previous_state_of_charge = sus.at[su, "state_of_charge_initial"] soc[su, sn][2] -= ( - 1 - sus.at[su, "standing_loss"] + 1 - standing_loss.at[su, sn] ) ** elapsed_hours * previous_state_of_charge else: previous_state_of_charge = model.state_of_charge[su, snapshots[i - 1]] soc[su, sn][0].append( ( - (1 - sus.at[su, "standing_loss"]) ** elapsed_hours, + (1 - standing_loss.at[su, sn]) ** elapsed_hours, previous_state_of_charge, ) ) @@ -873,13 +877,13 @@ def su_p_lower(model, su_name, snapshot): soc[su, sn][0].append( ( - sus.at[su, "efficiency_store"] * elapsed_hours, + eff_store.at[su, sn] * elapsed_hours, model.storage_p_store[su, sn], ) ) soc[su, sn][0].append( ( - -(1 / sus.at[su, "efficiency_dispatch"]) * elapsed_hours, + -(1 / eff_dispatch.at[su, sn]) * elapsed_hours, model.storage_p_dispatch[su, sn], ) ) @@ -908,8 +912,8 @@ def define_store_variables_constraints(network, snapshots): ext_stores = stores.index[stores.e_nom_extendable] fix_stores = stores.index[~stores.e_nom_extendable] - e_max_pu = get_switchable_as_dense(network, "Store", "e_max_pu", snapshots) - e_min_pu = get_switchable_as_dense(network, "Store", "e_min_pu", snapshots) + e_max_pu = get_as_dense(network, "Store", "e_max_pu", snapshots) + e_min_pu = get_as_dense(network, "Store", "e_min_pu", snapshots) model = network.model @@ -978,6 +982,8 @@ def store_e_lower(model, store, snapshot): e = {} + standing_loss = get_as_dense(network, "Store", "standing_loss").T + for store in stores.index: for i, sn in enumerate(snapshots): e[store, sn] = LConstraint(sense="==") @@ -989,13 +995,13 @@ def store_e_lower(model, store, snapshot): if i == 0 and not stores.at[store, "e_cyclic"]: previous_e = stores.at[store, "e_initial"] e[store, sn].lhs.constant += ( - 1 - stores.at[store, "standing_loss"] + 1 - standing_loss.at[store, sn] ) ** elapsed_hours * previous_e else: previous_e = model.store_e[store, snapshots[i - 1]] e[store, sn].lhs.variables.append( ( - (1 - stores.at[store, "standing_loss"]) ** elapsed_hours, + (1 - standing_loss.at[store, sn]) ** elapsed_hours, previous_e, ) ) @@ -1053,8 +1059,8 @@ def define_link_flows(network, snapshots): fixed_links_i = network.links.index[~network.links.p_nom_extendable] - p_max_pu = get_switchable_as_dense(network, "Link", "p_max_pu", snapshots) - p_min_pu = get_switchable_as_dense(network, "Link", "p_min_pu", snapshots) + p_max_pu = get_as_dense(network, "Link", "p_max_pu", snapshots) + p_min_pu = get_as_dense(network, "Link", "p_min_pu", snapshots) fixed_lower = p_min_pu.loc[:, fixed_links_i].multiply( network.links.loc[fixed_links_i, "p_nom"] @@ -1429,7 +1435,7 @@ def define_passive_branch_constraints(network, snapshots): s_max_pu = pd.concat( { - c: get_switchable_as_dense(network, c, "s_max_pu", snapshots) + c: get_as_dense(network, c, "s_max_pu", snapshots) for c in network.passive_branch_components }, axis=1, @@ -1512,9 +1518,9 @@ def define_nodal_balances(network, snapshots): (bus, sn): LExpression() for bus in network.buses.index for sn in snapshots } - efficiency = get_switchable_as_dense(network, "Link", "efficiency", snapshots) + efficiency = get_as_dense(network, "Link", "efficiency", snapshots) - filter = (get_switchable_as_dense(network, "Link", "p_min_pu", snapshots) < 0) & ( + filter = (get_as_dense(network, "Link", "p_min_pu", snapshots) < 0) & ( efficiency < 1 ) links = filter[filter].dropna(how="all", axis=1) @@ -1543,7 +1549,7 @@ def define_nodal_balances(network, snapshots): for col in network.links.columns if col[:3] == "bus" and col not in ["bus0", "bus1"] ]: - efficiency = get_switchable_as_dense( + efficiency = get_as_dense( network, "Link", "efficiency{}".format(i), snapshots ) for cb in network.links.index[network.links["bus{}".format(i)] != ""]: @@ -1561,7 +1567,7 @@ def define_nodal_balances(network, snapshots): (sign, network.model.generator_p[gen, sn]) ) - load_p_set = get_switchable_as_dense(network, "Load", "p_set", snapshots) + load_p_set = get_as_dense(network, "Load", "p_set", snapshots) for load in network.loads.index: bus = network.loads.at[load, "bus"] sign = network.loads.at[load, "sign"] @@ -1659,11 +1665,12 @@ def define_global_constraints(network, snapshots): continue # for generators, use the prime mover carrier gens = network.generators.index[network.generators.carrier == carrier] + efficiency = get_as_dense(network, "Generator", "efficiency").T c.lhs.variables.extend( [ ( attribute - * (1 / network.generators.at[gen, "efficiency"]) + * (1 / efficiency.at[gen, sn]) * network.snapshot_weightings.generators[sn], network.model.generator_p[gen, sn], ) @@ -1927,7 +1934,7 @@ def get_shadows(constraint, multiind=True): set_from_series(network.stores_t.e, get_values(model.store_e)) if len(network.loads): - load_p_set = get_switchable_as_dense(network, "Load", "p_set", snapshots) + load_p_set = get_as_dense(network, "Load", "p_set", snapshots) network.loads_t["p"].loc[snapshots] = load_p_set.loc[snapshots] if len(network.buses): @@ -1965,7 +1972,7 @@ def get_shadows(constraint, multiind=True): if len(network.links): set_from_series(network.links_t.p0, get_values(model.link_p)) - efficiency = get_switchable_as_dense(network, "Link", "efficiency", snapshots) + efficiency = get_as_dense(network, "Link", "efficiency", snapshots) network.links_t.p1.loc[snapshots] = ( -network.links_t.p0.loc[snapshots] * efficiency.loc[snapshots] @@ -1991,7 +1998,7 @@ def get_shadows(constraint, multiind=True): for col in network.links.columns if col[:3] == "bus" and col not in ["bus0", "bus1"] ]: - efficiency = get_switchable_as_dense( + efficiency = get_as_dense( network, "Link", "efficiency{}".format(i), snapshots ) p_name = "p{}".format(i) diff --git a/pypsa/optimization/constraints.py b/pypsa/optimization/constraints.py index 1783b55a6..22c7742bc 100644 --- a/pypsa/optimization/constraints.py +++ b/pypsa/optimization/constraints.py @@ -651,9 +651,9 @@ def define_storage_unit_constraints(n, sns): # elapsed hours eh = expand_series(n.snapshot_weightings.stores[sns], assets.index) # efficiencies - eff_stand = expand_series(1 - assets.standing_loss, sns).T.pow(eh) - eff_dispatch = expand_series(assets.efficiency_dispatch, sns).T - eff_store = expand_series(assets.efficiency_store, sns).T + eff_stand = (1 - get_as_dense(n, c, "standing_loss", sns)).pow(eh) + eff_dispatch = get_as_dense(n, c, "efficiency_dispatch", sns) + eff_store = get_as_dense(n, c, "efficiency_store", sns) soc = m[f"{c}-state_of_charge"] @@ -736,7 +736,7 @@ def define_store_constraints(n, sns): # elapsed hours eh = expand_series(n.snapshot_weightings.stores[sns], assets.index) # efficiencies - eff_stand = expand_series(1 - assets.standing_loss, sns).T.pow(eh) + eff_stand = (1 - get_as_dense(n, c, "standing_loss", sns)).pow(eh) e = m[c + "-e"] p = m[c + "-p"] diff --git a/pypsa/pf.py b/pypsa/pf.py index 6918b01de..8fc3c3681 100644 --- a/pypsa/pf.py +++ b/pypsa/pf.py @@ -35,9 +35,9 @@ Dict, allocate_series_dataframes, degree, - get_switchable_as_dense, zsum, ) +from pypsa.descriptors import get_switchable_as_dense as get_as_dense pd.Series.zsum = zsum @@ -97,7 +97,7 @@ def _calculate_controllable_nodal_power_balance( for c in sub_network.iterate_components( network.controllable_one_port_components ): - c_n_set = get_switchable_as_dense( + c_n_set = get_as_dense( network, c.name, n + "_set", snapshots, c.ind ) network.pnl(c.name)[n].loc[snapshots, c.ind] = c_n_set @@ -160,7 +160,7 @@ def _network_prepare_and_run_pf( # deal with links if not network.links.empty: - p_set = get_switchable_as_dense(network, "Link", "p_set", snapshots) + p_set = get_as_dense(network, "Link", "p_set", snapshots) network.links_t.p0.loc[snapshots] = p_set.loc[snapshots] for i in [ int(col[3:]) @@ -168,7 +168,7 @@ def _network_prepare_and_run_pf( if col[:3] == "bus" and col != "bus0" ]: eff_name = "efficiency" if i == 1 else "efficiency{}".format(i) - efficiency = get_switchable_as_dense(network, "Link", eff_name, snapshots) + efficiency = get_as_dense(network, "Link", eff_name, snapshots) links = network.links.index[network.links["bus{}".format(i)] != ""] network.links_t["p{}".format(i)].loc[snapshots, links] = ( -network.links_t.p0.loc[snapshots, links] @@ -387,7 +387,7 @@ def sub_network_pf_singlebus( sub_network, network, snapshots, buses_o ) - v_mag_pu_set = get_switchable_as_dense(network, "Bus", "v_mag_pu_set", snapshots) + v_mag_pu_set = get_as_dense(network, "Bus", "v_mag_pu_set", snapshots) network.buses_t.v_mag_pu.loc[snapshots, sub_network.slack_bus] = v_mag_pu_set.loc[ :, sub_network.slack_bus ] @@ -408,7 +408,7 @@ def sub_network_pf_singlebus( .fillna(0) ) elif slack_weights == "p_set": - generators_t_p_choice = get_switchable_as_dense( + generators_t_p_choice = get_as_dense( network, "Generator", slack_weights, snapshots ) assert ( @@ -623,7 +623,7 @@ def dfdx(guess, distribute_slack=False, slack_weights=None): return J # Set what we know: slack V and v_mag_pu for PV buses - v_mag_pu_set = get_switchable_as_dense(network, "Bus", "v_mag_pu_set", snapshots) + v_mag_pu_set = get_as_dense(network, "Bus", "v_mag_pu_set", snapshots) network.buses_t.v_mag_pu.loc[snapshots, sub_network.pvs] = v_mag_pu_set.loc[ :, sub_network.pvs ] @@ -641,7 +641,7 @@ def dfdx(guess, distribute_slack=False, slack_weights=None): if distribute_slack: if isinstance(slack_weights, str) and slack_weights == "p_set": - generators_t_p_choice = get_switchable_as_dense( + generators_t_p_choice = get_as_dense( network, "Generator", slack_weights, snapshots ) bus_generation = generators_t_p_choice.rename( @@ -814,7 +814,7 @@ def dfdx(guess, distribute_slack=False, slack_weights=None): ) for bus, group in sub_network.generators().groupby("bus"): if isinstance(slack_weights, str) and slack_weights == "p_set": - generators_t_p_choice = get_switchable_as_dense( + generators_t_p_choice = get_as_dense( network, "Generator", slack_weights, snapshots ) bus_generator_shares = ( @@ -1504,7 +1504,7 @@ def sub_network_lpf(sub_network, snapshots=None, skip_pre=False): # allow all one ports to dispatch as set for c in sub_network.iterate_components(network.controllable_one_port_components): - c_p_set = get_switchable_as_dense(network, c.name, "p_set", snapshots, c.ind) + c_p_set = get_as_dense(network, c.name, "p_set", snapshots, c.ind) network.pnl(c.name).p.loc[snapshots, c.ind] = c_p_set # set the power injection at each node diff --git a/test/test_lopf_basic_constraints.py b/test/test_lopf_basic_constraints.py index b822adc47..45bb3e6fa 100644 --- a/test/test_lopf_basic_constraints.py +++ b/test/test_lopf_basic_constraints.py @@ -38,10 +38,10 @@ def describe_storage_unit_contraints(n): description = {} - eh = expand_series(n.snapshot_weightings.stores, sus_i) - stand_eff = expand_series(1 - n.df(c).standing_loss, sns).T.pow(eh) - dispatch_eff = expand_series(n.df(c).efficiency_dispatch, sns).T - store_eff = expand_series(n.df(c).efficiency_store, sns).T + eh = expand_series(n.snapshot_weightings.stores[sns], sus_i) + stand_eff = (1 - get_as_dense(n, c, "standing_loss", sns)).pow(eh) + dispatch_eff = get_as_dense(n, c, "efficiency_dispatch", sns) + store_eff = get_as_dense(n, c, "efficiency_store", sns) inflow = get_as_dense(n, c, "inflow") * eh spill = eh[pnl.spill.columns] * pnl.spill @@ -161,8 +161,8 @@ def describe_store_contraints(n): c = "Store" pnl = n.pnl(c) - eh = expand_series(n.snapshot_weightings.stores, stores_i) - stand_eff = expand_series(1 - n.df(c).standing_loss, sns).T.pow(eh) + eh = expand_series(n.snapshot_weightings.stores[sns], stores_i) + stand_eff = (1 - get_as_dense(n, c, "standing_loss", sns)).pow(eh) start = pnl.e.iloc[-1].where(stores.e_cyclic, stores.e_initial) previous_e = stand_eff * pnl.e.shift().fillna(start) From cef630928fb4972ba35569f9bc992ca23f010641 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 Feb 2023 17:40:06 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pypsa/linopf.py | 4 ++-- pypsa/opf.py | 11 +++-------- pypsa/pf.py | 12 +++--------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/pypsa/linopf.py b/pypsa/linopf.py index b49ade04c..ce320073f 100644 --- a/pypsa/linopf.py +++ b/pypsa/linopf.py @@ -29,8 +29,9 @@ get_bounds_pu, get_extendable_i, get_non_extendable_i, - nominal_attrs, ) +from pypsa.descriptors import get_switchable_as_dense as get_as_dense +from pypsa.descriptors import nominal_attrs from pypsa.linopt import ( align_with_static_component, define_binaries, @@ -52,7 +53,6 @@ write_objective, ) from pypsa.pf import _as_snapshots -from pypsa.descriptors import get_switchable_as_dense as get_as_dense agg_group_kwargs = ( dict(numeric_only=False) if parse(pd.__version__) >= Version("1.3") else {} diff --git a/pypsa/opf.py b/pypsa/opf.py index 9fd571061..d9104aaf3 100644 --- a/pypsa/opf.py +++ b/pypsa/opf.py @@ -39,12 +39,9 @@ class PersistentSolver: logger = logging.getLogger(__name__) -from pypsa.descriptors import ( - allocate_series_dataframes, - get_switchable_as_iter, - zsum, -) +from pypsa.descriptors import allocate_series_dataframes from pypsa.descriptors import get_switchable_as_dense as get_as_dense +from pypsa.descriptors import get_switchable_as_iter, zsum from pypsa.opt import ( LConstraint, LExpression, @@ -1549,9 +1546,7 @@ def define_nodal_balances(network, snapshots): for col in network.links.columns if col[:3] == "bus" and col not in ["bus0", "bus1"] ]: - efficiency = get_as_dense( - network, "Link", "efficiency{}".format(i), snapshots - ) + efficiency = get_as_dense(network, "Link", "efficiency{}".format(i), snapshots) for cb in network.links.index[network.links["bus{}".format(i)] != ""]: bus = network.links.at[cb, "bus{}".format(i)] for sn in snapshots: diff --git a/pypsa/pf.py b/pypsa/pf.py index 8fc3c3681..2dbc58f5d 100644 --- a/pypsa/pf.py +++ b/pypsa/pf.py @@ -31,13 +31,9 @@ from scipy.sparse import vstack as svstack from scipy.sparse.linalg import spsolve -from pypsa.descriptors import ( - Dict, - allocate_series_dataframes, - degree, - zsum, -) +from pypsa.descriptors import Dict, allocate_series_dataframes, degree from pypsa.descriptors import get_switchable_as_dense as get_as_dense +from pypsa.descriptors import zsum pd.Series.zsum = zsum @@ -97,9 +93,7 @@ def _calculate_controllable_nodal_power_balance( for c in sub_network.iterate_components( network.controllable_one_port_components ): - c_n_set = get_as_dense( - network, c.name, n + "_set", snapshots, c.ind - ) + c_n_set = get_as_dense(network, c.name, n + "_set", snapshots, c.ind) network.pnl(c.name)[n].loc[snapshots, c.ind] = c_n_set # set the power injection at each node from controllable components From c3c35e43783b6dfbd5cee73c19bc6b109307d676 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 5 Mar 2023 16:30:38 +0100 Subject: [PATCH 3/6] add time-dependent standing losses and efficiencies unit tests --- test/test_io.py | 27 +++++++++++++++++++++++++++ test/test_lopf.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 test/test_lopf.py diff --git a/test/test_io.py b/test/test_io.py index 976e93f1c..d6de3209f 100644 --- a/test/test_io.py +++ b/test/test_io.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os from pathlib import Path +from numpy.testing import assert_array_almost_equal as equal import pandas as pd import pytest @@ -127,3 +128,29 @@ def test_import_from_pandapower_network( def test_netcdf_from_url(): url = "https://github.com/PyPSA/PyPSA/raw/master/examples/scigrid-de/scigrid-with-load-gen-trafos.nc" pypsa.Network(url) + + +def test_io_time_dependent_efficiencies(tmpdir): + n = pypsa.Network() + s = [1, .95, .99] + n.snapshots = range(len(s)) + n.add('Bus', 'bus') + n.add('Generator', 'gen', bus='bus', efficiency=s) + n.add('Store', "sto", bus='bus', standing_loss=s) + n.add('StorageUnit', 'su', bus='bus', efficiency_store=s, efficiency_dispatch=s, standing_loss=s) + + fn = os.path.join(tmpdir, "network-time-eff.nc") + n.export_to_netcdf(fn) + m = pypsa.Network(fn) + + assert not m.stores_t.standing_loss.empty + assert not m.storage_units_t.standing_loss.empty + assert not m.generators_t.efficiency.empty + assert not m.storage_units_t.efficiency_store.empty + assert not m.storage_units_t.efficiency_dispatch.empty + + equal(m.stores_t.standing_loss, n.stores_t.standing_loss) + equal(m.storage_units_t.standing_loss, n.storage_units_t.standing_loss) + equal(m.generators_t.efficiency, n.generators_t.efficiency) + equal(m.storage_units_t.efficiency_store, n.storage_units_t.efficiency_store) + equal(m.storage_units_t.efficiency_dispatch, n.storage_units_t.efficiency_dispatch) diff --git a/test/test_lopf.py b/test/test_lopf.py new file mode 100644 index 000000000..f77b4c2e6 --- /dev/null +++ b/test/test_lopf.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +import os + +import pytest +from conftest import SUPPORTED_APIS, optimize +from numpy.testing import assert_array_almost_equal as equal + +import pypsa + + +@pytest.mark.parametrize("api", SUPPORTED_APIS) +def test_time_dependent_generator_efficiency(api): + n = pypsa.Network() + s = [1, .25, .2] + limit = sum(1/i for i in s) + n.snapshots = range(len(s)) + n.add('Bus', 'bus') + n.add('Carrier', 'carrier', co2_emissions=1) + n.add('Generator', 'gen', carrier='carrier', marginal_cost=1, bus='bus', p_nom=1, efficiency=s) + n.add('Load', 'load', bus='bus', p_set=1) + n.add('GlobalConstraint', 'limit', constant=limit) + status, _ = optimize(n, api) + assert status == "ok" + + +@pytest.mark.parametrize("api", SUPPORTED_APIS) +def test_time_dependent_standing_losses_storage_units(api): + n = pypsa.Network() + s = [0, .1, .2] + n.snapshots = range(len(s)) + n.add('Bus', 'bus') + n.add('StorageUnit', 'su', bus='bus', marginal_cost=1, p_nom=1, max_hours=1, state_of_charge_initial=1, standing_loss=s) + status, _ = optimize(n, api) + assert status == "ok" + equal(n.storage_units_t.state_of_charge.su.values, [1., 0.9, 0.72]) + + +@pytest.mark.parametrize("api", SUPPORTED_APIS) +def test_time_dependent_standing_losses_stores(api): + n = pypsa.Network() + s = [0, .1, .2] + n.snapshots = range(len(s)) + n.add('Bus', 'bus') + n.add('Store', 'sto', bus='bus', marginal_cost=1, e_nom=1, e_initial=1, standing_loss=s) + status, _ = optimize(n, api) + assert status == "ok" + equal(n.stores_t.e.sto.values, [1., 0.9, 0.72]) \ No newline at end of file From 513c9b22a105bb9ad827c1ca238d5a4f28cace39 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 5 Mar 2023 16:31:20 +0100 Subject: [PATCH 4/6] fix: global constraint in linopy with time-dependent efficiency --- pypsa/optimization/global_constraints.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pypsa/optimization/global_constraints.py b/pypsa/optimization/global_constraints.py index a91d8b3d2..770db9af0 100644 --- a/pypsa/optimization/global_constraints.py +++ b/pypsa/optimization/global_constraints.py @@ -13,6 +13,7 @@ from xarray import DataArray from pypsa.descriptors import nominal_attrs +from pypsa.descriptors import get_switchable_as_dense as get_as_dense logger = logging.getLogger(__name__) @@ -268,9 +269,9 @@ def define_primary_energy_limit(n, sns): # generators gens = n.generators.query("carrier in @emissions.index") if not gens.empty: - w = weightings["generators"].to_frame("weight") - em_pu = (gens.carrier.map(emissions) / gens.efficiency).to_frame("weight") - em_pu = w @ em_pu.T + efficiency = get_as_dense(n, "Generator", "efficiency", inds=gens.index) + em_pu = gens.carrier.map(emissions) / efficiency + em_pu = em_pu.multiply(weightings.generators, axis=0) p = m["Generator-p"].loc[snapshots, gens.index] expr = (p * em_pu).sum() lhs.append(expr) From 3f77ed1fb6f5785a062150dcb9a1863443edd973 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 5 Mar 2023 15:31:54 +0000 Subject: [PATCH 5/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pypsa/optimization/global_constraints.py | 2 +- test/test_io.py | 19 +++++--- test/test_lopf.py | 55 +++++++++++++++++------- 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/pypsa/optimization/global_constraints.py b/pypsa/optimization/global_constraints.py index 770db9af0..7ccb881f4 100644 --- a/pypsa/optimization/global_constraints.py +++ b/pypsa/optimization/global_constraints.py @@ -12,8 +12,8 @@ from numpy import isnan, nan from xarray import DataArray -from pypsa.descriptors import nominal_attrs from pypsa.descriptors import get_switchable_as_dense as get_as_dense +from pypsa.descriptors import nominal_attrs logger = logging.getLogger(__name__) diff --git a/test/test_io.py b/test/test_io.py index d6de3209f..03c2b386d 100644 --- a/test/test_io.py +++ b/test/test_io.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- import os from pathlib import Path -from numpy.testing import assert_array_almost_equal as equal import pandas as pd import pytest +from numpy.testing import assert_array_almost_equal as equal import pypsa @@ -132,12 +132,19 @@ def test_netcdf_from_url(): def test_io_time_dependent_efficiencies(tmpdir): n = pypsa.Network() - s = [1, .95, .99] + s = [1, 0.95, 0.99] n.snapshots = range(len(s)) - n.add('Bus', 'bus') - n.add('Generator', 'gen', bus='bus', efficiency=s) - n.add('Store', "sto", bus='bus', standing_loss=s) - n.add('StorageUnit', 'su', bus='bus', efficiency_store=s, efficiency_dispatch=s, standing_loss=s) + n.add("Bus", "bus") + n.add("Generator", "gen", bus="bus", efficiency=s) + n.add("Store", "sto", bus="bus", standing_loss=s) + n.add( + "StorageUnit", + "su", + bus="bus", + efficiency_store=s, + efficiency_dispatch=s, + standing_loss=s, + ) fn = os.path.join(tmpdir, "network-time-eff.nc") n.export_to_netcdf(fn) diff --git a/test/test_lopf.py b/test/test_lopf.py index f77b4c2e6..29f323354 100644 --- a/test/test_lopf.py +++ b/test/test_lopf.py @@ -11,14 +11,22 @@ @pytest.mark.parametrize("api", SUPPORTED_APIS) def test_time_dependent_generator_efficiency(api): n = pypsa.Network() - s = [1, .25, .2] - limit = sum(1/i for i in s) + s = [1, 0.25, 0.2] + limit = sum(1 / i for i in s) n.snapshots = range(len(s)) - n.add('Bus', 'bus') - n.add('Carrier', 'carrier', co2_emissions=1) - n.add('Generator', 'gen', carrier='carrier', marginal_cost=1, bus='bus', p_nom=1, efficiency=s) - n.add('Load', 'load', bus='bus', p_set=1) - n.add('GlobalConstraint', 'limit', constant=limit) + n.add("Bus", "bus") + n.add("Carrier", "carrier", co2_emissions=1) + n.add( + "Generator", + "gen", + carrier="carrier", + marginal_cost=1, + bus="bus", + p_nom=1, + efficiency=s, + ) + n.add("Load", "load", bus="bus", p_set=1) + n.add("GlobalConstraint", "limit", constant=limit) status, _ = optimize(n, api) assert status == "ok" @@ -26,22 +34,39 @@ def test_time_dependent_generator_efficiency(api): @pytest.mark.parametrize("api", SUPPORTED_APIS) def test_time_dependent_standing_losses_storage_units(api): n = pypsa.Network() - s = [0, .1, .2] + s = [0, 0.1, 0.2] n.snapshots = range(len(s)) - n.add('Bus', 'bus') - n.add('StorageUnit', 'su', bus='bus', marginal_cost=1, p_nom=1, max_hours=1, state_of_charge_initial=1, standing_loss=s) + n.add("Bus", "bus") + n.add( + "StorageUnit", + "su", + bus="bus", + marginal_cost=1, + p_nom=1, + max_hours=1, + state_of_charge_initial=1, + standing_loss=s, + ) status, _ = optimize(n, api) assert status == "ok" - equal(n.storage_units_t.state_of_charge.su.values, [1., 0.9, 0.72]) + equal(n.storage_units_t.state_of_charge.su.values, [1.0, 0.9, 0.72]) @pytest.mark.parametrize("api", SUPPORTED_APIS) def test_time_dependent_standing_losses_stores(api): n = pypsa.Network() - s = [0, .1, .2] + s = [0, 0.1, 0.2] n.snapshots = range(len(s)) - n.add('Bus', 'bus') - n.add('Store', 'sto', bus='bus', marginal_cost=1, e_nom=1, e_initial=1, standing_loss=s) + n.add("Bus", "bus") + n.add( + "Store", + "sto", + bus="bus", + marginal_cost=1, + e_nom=1, + e_initial=1, + standing_loss=s, + ) status, _ = optimize(n, api) assert status == "ok" - equal(n.stores_t.e.sto.values, [1., 0.9, 0.72]) \ No newline at end of file + equal(n.stores_t.e.sto.values, [1.0, 0.9, 0.72]) From 241a1410c22fdbbfbad47f6196653553f7a131dc Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 31 Mar 2023 11:01:17 +0200 Subject: [PATCH 6/6] rename test/test_lopf.py to test_lopf_varying_inputs.py --- test/{test_lopf.py => test_lopf_varying_inputs.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{test_lopf.py => test_lopf_varying_inputs.py} (100%) diff --git a/test/test_lopf.py b/test/test_lopf_varying_inputs.py similarity index 100% rename from test/test_lopf.py rename to test/test_lopf_varying_inputs.py