Repository: PyPSA/PyPSA

Issue:
Time-dependent efficiencies / standing losses for generators, stores, storage_units
Links already support time-dependent efficiencies.

https://pypsa.readthedocs.io/en/latest/components.html#link

I see no reason why we shouldn't extend this to the other components:

- generators
- storage units
- stores

for efficiency (store/dispatch) and standing loss.


Discussion:

Patch:
diff --git a/doc/release_notes.rst b/doc/release_notes.rst
index 31995d5e..225aa5fa 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 <https://github.com/PyPSA/PyPSA/pull/564>`_, `#567 <https://github.com/PyPSA/PyPSA/pull/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.
+
 * The attributes ``lifetime`` and ``build_year`` are now aggregated with a
   capacity-weighted mean when clustering the network. Previously, these
   attributes had to carry identical values for components that were to be
diff --git a/pypsa/component_attrs/generators.csv b/pypsa/component_attrs/generators.csv
index 11ca0703..4b3f472e 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 407488aa..d4d87c8b 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 a856a579..83f6503f 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 e30c53e2..ce320073 100644
--- a/pypsa/linopf.py
+++ b/pypsa/linopf.py
@@ -29,8 +29,9 @@ from pypsa.descriptors import (
     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 @@ from pypsa.linopt import (
     write_objective,
 )
 from pypsa.pf import _as_snapshots
-from pypsa.pf 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 define_global_constraints(n, 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 940dc0c0..d9104aaf 100644
--- a/pypsa/opf.py
+++ b/pypsa/opf.py
@@ -39,12 +39,9 @@ import logging
 logger = logging.getLogger(__name__)
 
 
-from pypsa.descriptors import (
-    allocate_series_dataframes,
-    get_switchable_as_dense,
-    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,
@@ -109,8 +106,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 +686,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 +730,7 @@ def define_storage_variables_constraints(network, snapshots):
     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 +831,14 @@ def define_storage_variables_constraints(network, snapshots):
     # 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 +848,13 @@ def define_storage_variables_constraints(network, snapshots):
             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 +874,13 @@ def define_storage_variables_constraints(network, snapshots):
 
             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 +909,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 +979,8 @@ def define_store_variables_constraints(network, snapshots):
 
     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 +992,13 @@ def define_store_variables_constraints(network, snapshots):
             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 +1056,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 +1432,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 +1515,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,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_switchable_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:
@@ -1561,7 +1562,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 +1660,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 +1929,7 @@ def extract_optimisation_results(
         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 +1967,7 @@ def extract_optimisation_results(
     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 +1993,7 @@ def extract_optimisation_results(
             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 80f65be8..5e5b47a5 100644
--- a/pypsa/optimization/constraints.py
+++ b/pypsa/optimization/constraints.py
@@ -652,9 +652,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"]
 
@@ -737,7 +737,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/optimization/global_constraints.py b/pypsa/optimization/global_constraints.py
index a91d8b3d..7ccb881f 100644
--- a/pypsa/optimization/global_constraints.py
+++ b/pypsa/optimization/global_constraints.py
@@ -12,6 +12,7 @@ from linopy.expressions import merge
 from numpy import isnan, nan
 from xarray import DataArray
 
+from pypsa.descriptors import get_switchable_as_dense as get_as_dense
 from pypsa.descriptors import nominal_attrs
 
 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)
diff --git a/pypsa/pf.py b/pypsa/pf.py
index 6918b01d..2dbc58f5 100644
--- a/pypsa/pf.py
+++ b/pypsa/pf.py
@@ -31,13 +31,9 @@ from scipy.sparse import issparse
 from scipy.sparse import vstack as svstack
 from scipy.sparse.linalg import spsolve
 
-from pypsa.descriptors import (
-    Dict,
-    allocate_series_dataframes,
-    degree,
-    get_switchable_as_dense,
-    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_switchable_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
@@ -160,7 +154,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 +162,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 +381,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 +402,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 +617,7 @@ def sub_network_pf(
         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 +635,7 @@ def sub_network_pf(
 
     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 +808,7 @@ def sub_network_pf(
         )
         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 +1498,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


Test patch:
diff --git a/test/test_io.py b/test/test_io.py
index 976e93f1..03c2b386 100644
--- a/test/test_io.py
+++ b/test/test_io.py
@@ -4,6 +4,7 @@ from pathlib import Path
 
 import pandas as pd
 import pytest
+from numpy.testing import assert_array_almost_equal as equal
 
 import pypsa
 
@@ -127,3 +128,36 @@ 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, 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,
+    )
+
+    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_basic_constraints.py b/test/test_lopf_basic_constraints.py
index b822adc4..45bb3e6f 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)
diff --git a/test/test_lopf_varying_inputs.py b/test/test_lopf_varying_inputs.py
new file mode 100644
index 00000000..29f32335
--- /dev/null
+++ b/test/test_lopf_varying_inputs.py
@@ -0,0 +1,72 @@
+# -*- 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, 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)
+    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, 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,
+    )
+    status, _ = optimize(n, api)
+    assert status == "ok"
+    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, 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,
+    )
+    status, _ = optimize(n, api)
+    assert status == "ok"
+    equal(n.stores_t.e.sto.values, [1.0, 0.9, 0.72])