Repository: PyPSA/linopy

Issue:
Add support of xarray's `diff` function to Variable class
The class `linopy.variables.Variable` inherits methods from its parent `xarray.DataArray`. Some of these methods seem to lead to wrong optimization results.

Example: 

```
m = linopy.Model()
var = m.add_variables(name="var", lower=xr.DataArray(np.zeros(5), dims='x', coords={'x': np.arange(5)}))
m.add_constraints(1. * var.diff(dim='x') == 1.)
m.add_objective(var.sum())
```

Solving using Gurobi gives the solution: `var: 0.0 1.0 0.0 0.0 0.0`.
The expected solution would be: `var: 0.0 1.0 2.0 3.0 4.0`

Discussion:

Patch:
diff --git a/linopy/expressions.py b/linopy/expressions.py
index 3e9e9f0..d7c8b7b 100644
--- a/linopy/expressions.py
+++ b/linopy/expressions.py
@@ -495,6 +495,26 @@ class LinearExpression:
         cond = _expr_unwrap(cond)
         return self.__class__(self.data.where(cond, other=other, **kwargs))
 
+    def diff(self, dim, n=1):
+        """
+        Calculate the n-th order discrete difference along given axis.
+
+        This operation call ``xarray.Dataset.diff`` but ensures preserving the
+        linopy.LinearExpression type.
+
+        Parameters
+        ----------
+        dim : str
+            Dimension over which to calculate the finite difference.
+        n : int, optional
+            The number of times values are differenced.
+
+        Returns
+        -------
+        linopy.LinearExpression
+        """
+        return self - self.shift({dim: n})
+
     def groupby(
         self,
         group,
@@ -532,6 +552,7 @@ class LinearExpression:
         )
         return LinearExpressionGroupby(groups)
 
+    @deprecated("0.1.0", "0.1.2", details="Use groupby (followed by sum) instead.")
     def groupby_sum(self, group):
         """
         Sum expression over groups.
@@ -597,6 +618,7 @@ class LinearExpression:
         )
         return LinearExpressionRolling(rolling)
 
+    @deprecated("0.1.0", "0.1.2", details="Use rolling (followed by sum) instead.")
     def rolling_sum(self, **kwargs):
         """
         Rolling sum of the linear expression.
diff --git a/linopy/variables.py b/linopy/variables.py
index 9572f87..934cf28 100644
--- a/linopy/variables.py
+++ b/linopy/variables.py
@@ -442,6 +442,26 @@ class Variable:
         """
         return self.to_linexpr().sum(dims)
 
+    def diff(self, dim, n=1):
+        """
+        Calculate the n-th order discrete difference along the given dimension.
+
+        This function works exactly in the same way as ``LinearExpression.diff()``.
+
+        Parameters
+        ----------
+        dim : str
+            Dimension over which to calculate the finite difference.
+        n : int, default: 1
+            The number of times values are differenced.
+
+        Returns
+        -------
+        linopy.LinearExpression
+            Finite difference expression.
+        """
+        return self.to_linexpr().diff(dim, n)
+
     def where(self, cond, other=-1, **kwargs):
         """
         Filter variables based on a condition.


Test patch:
diff --git a/test/test_linear_expression.py b/test/test_linear_expression.py
index 8d81b0e..8a76c96 100644
--- a/test/test_linear_expression.py
+++ b/test/test_linear_expression.py
@@ -302,6 +302,16 @@ def test_linear_expression_shift(v):
     assert (shifted.vars.loc[:1] == -1).all()
 
 
+def test_linear_expression_diff(v):
+    diff = v.to_linexpr().diff("dim_2")
+    assert diff.nterm == 2
+
+
+def test_linear_expression_diff(v):
+    diff = v.diff("dim_2")
+    assert diff.nterm == 2
+
+
 def test_linear_expression_groupby(v):
     expr = 1 * v
     groups = xr.DataArray([1] * 10 + [2] * 10, coords=v.coords)