Repository: PyPSA/PyPSA

Issue:
Investment periods dropped when clustering by busmap
<!-- Please do not post usage questions here. Ask them on the PyPSA mailing list: https://groups.google.com/forum/#!forum/pypsa -->

## Checklist

- [x] I am using the current [`master`](https://github.com/pypsa/pypsa/tree/master) branch or the latest [release](https://github.com/pypsa/pypsa/releases). Please indicate:

## Describe the Bug

When using the spatial clustering functions of PyPSA, investment periods are dropped from the clustered network. I have confimed this bug when using both the `cluster.get_clustering_from_busmap(...)` and `cluster.cluster_by_busmap(...)` methods. The minimal example shown below is created using the clustering examples found [here](https://pypsa.readthedocs.io/en/latest/examples/spatial-clustering.html#Network-Clustering). While investment periods are dropped, the multi-index snapshots are all retained correctly. 

Thank you so much in advance! Please let me know if you require any further information on this bug. 

## Minimal Example

```python 
import pypsa
import re
import numpy as np
import pandas as pd

print(f"\nI am using PyPSA verion {pypsa.__version__}\n")

n = pypsa.examples.scigrid_de()
n.calculate_dependent_values()

groups = n.buses.operator.apply(lambda x: re.split(" |,|;", x)[0])
busmap = groups.where(groups != "", n.buses.index)

n.lines = n.lines.reindex(columns=n.components["Line"]["attrs"].index[1:])
n.lines["type"] = np.nan
n.buses = n.buses.reindex(columns=n.components["Bus"]["attrs"].index[1:])
n.buses["frequency"] = 50

# add in investment periods
n.set_investment_periods([2020, 2030])

print("\nCluster by custom basemap\n")

C = n.cluster.get_clustering_from_busmap(busmap)
nc = C.network

print(f"Original investment periods are {n.investment_periods}")
print(f"Clustered investment periods are {nc.investment_periods}")

print("\nCluster by busmap created from K-means\n")

weighting = pd.Series(1, n.buses.index)
busmap2 = n.cluster.busmap_by_kmeans(bus_weightings=weighting, n_clusters=50)
nc2 = n.cluster.cluster_by_busmap(busmap2)

print(f"Original investment periods are {n.investment_periods}")
print(f"Clustered investment periods are {nc2.investment_periods}")
```

## Minimal Example Result

```
I am using PyPSA verion 0.28.0

WARNING:pypsa.io:Importing network from PyPSA version v0.17.1 while current version is v0.28.0. Read the release notes at https://pypsa.readthedocs.io/en/latest/release_notes.html to prepare your network for import.
INFO:pypsa.io:Imported network scigrid-de.nc has buses, generators, lines, loads, storage_units, transformers
INFO:pypsa.components:Repeating time-series for each investment period and converting snapshots to a pandas.MultiIndex.

Cluster by custom basemap

Original investment periods are Index([2020, 2030], dtype='int64')
Clustered investment periods are Index([], dtype='object')

Cluster by busmap created from K-means

Original investment periods are Index([2020, 2030], dtype='int64')
Clustered investment periods are Index([], dtype='object')
```


Discussion:

Patch:
diff --git a/doc/release_notes.rst b/doc/release_notes.rst
index c5d9e040..0c8455b3 100644
--- a/doc/release_notes.rst
+++ b/doc/release_notes.rst
@@ -12,6 +12,8 @@ Release Notes
 
 * HiGHS becomes the new default solver for ``n.optimize()``.
 
+* Bugfix: Retain investment periods and weightings when clustering networks.
+
 PyPSA 0.28.0 (8th May 2024)
 =================================
 
diff --git a/examples/notebooks/scigrid-sclopf.ipynb b/examples/notebooks/scigrid-sclopf.ipynb
index c20d9439..00731bd7 100644
--- a/examples/notebooks/scigrid-sclopf.ipynb
+++ b/examples/notebooks/scigrid-sclopf.ipynb
@@ -61,9 +61,7 @@
    "outputs": [],
    "source": [
     "branch_outages = network.lines.index[:15]\n",
-    "network.optimize.optimize_security_constrained(\n",
-    "    now, branch_outages=branch_outages\n",
-    ")"
+    "network.optimize.optimize_security_constrained(now, branch_outages=branch_outages)"
    ]
   },
   {
diff --git a/pypsa/clustering/spatial.py b/pypsa/clustering/spatial.py
index ae01b33a..5852ca28 100644
--- a/pypsa/clustering/spatial.py
+++ b/pypsa/clustering/spatial.py
@@ -490,6 +490,11 @@ def get_clustering_from_busmap(
     if with_time:
         clustered.set_snapshots(n.snapshots)
         clustered.snapshot_weightings = n.snapshot_weightings.copy()
+        if not n.investment_periods.empty:
+            clustered.set_investment_periods(n.investment_periods)
+            clustered.investment_period_weightings = (
+                n.investment_period_weightings.copy()
+            )
         for attr, df in lines_t.items():
             if not df.empty:
                 io.import_series_from_dataframe(clustered, df, "Line", attr)


Test patch:
diff --git a/test/test_bugs.py b/test/test_bugs.py
index 53f54877..9d62858a 100644
--- a/test/test_bugs.py
+++ b/test/test_bugs.py
@@ -1,9 +1,34 @@
 # -*- coding: utf-8 -*-
 import numpy as np
+import pandas as pd
+from numpy.testing import assert_array_almost_equal as almost_equal
 
 import pypsa
 
 
+def test_890():
+
+    n = pypsa.examples.scigrid_de()
+    n.calculate_dependent_values()
+
+    n.lines = n.lines.reindex(columns=n.components["Line"]["attrs"].index[1:])
+    n.lines["type"] = np.nan
+    n.buses = n.buses.reindex(columns=n.components["Bus"]["attrs"].index[1:])
+    n.buses["frequency"] = 50
+
+    n.set_investment_periods([2020, 2030])
+
+    weighting = pd.Series(1, n.buses.index)
+    busmap = n.cluster.busmap_by_kmeans(bus_weightings=weighting, n_clusters=50)
+    nc = n.cluster.cluster_by_busmap(busmap)
+
+    C = n.cluster.get_clustering_from_busmap(busmap)
+    nc = C.network
+
+    almost_equal(n.investment_periods, nc.investment_periods)
+    almost_equal(n.investment_period_weightings, nc.investment_period_weightings)
+
+
 def test_331():
     n = pypsa.Network()
     n.add("Bus", "bus")