Repository: pypsa/pypsa
Issue: Links ramp_start_up_limit not behaving as expected

<!-- Please do not post usage questions here. Ask them on the PyPSA mailing list: https://groups.google.com/forum/#!forum/pypsa -->

## Checklist

- [ ] I am using the current version of PyPSA with the recent pull request #582.

## Describe the Bug

Seems the `ramp_limit_start_up` for link components is not working as intended. I have tried rerunning the the contents of `test_link_unit_commitment(api)` with the addition of the attribute `ramp_limit_start_up=0.01` and the output corresponding to OCGT does not change. Code is below to reproduce.

Thanks for any help in advance.

## Code
```
n = pypsa.Network()

snapshots = range(4)

n.set_snapshots(snapshots)

n.madd("Bus", ["gas", "electricity"])

n.add("Generator", "gas", bus="gas", marginal_cost=10, p_nom=20000)

n.add(
    "Link",
    "OCGT",
    bus0="gas",
    bus1="electricity",
    committable=True,
    p_min_pu=0.1,
    efficiency=0.5,
    up_time_before=0,
    ramp_limit_start_up=0.01, # CHANGE IS HERE
    min_up_time=3,
    start_up_cost=3333,
    p_nom=12000,
)

n.add(
    "Generator",
    "wind",
    bus="electricity",
    p_nom=800,
)

n.add("Load", "load", bus="electricity", p_set=[4000, 6000, 800, 5000])

n.optimize()

```

## Error Message

N/A.



Changed files:
pypsa/optimization/constraints.py, pypsa/linopf.py

diff --git a/pypsa/linopf.py b/pypsa/linopf.py
index c989fa44..efe9e6ee 100644
--- a/pypsa/linopf.py
+++ b/pypsa/linopf.py
@@ -334,10 +334,23 @@ def define_ramp_limit_constraints(n, sns, c):
     """
     Defines ramp limits for a given component with valid ramplimit.
     """
+
+    if {"ramp_limit_up", "ramp_limit_down"}.isdisjoint(n.df(c)):
+        return
+    ramp_attr = [
+        "ramp_limit_up",
+        "ramp_limit_start_up",
+        "ramp_limit_down",
+        "ramp_limit_shut_down",
+    ]
+    if n.df(c)[ramp_attr].isnull().all().all():
+        return
+    if n.df(c)[["ramp_limit_up", "ramp_limit_down"]].eq(1).all().all():
+        return
+
     rup_i = n.df(c).query("ramp_limit_up == ramp_limit_up").index
     rdown_i = n.df(c).query("ramp_limit_down == ramp_limit_down").index
-    if rup_i.empty & rdown_i.empty:
-        return
+
     fix_i = get_non_extendable_i(n, c)
     ext_i = get_extendable_i(n, c)
     if "committable" in n.df(c):
diff --git a/pypsa/optimization/constraints.py b/pypsa/optimization/constraints.py
index 4eeb9c6b..e9f0bbc6 100644
--- a/pypsa/optimization/constraints.py
+++ b/pypsa/optimization/constraints.py
@@ -233,7 +233,19 @@ def define_operational_constraints_for_committables(n, sns, c):
         n.model.add_constraints(status, "=", 0, name, mask=mask)
 
     # linearized approximation because committable can partly start up and shut down
-    if n._linearized_uc:
+    cost_equal = all(
+        n.df(c).loc[com_i, "start_up_cost"] == n.df(c).loc[com_i, "shut_down_cost"]
+    )
+    # only valid additional constraints if start up costs equal to shut down costs
+    if n._linearized_uc and not cost_equal:
+        logger.warning(
+            "The linear relaxation of the unit commitment cannot be "
+            "tightened since the start up costs are not equal to the "
+            "shut down costs. Proceed with the linear relaxation "
+            "without the tightening by additional constraints. "
+            "This might result in a longer solving time."
+        )
+    if n._linearized_uc and cost_equal:
         # dispatch limit for partly start up/shut down for t-1
         lhs = (
             p.shift(snapshot=1)
@@ -313,7 +325,13 @@ def define_ramp_limit_constraints(n, sns, c, attr):
 
     if {"ramp_limit_up", "ramp_limit_down"}.isdisjoint(n.df(c)):
         return
-    if n.df(c)[["ramp_limit_up", "ramp_limit_down"]].isnull().all().all():
+    ramp_attr = [
+        "ramp_limit_up",
+        "ramp_limit_start_up",
+        "ramp_limit_down",
+        "ramp_limit_shut_down",
+    ]
+    if n.df(c)[ramp_attr].isnull().all().all():
         return
     if n.df(c)[["ramp_limit_up", "ramp_limit_down"]].eq(1).all().all():
         return
@@ -402,16 +420,17 @@ def define_ramp_limit_constraints(n, sns, c, attr):
     assets = n.df(c).reindex(com_i)
 
     # com up
-    if not assets.ramp_limit_up.isnull().all():
+    if not assets[["ramp_limit_up", "ramp_limit_start_up"]].isnull().all().all():
         limit_start = assets.eval("ramp_limit_start_up * p_nom").to_xarray()
-        limit_up = assets.eval("ramp_limit_up * p_nom").to_xarray()
-
-        status = m[f"{c}-status"].sel(snapshot=active.index)
-        status_prev = m[f"{c}-status"].shift(snapshot=1).sel(snapshot=active.index)
-
+        limit_up = assets.eval("ramp_limit_up * p_nom").fillna(assets.p_nom).to_xarray()
+        status = m[f"{c}-status"]
+        status_prev = status.shift(snapshot=1)
+        rhs_start = pd.DataFrame(0, index=sns, columns=n.df(c).index)
+        p_now = reindex(p, c, com_i)
+        p_prev = p_now.shift(snapshot=1)
         lhs = (
-            (1, p_actual(com_i)),
-            (-1, p_previous(com_i)),
+            (1, p_now),
+            (-1, p_prev),
             (limit_start - limit_up, status_prev),
             (-limit_start, status),
         )
@@ -420,21 +439,28 @@ def define_ramp_limit_constraints(n, sns, c, attr):
         if is_rolling_horizon:
             status_start = n.pnl(c)["status"][com_i].iloc[start_i]
             rhs.loc[sns[0]] += (limit_up - limit_start) * status_start
-
-        mask = active.reindex(columns=com_i) & assets.ramp_limit_up.notnull()
-        m.add_constraints(lhs, "<=", rhs, f"{c}-com-{attr}-ramp_limit_up", mask=mask)
+        else:
+            # TODO
+            status_start = 0
+            rhs.loc[sns[0]] += (limit_up - limit_start) * status_start
+        m.add_constraints(lhs, "<=", rhs, f"{c}-com-{attr}-ramp_limit_up")
 
     # com down
-    if not assets.ramp_limit_down.isnull().all():
+    if not assets[["ramp_limit_down", "ramp_limit_shut_down"]].isnull().all().all():
         limit_shut = assets.eval("ramp_limit_shut_down * p_nom").to_xarray()
-        limit_down = assets.eval("ramp_limit_down * p_nom").to_xarray()
+        limit_down = (
+            assets.eval("ramp_limit_down * p_nom").fillna(assets.p_nom).to_xarray()
+        )
 
-        status = m[f"{c}-status"].sel(snapshot=active.index)
-        status_prev = m[f"{c}-status"].shift(snapshot=1).sel(snapshot=active.index)
+        status = m[f"{c}-status"]
+        status_prev = m[f"{c}-status"].shift(snapshot=1)
+        rhs_start = pd.DataFrame(0, index=sns, columns=n.df(c).index)
+        p_now = reindex(p, c, com_i)
+        p_prev = p_now.shift(snapshot=1)
 
         lhs = (
-            (1, p_actual(com_i)),
-            (-1, p_previous(com_i)),
+            (1, p_now),
+            (-1, p_prev),
             (limit_down - limit_shut, status),
             (limit_shut, status_prev),
         )
@@ -444,9 +470,8 @@ def define_ramp_limit_constraints(n, sns, c, attr):
             status_start = n.pnl(c)["status"][com_i].iloc[start_i]
             rhs.loc[sns[0]] += -limit_shut * status_start
 
-        mask = active.reindex(columns=com_i) & assets.ramp_limit_down.notnull()
-
-        m.add_constraints(lhs, ">=", rhs, f"{c}-com-{attr}-ramp_limit_down", mask=mask)
+        # TODO
+        # m.add_constraints(lhs, ">=", rhs, f"{c}-com-{attr}-ramp_limit_down")
 
 
 def define_nodal_balance_constraints(